/**
 * SOCLE DES PAGES D'ADMINISTRATION — source unique.
 *
 * Complète `feedback-shared.css`, qui porte les tokens (`:root`) et les
 * contrôles de formulaire (`.fbx-select`, checkbox, dropdown maison, éditeur).
 * Les deux se chargent ensemble ; ce fichier ne redéfinit AUCUN token.
 *
 *   <link rel="stylesheet" href="/widget/feedback-shared.css">
 *   <link rel="stylesheet" href="/widget/feedback-admin.css">
 *   <script src="/widget/feedback-shared.js"></script>
 *
 * POURQUOI CE FICHIER EXISTE — relevé du 2026-08-01, avant sa création :
 * 1 801 lignes de <style> inline réparties sur 9 pages, `--fbx-primary`
 * redéclaré à l'identique dans 7 fichiers, 8 pages sur 9 portant leur propre
 * bloc `:root`, 13 valeurs distinctes de `border-radius`, et le bloc de
 * connexion recopié dans 6 pages (`.login-box` : 48 déclarations).
 *
 * RÈGLE : un composant ne se redéfinit pas dans une page. S'il manque, il
 * s'ajoute ICI et la page de référence (/admin/design-system.html) le montre.
 *
 * Les valeurs ci-dessous sont EXTRAITES de l'existant, non réinventées : les
 * pages en service restent la référence visuelle.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   Jetons complémentaires — la GÉOMÉTRIE, et elle seule.

   ⚠️ CE BLOC PORTAIT SOIXANTE-DOUZE JETONS DE COULEUR JUSQU’À DS-2a
   (2026-08-27) : l’échelle de neutres `--fbx-n0..n9`, les huit rôles qui la
   lisaient, les quatre familles d’état, les trois ombres, la marque et les six
   jetons de la barre de navigation. **Ils vivent tous dans
   `widget/feedback-palette.css`**, avec leur version sombre. Ce qui reste ici
   n’a pas de thème : des rayons, et plus bas des mesures de composant.

   ⚠️ AUCUNE COULEUR NE SE REDÉCLARE ICI. La règle B0 de `tools/ds-check.mjs`
   échoue sur toute couleur littérale de cette feuille ; la règle U sur tout
   `var()` vers un jeton que le socle ne déclare pas.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Rayons : quatre valeurs, pas treize. Relevé de l’existant, par usage.
     (Les trois crans neufs de DS-2b — `xs`, `full`, `pill` — vivent dans
     `feedback-shared.css` avec les autres échelles.) */
  --fbx-radius-sm: 6px;   /* badges, petits boutons, champs de filtre */
  --fbx-radius:    8px;   /* boutons, champs, blocs courants (valeur majoritaire) */
  --fbx-radius-lg: 10px;  /* tables, cartes, popovers */
  --fbx-radius-xl: 16px;  /* boîte de connexion */
}

/* ═══════════════════════════════════════════════════════════════════════════
   Ossature de page
   ═══════════════════════════════════════════════════════════════════════════ */

/* box-sizing — AJOUTÉ LE 2026-08-01, et c'était un trou du socle.
   Les 9 pages en service posaient chacune leur `* { box-sizing: border-box }`
   dans leur <style> ; le socle, lui, ne l'a jamais posé. `corbeille.html`, la
   seule page rebranchée, n'en avait donc AUCUN : un `width: 100%` avec padding
   débordait de son conteneur (input de connexion 410px dans une boîte de 380,
   textarea 424px dans un corps de 400). Constaté à l'écran, pas déduit.
   Scopé sur `.fbx-body` : le socle ne s'autorise pas de sélecteur universel
   sur un document qu'il ne possède pas. */
.fbx-body, .fbx-body *, .fbx-body *::before, .fbx-body *::after { box-sizing: border-box; }

.fbx-body { font-family: system-ui, -apple-system, sans-serif; background: var(--fbx-bg); color: var(--fbx-text); margin: 0; }

/* `[hidden]` doit gagner. C'est l'attribut natif du « masqué », mais tout
   `display` posé par une classe le bat — un `.fbx-badge[hidden]` (inline-flex)
   resterait visible. Les pages contournaient ça avec une classe `.hidden`
   maison, une par page. Ici, l'attribut standard suffit. */
.fbx-body [hidden] { display: none !important; }

/* ── En-tête de page (P41) ──────────────────────────────────────────────────
   UN SEUL en-tête pour les 8 écrans. Deux moitiés, et la frontière est une
   règle, pas une habitude :
     GAUCHE  = qui l'on est et OÙ l'on est — le glyphe de marque, puis le
               chemin (`/ retours`). Rien ne s'y clique.
     DROITE  = tout ce qui s'ACTIONNE, `.fbx-header-actions`, retour au
               dashboard compris. Une seule place pour agir.
   Avant P41, cinq écrans posaient le retour à gauche et trois à droite : deux
   conventions pour la même action. La gauche étant désormais l'identité, la
   question ne se repose plus. Ce qui reste au milieu (sélecteurs de projet /
   d'onglet, lien Sheets) relève du CONTEXTE de l'écran, pas de l'action. */
/* HAUTEUR PARTAGEE avec la tete de la barre laterale (`--fbx-topbar-h`).
   Les deux filets du bas doivent former UNE SEULE LIGNE continue en travers de
   l'ecran : quand ils different de quelques pixels, le decrochage se voit et ne
   ressemble a rien. C'est pour ca que la valeur est un token et non deux
   `padding` accordes a la main de part et d'autre. */
/* ⚠️ CETTE RÈGLE LIT DES JETONS `--ui-*` (DS-1). Ce sont les rôles de
   `feedback-palette.css` ; le socle porte les siens sous `--fbx-*`, et les deux
   familles NE VALENT PAS LA MÊME CHOSE en clair — la palette porte les gris de
   la charte, le socle a gardé les siens (#e5e7eb contre #E7EAEE, #6b7280 contre
   #6B7178, #333333 contre #33383D). C'est ce que la cascade donnait déjà : une
   règle de la palette repassait derrière celle-ci. On l'écrit là où elle vit,
   au lieu de la laisser dans une feuille de jetons.
   ⚠️ L'UNIFICATION DES DEUX FAMILLES EST DS-2, et elle CHANGERA des valeurs :
   ce chantier-ci est à sortie constante, il ne la fait pas. */
.fbx-page-header {
  background: var(--fbx-surface); border-bottom: 1px solid var(--fbx-border);
  padding: var(--fbx-space-10) var(--fbx-space-24); min-height: var(--fbx-topbar-h); box-sizing: border-box;
  display: flex; align-items: center; gap: var(--fbx-space-14); flex-wrap: wrap;
  /* ⚠️ COLLANT PARTOUT (2026-09-03), ET IL NE L'ETAIT QUE SUR DEUX ECRANS.
     `feedbacks` et `corrections` enveloppaient le leur dans `.fbx-board-sticky`,
     les neuf autres n'avaient rien : le periMetre — quel projet, quelle
     livraison — et le geste de la page — « Enregistrer », « Nouvelle
     livraison » — disparaissaient des qu'on defilait. *Sur l'ecran d'une
     livraison, on redescend chercher un bouton dont on vient de perdre la
     trace.*
     ⚠️ CE N'EST PAS UNE SECONDE COUCHE POUR LES DEUX BOARDS : leur en-tete est
     DANS `.fbx-board-sticky`, et un `sticky` imbrique se cale sur son parent
     collant — donc rien ne bouge pour eux, la regle ne fait que rejoindre ce
     qu'ils avaient deja.
     ⚠️ MEME REPLI QUE `.fbx-board-sticky`, ET IL EST MESURE : sur un telephone,
     un bandeau collant prend sur l'axe RARE. Le sien laissait 1,3 retour
     visible sur un board de 189, et cela ne s'ameliorait jamais en defilant —
     *on cadre, PUIS on lit.* Meme seuil, meme raison, y compris en paysage
     (`max-height`). */
  position: sticky; top: 0; z-index: var(--fbx-z-sticky);
}
@media (max-width: 720px), (max-height: 600px) {
  .fbx-page-header { position: static; }
}
/* Bloc de marque — engendré par TPXShared.mountBrand(), jamais écrit en dur
   dans une page : huit copies du même <img>, c'est huit occasions de diverger. */
.fbx-brand { display: flex; align-items: center; gap: var(--fbx-space-10); flex-shrink: 0; }
.fbx-brand-glyph { width: 28px; height: 28px; display: block; border-radius: var(--fbx-radius-full); }
/* Modificateur de TAILLE, pas un second composant : le fichier PNG a un fond
   sombre CARRÉ, c'est le `border-radius: 50%` ci-dessus qui fait le rond — et il
   est posé partout, socle comme mails (`mail.js`, `border-radius:50%` sur le
   glyphe des digests). Un glyphe carré quelque part serait une divergence de
   marque, pas un choix. 64 px = la taille employée là où l'outil SE PRÉSENTE :
   la boîte de connexion, et la page d'accueil (P42). */
.fbx-brand-glyph--xl { width: 64px; height: 64px; }
/* Le titre EST le chemin. La page ne dit plus ce qu'elle est (« Corrections »,
   précédé d'une icône différente à chaque écran) mais où l'on se trouve. */
.fbx-path {
  font-size: 20px; font-weight: var(--fbx-weight-bold); margin: 0; color: var(--fbx-text);
  display: flex; align-items: center; gap: var(--fbx-space-6); letter-spacing: -.01em;
}
/* La barre oblique est une ponctuation, pas un mot : elle s'efface d'un cran
   pour que le nom de l'écran porte seul. */
.fbx-path-sep { color: var(--fbx-text-muted); font-weight: var(--fbx-weight-normal); }
.fbx-path-name { color: var(--fbx-text); }
/* ⚠️ UN SEGMENT DE CHEMIN PEUT ETRE UN LIEN (2026-08-28, P63). L'ecran d'un
   projet et celui d'une livraison sont des ECRANS DE DETAIL : le chemin y est la
   remontee (« / Projets / CUBE / Recette v3 »), et ses segments amont cliquent.
   Sans cette regle ils arrivaient soulignes et bleus au milieu d'un titre — le
   navigateur habille tout `<a>` qu'aucune regle ne reprend, et aucune ne le
   faisait ici puisque le chemin n'avait jamais porte de lien.
   ⚠️ LE SEGMENT COURANT RESTE UN `<span>` : un lien vers la page ou l'on se
   trouve ne dit rien — c'est deja la regle du glyphe de marque sur le tableau
   de bord (`data-path=""`, `<span>` inerte).
   ⚠️ `a.fbx-path-name` ET NON `.fbx-path a` : la specificite (0,1,1) doit
   battre la reprise de theme en fin de fichier, qui repose `.fbx-path-name` en
   couleur de texte. Une regle placee avant celle qu'elle annule perd. */
a.fbx-path-name { text-decoration: none; color: var(--fbx-text-muted); }
a.fbx-path-name:hover { color: var(--fbx-accent-fg); text-decoration: underline; }
/* Racine (`/admin/dashboard.html`) : le chemin n'a pas de nom, la barre reste
   seule et reprend donc le poids qu'elle cède ailleurs. */
.fbx-path--root .fbx-path-sep { color: var(--fbx-text); font-weight: var(--fbx-weight-bold); }

/* ⚠️ `flex-wrap` — SANS LUI, LE BLOC EST INDIVISIBLE ET DÉBORDE (2026-08-20).
   `.fbx-page-header` se replie déjà, donc ce bloc passe bien à la ligne ; mais
   son CONTENU, lui, restait sur une seule ligne. Mesuré à 375 px sur le tableau
   de bord : « N retours · Dernière synchro · Actualiser » demande 316 px pour
   287 disponibles, et le bouton d'actualisation sortait de l'écran — la seule
   action de l'en-tête, coupée. Le repli du parent ne suffit jamais : il place le
   bloc, il ne le découpe pas. */
.fbx-header-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--fbx-space-10); align-items: center; }
/* `.fbx-user-info` — RETIREE. L'identite vivait dans l'en-tete de chaque ecran ;
   elle est passee en pied de barre laterale (P44), puis dans les reglages. */

/* Le glyphe est le lien de retour (sauf à la racine, où c'est un <span>). */
.fbx-brand-link { display: flex; text-decoration: none; border-radius: var(--fbx-radius-full); }
a.fbx-brand-link { transition: opacity .15s; }
a.fbx-brand-link:hover { opacity: .72; }
a.fbx-brand-link:focus-visible { outline: 2px solid var(--fbx-accent-fg); outline-offset: 3px; }

/* ════════════════════════════════════════════════════════════════════════════
   Barre latérale de navigation (P44)

   UN SEUL MENU, visible en permanence. Il remplace la pastille déroulante que
   P41 avait posée le matin même (`.fbx-usermenu-btn`, `.fbx-menu*`, retirées) :
   sur un outil de huit écrans, la navigation doit être VISIBLE, pas
   découvrable — derrière un clic, un écran reste invisible à qui ne cherche pas.

   La barre est en `position: fixed` et le corps est calé par un `padding-left`
   sur `.fbx-body.has-sidebar`. **C'est délibéré** : la solution « propre »
   (une grille enveloppant tout le document) aurait demandé de réécrire la
   structure des huit pages, dont trois portent un bandeau collant. Ici, aucune
   page ne change de squelette, et `position: sticky` continue de fonctionner.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  --fbx-sidebar-w: 244px; --fbx-sidebar-rail: 64px;
  /* CIBLES TACTILES (2026-09-25, boards sur téléphone). Deux tailles, nommées
     par leur rôle : 44 px, le seuil de confort d'un doigt, pour tout contrôle ;
     48 px pour les gestes principaux d'un panneau (Envoyer, barre du bas d'une
     sélection). Elles s'appliquent sous `(hover: none)`, jamais sous une
     largeur : la question est le pointeur, pas l'écran.
     `--fbx-pastille-tactile` est le DESSIN d'une pastille de filtre en
     tactile : 36 px visibles, portés à 44 px de toucher par un `::before`. */
  /* LA LARGEUR DU GABARIT À DEUX DATES de l'intervalle, posée par
     `mountDateRange` sur son bouton (2026-09-25, même raison que `--fbx-dd-min`). */
  --fbx-daterange-min: auto;
  /* CE QU'UN ÉLÉMENT FLOTTANT DU TÉLÉPHONE RECOUVRE EN BAS DE PAGE (2026-09-25) :
     le bouton « Demande » ou la barre de lot. La page réserve cette hauteur
     pour que la dernière ligne ne reste pas dessous. */
  --fbx-reserve-flottant: 128px;
  --fbx-cible-tactile: 44px;
  --fbx-cible-tactile-lg: 48px;
  --fbx-pastille-tactile: 36px;
  /* Hauteur du bandeau du haut. PARTAGEE par la tete de la barre laterale et
     l'en-tete de page : c'est ce qui fait que leurs deux filets s'alignent. */
  --fbx-topbar-h: 64px;
  /* ⚠️ `--fbx-page-w` SUPPRIMEE le 2026-08-04 : il n'y a plus de plafond de
     page du tout. `.fbx-main` en etait le seul lecteur, et un jeton que plus
     personne ne lit est du code mort — on ne le garde pas « au cas ou ».
     Ce qui doit etre etroit se contraint AU NIVEAU DU BLOC : `.fbx-card--form`
     (760 px) pour un formulaire, `.fbx-cell-comment` (680 px) pour une mesure
     de lecture. Voir le pave de `.fbx-main`. */

  /* PLANCHER DE LA COLONNE DE COMMENTAIRE des boards (2026-08-20). C'est de lui
     que se DEDUISENT les deux seuils de repli en cartes, et non l'inverse : un
     tableau se replie quand sa colonne principale n'est plus lisible, pas a une
     largeur ronde choisie d'avance. 220 px = environ 30 caracteres par ligne,
     le minimum pour qu'un commentaire reste du texte et non une colonne de mots
     coupes. Le faire varier deplace les deux seuils : recalculer. */
  --fbx-board-comment-min: 220px;

  /* COLONNE EXTENSIBLE D'UNE TABLE — la part que prend la colonne qui absorbe
     la place restante. Une `<table>` repartit selon le CONTENU : un champ d'URL
     vide a la creation recevait le minimum pendant qu'un `<select>` aux
     libelles longs prenait 150 px, si bien que le champ le plus utile de la
     rangee etait le plus etroit.
     ⚠️ 100 % EST L'IDIOME, PAS UN REGLAGE : dans une table qui fait deja 100 %,
     une colonne a 100 % prend ce que les autres n'ont pas pris, sans jamais les
     ecraser sous leur contenu. Un pourcentage arbitraire (« 40 % ») se deregle
     des qu'on ajoute une colonne. */
  --fbx-col-grow-w: 100%;

  /* COLONNES D'UNE TABLE FIXE (`.fbx-table--fixe`) — une largeur par RÔLE de
     colonne, calée sur son contenu le plus long, rembourrage de cellule
     compris (2 × 14 px) :
     · date empilée « 18/09/2026 » sur deux lignes ;
     · gravité « Avertissement », la plus longue pastille ;
     · projet : un libellé courant, coupé au-delà ;
     · contexte : une route d'API en chasse fixe (« /api/feedback/avatar-config ») ;
     · actions : une icône de 28 px et l'intitulé « Détail ».
     ⚠️ Un libellé qui s'allonge demande de les recalculer. */
  --fbx-col-date-w: 124px;
  --fbx-col-gravite-w: 144px;
  --fbx-col-projet-w: 180px;
  --fbx-col-contexte-w: 240px;
  --fbx-col-actions-w: 88px;
  --fbx-table-fixe-min-w: 960px;

  /* CAPSULE D'ETAT — largeur commune aux trois etats d'une livraison.
     « Actif », « À venir » et « Fermé » ne font pas le meme nombre de
     caracteres : empilees dans un menu, leurs capsules formaient un escalier.
     Une largeur commune les aligne des DEUX cotes, et une colonne d'etats se
     lit alors d'un seul balayage vertical.
     ⚠️ CALEE SUR LE PLUS LONG (« À venir »), pas sur une valeur ronde : elle
     doit etre recalculee si un libelle change. */
  --fbx-etat-w: 68px;

  /* BLOC DE CODE — hauteur au-dela de laquelle il defile plutot que de pousser
     la page. Un snippet fait douze lignes, un YAML de projet en fait trente :
     sans plafond, la carte du YAML repousse tout le reste hors de l'ecran. */
  --fbx-code-max-h: 400px;

  /* FRISE D'ÉVÉNEMENTS — largeur d'un trait. Un instant sur une frise de sept
     jours ne mesure rien : le trait a une largeur fixe, assez pour se voir et
     se survoler, et des traits rapprochés se fondent en un bloc — ce qui est
     juste, c'est une rafale. */
  --fbx-frise-trait: 3px;

  /* CHAMP COULEUR — le nuancier natif et la valeur en clair. Le nuancier prend
     la hauteur d'un champ (34 px) ; le champ hexadecimal, la largeur de sept
     caracteres et d'une marge, pas davantage. */
  --fbx-color-swatch-w: 48px;
  /* L'AVATAR DE LA LIGNE COMPACTE (2026-09-25, boards sur téléphone) : 40 px,
     la colonne de gauche d'une ligne façon messagerie. */
  --fbx-board-avatar-compact: 40px;
  /* LE PANNEAU QUI MONTE DU BAS (2026-09-25) : il laisse voir le haut de la
     page, pour qu'on sache toujours sur quoi il agit. */
  --fbx-sheet-max-h: 85vh;
  --fbx-poignee-w: 36px;
  --fbx-poignee-h: 4px;
  /* Les vignettes d'une ligne compacte dépliée : 80 × 60, le format 4:3 d'une
     capture d'écran réduite. */
  --fbx-board-vignette-w: 80px;
  --fbx-board-vignette-h: 60px;
  --fbx-color-swatch-h: 34px;
  --fbx-color-hex-w: 120px;

  /* HAUTEUR DE CONTENU D'UNE LIGNE DE BOARD AU REPOS (2026-08-21). Avec les
     16 px de rembourrage de `.fbx-grid-row`, c'est elle qui donne la ligne de
     58 px. Elle sert de PLANCHER a chaque cellule, ce qui a deux effets qu'on
     ne peut pas obtenir autrement :
       · toutes les cellules ont la meme hauteur au repos, donc leur contenu s'y
         centre reellement — sans plancher, une cellule courte se centre dans sa
         propre hauteur, c'est-a-dire nulle part ;
       · une ligne DEPLIEE ne fait plus bouger les cellules voisines : elles
         gardent leurs 42 px en haut, la seule cellule qui grandit est celle
         qu'on a ouverte.
     La faire varier change la hauteur de ligne, pas les seuils de repli. */
  --fbx-board-cell-h: 42px;

  /* Hauteur d'UNE ligne de commentaire (14 px x 1,5). Elle sert a centrer la
     PREMIERE ligne du texte dans le plancher ci-dessus, pour qu'elle tombe a la
     meme hauteur que la date et le nom — y compris quand le texte est deplie et
     que la cellule fait dix fois cette hauteur. Si la taille ou l'interligne du
     commentaire change, cette valeur change avec. */
  --fbx-board-ligne-txt: 21px;

  /* MESURE DE LECTURE DE LA CELLULE DE COMMENTAIRE (2026-09-24). Elle ne
     plafonnait que le texte du retour : le fil de reponses et le formulaire de
     reponse s'etalaient sur toute la cellule, soit pres de 1 900 px sur un tres
     grand ecran, boutons « Envoyer » compris (retour FEEDBACK 7fd730f1). Un seul
     jeton pour les trois : le fil s'aligne sous le texte qu'il prolonge. Pourquoi
     560 : voir le pave de `.fbx-cell-comment`. */
  --fbx-board-lecture-w: 560px;

  /* Epaisseur du filet d'accent du fil de reponses. Lue deux fois : par le fil,
     et par l'hote du formulaire, qui se decale d'autant pour que l'avatar de
     celui qui repond tombe dans la colonne des avatars du fil. */
  --fbx-fil-filet: 3px;

  /* ÉCART ENTRE LES COLONNES DU BOARD. Écrit en dur (14 px) jusqu'au
     2026-08-21 : il entre pourtant dans les DEUX seuils de repli, huit fois sur
     le board d'administration, et le lire à un seul endroit évite d'avoir à le
     retrouver dans deux gabarits pour le régler.
     12 px et non 14 : sur neuf colonnes, les deux pixels rendent 16 px au
     commentaire — c'est peu par colonne, c'est une ligne de plus à lire.
     ⚠️ LE CHANGER OBLIGE À REFAIRE LES DEUX SEUILS `@container`. */
  /* ⚠️ DEUX LARGEURS, ET NON PLUS UNE PARTAGEE (2026-08-31). Elles l'ont ete une
     demi-journee, au motif qu'elles portent « le meme objet, un jeton ». C'etait
     vrai de la FORME et faux de la MESURE : un jeton de piece est une icone
     seule (25 px), un marqueur de « Reponses » porte une icone ET un nombre
     (34 px). Les deux marqueurs — compteur et note — demandent donc 71 px la ou
     deux pieces en demandent 53.
     ⚠️ TROIS EMPILEMENTS SUCCESSIFS AVANT DE LE VOIR : 56 px, puis 64 quand le
     compteur a ete mesure, puis celui-ci quand la note a recu son nombre a son
     tour. *Une largeur qui se derive d'un contenu doit se recalculer quand le
     contenu change — et un jeton partage empechait justement de le faire pour
     une colonne sans le faire pour l'autre.*
     ⚠️ LE TOTAL NE BOUGE PAS : 72 + 56 = 128, exactement ce que valaient les
     deux pistes a 64. Le seuil de repli est inchange. */
  --fbx-board-reponses-w: 72px;
  --fbx-board-pieces-w: 56px;
  /* La colonne « Bloquant » (2026-09-21) : l'intitule en capitales de l'en-tete,
     pas la piste de l'interrupteur (40 px). ⚠️ LA CHANGER OBLIGE, COMME TOUTE
     LARGEUR DE COLONNE, A REFAIRE LES DEUX SEUILS `@container`. */
  --fbx-board-bloquant-w: 72px;
  /* La colonne « Réf. » (2026-09-29) : `FCU-2717`, huit caractères en chiffres
     tabulaires, et l'intitulé de l'en-tête. ⚠️ LA CHANGER OBLIGE A REFAIRE LES
     DEUX SEUILS `@container`, comme toute largeur de colonne. */
  --fbx-board-ref-w: 72px;
  --fbx-board-gap: 12px;

  /* Hauteur de la barre de navigation quand elle bascule A L'HORIZONTALE
     (mobile). 56 px est la convention des barres d'onglets mobiles, et elle
     donne des cibles de 44 px de haut — le seuil de confort tactile. */
  --fbx-navbar-h: 56px;

  /* LA PLACE QUE PREND LE WIDGET POSE SUR L'OUTIL (2026-09-11) — son rail
     vertical (48 px) et l'ecart qui le separe du bord droit (20 px). Ces deux
     valeurs appartiennent au widget, qui vit dans son propre univers de jetons
     (`--fb-*`) : le socle ne les lit pas, il en recopie la SOMME ici, nommee par
     ce qu'elle reserve. ⚠️ SI LE RAIL CHANGE DE LARGEUR OU DE MARGE
     (`feedback.css`, `#feedback-panel` et `.fbv2-rail`), CETTE VALEUR CHANGE
     AVEC — sinon la derniere colonne des boards repasse sous le rail. */
  --fbx-widget-outil-reserve: 68px;
}

.fbx-sidebar {
  position: fixed; inset: 0 auto 0 0; width: var(--fbx-sidebar-w); z-index: var(--fbx-z-sidebar);
  /* `transparent` et non une couleur : la barre laisse voir le canvas (2026-08-21).
     Peinte en surface, elle formait un bandeau blanc contre un fond blanc — le
     filet de droite portait a lui seul toute la separation. Sur le canvas, la
     hierarchie se lit sans filet : les cartes sont ce qui est POSE, la barre est
     le plan. ⚠️ Elle suit donc `--fbx-bg` : un canvas repeint la repeint. */
  /* On remappe LES JETONS, jamais les onze regles qui les lisent : survol,
     texte, titres de groupe, filet, pied de compte et icones suivent sans
     qu'aucune soit touchee. ⚠️ `--fbx-surface` n'est VOLONTAIREMENT pas
     remappe : la pastille de repli en mode rail s'en sert pour se detacher de
     la barre ET de la page (cf. son commentaire), elle doit rester claire.

     ⚠️ `--fbx-text-soft` MANQUAIT À CETTE LISTE, ET LE LIBELLÉ DE CHAQUE ENTRÉE
     LE LIT (DS-2a, 2026-08-27). Il gardait donc le gris de la PAGE sur le fond
     de la BARRE : #6b7280 sur #14181B, soit **3,69:1**, sous AA — pendant que
     l'icône juste à côté, elle remappée, tenait 6,99. *L'icône était plus
     lisible que le libellé qu'elle accompagne.* Le commentaire ci-dessus
     annonçait pourtant que « texte, titres de groupe, filet et icônes suivent » :
     il énumérait cinq jetons et le composant en lit six. */
  --fbx-bg:         var(--fbx-sidebar-hover);
  --fbx-text:       var(--fbx-sidebar-fg);
  --fbx-text-soft:  var(--fbx-sidebar-mute);
  --fbx-text-muted: var(--fbx-sidebar-mute);
  --fbx-border:     var(--fbx-sidebar-border);
  --fbx-accent-fg:  var(--fbx-sidebar-accent);
  background: var(--fbx-sidebar-bg); border-right: 1px solid var(--fbx-border);
  display: flex; flex-direction: column; overflow: hidden;
  transition: width .16s ease;
}
/* ⚠️ LA TRANSITION NE VAUT QUE POUR LE REPLI DU RAIL, PAS POUR LE PREMIER
   RENDU (2026-08-29). `has-sidebar` est posee PAR LE JS, apres l'affichage :
   la page naissait donc a `padding-left: 0`, et la transition animait
   0 -> 244 px — tout le contenu GLISSAIT de la gauche vers la droite a chaque
   chargement. L'effet etait un effet de bord, jamais une intention.
   La classe `is-nav-anime` est posee au coup d'apres par `mountSidebar` : le
   premier calage est instantane, les suivants — le repli, la bascule
   horizontale en etroit — gardent leur glissement, qui la est le sujet. */
.fbx-body.has-sidebar { padding-left: var(--fbx-sidebar-w); }
.fbx-body.is-nav-anime { transition: padding-left .16s ease; }

/* L'ENTREE DE PAGE — un fondu, pas un glissement.
   ⚠️ UNE ANIMATION, JAMAIS UNE TRANSITION DEPUIS `opacity: 0` : ecrite en
   transition, il faudrait poser l'opacite a zero dans la feuille, et une page
   dont le JS echoue resterait BLANCHE. Ici, sans classe, il n'y a ni animation
   ni opacite nulle — le contenu s'affiche, simplement sans fondu.
   ⚠️ AUCUN `animation-fill-mode` : hors de sa duree, l'element reprend son
   style normal. C'est la lecon du toast du widget, ou un `both` avait fige la
   premiere image sur un onglet d'arriere-plan.
   ⚠️ AUCUN DEPLACEMENT : un `translate` d'entree redonnerait le glissement
   qu'on vient de retirer, en plus court. */
@keyframes fbx-page-entree { from { opacity: 0; } }
.fbx-body.has-sidebar > .fbx-page-header,
.fbx-body.has-sidebar > .fbx-main { animation: fbx-page-entree .2s ease-out; }

/* ⚠️ ON N'ANIME PAS CE QUE LA PERSONNE A DEMANDE DE NE PAS ANIMER. */
@media (prefers-reduced-motion: reduce) {
  .fbx-body.is-nav-anime { transition: none; }
  .fbx-body.has-sidebar > .fbx-page-header,
  .fbx-body.has-sidebar > .fbx-main { animation: none; }
}
.fbx-body.has-sidebar.is-rail { padding-left: var(--fbx-sidebar-rail); }
.fbx-body.is-rail .fbx-sidebar { width: var(--fbx-sidebar-rail); }

/* Tête : la marque. Le glyphe reste, le mot disparaît en mode réduit — c'est
   le seul endroit de l'outil où le nom s'écrit en toutes lettres. */
.fbx-sidebar-head {
  display: flex; align-items: center; gap: var(--fbx-space-8); padding: 0 var(--fbx-space-12) 0 var(--fbx-space-16);
  border-bottom: 1px solid var(--fbx-border);
  height: var(--fbx-topbar-h); box-sizing: border-box; flex-shrink: 0;
}
/* `color` déclarée bien qu'AUCUN texte ne l'emploie aujourd'hui : le mot-marque
   vit dans `.fbx-sidebar-word`, qui porte la sienne. Sans elle, ce lien est à
   #0000EE — invisible tant qu'il ne contient qu'un glyphe et un `<span>` coloré,
   et bleu vif le jour où quelqu'un y ajoute un mot. Même famille que le lien
   d'encart ci-dessous : on ferme le piège, on ne corrige pas un symptôme. */
.fbx-sidebar-brand {
  display: flex; align-items: center; gap: 9px; text-decoration: none;
  color: var(--fbx-text); min-width: 0; flex: 1;
}
.fbx-sidebar-word {
  font-size: 15px; font-weight: var(--fbx-weight-bold); color: var(--fbx-text); letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden;
}
.fbx-sidebar-toggle {
  width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: 0; border-radius: var(--fbx-radius-sm);
  background: none; color: var(--fbx-text-muted); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.fbx-sidebar-toggle:hover { background: var(--fbx-bg); color: var(--fbx-text); }

/* ⚠️ `overflow-x: hidden` EST DÉCLARÉ, ET IL NE L'ÉTAIT PAS (2026-09-17, retour
   34997f40). `overflow-y: auto` seul fait passer l'AUTRE axe en `auto` lui aussi —
   c'est la règle de CSS, pas un choix du socle. Il suffisait donc qu'un élément
   dépasse d'un pixel pour qu'une barre de défilement horizontale naisse sous la
   colonne. Constaté en mode réduit : le widget pose son marqueur DANS l'élément
   commenté, et ce marqueur incliné à 45° débordait d'un pixel les 63 px du rail.
   Une colonne ne défile jamais en largeur. */
.fbx-sidebar-nav { flex: 1; overflow-y: auto; overflow-x: hidden; padding: var(--fbx-space-20) var(--fbx-space-10); }
/* En colonne, l'enveloppe des groupes secondaires n'existe PAS pour la mise en
   page : `display: contents` la fait disparaitre et ses groupes redeviennent
   enfants directs du menu. Elle ne prend corps que sous 720 px, ou elle devient
   le panneau de « Plus ». ⚠️ Le bouton, lui, n'a aucun sens ici : neuf entrees
   tiennent dans une colonne, et les cacher derriere un clic serait exactement la
   regression que P44 a corrigee en retirant le menu deroulant. */
.fbx-nav-secondaire { display: contents; }
.fbx-nav-plus { display: none; }
/* ⚠️ CETTE RÈGLE NE S'APPLIQUAIT PLUS À RIEN, ET RIEN NE LE DISAIT (2026-08-24).
   Elle vise deux groupes FRÈRES ; depuis que le second est enveloppé dans
   `.fbx-nav-secondaire` — l'enveloppe du panneau « Plus », posée le 21/08 pour
   le mobile —, ils ne le sont plus dans le DOM. `display: contents` les fait
   pourtant paraître voisins à l'écran : *la boîte est fusionnée, l'arbre non*,
   et un sélecteur de fratrie lit l'arbre. L'écart annoncé de 26 px valait donc
   ZÉRO, et « Corbeille » touchait le titre « Administration ».
   ⚠️ C'est la même famille de défaut que le commentaire CSS mal fermé du 20/08 :
   la règle est dans le fichier, elle ne s'applique pas, et aucun outil du projet
   ne peut le voir — seul le rendu le montre. Porté à 30 px au passage, l'écart
   demandé étant un peu plus large que l'ancien. */
.fbx-sidebar-group + .fbx-sidebar-group,
.fbx-nav-secondaire > .fbx-sidebar-group:first-child { margin-top: 30px; }
.fbx-sidebar-group-title {
  font-size: 10.5px; font-weight: var(--fbx-weight-bold); text-transform: uppercase; letter-spacing: .07em;
  color: var(--fbx-text-muted); padding: 0 var(--fbx-space-10) var(--fbx-space-10); white-space: nowrap;
}
.fbx-sidebar-link {
  display: flex; align-items: center; gap: 11px; padding: 9px var(--fbx-space-10); margin-bottom: var(--fbx-space-3);
  border-radius: var(--fbx-radius); font-size: 13.5px; font-weight: var(--fbx-weight-medium);
  color: var(--fbx-text-soft); text-decoration: none; cursor: pointer;
  white-space: nowrap; transition: background .12s, color .12s;
}
.fbx-sidebar-link svg { flex-shrink: 0; color: var(--fbx-text-muted); }
.fbx-sidebar-link:hover { background: var(--fbx-bg); color: var(--fbx-text); }
.fbx-sidebar-link:hover svg { color: var(--fbx-text); }
/* L'écran courant : surligné, et NON cliquable — il reste listé pour que la
   colonne garde la même longueur d'un écran à l'autre. */
.fbx-sidebar-link.is-current {
  background: rgba(var(--fbx-accent-rgb), .09); color: var(--fbx-accent-fg);
  font-weight: var(--fbx-weight-semibold); cursor: default;
}
.fbx-sidebar-link.is-current svg { color: var(--fbx-accent-fg); }
.fbx-sidebar-label { overflow: hidden; }

/* Pied : le compte. Il vit ICI et non dans l'en-tête — c'est de l'identité,
   pas une action de l'écran, et le bas de la colonne est là où on la cherche. */
.fbx-sidebar-foot { border-top: 1px solid var(--fbx-border); padding: var(--fbx-space-10); }
/* Le pied est un BOUTON : il ouvre le menu de compte. La deconnexion n'est plus
   offerte a demeure — une action irreversible n'a pas a se tenir sous le pouce. */
.fbx-sidebar-user {
  display: flex; align-items: center; gap: var(--fbx-space-10); width: 100%; min-width: 0;
  padding: var(--fbx-space-8) var(--fbx-space-10); border: 0; border-radius: var(--fbx-radius);
  background: none; font: inherit; text-align: left; cursor: pointer;
  transition: background .12s;
}
.fbx-sidebar-user:hover:not(:disabled) { background: var(--fbx-bg); }
.fbx-sidebar-user:disabled { cursor: default; }
.fbx-sidebar-user[aria-expanded="true"] { background: var(--fbx-bg); }
/* 36px et non 28 : a cette taille l'avatar redevient un VISAGE qu'on reconnait,
   ce qui est tout son interet — en dessous, c'est une pastille de couleur. */
.fbx-sidebar-user .fbx-avatar { width: 36px; height: 36px; border: 0; flex-shrink: 0; }
.fbx-sidebar-user-more { margin-left: auto; color: var(--fbx-text-muted); display: flex; }
.fbx-sidebar-user:disabled .fbx-sidebar-user-more { display: none; }
.fbx-body.is-rail .fbx-sidebar-user-more { display: none; }

/* Modale « Mes informations » : l'avatar et son bouton, cote a cote. */
.fbx-profil-avatar { display: flex; align-items: center; gap: var(--fbx-space-16); margin-bottom: var(--fbx-space-20); }
.fbx-profil-avatar .fbx-avatar { width: 72px; height: 72px; border: 0; }
.fbx-sidebar-user-id { min-width: 0; line-height: var(--fbx-leading-tight); overflow: hidden; }
.fbx-sidebar-user-id b {
  display: block; font-size: 12.5px; color: var(--fbx-text); font-weight: var(--fbx-weight-semibold);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fbx-sidebar-user-id span { font-size: var(--fbx-font-xs); color: var(--fbx-text-muted); }

/* ── Mode réduit ── Il ne cache pas la moitié du menu : il cache les MOTS.
   Toutes les entrées restent là, et leur mot revient au survol dans une BULLE
   (`.fbx-bulle`, ci-dessous) — sans quoi le repli échangerait de la place contre
   de la mémoire à fournir. Elles portaient un `title` jusqu'au 2026-09-17. */
.fbx-body.is-rail .fbx-sidebar-word,
.fbx-body.is-rail .fbx-sidebar-label,
.fbx-body.is-rail .fbx-sidebar-group-title,
.fbx-body.is-rail .fbx-sidebar-user-id { display: none; }
.fbx-body.is-rail .fbx-sidebar-head { justify-content: center; padding: 0; gap: 0; position: relative; overflow: visible; }
/* ⚠️ IL ÉTAIT `display: none` ICI — le seul contrôle capable de ROUVRIR la
   barre disparaissait avec elle. Le seul chemin de retour passait par
   Réglages → Préférences → décocher « garder le menu réduit » : un écran et
   trois clics pour annuler un clic, et rien à l'écran ne l'indiquait.
   ⚠️ RÈGLE : UNE COMMANDE NE SE CACHE PAS DANS L'ÉTAT QU'ELLE SERT À QUITTER.
   Le repli n'est pas un mode dont on sort par les réglages, c'est une bascule.
   Elle devient une pastille À CHEVAL sur le bord : rattachée à la barre — c'est
   elle qu'elle commande — tout en mordant sur la page, seul endroit où 28 px
   sont disponibles dans un rail qui en fait 56 et qui est déjà pris par le
   glyphe. Le fond blanc et le filet la détachent de la page comme de la barre. */
.fbx-body.is-rail .fbx-sidebar { overflow: visible; }
/* ⚠️ PLUS PETITE QU'EN MODE DÉPLIÉ (22 px contre 28) : en rail, la barre ne
   fait que 56 px, et une pastille de 28 y pesait presque autant que le glyphe
   de marque juste à côté. Une commande secondaire ne doit pas rivaliser avec
   ce qu'elle borde — c'est justement en mode réduit qu'elle doit se faire
   oublier jusqu'à ce qu'on la cherche. */
.fbx-body.is-rail .fbx-sidebar-toggle {
  position: absolute; top: 50%; right: -11px; transform: translateY(-50%); z-index: 2;
  width: 22px; height: 22px;
  /* ⚠️ Elle reprend les neutres de la PAGE : posee sur un fond clair, elle
     heriterait sinon du texte attenue CLAIR de la barre — un glyphe pale sur
     une pastille blanche. Elle est a cheval, ses couleurs le sont aussi.
     ⚠️ ET C'EST POURQUOI ELLE A QUATRE JETONS À ELLE (DS-2a). Elle vit dans
     `.fbx-sidebar`, qui remappe les rôles neutres vers ceux de la barre ; un
     bloc scopé NE PEUT PAS référencer le rôle qu'il écrase (`--fbx-text:
     var(--fbx-text)` ne veut rien dire), et référencer une primitive figerait
     le clair. `--fbx-rail-*` porte donc les valeurs des rôles de PAGE, et
     bascule avec le thème. Auparavant ce bloc lisait `--fbx-n6/n9/n1/n3`,
     c'est-à-dire l'échelle de neutres — la seule chose qui, à l'époque, était à
     la fois thémée et hors d'atteinte du remappage de la barre. */
  --fbx-text-muted: var(--fbx-rail-fg); --fbx-text: var(--fbx-rail-fg-strong);
  --fbx-bg: var(--fbx-rail-bg); --fbx-border: var(--fbx-rail-bd);
  background: var(--fbx-surface); border: 1px solid var(--fbx-border);
  border-radius: var(--fbx-radius-full); box-shadow: var(--fbx-shadow-card); color: var(--fbx-text-muted);
}
.fbx-body.is-rail .fbx-sidebar-toggle svg { width: 13px; height: 13px; }
.fbx-body.is-rail .fbx-sidebar-toggle:hover { background: var(--fbx-bg); color: var(--fbx-text); }
.fbx-body.is-rail .fbx-sidebar-brand { flex: none; justify-content: center; }
.fbx-body.is-rail .fbx-sidebar-nav { padding: var(--fbx-space-10) var(--fbx-space-6); }
.fbx-body.is-rail .fbx-sidebar-link { justify-content: center; padding: 9px 0; gap: 0; }
.fbx-body.is-rail .fbx-sidebar-group + .fbx-sidebar-group {
  margin-top: var(--fbx-space-10); padding-top: var(--fbx-space-10); border-top: 1px solid var(--fbx-border);
}
.fbx-body.is-rail .fbx-sidebar-user { justify-content: center; padding: var(--fbx-space-6) 0 var(--fbx-space-8); }
.fbx-body.is-rail .fbx-sidebar-foot { padding: var(--fbx-space-8) var(--fbx-space-6); }

/* ── LA BULLE — le mot d'une entrée, rendu au survol et au focus clavier
   (2026-09-17, retour 22b9c277). Posée par `TPXShared.brancherBulles`.
   ⚠️ C'EST LA BULLE DU WIDGET (`.fbv2-rail-bulle`), PORTÉE AU SOCLE : même
   forme, même mécanique. Les deux univers n'ont aucun fichier commun (règles P
   et Q) — on la reprend, on ne la partage pas.
   ⚠️ ELLE REMPLACE LE `title` NATIF, et c'est tout le sujet : celui-là, le
   NAVIGATEUR le pose — ni délai, ni position, ni largeur maîtrisés — et il
   paraissait avec une seconde de retard, sous le curseur, sur une colonne
   d'icônes qu'on parcourt justement pour lire les mots.
   ⚠️ ELLE VIT DANS `body`, JAMAIS DANS LA BARRE : la liste des entrées défile
   (`overflow-y: auto`), et une bulle rendue dedans serait coupée au bord du
   rail. `fixed`, avec des coordonnées posées en propriétés personnalisées au
   survol (règle 5 : une valeur calculée par JS n'est pas une propriété).
   ⚠️ `pointer-events: none` : posée à côté du curseur, elle ne doit pas
   recevoir le survol et faire clignoter ce qui l'a fait naître.
   ⚠️ UNE PLAQUE, INVARIANTE : du blanc sur un gris foncé dans les deux thèmes,
   comme le toast neutre. */
.fbx-bulle {
  position: fixed; left: var(--fbx-bulle-x, 0); top: var(--fbx-bulle-y, 0);
  z-index: var(--fbx-z-bulle);
  padding: var(--fbx-space-6) var(--fbx-space-10); border-radius: var(--fbx-radius-sm);
  background: var(--fbx-bulle-bg); color: var(--fbx-on-accent); box-shadow: var(--fbx-shadow-pop);
  font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-medium); line-height: var(--fbx-leading-none);
  white-space: nowrap; pointer-events: none;
}
/* ⚠️ `display` N'EST PAS POSÉ PAR LA CLASSE, mais la règle est écrite quand
   même : la bulle vit dans `body`, et la vitrine du design system ne porte pas
   `.fbx-body` — la règle globale `[hidden]` du socle ne l'y atteint pas. Même
   leçon que la pastille du rail du widget : poser l'attribut n'est pas le faire
   respecter. */
.fbx-bulle[hidden] { display: none; }
/* Sans survol, elle ne paraît pas : voir le bloc `(hover: none)` du socle, plus
   bas — il n'y en a qu'un, et c'est là qu'on cherche ce que le tactile change. */

/* ── SOUS 720 px, LA BARRE BASCULE À L'HORIZONTALE (2026-08-20) ──────────────
   Elle devenait un RAIL vertical de 64 px, imposé et non repliable. Le calcul
   qui le justifiait — « la colonne mangerait la moitié de l'écran » — s'arrêtait
   au milieu : sur 375 px, 64 px de rail, c'est encore **17 % de la largeur
   prélevés en permanence**, sur l'axe qui manque, pour une bande d'icônes que
   rien ne pouvait replier (le bouton de bascule y était `display: none`). Et
   c'est la largeur qui est rare sur un téléphone, pas la hauteur : un tableau
   perd sa colonne à 20 px près, une page défile.

   ⚠️ EN BAS, ET C'EST UN CHOIX MESURÉ, PAS UNE CONVENTION RECOPIÉE. Placée en
   haut, elle se serait ajoutée aux 356 px de bandeau DÉJÀ collants des boards,
   sur un écran où il ne reste que 488 px pour le contenu — on aurait corrigé la
   largeur en aggravant la hauteur. En bas, elle ne dispute rien au bandeau et
   tombe sous le pouce, là où les barres d'onglets mobiles se tiennent.

   ⚠️ LE MODE RÉDUIT CACHE LES MOTS, PAS LES ENTRÉES — cette règle-là vaut
   toujours, on la reconduit ici : les neuf destinations restent présentes, avec
   leur `title`, et la rangée DÉFILE plutôt que d'en escamoter. Une barre qui
   masque des destinations pour tenir échangerait de la place contre de la
   mémoire à fournir.

   ⚠️ LA MARQUE DISPARAÎT, LA DESTINATION NON. Le glyphe est le lien de retour au
   tableau de bord — mais celui-ci est déjà la PREMIÈRE entrée de la barre : on
   ne retire ici qu'un doublon, jamais un chemin.

   ⚠️ `env(safe-area-inset-bottom)` : sur un iPhone à barre d'accueil, une barre
   collée à `bottom: 0` passe SOUS elle et ses cibles deviennent inatteignables.

   ⚠️ SPÉCIFICITÉ : les sélecteurs portent `.fbx-body.has-sidebar` — TROIS
   classes, pour égaler `.fbx-body.is-rail .fbx-sidebar-…` et l'emporter par
   l'ordre du fichier. L'utilisateur peut avoir laissé le mode réduit actif, et
   ce choix ne doit pas repeindre la barre horizontale. Écrit d'abord avec un
   simple `.fbx-body` (deux classes), le bloc perdait en silence : la barre
   basculait bien à l'horizontale — ces règles-là étaient plus spécifiques —
   mais gardait le `padding: 10px 6px` du rail, ce qui la faisait monter à 98 px
   au lieu de 56. Un demi-basculement est plus trompeur qu'aucun. */
@media (max-width: 720px) {
  .fbx-body.has-sidebar,
  .fbx-body.has-sidebar.is-rail {
    padding-left: 0;
    padding-bottom: calc(var(--fbx-navbar-h) + env(safe-area-inset-bottom, 0px));
  }
  .fbx-body.has-sidebar .fbx-sidebar,
  .fbx-body.is-rail .fbx-sidebar {
    inset: auto 0 0 0; width: 100%; height: auto;
    flex-direction: row; align-items: stretch; overflow: visible;
    border-right: 0; border-top: 1px solid var(--fbx-border);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .fbx-body.has-sidebar .fbx-sidebar-head { display: none; }
  .fbx-body.has-sidebar .fbx-sidebar-toggle { display: none; }
  /* ⚠️ LE LIBELLE RESTE, ET C'EST TOUT LE POINT DU DISPOSITIF (2026-08-21) : cinq
     cellules dans 299 px laissent la place au mot sous l'icone, la ou neuf
     imposaient des glyphes muets. Seuls le mot de la marque, les titres de groupe
     et l'identifiant du compte restent masques. */
  .fbx-body.has-sidebar .fbx-sidebar-word,
  .fbx-body.has-sidebar .fbx-sidebar-group-title,
  .fbx-body.has-sidebar .fbx-sidebar-user-id { display: none; }

  .fbx-body.has-sidebar .fbx-sidebar-nav {
    flex: 1; min-width: 0; display: flex; align-items: stretch;
    /* ⚠️ PLUS DE `overflow-x: auto` : c'est LUI le defaut qu'on corrige. Il rendait
       quatre destinations atteignables sans que rien ne les annonce. */
    padding: 0;
  }
  /* `contents` et non `flex` : le groupe cesse d'etre une boite, ses liens
     deviennent enfants directs de la rangee et se partagent la largeur avec le
     bouton. En boite, les quatre liens se seraient serres dans une seule part. */
  .fbx-body.has-sidebar .fbx-sidebar-group { display: contents; }
  /* Les groupes secondaires quittent la rangee : ils forment le panneau de « Plus »,
     pose AU-DESSUS de la barre et non dedans — la barre garde sa hauteur, le panneau
     recouvre le contenu et se referme au premier clic ailleurs. Ferme, il ne prend
     pas de place ; ouvert, il en prend autant qu'il en faut. */
  .fbx-body.has-sidebar .fbx-nav-secondaire {
    display: none;
    position: absolute; bottom: 100%; left: 0; right: 0;
    background: var(--fbx-sidebar-bg); border-top: 1px solid var(--fbx-border);
    padding: var(--fbx-space-10) var(--fbx-space-8); max-height: 60vh; overflow-y: auto;
    box-shadow: var(--fbx-shadow-pop);
  }
  .fbx-body.has-sidebar .fbx-sidebar.is-plus-open .fbx-nav-secondaire,
  .fbx-body.is-rail .fbx-sidebar.is-plus-open .fbx-nav-secondaire { display: block; }
  /* Dans le panneau on retrouve la colonne : titre de groupe visible, mot A COTE
     de l'icone, pleine largeur. C'est la grammaire du desktop, rendue a l'endroit
     ou la place existe enfin. */
  /* ⚠️ ET L'ÉCART DES GROUPES NE VAUT PAS ICI : dans le panneau, l'enveloppe
     redevient une vraie boîte avec son propre rembourrage — la marge du premier
     groupe s'y ajouterait en pur vide sous le bord supérieur. Elle n'existe que
     pour séparer deux groupes qui se suivent dans la COLONNE. */
  .fbx-body.has-sidebar .fbx-nav-secondaire > .fbx-sidebar-group { margin-top: 0; }
  .fbx-body.has-sidebar .fbx-nav-secondaire .fbx-sidebar-group { display: block; }
  .fbx-body.has-sidebar .fbx-nav-secondaire .fbx-sidebar-group-title { display: block; }
  .fbx-body.has-sidebar .fbx-nav-secondaire .fbx-sidebar-link {
    flex-direction: row; justify-content: flex-start; gap: var(--fbx-space-10);
    height: auto; min-height: var(--fbx-cible-tactile); padding: 0 var(--fbx-space-10);
    border-radius: var(--fbx-radius); font-size: var(--fbx-font-md); text-align: left;
  }
  .fbx-body.has-sidebar .fbx-nav-secondaire .fbx-sidebar-label { font-size: var(--fbx-font-md); }
  .fbx-body.has-sidebar .fbx-nav-secondaire .fbx-sidebar-link.is-current {
    box-shadow: none; background: rgba(var(--fbx-accent-rgb), .12);
  }

  /* ⚠️ `flex: 1 1 0` sur CHAQUE cellule, bouton compris : sans lui, une entree au
     libelle long (« Tableau de bord ») prendrait plus de place que ses voisines et
     la rangee cesserait d'etre reguliere — or c'est sa regularite qui la rend
     lisible d'un coup d'oeil. */
  .fbx-body.has-sidebar .fbx-sidebar-group > .fbx-sidebar-link,
  .fbx-body.has-sidebar .fbx-nav-plus { flex: 1 1 0; min-width: 0; }
  /* Le bouton EST une cellule de la rangee : meme hauteur, meme empilement, meme
     mot dessous. Il se distingue par son glyphe, jamais par sa forme. */
  .fbx-body.has-sidebar .fbx-nav-plus {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--fbx-space-3); height: var(--fbx-navbar-h); padding: 0 var(--fbx-space-4);
    border: 0; background: none; cursor: pointer; font: inherit;
    font-size: var(--fbx-font-2xs); line-height: 1.1; color: var(--fbx-text-muted);
  }
  .fbx-body.has-sidebar .fbx-sidebar.is-plus-open .fbx-nav-plus { color: var(--fbx-text); }

  /* Le filet remplace le titre de groupe qu'on vient de masquer : sans lui,
     « Suivi » et « Administration » deviennent une seule file d'icônes. */
  .fbx-body.has-sidebar .fbx-sidebar-group + .fbx-sidebar-group {
    margin-top: 0; padding-top: 0; border-top: 0;
    margin-left: var(--fbx-space-6); padding-left: var(--fbx-space-6); border-left: 1px solid var(--fbx-border);
  }
  .fbx-body.has-sidebar .fbx-sidebar-link {
    flex-direction: column; justify-content: center; gap: var(--fbx-space-3); margin-bottom: 0;
    min-width: 0; height: var(--fbx-navbar-h); padding: 0 var(--fbx-space-4);
    border-radius: 0; font-size: var(--fbx-font-2xs); line-height: 1.15; text-align: center;
    /* ⚠️ `normal` ET NON `nowrap` : le lien herite du `nowrap` de la colonne, ou
       la place ne manque jamais. Dans une cellule de 60 px, « Tableau de bord »
       debordait alors SUR SES VOISINES — le mot passe a la ligne plutot que de
       sortir de sa case. La barre fait 56 px : icone 17 + deux lignes de 11,
       cela tient. */
    white-space: normal;
  }
  /* L'écran courant se marque par un FILET EN HAUT, pas par un fond arrondi :
     dans une rangée serrée, sept pastilles teintées se disputent l'œil, et le
     filet dit « la page sous cette barre, c'est celle-ci ». Même grammaire que
     l'onglet de `.fbx-tabs`, dont le filet rattache le panneau qu'il révèle. */
  .fbx-body.has-sidebar .fbx-sidebar-link.is-current {
    background: none; box-shadow: inset 0 2px 0 0 var(--fbx-accent-fg);
  }
  /* Le compte ne défile pas avec les destinations : il reste au bord, toujours
     à la même place — c'est de l'identité, pas une destination de plus. */
  .fbx-body.has-sidebar .fbx-sidebar-foot {
    border-top: 0; border-left: 1px solid var(--fbx-border);
    padding: 0; display: flex; align-items: center; flex-shrink: 0;
  }
  .fbx-body.has-sidebar .fbx-sidebar-user {
    height: var(--fbx-navbar-h); padding: 0 var(--fbx-space-12); border-radius: 0;
    justify-content: center;
  }
  .fbx-body.has-sidebar .fbx-sidebar-user-more { display: none; }

  /* ═══ BANDEAU DE PERIMETRE — une colonne, et chaque controle la remplit ═══
     Mesure a 375 px avant correction : dans un bandeau de 360 px utiles, le
     selecteur de projet faisait 236, l'intervalle de dates 222, et chacun tombait
     deja sur SA propre ligne — donc sans rien gagner a rester etroit. Trois
     rangees de largeurs differentes, alignees a gauche, dont aucune ne remplit :
     l'oeil ne sait plus si un controle est coupe ou simplement court.
     ⚠️ ON NE FIXE PAS DE LARGEUR, on laisse le flux la donner (`stretch`) : une
     valeur en dur redeviendrait fausse au premier controle ajoute. */
  .fbx-page-header {
    flex-direction: column; align-items: stretch; gap: var(--fbx-space-8);
    height: auto; padding-top: var(--fbx-space-10); padding-bottom: var(--fbx-space-10);
  }
  .fbx-page-header > * { width: 100%; max-width: 100%; }
  /* L'enveloppe du selecteur ET le bouton qu'elle contient : `enhanceSelect` pose
     un `min-width` EN LIGNE sur le bouton (la largeur de l'option la plus longue),
     qu'aucune regle ne peut battre. On ne cherche pas a le battre — on donne
     simplement au bouton une largeur SUPERIEURE, ce que `width: 100%` fait tant que
     la place existe ; `max-width` garde le cas contraire sans debordement. */
  .fbx-select-icon, .fbx-select-icon > .fbx-select,
  .fbx-select, .fbx-daterange, .fbx-input--search, .fbx-search {
    width: 100%; max-width: 100%;
  }
  /* Les marqueurs tiennent sur UNE ligne : combien · depuis quand · recharger.
     ⚠️ `nowrap` SEUL n'aurait pas suffi — c'est le mot « Actualiser » qui prenait
     la place, et il s'efface juste en dessous. Le compteur peut se serrer, la
     fraicheur non : c'est une date, tronquee elle ne veut plus rien dire. */
  .fbx-header-actions {
    margin-left: 0; flex-wrap: nowrap; gap: var(--fbx-space-8); min-width: 0;
  }
  .fbx-header-actions .fbx-filter-count { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .fbx-header-actions .fbx-sync-info { flex-shrink: 0; }
  .fbx-header-actions .fbx-btn { flex-shrink: 0; margin-left: auto; }
  /* Le glyphe reste, le mot part. Le bouton devient carre — meme cible tactile,
     largeur divisee par trois. */
  .fbx-btn-mot { display: none; }
  .fbx-header-actions .fbx-btn { padding: 0; width: 32px; height: 32px; justify-content: center; }

  /* La barre de filtres suit la meme regle que le bandeau : un controle par
     ligne, sur toute la largeur. */
  .fbx-filter-bar { flex-direction: column; align-items: stretch; gap: var(--fbx-space-8); }
  .fbx-filter-bar > * { width: 100%; max-width: 100%; }
}

/* Largeur de page : AUCUNE — le plafond a été retiré le 2026-08-04.
   La contrainte vit AU NIVEAU DU BLOC (`.fbx-card--form` 760 px,
   `.fbx-cell-comment` 680 px), jamais de la page. Voir `.fbx-main` plus bas. */

/* ⚠️ LA GOUTTIERE ENTRE DEUX BLOCS NE DEPASSE PLUS LE REMBOURRAGE DE LA PAGE
   (2026-09-01). Elle valait 32 px quand `.fbx-main` en pose 24 : l'INTERIEUR de
   la page respirait plus que son EXTERIEUR, si bien que deux cartes paraissaient
   moins liees entre elles qu'au bord de l'ecran. Constate a l'oeil sur le
   tableau de bord du testeur, verifie sur les autres ecrans.
   ⚠️ ELLE SE LIT AVEC `.fbx-main > .fbx-card + .fbx-card` (16 px) : les deux
   marges se COLLAPSENT, donc c'est le plus grand des deux qui vaut. Abaisser
   celle-ci a 24 rend la gouttiere egale au rembourrage, ce qui est la seule
   valeur qui ne se remarque pas.
   ⚠️ PORTEE : tous les ecrans d'administration. C'est voulu — le rapport entre
   dedans et dehors est une regle de page, pas un reglage d'ecran. */
.fbx-section { margin-bottom: var(--fbx-space-24); }
/* Le titre d'une section respire sous lui — 16 px, PARTOUT (retour d'usage du
   2026-09-03). Il en avait 4, au motif qu'un sous-titre suivrait juste dessous ;
   aucun composant de sous-titre n'existe, et deux regles reprenaient deja 16 px
   la ou le titre enchainait sur son contenu (enfant direct d'une carte, colonne
   d'une livraison). Les titres des sections des reglages, DANS une section et
   non enfants directs de la carte, gardaient leurs 4 px et collaient a leur
   premier champ — le meme titre, deux espaces, a un ecran d'ecart. Une seule
   regle desormais ; les deux surcharges sont parties avec leur motif. */
.fbx-section-title { font-size: 15px; font-weight: var(--fbx-weight-bold); margin: 0 0 var(--fbx-space-16); display: flex; align-items: center; gap: var(--fbx-space-8); }
/* L'ACTION D'UNE SECTION se pose sur son titre, poussée à droite. Un écran
   entièrement consacré à un objet met son « + Ajouter » dans l'en-tête de page
   (`setup`, `admin-users`) ; une SECTION parmi d'autres ne le peut pas — le
   dashboard n'est pas l'écran des testeurs, il en a une section. L'action va
   donc là où vit son objet, et le titre la porte. */
.fbx-section-title > .fbx-btn { margin-left: auto; }

/* LE BLOC REPLIABLE (2026-10-02) — une section qu'on ne lit pas d'office.
   Premier emploi : « Erreurs de la page » dans « Détail du retour », un contenu
   technique, long, utile une fois sur dix.

   ⚠️ UN `<details>` NATIF, ET C'EST TOUT LE COMPOSANT : clavier, lecteurs
   d'écran et état ouvert/fermé viennent du navigateur, sans une ligne de JS.
   Le titre RÉEMPLOIE `.fbx-section-title` — `<summary class="fbx-section-title">` —
   pour avoir la forme exacte des sections voisines ; le compte, `.fbx-count`.
   ⚠️ CE N'EST PAS LE « GROUPE DE PAGE REPLIABLE » RETIRÉ PAR P23 : celui-là
   découpait une LISTE, ce que les filtres font mieux ; celui-ci tient un
   contenu SECONDAIRE à l'écart de la lecture.
   ⚠️ LE CHEVRON EST UN MASQUE (`--fbx-chevron-masque`, le tracé du menu
   déroulant), coloré par `background-color` : il suit le thème. Tourné vers la
   droite replié, vers le bas ouvert.
   ⚠️ REPLIÉ, LE TITRE PERD SA MARGE BASSE : sans cela, le bloc fermé laisse un
   vide de 16 px sous une ligne qui n'a rien à séparer. */
.fbx-repli { margin: 0 0 var(--fbx-space-16); }
.fbx-repli > summary { cursor: pointer; list-style: none; user-select: none; }
.fbx-repli > summary::-webkit-details-marker { display: none; }
.fbx-repli > summary::before {
  content: ''; flex: 0 0 auto;
  width: var(--fbx-dd-chevron-w); height: var(--fbx-dd-chevron-h);
  background-color: var(--fbx-text-muted);
  -webkit-mask: var(--fbx-chevron-masque) no-repeat center / var(--fbx-dd-chevron-w) var(--fbx-dd-chevron-h);
          mask: var(--fbx-chevron-masque) no-repeat center / var(--fbx-dd-chevron-w) var(--fbx-dd-chevron-h);
  transform: rotate(-90deg); transition: transform .15s;
}
.fbx-repli[open] > summary::before { transform: none; }
.fbx-repli:not([open]) > summary { margin-bottom: 0; }
.fbx-repli > summary:focus-visible { outline: 2px solid var(--fbx-accent-fg); outline-offset: 3px; border-radius: var(--fbx-radius-xs); }
.fbx-repli > :last-child > :last-child { margin-bottom: 0; }
@media (prefers-reduced-motion: reduce) { .fbx-repli > summary::before { transition: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
   ONGLETS DE PAGE — un commutateur entre deux contenus d'un même écran.
   Monté au socle le 2026-08-04 pour `/corbeille`, dont les deux populations
   étaient EMPILÉES : *Disparus du classeur* (une anomalie à traiter) et
   *Écartés à l'ingestion* (un refus à corriger dans le classeur) ne disent pas
   la même chose, et l'une sous l'autre on ne savait plus laquelle on lisait.

   ⚠️ CE N'EST PAS `.fbx-reglages-tab`, ET C'EST ARBITRÉ, PAS SUBI.
   Celui-là est un RAIL : largeur pleine, aligné à gauche, mené par une icône,
   état courant par fond teinté — exactement la grammaire de `.fbx-sidebar-link`,
   parce qu'il navigue entre des DESTINATIONS dans une modale. Celui-ci est un
   COMMUTATEUR posé dans le flux, dont l'état courant est un filet **attaché au
   panneau qu'il révèle** : c'est le filet qui dit « ce qui suit dépend de moi ».
   Les fondre sous un nom aurait demandé un modificateur réécrivant la largeur,
   l'alignement, le rayon ET tout le traitement de l'état courant — soit deux
   rendus sous un seul nom, ce que la règle interdit. Arbitrage inscrit en §9.8
   de `doc/PLAN-mutualisation-front.md`, avec sa condition de réouverture.

   ⚠️ LE COMPTE VIT SUR L'ONGLET, et nulle part ailleurs. Sans lui on clique
   pour découvrir un tableau vide. Le marqueur du bandeau garde le PÉRIMÈTRE
   (« 4 lignes »), l'onglet porte la répartition : chaque nombre est écrit une
   fois. Deux endroits qui comptent la même chose divergeront. */
/* ⚠️ LES ONGLETS DÉFILENT, ILS NE SE REPLIENT PAS (2026-08-20). Mesuré sur
   `/corbeille` à 390 px : deux onglets et leurs comptes demandent 374 px pour
   278 disponibles, et ils poussaient la PAGE de 72 px. Un `flex-wrap` les
   mettrait sur deux lignes, mais le filet d'état courant — celui qui rattache
   l'onglet au panneau qu'il révèle — ne se lirait plus : il désignerait un
   panneau situé sous une AUTRE rangée d'onglets. Le défilement conserve la
   rangée unique, donc le sens du filet ; `flex-shrink: 0` empêche les onglets
   de se comprimer au point de tronquer leur libellé, et le compte reste sur son
   onglet (règle : chaque nombre est écrit une fois). */
/* ⚠️ LE FILET DE LA RANGÉE EST UNE OMBRE INTÉRIEURE, PLUS UNE BORDURE
   (2026-09-18, retour `ff149d3c`). L'onglet descendait d'un pixel sous la
   rangée (`margin-bottom: -1px`) pour que son filet recouvre la bordure ; or
   `overflow-x: auto` fait passer l'AUTRE axe en `auto` lui aussi, et ce pixel
   de trop suffisait à poser un ascenseur vertical dans la barre — mesuré 41
   contre 40 px sur « Journal » et « Corbeille ». L'ombre intérieure est peinte
   SOUS les onglets : le filet de l'onglet courant la recouvre sans rien
   déborder. `overflow-y: hidden` en plus, pour qu'un élément posé dans la
   barre (le marqueur du widget, incliné à 45°) n'en repose pas un — même
   défaut que `.fbx-sidebar-nav`, retour `34997f40`. */
.fbx-tabs {
  display: flex; align-items: stretch; gap: var(--fbx-space-4);
  box-shadow: inset 0 -1px 0 var(--fbx-border); margin-bottom: var(--fbx-space-20);
  overflow-x: auto; overflow-y: hidden; scrollbar-width: thin;
}
.fbx-tabs > .fbx-tab { flex: 0 0 auto; }
.fbx-tab {
  display: inline-flex; align-items: center; gap: var(--fbx-space-8);
  padding: var(--fbx-space-10) var(--fbx-space-14); border: 0; background: none; font: inherit;
  font-size: var(--fbx-font-md); font-weight: var(--fbx-weight-semibold); color: var(--fbx-text-soft);
  cursor: pointer; white-space: nowrap;
  /* Le filet est DÉCLARÉ dès le repos, en transparent : sans lui, l'onglet
     courant serait 2px plus haut que les autres et la rangée sauterait au
     changement. Même raison que la bordure transparente de `.fbx-status-select`.
     ⚠️ Plus de `margin-bottom: -1px` : c'est lui qui débordait (cf. `.fbx-tabs`). */
  border-bottom: 2px solid transparent;
  transition: color .12s, border-color .12s;
}
.fbx-tab:hover { color: var(--fbx-text); }
.fbx-tab.is-current { color: var(--fbx-accent-fg); border-bottom-color: var(--fbx-accent-fg); }
/* Le compte suit la couleur de son onglet : sur l'onglet courant il fait partie
   du titre, sur les autres il reste une indication. */
.fbx-tab.is-current .fbx-count {
  background: rgba(var(--fbx-accent-rgb), .12); color: var(--fbx-accent-fg);
}
/* Panneau : caché par attribut, pas par classe — `hidden` est ce que le HTML
   prévoit pour ça, et il reste vrai si la feuille ne charge pas. */
.fbx-tab-panel[hidden] { display: none; }
.fbx-hint { font-size: var(--fbx-font-sm); color: var(--fbx-text-muted); margin-bottom: var(--fbx-space-12); }

/* Séparateur — deux groupes de champs dans une même carte. Un filet, pas une
   marge : la marge dit « c'est plus loin », le filet dit « c'est autre chose ». */
.fbx-separator { height: 1px; background: var(--fbx-border); margin: var(--fbx-space-20) 0; border: 0; }

/* Encart d'explication en tête de page (filet marque à gauche).

   ⚠️ ANGLES DROITS, DÉLIBÉRÉMENT — arbitré le 2026-08-05. Le rayon a été retiré
   des TROIS états (neutre, avertissement, erreur) parce que, dans cette
   interface, **le rayon annonce qu'une chose se manipule** : boutons, contrôles
   de filtre, cartes cliquables, tags. Un bandeau d'information ne se clique pas ;
   lui donner la même forme qu'un bouton est une promesse d'interaction que rien
   ne tient. Le filet à gauche suffit à le détacher du fond, et l'angle vif le
   distingue au premier coup d'œil de ce qui est actionnable.
   ⚠️ Ne pas le réintroduire « pour l'harmonie » : l'harmonie recherchée ici
   n'est pas que tout se ressemble, c'est que la forme dise la fonction. */
.fbx-callout {
  background: var(--fbx-surface); border: 1px solid var(--fbx-border);
  border-left: 3px solid var(--fbx-accent); border-radius: 0;
  padding: var(--fbx-space-14) 18px; margin-bottom: var(--fbx-space-20); font-size: var(--fbx-font-base); line-height: 1.6; color: var(--fbx-text-muted);
}
.fbx-callout strong { color: var(--fbx-text); }
/* ⚠️ UN LIEN SANS RÈGLE EST BLEU #0000EE — celui du navigateur, pas le nôtre.
   « Voir l'état du service », dans l'encart de sauvegarde, n'était habillé par
   RIEN : en clair, ce bleu sur un fond crème passe pour un choix, si bien que
   personne ne l'a relevé ; en sombre, c'est un bleu foncé sur une plaque foncée.
   Encore une ABSENCE, comme les champs de la boîte de connexion — la famille de
   défauts que ce chantier aura le plus révélée, et la seule qu'aucun balayage de
   couleurs ne peut trouver : il n'y a pas de valeur à chercher.
   `inherit` plutôt qu'un jeton : l'encart porte DÉJÀ la couleur juste pour son
   fond (ambre sur avertissement, rouge sur erreur, neutre sinon) — un jeton fixe
   redeviendrait faux à la première variante ajoutée. Le soulignement porte alors
   seul le fait que c'est un lien. */
.fbx-callout a { color: inherit; text-decoration: underline; }
/* Même encart, mais il annonce un ÉCHEC (« chargement impossible ») : un message
   d'erreur qui reste à l'écran n'est pas un toast, il ne doit pas s'effacer. */
.fbx-callout--error {
  background: var(--fbx-danger-bg); border-color: var(--fbx-danger-bd); border-left-color: var(--fbx-danger);
  color: var(--fbx-danger-fg);
}
/* Même encart pour un AVERTISSEMENT : la lecture a réussi mais elle ne rend
   rien d'exploitable (« aucune donnée sur la période »). Ce n'est pas un échec,
   et le rouge le ferait lire comme tel.
   Ajouté le 2026-08-04 sur un défaut RÉEL : le dashboard écrivait sa mise en
   garde dans un `.badge-warning` qui n'était déclaré NULLE PART — un texte nu,
   sans encadré ni couleur, à l'endroit précis où la ligne d'à côté rendait un
   `.fbx-callout--error` en bonne et due forme. Une classe sans règle ne casse
   rien de visible au chargement : elle se contente de ne pas dire ce qu'on
   croyait avoir dit. */
.fbx-callout--warning {
  background: var(--fbx-warn-bg-soft); border-color: var(--fbx-warn-bd); border-left-color: var(--fbx-warn);
  color: var(--fbx-warn-fg);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Boutons — une base, des variantes. Remplace btn-export / btn-copy /
   btn-save / btn-back, qui ne différaient que par la couleur.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-btn {
  /* ⚠️ 7px ET NON 8 : LA HAUTEUR DE CONTRÔLE DU SOCLE EST 33px, ET LE BOUTON
     ÉTAIT LE SEUL À NE PAS LA RESPECTER. Mesuré au navigateur le 2026-08-24 :
     champ 32,9 · intervalle de dates 33 (déclarée en dur) · bouton **35**.
     L'écart de 2 px est invisible tant qu'un bouton est seul — il saute aux
     yeux dès qu'il se pose À CÔTÉ d'un champ, ce qui n'arrivait presque jamais
     avant la rangée « libellé + Émettre ». *Un défaut ancien qu'un emploi
     nouveau révèle : ce n'est pas la rangée qui était fausse.*
     ⚠️ RÈGLE : un bouton posé à côté d'un champ partage sa hauteur. Elle ne se
     tient pas par une variante — elle se tient parce que les deux familles
     valent 33. */
  padding: 7px var(--fbx-space-16); border-radius: var(--fbx-radius); font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-semibold);
  cursor: pointer; border: 1px solid transparent; transition: all .15s;
  text-decoration: none; display: inline-flex; align-items: center; gap: var(--fbx-space-6);
  background: var(--fbx-surface); color: var(--fbx-text-soft); font-family: inherit;
}
/* Fond = la PLAQUE, pas l'accent-texte : en sombre, `--fbx-primary` s'éclaircit
   pour rester lisible SUR la surface, et du blanc posé dessus tombe à 1,81:1. */
.fbx-btn--primary { background: var(--fbx-accent-fill); color: var(--fbx-on-accent); border-color: transparent; }
.fbx-btn--primary:hover { background: var(--fbx-accent-fill-hover); }
/* Variante d'appel : dégradé, réservée à l'action principale d'un écran. */
/* ⚠️ LE SENS DU DÉGRADÉ EST CELUI-CI, ET IL ÉTAIT INVERSÉ (DS-1). La règle
   écrivait `--fbx-primary` puis `--fbx-primary-light`, soit la teinte SOMBRE vers
   la teinte CLAIRE, pendant qu'une règle de la palette repassait derrière avec
   l'ordre inverse. Ce n'est pas un remappage de jeton : c'est le sens de la
   lumière, et il ne se lit nulle part ailleurs que dans cette ligne. */
.fbx-btn--gradient {
  background: linear-gradient(135deg, var(--fbx-accent) 0%, var(--fbx-accent-fg) 100%);
  color: var(--fbx-on-accent); border: none;
}
.fbx-btn--gradient:hover { opacity: .9; box-shadow: 0 2px 12px var(--fbx-focus); }
.fbx-btn--outline { background: var(--fbx-surface); color: var(--fbx-text-muted); border-color: var(--fbx-border); }
.fbx-btn--outline:hover { border-color: var(--fbx-border-strong); color: var(--fbx-text); }
/* FANTOME — ni fond ni filet. Pour une action de SERVICE, qui accompagne une
   information plutot qu'elle ne conclut un geste : « Actualiser » a cote de
   « Derniere synchro ». En bouton plein, elle pesait autant que l'export. */
.fbx-btn--ghost {
  background: none; border-color: transparent; color: var(--fbx-text-muted);
  padding: var(--fbx-space-6) var(--fbx-space-10); font-size: var(--fbx-font-sm);
}
.fbx-btn--ghost:hover { background: var(--fbx-bg); color: var(--fbx-text); }
.fbx-btn--danger { background: var(--fbx-danger); color: var(--fbx-on-accent); }
.fbx-btn--danger:hover { filter: brightness(.93); }
/* Destructif DISCRET — le cran qui manquait (2026-08-06). `--ghost` seul ne
   dit pas le danger, `--danger` seul crie : six aplats rouges pleins alignés
   dans une grille de cartes, pour une action qu'on exerce trois fois par an.
   La table d'admins de ce même écran le disait déjà mieux avec une corbeille
   discrète ; il manquait l'équivalent pour un bouton PORTANT UN LIBELLÉ, parce
   qu'ici « Révoquer l'accès » ne se devine pas d'une icône.
   Combiné à `--ghost`, il n'en change que la couleur : c'est bien une teinte
   posée sur une forme existante, pas une deuxième grammaire de bouton. */
.fbx-btn--ghost.fbx-btn--danger { background: none; color: var(--fbx-danger-fg); }
.fbx-btn--ghost.fbx-btn--danger:hover { background: rgba(var(--fbx-danger-rgb), .08); color: var(--fbx-danger-fg); filter: none; }
.fbx-btn--sm { padding: var(--fbx-space-6) var(--fbx-space-12); font-size: var(--fbx-font-sm); border-radius: var(--fbx-radius-sm); }
/* Pleine largeur : un bouton qui conclut un bloc étroit (repli legacy, pied de
   formulaire en colonne). `justify-content` parce que `.fbx-btn` est un flex —
   sans lui, le libellé resterait collé à gauche une fois le bouton étiré. */
.fbx-btn--block { width: 100%; justify-content: center; }
.fbx-btn:disabled, .fbx-btn[disabled] {
  background: var(--fbx-fill-strong); color: var(--fbx-text-muted); border-color: transparent;
  cursor: not-allowed; opacity: 1; box-shadow: none; filter: none;
}

/* `.fbx-btn-back` — RETIRÉE en P41. C'était un bouton de retour au traitement
   propre (lien discret, posé à gauche du titre), et c'est précisément ce qui
   faisait cohabiter deux conventions : cinq écrans le posaient à gauche, trois
   mettaient le même retour à droite en `.fbx-btn--outline`. Un retour est une
   action comme les autres — il vit dans `.fbx-header-actions`, avec elles, et
   n'a donc plus besoin d'une classe à lui. */

/* ═══════════════════════════════════════════════════════════════════════════
   Barre de filtres
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-filter-bar {
  padding: var(--fbx-space-12) 0; display: flex; gap: var(--fbx-space-10); flex-wrap: wrap; align-items: center;
}
.fbx-filter-bar label { font-size: var(--fbx-font-base); color: var(--fbx-text-soft); font-weight: var(--fbx-weight-medium); }
.fbx-filter-count { font-size: var(--fbx-font-sm); color: var(--fbx-text-muted); margin-left: auto; }
/* Indicateur d'etat, en bout de bandeau : depuis quand la donnee est la, et
   combien il y en a. Il vivait en local sur le dashboard (`.sync-info`) ; les
   boards portent le meme, donc il monte au socle. */
.fbx-sync-info { font-size: var(--fbx-font-sm); color: var(--fbx-text-muted); white-space: nowrap; }
/* Point médian entre les deux informations : elles se suivent et se lisent
   d'un trait, le séparateur dit qu'il y en a deux sans les éloigner. Posé en
   `::before` de la SECONDE et conditionné à la présence de la première (`+`) :
   un séparateur écrit dans l'un des deux textes appartiendrait à l'un d'eux,
   et resterait à l'écran quand l'autre est vide. */
.fbx-filter-count + .fbx-sync-info::before {
  content: '·'; margin-right: var(--fbx-space-6); opacity: .55;
}

/* `.fbx-filter-search` — RETIREE : elle n'a servi que le temps ou la recherche
   fermait la barre de filtres. Cette barre a disparu, la recherche vit dans le
   bandeau avec sa loupe (`.fbx-search`). */
/* Les <select> passent par .fbx-select (feedback-shared.css) et sont rehaussés
   en dropdown maison par TPXShared.enhanceAll(). Ne pas styler <select> ici. */

/* ═══════════════════════════════════════════════════════════════════════════
   Statuts — DEUX composants distincts, longtemps confondus sous trois noms
   (`status-chip`, `status-tag`, `kanban-status-tag`) :
     .fbx-status-badge : AFFICHAGE, non cliquable — fond teinté, angles 6px ;
     .fbx-status-chip  : FILTRE cliquable, sélectionnable — pilule, point coloré.
   Les couleurs viennent de `--st-*` (miroir de TPXShared.STATUSES).
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-status-badge {
  display: inline-block; font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold);
  padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-sm);
  background: var(--fbx-fill); color: var(--fbx-text-soft);
}
/* ⚠️ LE TEXTE LIT `--st-*-fg`, PAS UNE COULEUR D'ÉTAT GÉNÉRIQUE (DS-1). Ces six
   jetons sont calculés POUR CE BADGE, chacun avec son contraste mesuré contre
   SON fond teinté — et le socle ne les consommait nulle part : il lisait
   `--fbx-warning`, `--fbx-info`, `--fbx-danger`, `--fbx-success`, `--fbx-n8` et la
   pastille `--st-cancelled`, pendant qu'une règle de `feedback-palette.css`
   repassait derrière pour poser les bons. Le rendu ne bouge pas d'un pixel : ce
   sont les mêmes valeurs, lues au bon endroit et par une seule règle.
   ⚠️ La PASTILLE (`--st-*`) et le TEXTE (`--st-*-fg`) ne sont pas le même rôle :
   un objet graphique se juge à 3:1, un libellé à 4,5:1, et le contraste étant
   symétrique, la même mesure sert les deux verdicts. Ne pas les confondre. */
.fbx-status-badge[data-status="active"]         { background: var(--st-active-bg); color: var(--st-active-fg); }
.fbx-status-badge[data-status="tocheck"]        { background: var(--st-tocheck-bg); color: var(--st-tocheck-fg); }
.fbx-status-badge[data-status="needs-revision"] { background: var(--st-needs-revision-bg); color: var(--st-needs-revision-fg); }
.fbx-status-badge[data-status="pending"]        { background: var(--st-pending-bg); color: var(--st-pending-fg); }
.fbx-status-badge[data-status="resolved"]       { background: var(--st-resolved-bg); color: var(--st-resolved-fg); }
/* « Écarté » se lit sans avoir à distinguer deux gris — donc en niveaux de gris
   aussi. Le barré ne vaut PAS pour le badge de mail, qui l'annule (bloc de fin). */
.fbx-status-badge[data-status="cancelled"]      { background: var(--st-cancelled-bg); color: var(--st-cancelled-fg); text-decoration: line-through; }
.fbx-status-badge[data-status="deleted"]        { background: var(--fbx-fill); color: var(--fbx-text-soft); }

.fbx-status-chip {
  font: inherit; font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold); padding: var(--fbx-space-4) var(--fbx-space-10); border-radius: var(--fbx-radius-pill);
  border: 1px solid var(--fbx-border); background: var(--fbx-surface); color: var(--fbx-text-muted);
  cursor: pointer; display: inline-flex; align-items: center; gap: var(--fbx-space-6); transition: .15s;
}
/* ⚠️ L'ACCENT DU SURVOL EST `--ui-accent`, PAS `--fbx-primary` (DS-1). Les deux ne
   valent pas la même chose : `--fbx-primary` est remappé sur `--ui-accent-text`
   (#0F6E96, la teinte qui porte du texte), quand une BORDURE prend la teinte de
   marque (#2BA8DD). Écrire l'un pour l'autre assombrit le contour d'un cran, et
   c'est ce que la règle venue de la palette corrigeait derrière celle-ci. */
.fbx-status-chip:hover { border-color: var(--fbx-accent); }
.fbx-status-chip.is-on { border-color: var(--fbx-accent); background: var(--fbx-accent-bg); color: var(--fbx-accent-fg); }
.fbx-status-chip .fbx-dot { width: 8px; height: 8px; border-radius: var(--fbx-radius-full); display: inline-block; }
/* LE POINT D'UNE PASTILLE PREND SA COULEUR PAR `data-status` (2026-09-25),
   comme le badge : une valeur de statut se dit par attribut. Les pastilles des
   pages la posent encore en `style=` ; le panneau « Répondre » ne le fait pas.
   ⚠️ UN STATUT AJOUTÉ DEMAIN S'AJOUTE ICI AUSSI (liste des sites, CLAUDE.md). */
.fbx-dot[data-status="active"]         { background: var(--st-active); }
.fbx-dot[data-status="tocheck"]        { background: var(--st-tocheck); }
.fbx-dot[data-status="needs-revision"] { background: var(--st-needs-revision); }
.fbx-dot[data-status="pending"]        { background: var(--st-pending); }
.fbx-dot[data-status="resolved"]       { background: var(--st-resolved); }
.fbx-dot[data-status="cancelled"]      { background: var(--st-cancelled); }
/* UNE RANGÉE DE PASTILLES QUI DÉFILE (2026-09-25) au lieu de passer à la ligne :
   au téléphone, sept pastilles sur trois rangées prenaient 110 px de hauteur. */
/* ⚠️ `.fbx-chips.fbx-chips--defile` : `.fbx-chips` est déclarée plus bas dans ce
   fichier, et à spécificité égale elle rendait le retour à la ligne. */
.fbx-chips.fbx-chips--defile { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.fbx-chips--defile::-webkit-scrollbar { display: none; }
.fbx-chips--defile > * { flex-shrink: 0; }

/* Conteneur de chips. La rangée de filtres par statut d'un board. */
.fbx-chips { display: flex; flex-wrap: wrap; gap: var(--fbx-space-8); align-items: center; }
/* Compteur DANS un chip — le manque relevé en P37 §1.5 : le socle avait repris
   le chip de `review` mais pas son compteur `.n`, donc aucune page ne pouvait
   l'employer sans le redéclarer. Pilule dans la pilule, en gris neutre : la
   couleur du chip dit déjà le statut, la répéter n'ajoute rien. */
.fbx-status-chip .fbx-chip-count {
  background: var(--fbx-fill); border-radius: var(--fbx-radius-pill); padding: 0 var(--fbx-space-6); font-size: var(--fbx-font-xs); color: var(--fbx-text-soft);
}

/* Étiquette d'identité — le nom du projet à côté d'un titre d'écran. Ni un
   statut (pas d'état), ni un compteur (pas un nombre) : un nom qu'on marque. */
/* Badge de CONTEXTE — le nom du projet à côté d'un titre de page. Il s'appelait
   `.fbx-tag`, ce qui était un faux ami : le seul écran qui l'employait (`review`)
   n'y met pas un tag mais une pastille de contexte, et le vrai tag de
   catégorisation vivait pendant ce temps en local dans `feedbacks`. Renommé le
   2026-08-03 (P39) pour libérer le nom au profit du composant qui le mérite. */
.fbx-badge {
  display: inline-block; font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold); padding: var(--fbx-space-3) var(--fbx-space-10);
  border-radius: 12px; background: rgba(var(--fbx-accent-rgb), .1); color: var(--fbx-accent-fg);
}

/* Badge de RÔLE et de méthode de connexion — ce qu'une personne EST dans
   l'outil, et par où elle y entre. Monté au socle le 2026-08-04 ; `admin-users`
   le portait sous six classes composées (`.badge-superadmin`, `-admin`,
   `-testeur`, `-local`, `-google`, `-both`).

   ⚠️ CE N'EST PAS UN STATUT DE RETOUR. Il ne suit pas `--st-*`, il n'est pas
   filtrable, et le ranger sous `data-status` violerait la règle des « quatre
   objets de statut, quatre noms » — `.fbx-status-badge` les rendrait tous gris.
   Il se dit par `data-role`, comme un statut se dit par `data-status` : une
   VALEUR va dans un attribut, un état d'interface dans une classe `is-*`.

   Rôle et méthode d'auth partagent l'attribut à dessein : le jeu de valeurs est
   disjoint (superadmin/admin/testeur contre local/google/both), et deux
   attributs pour un même gabarit auraient doublé les six règles sans rien
   distinguer à l'œil. Une valeur inconnue retombe sur le gris du gabarit plutôt
   que de ne rien rendre.

   Plus arrondi que `.fbx-status-badge` (rayon 10 contre 6), et c'est voulu : un
   statut change à la demande, un rôle est un attribut de la personne. */
.fbx-badge--role {
  display: inline-block; padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-lg);
  font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold); background: var(--fbx-fill); color: var(--fbx-text);
}
.fbx-badge--role[data-role="superadmin"] { background: var(--fbx-warn-bg); color: var(--fbx-warn-fg); }
.fbx-badge--role[data-role="admin"]      { background: var(--fbx-info-bg); color: var(--fbx-info-fg); }
.fbx-badge--role[data-role="testeur"]    { background: var(--fbx-ok-bg); color: var(--fbx-ok-fg); }
.fbx-badge--role[data-role="local"]      { background: var(--fbx-fill); color: var(--fbx-text); }
.fbx-badge--role[data-role="google"]     { background: var(--fbx-ok-bg); color: var(--fbx-ok-fg); }
.fbx-badge--role[data-role="both"]       { background: var(--fbx-info-bg); color: var(--fbx-info-fg); }
/* NIVEAU de gamification (classement du dashboard) — troisième jeu de valeurs,
   ajouté le 2026-08-04 même. `.leaderboard-level` déclarait le MÊME corps de
   règle, au caractère près, trois cents lignes plus bas et le jour même de la
   création de ce composant : je regardais `admin-users`, d'où venait la
   demande, pas `dashboard`. ⚠️ Les trois teintes s'y disaient par des classes
   COMPOSÉES (`.eclaireur`, `.faucon`, `.aigle`) — une valeur dans un nom de
   classe, introuvable au `grep` et invisible du compteur d'usage.
   Rôle, méthode de connexion et niveau tiennent sous le même attribut parce
   que les trois répondent à la même question : **ce qu'une personne EST dans
   l'outil**. Les jeux de valeurs restent disjoints, et le gabarit est un. */
.fbx-badge--role[data-role="eclaireur"]  { background: var(--fbx-fill-strong); color: var(--fbx-text-soft); }
.fbx-badge--role[data-role="faucon"]     { background: rgba(var(--fbx-accent-rgb), .1); color: var(--fbx-accent-fg); }
.fbx-badge--role[data-role="aigle"]      { background: var(--fbx-warn-bg); color: var(--fbx-warn-fg); }
/* ═══════════════════════════════════════════════════════════════════════════
   STATUT D'UNE LIVRAISON — le cinquieme objet de statut, et il ne suit pas les
   quatre autres (2026-08-28, P63).

   ⚠️ IL NE SE DIT PAS PAR `data-status` MAIS PAR `data-statut`, et ce n'est pas
   une coquetterie de langue : `data-status` porte les etats d'un RETOUR, dont
   la liste est tenue par `TPXShared.STATUSES` et controlee dans la vitrine par
   la regle M de `ds-check`. Y ranger « ouverte » ferait echouer ce controle —
   a juste titre : ce n'est pas un statut de retour, et un `.fbx-status-badge`
   portant « ouverte » serait sorti gris sans que rien ne le signale.

   ⚠️ UN SEUL DES TROIS ETATS PORTE UNE COULEUR, et c'est arbitre
   (`doc/decisions.md`, 2026-08-28) : « ouverte — elle recoit » est le seul qui
   appelle une decision, les trois autres sont du contexte. Elle prend l'ACCENT
   de l'outil, jamais le vert ni l'orange : dans cet outil le vert dit deja
   « resolu » et l'orange « actif » — deux objets sous la meme couleur, c'est le
   defaut corrige le matin meme sur le donut.

   ⚠️ AUCUN JETON NEUF. L'accent existe et bascule deja dans les deux themes
   (`--fbx-accent-bg` / `--fbx-accent-fg`, `feedback-palette.css`) ; en declarer
   un cinquieme de meme valeur aurait ete le doublon que `--fbx-on-accent-fill`
   a deja evite une fois.

   ⚠️ « A VENIR » GARDE UN LISERE : elle ne recoit pas ENCORE, ce qui n'est pas
   la meme chose que ne plus rien attendre — un aplat mort l'aurait rangee avec
   les recettes closes. Le lisere est un `box-shadow: inset` et non une
   `border` : sur un badge de cette taille, une bordure decalerait le texte d'un
   pixel par rapport a ses voisins, et la rangee sauterait.
   ⚠️ CE PAVE DECRIVAIT « EN CLOTURE » ET « QUATRE ETATS » (corrige le
   2026-09-01) — un statut retire le 2026-08-28, dont il expliquait encore le
   traitement. *Un commentaire qui survit a son objet se lit comme une regle en
   vigueur*, et celui-ci envoyait chercher un quatrieme etat qui n'existe plus. */
.fbx-livr-statut {
  display: inline-block; font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold);
  text-align: center;
  padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-sm);
  white-space: nowrap;
  background: var(--fbx-fill); color: var(--fbx-text-soft);
}
.fbx-livr-statut[data-statut="actif"]    { background: var(--fbx-accent-bg); color: var(--fbx-accent-fg); }
.fbx-livr-statut[data-statut="a_venir"] { background: var(--fbx-surface); color: var(--fbx-text-soft); box-shadow: inset 0 0 0 1px var(--fbx-border-strong); }
/* Passe : le meme gabarit, un cran plus efface. Elle n'appelle rien. */
.fbx-livr-statut[data-statut="ferme"]     { color: var(--fbx-text-muted); }

/* GRAVITÉ D'UNE ENTRÉE DU JOURNAL (2026-09-18, retour `962d5637`) — erreur en
   rouge, avertissement en orange. Rendue par `TPXShared.graviteBadge()`, jamais
   écrite dans une page.
   ⚠️ UN ATTRIBUT À ELLE, `data-gravite` : `.fbx-status-badge` porte les états
   d'un RETOUR, `.fbx-badge--role` ce qu'une PERSONNE est, `.fbx-livr-statut`
   l'état d'une LIVRAISON. Rangé sous l'un d'eux, un niveau de journal retombait
   au gris du gabarit — c'est pourquoi l'écran l'écrivait en texte en attendant
   ce composant.
   ⚠️ LE ROUGE ET L'ORANGE SONT CEUX DES ENCARTS (`--fbx-danger-*`,
   `--fbx-warn-*`), pas ceux des statuts : l'orange d'un statut dit « actif »,
   celui-ci dit « à surveiller ». Aucun jeton neuf, et les deux basculent déjà
   en thème sombre. Même gabarit que les autres pastilles d'état. */
.fbx-gravite-badge {
  display: inline-block; font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold);
  padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-sm);
  white-space: nowrap;
  background: var(--fbx-fill); color: var(--fbx-text-soft);
}
.fbx-gravite-badge[data-gravite="error"] { background: var(--fbx-danger-bg); color: var(--fbx-danger-fg); }
.fbx-gravite-badge[data-gravite="warn"]  { background: var(--fbx-warn-bg); color: var(--fbx-warn-fg); }
.fbx-gravite-badge[data-gravite="info"]  { background: var(--fbx-info-bg); color: var(--fbx-info-fg); }

/* ═══════════════════════════════════════════════════════════════════════════
   CARTE DE LIVRAISON — ce qui RECOIT des retours (2026-09-01).

   ⚠️ POURQUOI UNE CARTE ALORS QUE L'OUTIL LISTE PARTOUT. Le tableau de bord du
   testeur porte DEUX populations qui n'appellent pas le meme geste : ce qui
   recoit se travaille, ce qui est ferme se consulte. Une seule liste les mettait
   au meme rang — une recette close depuis six semaines pesait autant que celle
   ou l'on attend un retour aujourd'hui, et c'est precisement la distinction que
   l'ecran doit rendre immediate. La carte porte l'une, la liste garde l'autre.

   ⚠️ CE N'EST PAS L'ENTORSE A « le tableau principal d'un ecran ne va pas dans
   une carte ». Cette regle interdit d'envelopper l'unique objet d'un ecran dans
   un cadre qui ne separe rien. Ici les cartes ne CONTIENNENT aucun tableau :
   elles sont une section parmi deux, et la liste des livraisons fermees reste
   posee a plat dans son `.fbx-table-wrap`.

   ⚠️ LA CARTE EST LE LIEN — pas un bouton pose dedans. C'est la regle des
   boards (« la rangee est le lien, pas un libelle cliquable dedans ») : un
   bouton qui mene ou la carte mene deja est un second controle pour un seul
   geste, et il fait croire a une action differente. `.fbx-livr-card-aller` est
   donc une MENTION, sans bordure ni fond : elle dit ou l'on va, elle ne se
   clique pas a part.

   ⚠️ UN SEUL ETAT SE DISTINGUE, ET C'EST LE MEME ARBITRAGE QUE LA CAPSULE :
   `actif` prend l'accent, en lisere. Les autres gardent la bordure ordinaire —
   colorer chaque etat rendrait la grille bariolee et ferait perdre le seul
   signal qu'on cherche : laquelle recoit.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ⚠️ DEUX COLONNES, ET C'EST UN NOMBRE, PAS UN `auto-fit` (2026-09-01).
   La première version laissait la grille se remplir toute seule
   (`repeat(auto-fit, minmax(290px, 1fr))`) : sur un écran large elle montait a
   QUATRE colonnes de ~330 px, et la rangée de faits — echeance, vos retours,
   testeurs — n'y tenait plus. Un contenu qui deborde ne se regle pas en
   abaissant son plancher : on fixe le nombre de colonnes que la carte sait
   remplir. Sous 720 px, une seule. */
.fbx-livr-deck {
  display: grid; gap: var(--fbx-space-12);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 720px) {
  .fbx-livr-deck { grid-template-columns: minmax(0, 1fr); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   LES COLONNES D'UNE LIVRAISON — ce qu'elle couvre, ses documents, ses tickets.
   ═══════════════════════════════════════════════════════════════════════════
   Le bandeau du tableau de bord du testeur posait le perimetre sur toute la
   largeur d'une carte de ~1150 px, plafonne a 70 caracteres par ligne : le
   texte occupait un tiers de la place et le reste etait du blanc. Elargir la
   mesure de lecture aurait degrade ce qu'on voulait ameliorer (60-80
   caracteres est la plage confortable, illisible au-dela de ~90, arbitrage du
   04/08). On occupe donc le blanc avec autre chose plutot que d'etirer la
   ligne.

   ⚠️ LE NOMBRE DE COLONNES SUIT LE CONTENU, PAS LA LARGEUR. Une livraison sans
   document n'en montre pas la colonne, un projet qui ne suit pas de tickets non
   plus, et les colonnes restantes se repartissent la place. Une colonne vide
   avec son intitule dirait « il manque quelque chose ici » ; il ne manque rien.

   ⚠️ `auto-fit` ICI, ALORS QUE `.fbx-livr-deck` LE REJETTE EXPLICITEMENT douze
   lignes plus haut — et ce n'est pas un oubli. La-bas les enfants sont NOMBREUX
   (une carte par livraison) et la grille montait a quatre colonnes de 330 px ou
   la rangee de faits ne tenait plus. Ici ils sont TROIS AU PLUS, et ce sont eux
   qu'on veut voir decider : `auto-fit` effondre les pistes vides, donc trois
   enfants donnent trois colonnes, un seul en donne une, pleine largeur.

   ⚠️ `min(260px, 100%)` ET NON `260px` SEC : sous 260 px de large, une piste au
   plancher fixe deborde son conteneur — la carte pousserait la page au lieu de
   se replier. Le `min()` rend le plancher inoperant quand il n'y a plus la
   place, ce qui est exactement le cas ou l'on veut une seule colonne.

   ⚠️ `minmax(0, …)` DU COTE HAUT : sans le zero, un jeton de ticket long ou une
   URL de document impose sa largeur a la piste. Meme raison que `.fbx-colonnes`.

   ⚠️ `align-items: start` : sans lui, une colonne courte s'etire a la hauteur de
   la plus longue et son fond (s'il en prend un un jour) descendrait dans le
   vide. */
/* ⚠️ DES POIDS, PAS DES COLONNES ÉGALES (corrigé le 2026-09-02). La première
   version donnait trois pistes de meme largeur (`1fr`) a trois contenus qui
   n'ont rien de comparable : mesure sur la recette VILLE-2.0, un perimetre de
   dix lignes serre dans 440 px, UN SEUL bouton de document qui en mobilisait
   460, et quatorze jetons de ticket dans 470. *Une grille egale suppose des
   contenus egaux, et il n'y a aucune raison qu'ils le soient.*

   ⚠️ FLEX ET NON GRID, ET C'EST CE QUI CHANGE. `auto-fit` sait faire
   disparaitre une piste vide — ce qu'on voulait — mais ne sait pas donner plus
   de place a une colonne qu'a une autre quand leur NOMBRE varie : un
   `grid-template-columns` fixe suppose de savoir lesquelles sont la. En flex,
   chaque colonne porte son propre poids et une colonne absente ne laisse rien
   derriere elle.

   ⚠️ `min-width: 0` SUR CHAQUE COLONNE : sans lui, un jeton de ticket long
   (« XCU-4176 · Mise en place API Adresse pour autocompletion / preformatage »,
   ~380 px mesures) impose sa largeur a sa colonne et pousse les autres. Le
   defaut par defaut de flex est `min-width: auto`, soit la taille du contenu. */
.fbx-livr-cols {
  display: flex;
  flex-wrap: wrap;
  gap: var(--fbx-space-24);
  align-items: flex-start;
  margin-top: var(--fbx-space-20);
}
.fbx-livr-cols > * { flex: 1 1 260px; min-width: 0; }
/* Du TEXTE suivi : il lui faut de quoi tenir une ligne de lecture. */
.fbx-livr-cols > .fbx-livr-col--large { flex: 2 1 340px; }
/* Une poignee de liens courts : elle prend ce qu'elle occupe, pas un tiers de
   la carte. C'est la colonne qui paraissait vide, et son contenu n'y etait
   pour rien.
   ⚠️ 280px EST UN POINT DE DEPART, PAS UN PLAFOND (2026-09-14, corrige le
   2026-09-15). La premiere version fixait 280px ET min-width:0 (herite de la
   regle generale ci-dessus) en se calant sur le document le plus long connu
   alors (269px) — un document RENOMME LE LENDEMAIN (« Ce que le site
   declenche automatiquement, et quand », 366px mesures) a suffi a faire
   deborder le bouton hors de la page : la colonne restait a 280px, son
   contenu en nowrap continuait bien au-dela. *Un plafond cale sur le contenu
   du jour casse des que le contenu change — et un intitule de document est
   une donnee saisie par un administrateur, pas une constante.*
   ⚠️ `min-width: auto` ET NON 0 : seule colonne de la rangee a NE PAS heriter
   du 0 general. `auto` redonne au flex sa taille minimale automatique (le
   min-content du bouton le plus large) : la ligne se redistribue — la colonne
   « Ce que cette livraison couvre » cede la place, ou Documents repasse EN
   DESSOUS d'elle si la fenetre est trop etroite (`.fbx-livr-cols` est deja en
   `flex-wrap: wrap`) — plutot que de laisser un bouton sortir du cadre. */
.fbx-livr-cols > .fbx-livr-col--etroite { flex: 0 1 280px; min-width: auto; }
/* Les boutons-documents ne replient jamais leur texte : avec le min-width
   restaure ci-dessus, un libelle plus long agrandit sa colonne (ou renvoie
   Documents sur sa propre ligne) au lieu de se lire sur deux lignes OU de
   deborder. Seul emploi actuel d'un `.fbx-btn` dans un `.fbx-chips`. */
.fbx-livr-cols .fbx-chips .fbx-btn { white-space: nowrap; }
.fbx-livr-card {
  display: grid; gap: var(--fbx-space-12); min-width: 0;
  padding: var(--fbx-space-16);
  background: var(--fbx-surface);
  border: 1px solid var(--fbx-border-strong); border-radius: var(--fbx-radius-lg);
  color: var(--fbx-text); text-decoration: none;
}
/* ⚠️ AUCUNE COULEUR SUR LA BORDURE (2026-09-01). La carte active portait un
   lisere d'accent a gauche et une bordure teintee. Les deux n'ont plus d'objet
   depuis que **le bloc ne contient QUE des livraisons actives** : distinguer
   l'active de ses voisines, quand toutes le sont, ne distingue rien — et un
   lisere sur un seul cote se lit comme une bordure de travers.
   La capsule « Actif » dit l'etat ; la bordure ordinaire suffit au cadre. */
.fbx-livr-card:hover { background: var(--fbx-row-hover); }
/* ⚠️ CENTRE, ET NON `flex-start` (2026-09-01). La vignette du projet fait 30 px,
   le nom une ligne de 18 : alignes par le haut, le texte flottait au-dessus du
   carre et la tete de carte paraissait de travers. Le carre est le repere de la
   ligne — c'est sur lui que le nom se cale. */
.fbx-livr-card-tete {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--fbx-space-10);
}
/* Les faits de la livraison, en rangee qui va a la ligne. ⚠️ INTITULE AU-DESSUS
   DE LA VALEUR, comme l'en-tete de la liste des livraisons : deux grammaires
   pour les memes faits obligeraient a les relire d'un bloc a l'autre. */
/* LE DESCRIPTIF DE LA LIVRAISON — ce sur quoi porte la recette.
   ⚠️ DEUX LIGNES AU PLUS, ET C'EST UN APERCU, PAS UN RESUME : le texte entier
   vit sur l'ecran de la recette, ou il est rendu avec sa mise en forme. Ici on
   en donne le debut pour que la carte dise DE QUOI il s'agit, sans devenir un
   pave qui deplacerait tout ce qui suit.
   ⚠️ EN TEXTE BRUT, PAS EN HTML : le perimetre est riche (listes, titres), et
   un `-webkit-line-clamp` sur des blocs imbriques coupe n'importe ou. Le texte
   est depouille par l'ecran avant d'arriver ici. */
.fbx-livr-card-desc {
  color: var(--fbx-text-muted); font-size: var(--fbx-font-sm);
  line-height: var(--fbx-leading-normal);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.fbx-livr-faits { display: flex; flex-wrap: wrap; gap: var(--fbx-space-20); }
/* ⚠️ AUCUN `letter-spacing` ICI, ET C'EST LE CLIQUET QUI A TRANCHE. La gamme
   `interlettre` n'existe pas au socle — DS-2b a refuse d'en nommer une (« huit
   valeurs, deux unites melees, seize emplois : il n'y a rien a nommer, et en
   inventer une reviendrait a decider a l'oeil sans regarder »), si bien que
   TOUT litteral d'interlettrage est compte en dette. En ajouter un dix-septieme
   pour un intitule de 10 px aurait fait monter un cliquet contre une
   refinement typographique. Les capitales tiennent sans. */
.fbx-livr-fait-k {
  display: block; font-size: var(--fbx-font-2xs);
  text-transform: uppercase; color: var(--fbx-text-muted);
}
.fbx-livr-fait-v { font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-semibold); }
/* ⚠️ L'ETAT APPARTIENT AU COMPOSANT (2026-09-01). L'ecran posait une classe
   `.tdb-urgent` QUI N'EXISTAIT NULLE PART : l'echeance proche n'etait donc pas
   signalee du tout — une classe sans regle ne casse rien de visible, elle se
   contente de ne pas dire ce qu'on croyait avoir dit. Un etat d'interface se dit
   par `is-*`, et il vit la ou vit le composant. */
.fbx-livr-fait-v.is-urgent { color: var(--st-needs-revision-fg, var(--fbx-text)); }
.fbx-livr-card-pied {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--fbx-space-12);
}
.fbx-livr-card-aller {
  display: inline-flex; align-items: center; gap: var(--fbx-space-4);
  font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold);
  color: var(--fbx-accent-fg);
}
/* ⚠️ LE DEPLACEMENT EST SUR LA MENTION, PAS SUR LA CARTE : une carte qui bouge
   au survol deplace ses voisines du regard. Le chevron suffit a dire que ca
   mene quelque part. */
.fbx-livr-card:hover .fbx-livr-card-aller { transform: translateX(var(--fbx-space-2)); }
@media (prefers-reduced-motion: reduce) {
  .fbx-livr-card:hover .fbx-livr-card-aller { transform: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   TAG de catégorisation — l'étiquette qu'un admin pose sur un retour.
   Monté au socle le 2026-08-03 avec P39 : il vivait dans le `<style>` de
   `feedbacks.html` alors que DEUX phases à venir le réemploieront — le type de
   retour (P24) et les mentions (P32), qui écrivent au même endroit
   (`metadata.tags`). Un composant que trois écrans vont poser n'appartient pas
   à l'un d'eux.

   Angles à 4px et non arrondis : un tag n'est pas une pastille de statut. Les
   `.fbx-status-chip` sont pleinement arrondies parce qu'elles disent un ÉTAT,
   fermé et connu ; un tag est une étiquette libre, et la distinction se lit
   d'abord à la forme. Gris allégé pour la même raison — il annote, il ne classe
   pas, et il ne doit pas concurrencer le statut à côté.
   ═══════════════════════════════════════════════════════════════════════════ */
/* Rangée d'étiquettes qui va à la ligne. Sert aux tags de catégorisation, mais
   pas seulement : la cellule « Projets » d'`admin-users` y aligne des
   `.fbx-badge`. C'est une MISE EN PLACE, pas un type de contenu — un deuxième
   nom pour la même rangée n'aurait rien distingué à l'œil. */
.fbx-tags { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fbx-space-4); }
.fbx-tag {
  position: relative; display: inline-flex; align-items: center;
  padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-xs); font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold);
  cursor: pointer; user-select: none;
  background: var(--fbx-fill); color: var(--fbx-text-soft); border: 1px solid var(--fbx-border);
  transition: background .15s, border-color .15s;
}
.fbx-tag:hover { background: var(--fbx-fill); border-color: var(--fbx-border-strong); }
/* ⚠️ UN TAG QUI EST UN LIEN RESTE UN TAG (2026-08-11). `.fbx-tag` a été écrit
   pour un `<span>` ; posé sur un `<a>`, il héritait du soulignement et du bleu
   de lien de la page, et la rangée de projets de `/testeurs` ne ressemblait plus
   à des jetons mais à une phrase de liens. On lui rend son apparence et on lui
   donne un survol franc — ce qui indique qu'il mène quelque part, c'est le
   curseur et le contraste, pas un soulignement qui casse la forme. */
/* « BLOQUANT » EN LECTURE (2026-09-25) — le gabarit du tag, en plaque sombre.
   L'interrupteur `.fbx-switch` CHANGE la marque ; ce badge la DIT, là où rien
   ne se bascule : la ligne compacte d'un board au téléphone. Même partage que
   `.fbx-status-badge` (affiche) et `.fbx-status-select` (change).
   ⚠️ `--fbx-text` SUR `--fbx-surface` s'inverse avec le thème : plaque sombre
   en clair, plaque claire en sombre, le contraste tient dans les deux.
   Le rendre par `TPXShared.badgeBloquant()`, jamais à la main. */
.fbx-bloquant-badge {
  display: inline-flex; align-items: center; gap: var(--fbx-space-4); white-space: nowrap;
  padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-xs);
  font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold); line-height: var(--fbx-leading-tight);
  background: var(--fbx-text); color: var(--fbx-surface); border: 1px solid var(--fbx-text);
}
a.fbx-tag { text-decoration: none; color: var(--fbx-text-soft); }
a.fbx-tag:hover { background: var(--fbx-fill-strong); border-color: var(--fbx-faint); color: var(--fbx-text); }
/* La croix ne se montre qu'au survol du jeton : sur une ligne qui en porte
   quatre, quatre croix permanentes font quatre cibles de suppression avant
   même qu'on ait voulu en retirer une. */
.fbx-tag-x {
  position: absolute; top: -6px; right: -6px; width: 14px; height: 14px; display: none;
  background: var(--fbx-tag-x-bg); color: var(--fbx-on-accent); border-radius: var(--fbx-radius-full);
  font-size: var(--fbx-font-2xs); line-height: 14px; text-align: center; font-weight: var(--fbx-weight-bold);
}
.fbx-tag:hover .fbx-tag-x { display: block; }
/* Le déclencheur d'ajout : même gabarit que le tag, mais en pointillé — il
   annonce une place à remplir, pas une valeur posée. */
.fbx-tag-add {
  display: inline-flex; align-items: center; padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-xs);
  background: var(--fbx-surface); color: var(--fbx-text-soft); border: 1px dashed var(--fbx-border-strong); font-size: var(--fbx-font-xs); cursor: pointer;
}
.fbx-tag-add:hover { background: var(--fbx-fill); color: var(--fbx-text); }

/* CHAMP à tags — la saisie libre qui en fabrique. `.fbx-tags` est une rangée qui
   AFFICHE ; ceci est un champ de formulaire qui en PRODUIT, et il doit donc
   ressembler à un `.fbx-input` : même bordure, même rayon, même halo au focus.
   Monté au socle le 2026-08-04 — `setup` le portait sous `.tag-input-wrap` /
   `.tag-input`, avec des tags à lui (`.tag`, fond teinté marque) qui doublaient
   `.fbx-tag`. Trois champs de cet écran l'emploient (origines, onglets, labels
   Jira), et P24/P32 écriront au même endroit.

   `cursor: text` et le focus délégué à la saisie : la zone vide à droite des
   tags fait partie du champ, cliquer dedans doit poser le curseur — c'est ce
   qu'on attend d'une boîte qui a l'air d'un champ de texte. */
.fbx-tag-field {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--fbx-space-6);
  padding: var(--fbx-space-6) var(--fbx-space-8); min-height: 38px; cursor: text;
  border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius);
  transition: border-color .15s, box-shadow .15s;
}
.fbx-tag-field:focus-within {
  border-color: var(--fbx-accent-fg);
  box-shadow: 0 0 0 3px rgba(var(--fbx-accent-rgb), .12);
}
/* La saisie n'a NI bordure NI halo : c'est le champ qui les porte. Deux cadres
   emboîtés, c'est ce que donnait un `.fbx-input` posé là. */
.fbx-tag-input {
  flex: 1; min-width: 90px; padding: var(--fbx-space-2) 0;
  border: none; outline: none; background: none;
  font: inherit; font-size: var(--fbx-font-sm); color: var(--fbx-text);
}

/* Compteur neutre (nombre d'éléments d'une section). N'est PAS un statut. */
.fbx-count {
  display: inline-block; padding: var(--fbx-space-2) var(--fbx-space-8); border-radius: var(--fbx-radius-sm);
  font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold); background: var(--fbx-fill); color: var(--fbx-text-soft);
}
/* ⚠️ `.fbx-count--warn` A ÉTÉ RETIRÉE LE 2026-09-04, et elle a été REGARDÉE
   avant : elle teintait le compteur de passes de l'ancienne corbeille (« absent
   du classeur depuis plus de cinq passes »). Ce compteur est parti avec le
   classeur ; aucune capacité n'a été perdue, c'est la mesure qui n'existe plus.
   Un compte qui doit alerter se dit par `.fbx-status-count` s'il compte un
   statut, sinon par le texte qui l'accompagne. */
/* ⚠️ UNE PASTILLE RENDUE EN LIEN GARDE SA COULEUR (2026-09-01). `.fbx-table a`
   (specificite 0,1,1) l'emporte sur `.fbx-count` et `.fbx-status-count`
   (0,1,0) : dans une table, un compteur cliquable prenait la couleur d'accent —
   et pour `.fbx-status-count`, **du bleu sur son propre aplat bleu**, donc un
   chiffre invisible. Constate a l'ecran sur `/testeurs`, colonne « A verifier ».
   ⚠️ CE N'ETAIT NI UN JETON FAUX NI UNE COULEUR MAL CHOISIE : c'est une regle
   qui GAGNE, et le jeton resolvait juste. Troisieme occurrence de cette famille
   — avant de conclure qu'un jeton est faux, verifier que la regle qui le lit
   l'emporte.
   ⚠️ SPECIFICITE EXPLICITE (0,2,1), PAS UN PARI SUR L'ORDRE : ecrite
   `a.fbx-status-count`, la regle egalerait `.fbx-table a` et ne gagnerait que
   par sa position dans le fichier — ce que ce socle a deja paye deux fois. */
.fbx-table a.fbx-count { color: var(--fbx-text-soft); }
.fbx-table a.fbx-status-count { color: var(--fbx-on-accent); }

/* ═══════════════════════════════════════════════════════════════════════════
   Tables
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-table {
  width: 100%; border-collapse: collapse; background: var(--fbx-surface);
  border-radius: var(--fbx-radius-lg); overflow: hidden; box-shadow: var(--fbx-shadow-card);
}
.fbx-table thead th {
  text-align: left; padding: var(--fbx-space-10) var(--fbx-space-14); font-size: var(--fbx-font-xs); text-transform: uppercase;
  letter-spacing: .5px; color: var(--fbx-text-muted); background: var(--fbx-thead-bg); border-bottom: 1px solid var(--fbx-border);
  font-weight: var(--fbx-weight-semibold);
}
/* EN-TÊTE TRIABLE — `TPXShared.mountTableSort` pose la classe, l'état vit dans
   `aria-sort` et le marqueur en découle. Un seul état, porté par l'attribut que
   les lecteurs d'écran annoncent déjà : une classe `is-*` en plus aurait été un
   second état à tenir synchronisé, donc un second état à désynchroniser.

   ⚠️ LE MARQUEUR EST UN CARACTÈRE, PAS UNE ICÔNE DU CATALOGUE, et c'est une
   contrainte technique assumée : un SVG en `::after` ne peut passer que par une
   data-URI, et une data-URI est OPAQUE AU CSS — la flèche y figerait sa couleur
   et resterait identique en thème sombre. En caractère, elle hérite de `color`.

   ⚠️ LA COLONNE NON TRIÉE GARDE SON MARQUEUR, en gris pâle : sans lui, rien ne
   dit qu'une colonne est triable avant de l'avoir survolée — on ne découvre une
   capacité qu'en la cherchant, et personne ne cherche ce qu'il ignore. */
.fbx-th-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.fbx-th-sort:hover { color: var(--fbx-text); }
.fbx-th-sort:focus-visible { outline: 2px solid var(--fbx-accent-fill); outline-offset: -2px; }
.fbx-th-sort::after { content: '\2195'; margin-left: var(--fbx-space-6); opacity: .35; font-size: var(--fbx-font-2xs); }
.fbx-th-sort[aria-sort="ascending"]  { color: var(--fbx-text); }
.fbx-th-sort[aria-sort="descending"] { color: var(--fbx-text); }
.fbx-th-sort[aria-sort="ascending"]::after  { content: '\2191'; opacity: 1; }
.fbx-th-sort[aria-sort="descending"]::after { content: '\2193'; opacity: 1; }

.fbx-table tbody td { padding: var(--fbx-space-10) var(--fbx-space-14); border-bottom: 1px solid var(--fbx-border-list); font-size: var(--fbx-font-base); vertical-align: top; }
.fbx-table tbody tr:last-child td { border-bottom: none; }

/* EN-TÊTE DE RANGÉE — le tableau clé/valeur (2026-08-28, P59).
   Un `<th scope="row">` dans le corps n'avait AUCUNE règle : il prenait le
   `text-align: center` et le gras du navigateur, sans rembourrage ni filet, à
   côté de cellules qui en ont — donc une colonne visiblement d'un autre tableau.

   ⚠️ C'EST UNE VARIANTE DU COMPOSANT, PAS UN COMPOSANT NEUF, et c'est ce qui a
   fait écarter la liste de paires clé/valeur que le cadrage prévoyait : dès lors
   que la donnée s'affiche dans une modale, `.fbx-table` répond au besoin — il ne
   lui manquait que le cas où l'intitulé est à GAUCHE au lieu d'être en haut.
   `.fbx-modal-body .fbx-table-wrap` anticipait déjà « modale + tableau ».

   ⚠️ MÊME RYTHME ET MÊME FILET QUE `td` : ce qui change est le RÔLE de la
   cellule (elle nomme au lieu de porter), donc sa graisse, sa couleur et sa
   largeur — jamais son alignement sur la grille du tableau.
   ⚠️ `width: 1%` EST L'IDIOME DU SOCLE (déjà employé par `.fbx-cell-portrait`) :
   ce n'est pas « 1 % de la table », c'est la demande de la plus petite largeur
   possible, le contenu faisant plancher. Sans lui, le navigateur répartit au
   prorata et donne la moitié de la largeur à des intitulés de deux mots. */
.fbx-table tbody th {
  text-align: left; padding: var(--fbx-space-10) var(--fbx-space-14);
  border-bottom: 1px solid var(--fbx-border-list); font-size: var(--fbx-font-base);
  vertical-align: top; font-weight: var(--fbx-weight-semibold);
  color: var(--fbx-text-muted); white-space: nowrap;
}
.fbx-table tbody tr:last-child th { border-bottom: none; }

/* UN LIEN DANS UNE TABLE — RELEVÉ AU NAVIGATEUR, EN THÈME SOMBRE (2026-08-28).
   Le socle habillait les liens CAS PAR CAS (`.fbx-cell-comment a`,
   `.fbx-reply-comment a`, `.fbx-row-page a`) et n'avait rien pour la table :
   un `<a>` y prenait le `#0000EE` par défaut du navigateur, soit **1,73:1** sur
   le fond sombre — mesuré. En thème clair le même bleu tient largement, donc le
   défaut n'existe QUE dans un thème, et il ne se voit que là.

   ⚠️ NI LE `grep` NI LES TESTS NE POUVAIENT LE MONTRER : il n'y a pas de règle
   fautive à trouver, il y a une règle ABSENTE — et l'absence ne se lit qu'en
   comparant le rendu au thème. Quatrième occurrence de la même famille, après
   la data-URI opaque, le style en ligne du sélecteur de statut et la règle
   jumelle oubliée par la palette : *avant de conclure qu'un composant est juste,
   le regarder dans les deux thèmes.*

   ⚠️ PORTÉE À `.fbx-table`, PAS AUX LIENS EN GÉNÉRAL : une règle `a` globale
   entrerait en égalité de spécificité avec les trois règles ci-dessus et se
   départagerait par l'ordre du fichier — donc par un hasard d'écriture. */
.fbx-table a { color: var(--fbx-accent-fg); word-break: break-all; }

/* Toute table doit pouvoir défiler seule : la PAGE ne déborde jamais. */
/* COLONNE D'ACTIONS — MÊME LARGEUR PARTOUT (2026-08-06).
   Dimensionnée pour QUATRE icônes côte à côte, et pas plus : 4 × 28 px de cible
   + 3 × 4 px d'écart + le rembourrage de cellule. Au-delà de quatre, la barre
   d'actions n'est plus une barre — c'est un menu, et il se pose autrement.
   ⚠️ POURQUOI FIXE : sans elle, la colonne prend la largeur de son contenu et
   varie d'un tableau à l'autre — deux écrans côte à côte n'ont alors pas leurs
   icônes au même endroit, et sur un même tableau une ligne sans corbeille (le
   compte de la personne connectée, qui ne peut pas se supprimer) décale sa
   voisine. On cherche une cible du regard, pas seulement du curseur. */
/* ⚠️ ALIGNÉE À DROITE depuis le 2026-08-24, et c'est la colonne ENTIÈRE qui
   l'est — en-tête compris, sinon l'intitulé et ce qu'il coiffe ne sont plus sur
   le même bord. Elle réserve 140 px pour une à trois icônes de 28 : alignée à
   gauche, le bouton se retrouvait à **98 px mesurés** du bord de la table, un
   vide que l'œil traverse à chaque ligne pour atteindre l'action.
   ⚠️ POURQUOI AU SOCLE ET PAS DANS L'ÉCRAN QUI L'A RÉVÉLÉ : deux écrans
   l'emploient (« Administrateurs » et « Testeurs »), et la règle vaut plus que
   le cas — une même action doit se trouver à la même place d'un écran à
   l'autre, sinon on la cherche des yeux à chaque changement. La corriger sur un
   seul aurait créé la divergence qu'on passe son temps à solder. */
/* ⚠️ `width: 1%` A REMPLACÉ `140px` le 2026-08-24, et c'est l'idiome que le
   socle emploie déjà vingt lignes plus bas pour la colonne de portrait : ce
   n'est pas « 1 % de la table », c'est la demande de la plus petite largeur
   possible, le contenu faisant plancher. Les 140 px étaient calibrés pour les
   QUATRE icônes de l'écran Testeurs, et imposés à tous : dans une modale de
   520 px portant une seule corbeille, cette colonne prenait **27 % de la
   largeur** pour une icône de 28. Une largeur fixe calibrée sur le cas le plus
   chargé est un vide permanent pour tous les autres. */
.fbx-cell-actions { text-align: right; }

/* LA COLONNE QUI PREND LA PLACE RESTANTE (2026-08-29). Le pourquoi est au
   jeton `--fbx-col-grow-w`, avec la valeur.
   ⚠️ SE POSE SUR LE `<th>`, jamais sur les cellules : c'est l'en-tete qui fixe
   la grille d'une table. */
.fbx-table th.fbx-col-grow { width: var(--fbx-col-grow-w); }

/* TABLE À COLONNES FIXES (2026-09-18, retour `962d5637`). Une `<table>`
   répartit selon son CONTENU : trois onglets qui montrent la même table sur
   des lignes différentes avaient trois jeux de largeurs, et les colonnes
   bougeaient à chaque changement d'onglet — « Niveau » mesuré à 157 px sur un
   onglet, 219 sur l'autre. Ici les largeurs sont DÉCLARÉES, sur l'en-tête, et
   la colonne sans largeur prend le reste.
   ⚠️ UNE CELLULE TROP LONGUE SE COUPE, ELLE N'ÉLARGIT RIEN : c'est le prix des
   colonnes fixes. Ce qui est coupé doit porter son texte entier en `title`.
   ⚠️ LA LARGEUR MINIMALE (`--fbx-table-fixe-min-w`) : sous elle, la colonne
   restante tomberait à zéro ; la table défile alors dans `.fbx-table-wrap`. */
.fbx-table--fixe { table-layout: fixed; min-width: var(--fbx-table-fixe-min-w); }
.fbx-table--fixe tbody td,
.fbx-table--fixe tbody td > div { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fbx-table--fixe th.fbx-col-date     { width: var(--fbx-col-date-w); }
.fbx-table--fixe th.fbx-col-gravite  { width: var(--fbx-col-gravite-w); }
.fbx-table--fixe th.fbx-col-projet   { width: var(--fbx-col-projet-w); }
.fbx-table--fixe th.fbx-col-contexte { width: var(--fbx-col-contexte-w); }
.fbx-table--fixe th.fbx-cell-actions { width: var(--fbx-col-actions-w); }
/* ⚠️ `text-align` EST REPRIS ICI, ET PAS SEULEMENT SUR `.fbx-cell-actions` :
   `.fbx-table th` aligne à gauche avec une spécificité (0,1,1) que la classe
   seule (0,1,0) ne bat pas. Sans cette ligne, les CELLULES passaient à droite
   et leur EN-TÊTE restait à gauche — un demi-basculement, plus trompeur que
   pas de basculement du tout. Troisième occurrence de la même leçon : avant de
   conclure qu'une règle est fausse, vérifier qu'elle gagne. */
.fbx-table td.fbx-cell-actions, .fbx-table th.fbx-cell-actions { white-space: nowrap; text-align: right; }

/* Tableau À PORTRAITS — lignes hautes, contenu centré (2026-08-06).
   `vertical-align: top`, le défaut du socle, est juste pour une ligne de 20 px :
   sur une ligne de 96 px il colle le nom au plafond et laisse soixante pixels
   de vide sous chaque cellule, si bien qu'on ne sait plus quelle ligne on lit.
   ⚠️ `width: 1%` SUR LA COLONNE DU PORTRAIT — sans elle, le navigateur répartit
   la largeur au prorata du contenu et accorde 240 px à une image de 80. Ce
   n'est pas « 1 % de la table » : c'est l'idiome qui demande la plus petite
   largeur possible, le contenu faisant plancher. */
.fbx-table--portraits tbody td { vertical-align: middle; }
/* ⚠️ LA MÊME RÈGLE, DÉCOUPLÉE DE SON CAS (2026-08-24). Le raisonnement écrit
   juste au-dessus — « `top` est juste pour une ligne de 20 px ; sur une ligne
   haute il colle le texte au plafond » — ne doit rien aux portraits : il vaut
   dès que la ligne est HAUTE. Or une ligne le devient aussi par son ACTION,
   un bouton de 28 px portant la ligne à ~48. Constaté sur la modale des jetons
   ET sur la page des comptes : dates et libellés collés en haut pendant que la
   corbeille, elle, était centrée — deux alignements dans la même ligne.
   ⚠️ `:has()` PLUTÔT QU'UNE CLASSE À POSER : la condition est un FAIT du
   tableau (il porte des actions), pas une déclaration de l'auteur. Une classe
   se serait oubliée sur la prochaine table, et le défaut serait revenu sans
   bruit. L'idiome est déjà celui du socle (cellule Pièces des boards). */
.fbx-table tbody tr:has(td.fbx-cell-actions) td { vertical-align: middle; }
.fbx-table--portraits .fbx-cell-portrait { white-space: nowrap; padding-right: var(--fbx-space-4); }

/* L'IDIOME « LA PLUS PETITE LARGEUR POSSIBLE », ÉCRIT UNE FOIS (2026-08-28).
   Ce n'est pas « 1 % de la table » : c'est la demande de la largeur minimale, le
   contenu faisant plancher. Sans lui, le navigateur répartit au prorata et donne
   240 px à une image de 80, ou la moitié de la table à des intitulés de deux
   mots.

   ⚠️ IL ÉTAIT ÉCRIT TROIS FOIS, chacune avec son propre paragraphe expliquant la
   même chose — et P59 s'apprêtait à l'écrire une quatrième. *Un idiome qu'on
   recopie est un idiome qu'on finit par appliquer de travers ;* les trois
   colonnes qui le portent le disent maintenant au même endroit, et une
   quatrième s'ajoute au sélecteur au lieu de s'ajouter au fichier.
   ⚠️ CHAQUE PORTEUR GARDE SES AUTRES DÉCLARATIONS là où elles sont : ce qui est
   commun est la largeur, pas l'alignement ni le rembourrage. */
.fbx-cell-actions,
.fbx-table--portraits .fbx-cell-portrait,
.fbx-table tbody th { width: 1%; }
/* LIGNE MARQUEE — celle qu'on cherchait en ouvrant le tableau (2026-08-28, P63).
   ⚠️ UN ETAT D'INTERFACE, DONC UNE CLASSE `is-*`, et `is-on` parce que c'est
   celle que le socle emploie deja partout pour « celui-ci » (`.fbx-status-chip`,
   `.fbx-cal-rac`, `.fbx-ae-vignette`). En ajouter une cinquieme sous un autre
   nom aurait cree le doublon que le design system passe son temps a solder.
   ⚠️ ELLE PREND `--fbx-row-hover`, LE MEME APLAT QUE LE SURVOL, et c'est voulu :
   deux teintes proches sur la meme ligne se seraient parasitees, et la ligne
   marquee aurait clignote au passage du curseur. Le survol reste lisible parce
   qu'il change pour TOUTES les autres lignes.
   ⚠️ ELLE NE REMPLACE PAS LE MOT. La ligne teintee dit « c'est celle-la » d'un
   coup d'oeil ; la colonne de statut dit LEQUEL des quatre etats. Un aplat seul
   ne distingue pas « ouverte » de « en clôture ». */
.fbx-table tbody tr.is-on td { background: var(--fbx-row-hover); }

/* CELLULE DE NOMBRE — alignee a droite, chiffres de meme chasse (2026-08-28).
   ⚠️ UNE COLONNE DE NOMBRES SE COMPARE PAR SON BORD DROIT : alignes a gauche,
   « 128 » et « 7 » ne se comparent qu'en lisant, ce qui est exactement ce que
   la colonne evite. `tabular-nums` empeche en plus la colonne de bouger d'un
   rendu a l'autre.
   ⚠️ `text-align` REPRIS SUR `.fbx-table th` POUR LA MEME RAISON QUE
   `.fbx-cell-actions` : `.fbx-table th` aligne a gauche avec une specificite
   (0,1,1) que la classe seule ne bat pas — l'en-tete serait reste a gauche
   au-dessus de cellules passees a droite, le demi-basculement deja rencontre
   deux fois. */
.fbx-cell-num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.fbx-table td.fbx-cell-num, .fbx-table th.fbx-cell-num { text-align: right; }
.fbx-table-wrap { overflow-x: auto; }
/* ⚠️ DANS UN CORPS DE MODALE, LE TABLEAU REPREND LE RYTHME DES BLOCS (16 px).
   Le corps d'une modale espace ses blocs par la marge basse de `.fbx-form-group` ;
   `.fbx-table-wrap` n'en a aucune, si bien qu'un tableau suivi d'un bloc de
   formulaire les collait — mesuré à **0 px** sur la modale des jetons, où la
   rangée « Émettre » semblait appartenir à la dernière ligne du tableau.
   Portée au corps de modale et non à la classe : dans une PAGE, le tableau est
   souvent le dernier élément, et une marge y ajouterait du vide en bas. */
.fbx-modal-body .fbx-table-wrap { margin-bottom: var(--fbx-space-16); }

/* Table en GRILLE — les deux boards (`review`, `feedbacks`) n'emploient pas de
   <table> : leurs lignes sont des `display:grid`, parce qu'une cellule y contient
   un fil de réponses et un formulaire dépliable, ce qu'une table rend mal.
   L'HABILLAGE est le même que `.fbx-table` (mêmes surfaces, même en-tête, même
   survol).
   ⚠️ 2026-08-04 — LA RÉPARTITION DES COLONNES EST MONTÉE ICI ELLE AUSSI. Elle
   était « de la mise en page, propre à l'écran » tant que les deux boards ne
   partageaient pas leur ligne. Ils la partagent (`TPXShared.renderBoard`) : le
   gabarit devient une propriété du composant, avec DEUX variantes déclarées —
   `--board` (relecteur) et `--board-admin` (sélection en lot + tags). */
.fbx-grid {
  background: var(--fbx-surface); border-radius: var(--fbx-radius-lg);
  overflow: hidden; box-shadow: var(--fbx-shadow-card);
}
.fbx-grid-head {
  padding: var(--fbx-space-10) var(--fbx-space-14); background: var(--fbx-thead-bg); border-bottom: 1px solid var(--fbx-border);
  font-size: var(--fbx-font-xs); text-transform: uppercase; letter-spacing: .5px; color: var(--fbx-text-muted); font-weight: var(--fbx-weight-semibold);
}
.fbx-grid-row { padding: var(--fbx-space-8) var(--fbx-space-14); border-bottom: 1px solid var(--fbx-border-list); position: relative; }
.fbx-grid-row:last-child { border-bottom: none; }
/* ZÉBRAGE — une ligne sur deux. Sur les grands écrans, une liste toute blanche
   oblige à suivre la ligne des yeux jusqu'au bout ; un gris à peine posé rend la
   ligne lisible d'un coup.
   ⚠️ DÉCLARÉ AVANT LE SURVOL : à spécificité égale, la dernière règle gagne —
   posé après, le zébrage aurait mangé le survol une ligne sur deux.
   ⚠️ `(odd)` SUR LA GRILLE, `(even)` SUR LA TABLE — et ce n'est pas une
   coquille. L'en-tête `.fbx-grid-head` est un `<div>` FRÈRE des lignes, donc il
   occupe la position 1 et il est compté : la première ligne de données tombe
   sur `even`, et c'est elle qui se teintait — juste sous un en-tête déjà gris.
   Une `<table>` n'a pas ce problème, son `<thead>` étant hors du `<tbody>`.
   ⚠️ Un commentaire disait ici que `:nth-of-type` réglait le cas : c'était FAUX.
   `:nth-of-type` compte par NOM DE BALISE, pas par classe — l'en-tête étant un
   `<div>` comme les lignes, il est compté dans les deux écritures. */
.fbx-grid-row:nth-of-type(odd) { background: var(--fbx-row-alt); }
/* ⚠️ `td` ET `th` — LA COLONNE DES INTITULES EST DANS LA LIGNE (2026-08-31).
   Le zebrage ne visait que les `td`, si bien que dans un tableau cle/valeur —
   celui de la modale de contexte — la colonne de gauche restait blanche pendant
   que la droite alternait. Une ligne sur deux etait donc zebree a MOITIE, et le
   defaut ne pouvait se voir que la ou un `<th scope="row">` existe : un seul
   tableau de l'outil. */
.fbx-table tbody tr:nth-child(even) td,
.fbx-table tbody tr:nth-child(even) th { background: var(--fbx-row-alt); }

/* ⚠️ LE SURVOL VIENT APRES LE ZEBRAGE, ET C'EST TOUT CE QUI LE FAIT GAGNER.
   Il etait declare 200 lignes plus haut, dans la section des tables — a
   specificite EGALE (0,2,3 des deux cotes), donc l'ordre du fichier tranchait,
   et il tranchait contre lui : **une ligne paire ne s'eclairait pas au survol**,
   une ligne impaire oui. Un demi-comportement, sur une regle pourtant juste.
   *Une regle placee avant celle qu'elle doit annuler perd en silence* — meme
   piege que le repli du kanban et que la barre de navigation en mobile, deux
   fois le meme jour le 2026-08-20. */
.fbx-table tbody tr:hover td,
.fbx-table tbody tr:hover th { background: var(--fbx-row-hover); }

/* ⚠️ `.is-menu-open` ACCOMPAGNE `:hover` PARTOUT OU IL PARAIT (2026-08-31).
   Le menu « … » vit dans `document.body`, pas dans la ligne : `:hover` retombe
   des qu'on quitte le bouton pour aller le lire, si bien que la ligne se
   decolorait et que sa barre disparaissait sous le curseur — on choisissait une
   action pour une ligne que plus rien ne designait. */
.fbx-grid-row:hover,
.fbx-grid-row.is-menu-open { background: var(--fbx-row-hover); }

/* LA LIGNE QU'UN LIEN DESIGNE (2026-09-20, retour 754a1405) — posee par
   `TPXShared.viserLigneBoard` le temps qu'on la trouve des yeux, puis retiree.

   ⚠️ ELLE EST ECRITE APRES LE SURVOL, ET C'EST LA CONDITION POUR QU'ELLE SE
   VOIE : les deux selecteurs pesent le meme poids (deux classes), donc seul
   l'ordre du fichier departage — placee avant, elle perdait des que le curseur
   passait sur la ligne, c'est-a-dire au moment meme ou on la cherche. Meme
   piege que la barre basse du 2026-08-20, rencontre deux fois ce jour-la.
   ⚠️ UN ETAT D'INTERFACE, DONC UNE CLASSE `is-*` : `data-status` porte les
   etats d'un RETOUR, et y ranger « visee » melerait deux vocabulaires.
   ⚠️ LE FILET EST INTERIEUR (`inset`) et non une bordure : une bordure de plus
   decalerait le contenu de la ligne de 3 px, donc toute la grille, au moment
   precis ou l'oeil s'y pose. */
.fbx-grid-row.is-visee {
  background: var(--fbx-accent-bg);
  box-shadow: inset 3px 0 0 0 var(--fbx-accent-fg);
}
/* LA LIGNE QU'UNE MISE A JOUR AUTOMATIQUE VIENT D'AMENER OU DE CHANGER
   (2026-09-24) — posee par `TPXShared.signalerLignesActualisees` quatre
   secondes, puis retiree ; elle s'efface par la transition ci-dessous.
   ⚠️ LE MEME FOND QUE LA LIGNE VISEE, SANS SON FILET : la visee dit « c'est
   celle-ci que tu cherchais », l'actualisation seulement « ceci a bouge ». Deux
   faits, deux marques ; un seul fond, parce que c'est la meme famille.
   ⚠️ ECRITE APRES LE SURVOL, meme raison que `is-visee`. */
.fbx-grid-row.is-actualise { background: var(--fbx-accent-bg); }
/* L'effacement est progressif, l'apparition non : la ligne est deja sous les
   yeux quand le marquage tombe, et une disparition nette se lit comme un
   clignotement. La pose, elle, suit un defilement — l'animer la retarderait. */
.fbx-grid-row { transition: background-color .5s ease; }

/* ⚠️ CES DEUX GABARITS NE SE MODIFIENT PLUS ICI EN PREMIER (2026-08-21).
   Ils DÉRIVENT de `BOARD_COLONNES` (`widget/feedback-shared.js`), la table de
   colonnes d'où viennent aussi l'en-tête, l'ordre des cellules rendues et les
   deux seuils `@container` plus bas. On ajoute une colonne DANS LA TABLE ; ces
   deux lignes et les deux seuils la suivent, et `test/board-gabarit.test.js`
   refuse toute valeur qui ne correspondrait plus.
   *Le CSS reste écrit à la main — engendrer la feuille au chargement coûterait
   un rendu transitoire —, mais il n'est plus la source.* */
/* GABARIT DE COLONNES DU BOARD — deux variantes, et l'écart est déclaré :
   le board d'administration ajoute la SÉLECTION EN LOT (44) et les TAGS (160),
   qui n'ont pas de sens pour un relecteur. Le reste est identique au pixel,
   sans quoi le même retour ne se lit pas au même endroit d'un écran à l'autre.

   ⚠️ LE COMMENTAIRE A UN PLANCHER, ET C'EST UN GARDE-FOU, PAS UN RÉGLAGE
   (2026-08-20). En `1fr` nu, la colonne qui porte TOUTE l'information du board
   reçoit « ce qui reste » — donc ZÉRO dès que la somme des colonnes fixes
   dépasse la largeur disponible. Mesuré à 834 px sur `--board-admin`, le
   gabarit calculé valait `44px 92px 150px 0px 160px 120px 60px 60px 44px` :
   le commentaire invisible, et la page du retour cassée un caractère par ligne
   sur 700 px de haut. Rien ne le signalait — ni erreur, ni débordement.
   Avec `minmax()`, le même dépassement produit une barre de défilement : un
   défaut qui SE VOIT plutôt qu'une colonne qui s'évapore en silence. */
/* ⚠️ SIX COLONNES DEPUIS LE 2026-08-21, ET LES DEUX GABARITS ONT ETE REFAITS
   DANS LE MEME GESTE QUE LES DEUX SEUILS `@container` PLUS BAS. C'est
   exactement l'omission qui a produit le defaut du 20/08 : la variante admin
   avait recu deux colonnes sans que son seuil bouge, et le commentaire tombait
   a zero entre 721 et 954 px.

     Date | Testeur | Commentaire | Statut | Pieces | Lien

   Ce qui a disparu : les colonnes IMAGE et CAPTURE, fondues en PIECES. La date
   passe de 92 a 76 px : elle tient desormais sur une ligne (`21/08/2026` mesure
   ~60 px a 12 px), la forme empilee ayant disparu avec la densite.

   ⚠️ LA COLONNE TAGS A ETE RETIREE PUIS REMISE LE MEME JOUR, et le seuil a ete
   refait avec — c'est exactement le geste qui manquait le 20/08. Verses dans la
   rangee meta de la cellule commentaire, les tags se lisaient comme une SUITE du
   texte, sans intitule au-dessus pour dire ce qu'ils sont.

   ⚠️ DEUX COLONNES ONT ETE RE-DIMENSIONNEES SUR CE QU'ELLES PORTENT VRAIMENT
   (2026-08-21), et l'echange est a somme nulle sur les seuils.
     · SELECTION 44 -> 28 px. Elle ne porte qu'une case de 16 px : 28 px de vide
       plus la gouttiere faisaient 42 px entre la case et la date, l'ecart le
       plus large du tableau et le seul qui ne separait rien. La case se centre
       en plus dans sa cellule, ce qui repartit le peu qui reste des deux cotes.
       ⚠️ Sa CIBLE ne diminue pas : c'est le plancher de 42 px qui la porte en
       hauteur, et la largeur d'une case ne se clique pas au-dela d'elle-meme.
     · LIEN 44 -> 60 px, cote ADMIN SEULEMENT. Il y porte DEUX boutons — l'ancre
       et la reassignation d'URL — soit 2 x 28 + 2 d'ecart = 58 px pour 44
       declares : ils se comprimaient a 21 px, SOUS le minimum de 24 px de
       WCAG 2.5.8. Le board relecteur n'a qu'une ancre et garde ses 44 px.
   ⚠️ TROIS LARGEURS SONT DES VALEURS SERREES, PAS DES ARRONDIS. Tags 120 px :
   `ajustement-mineur`, le tag le plus employe en production, mesure ~120 px et
   passe donc a la ligne — deux rangees de 19 px tiennent dans le plancher de 42,
   la ligne ne grandit pas. Fil et Pieces 56 px : un jeton fait 25 px, deux plus
   leur ecart en font 53. Chaque pixel de ces colonnes est pris au commentaire,
   et c'est le commentaire qui porte l'information. */
.fbx-grid--board > .fbx-grid-head,
.fbx-grid--board > .fbx-grid-row {
  display: grid;
  grid-template-columns: var(--fbx-board-ref-w) 40px minmax(var(--fbx-board-comment-min), 1fr) var(--fbx-board-reponses-w) var(--fbx-board-pieces-w) 132px var(--fbx-board-bloquant-w) 88px;
  gap: var(--fbx-board-gap); align-items: start;
}
.fbx-grid--board-admin > .fbx-grid-head,
.fbx-grid--board-admin > .fbx-grid-row {
  grid-template-columns: 28px var(--fbx-board-ref-w) 40px minmax(var(--fbx-board-comment-min), 1fr) var(--fbx-board-reponses-w) var(--fbx-board-pieces-w) 120px 132px var(--fbx-board-bloquant-w) 118px;
}
/* LA RÉFÉRENCE DU RETOUR (2026-09-29) — `FMP-712`, qui est aussi le lien vers
   la page du retour quand il en a une. Centrée dans le plancher de la ligne,
   comme le statut, pour s'aligner sur la première ligne du commentaire.
   ⚠️ CHIFFRES TABULAIRES : les références se lisent en colonne, et `FMP-711`
   ne doit pas être plus étroit que `FMP-708`.
   ⚠️ LE LIEN GARDE LA COULEUR D'ACCENT DES LIENS DU SOCLE, et la référence sans
   page (demande transverse) celle du texte courant : c'est la couleur qui dit
   « ceci s'ouvre ». */
.fbx-grid-row > .fbx-cell-ref {
  min-height: var(--fbx-board-cell-h); display: flex; align-items: center;
}
.fbx-ref {
  font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold);
  font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--fbx-text-soft);
}
a.fbx-ref { display: inline-flex; align-items: center; color: var(--fbx-accent-fg); text-decoration: none; }
a.fbx-ref:hover, a.fbx-ref:focus-visible { text-decoration: underline; }
/* EN TABLEAU, L'AVATAR SEUL (2026-09-29) — le nom passe dans la bulle du socle
   (`brancherBulles`, posée par `bindBoard`). Il RESTE dans le DOM, masqué à
   l'œil seulement : `clip-path` le rend invisible sans le retirer de l'arbre
   d'accessibilité, et `position: absolute` le sort du flux sans lui donner de
   dimension littérale. Les cartes et la ligne du téléphone le rendent visible
   (voir les `@container board` plus bas), faute de survol possible au doigt.
   ⚠️ LE JS LIT `position` POUR SAVOIR S'IL FAUT UNE BULLE : les deux règles
   se répondent, en changer une oblige à relire l'autre. */
.fbx-grid--board .fbx-grid-row > .fbx-author > .fbx-author-name {
  position: absolute; clip-path: inset(50%); white-space: nowrap;
}
.fbx-grid--board .fbx-grid-row > .fbx-author { justify-content: center; }
/* La colonne Repères. ⚠️ `min-width: 0` N'EST PAS COSMETIQUE : un element de
   grille vaut `min-width: auto` par defaut, donc il refuse de descendre sous la
   largeur de son contenu — un tag long (`ajustement-mineur`, le plus employe en
   production, mesure ~120 px) elargirait la piste au-dela des 120 px declares et
   volerait sa place au commentaire, en silence. Avec ce plancher, c'est le TAG
   qui passe a la ligne. ⚠️ IL COMPTE D'AUTANT PLUS DEPUIS LA FUSION (2026-08-31) :
   la piste porte desormais les jetons ET les tags, donc son contenu naturel est
   plus large qu'avant. */
.fbx-cell-tags { min-width: 0; }

/* LA CELLULE COMMENTAIRE EST ELLE-MEME UNE GRILLE A DEUX COLONNES (2026-08-21).
   ⚠️ ET C'EST LA CLE DE LA HAUTEUR DE LIGNE, PAS UN RAFFINEMENT. La barre
   d'actions etait revelee par `opacity`, donc elle occupait sa place EN FLUX
   meme invisible : 8 px de marge plus 28 px de bouton, soit 36 px preleves sur
   CHAQUE ligne pour montrer quatre boutons qu'on ne voit qu'au survol. La
   passer en `position: absolute` aurait recouvert le texte ; lui donner sa
   propre colonne la sort de l'axe vertical sans rien recouvrir, et sans que
   rien ne bouge a l'apparition.
   Tout le reste — commentaire, meta, note, fil, hote de reponse — reste en
   colonne 1 et s'empile comme avant. */
.fbx-cell-comment-wrap {
  min-width: 0;
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: start; column-gap: var(--fbx-space-8);
  min-height: var(--fbx-board-cell-h); position: relative;
}
/* ── LA PREMIÈRE LIGNE DU TEXTE TOMBE À LA HAUTEUR DE LA DATE, TOUJOURS
   (2026-08-21) ────────────────────────────────────────────────────────────
   Y compris texte déplié, fil ouvert, note affichée. La boîte du commentaire
   reçoit un rembourrage vertical qui centre SA PREMIÈRE LIGNE dans le plancher
   de cellule : (42 − 21) / 2 = 10,5 px de chaque côté.

   ⚠️ POURQUOI PAS UN CENTRAGE. `align-content: center` sur la cellule — la
   version précédente — centre le BLOC entier : au repos c'est juste, mais dès
   que le texte fait deux lignes le bloc remplit le plancher, la première ligne
   remonte de 10,5 px et la date se retrouve à cheval entre les deux lignes.
   C'est exactement ce qu'on voyait après un clic sur « Voir plus ».

   ⚠️ ET C'EST LE MÊME NOMBRE EN HAUT ET EN BAS, ce qui donne la symétrie
   demandée : 8 px de ligne + 10,5 px de boîte au-dessus de la première ligne,
   10,5 px + 8 px sous la dernière. Le texte déplié pousse vers le bas sans
   jamais décaler ce qui est à sa gauche. */
.fbx-grid-row .fbx-comment-box {
  min-height: var(--fbx-board-cell-h); box-sizing: border-box;
  padding-block: calc((var(--fbx-board-cell-h) - var(--fbx-board-ligne-txt)) / 2);
}
/* ⚠️ `.fbx-cell-meta` A ETE RETIREE LE JOUR DE SA CREATION, et ses deux
   emplacements successifs disent pourquoi : SOUS le texte, elle ajoutait une
   seconde rangee a la cellule et remontait la premiere ligne ; A COTE, dans la
   meme cellule, elle passait SOUS la barre d'actions des qu'on survolait la
   ligne — donc au moment precis ou l'on approche le curseur pour cliquer
   dessus. Ses jetons vivent maintenant dans une COLONNE (`.fbx-cell-media`), ou
   ils ne partagent leur place avec rien. */
.fbx-cell-comment-wrap > * { grid-column: 1; min-width: 0; }
/* ── LA BARRE D'ACTIONS SE SUPERPOSE, ELLE NE RÉSERVE PLUS DE COLONNE
   (2026-08-21) ────────────────────────────────────────────────────────────
   Elle est passée en colonne de grille le 20/08 pour sortir de l'axe vertical
   (elle y coûtait 36 px de hauteur par ligne, `opacity: 0` occupant le flux).
   Le remède avait son propre défaut, non relevé alors : elle réservait ~120 px
   de LARGEUR en permanence, pris sur le commentaire. Le plancher
   `--fbx-board-comment-min` (220 px) protège la PISTE, pas le texte : près du
   seuil de repli, il ne restait qu'une centaine de pixels au commentaire.

   Posée en superposition, elle ne coûte ni hauteur ni largeur. Elle n'est
   visible qu'au survol de la ligne — c'est-à-dire au moment précis où le fond
   de la ligne vaut `--fbx-row-hover`, ce qui donne à son fond une valeur unique
   et connue, sans avoir à suivre le zébrage.

   ⚠️ Le dégradé à gauche évite la coupure franche sur le texte qu'elle
   recouvre quand la colonne est étroite ; sur une colonne large elle tombe dans
   le vide à droite du texte, plafonné à 560 px, et ne recouvre rien.
   ⚠️ Elle reprend le PLANCHER de cellule, pas la hauteur de la ligne : sur une
   ligne dépliée elle reste en face de la première ligne du texte, comme la date
   et le statut. */
/* ⚠️ SON FOND EST UN JETON, PAS UNE COULEUR (2026-08-21) — parce qu'en TACTILE
   la ligne n'est jamais survolée. Sous `(hover: none)` la barre est visible en
   permanence (plus bas) : elle posait donc la teinte de SURVOL sur une ligne au
   repos, soit une plaque de couleur qui annonce un état que rien ne produit, et
   qui plus est fausse une ligne sur deux — le zébrage passe dessous. Le défaut
   ne se voyait qu'au doigt sur un écran LARGE, la barre repassant dans le flux
   sans fond dès que le tableau se replie en cartes.
   Le repli `var(--fbx-actions-bg, …)` garde le comportement à la souris **au
   littéral près** : hors tactile, personne ne définit ce jeton. */
/* ── LA BARRE VIT DANS LA CELLULE DE DATE (2026-08-31) ────────────────────
   Elle etait superposee au COMMENTAIRE, seule colonne souple : au survol elle
   en masquait 101 px sur les 560 rendus a 1600 px de fenetre, et jusqu'a 75 %
   du texte sur une fenetre etroite. On cachait l'information qu'on vient lire
   pour montrer des commandes.
   La date, elle, ne perd rien a etre recouverte : l'ordre de la liste porte
   deja « du plus recent au plus ancien », donc la date exacte se CONSULTE, elle
   ne se balaie pas.
   ⚠️ PLUS DE FOND DU TOUT, ET C'EST LE THEME SOMBRE QUI L'A TRANCHE. Le premier
   jet posait `var(--fbx-row-hover)` pour couvrir la date — juste en thème clair,
   ou ce jeton vaut `#EAF6FC`, opaque. **En sombre il vaut `rgba(43,168,221,.16)`,
   donc la date se lisait A TRAVERS les icones.** Le zebrage aurait pose le meme
   probleme dans l'autre sens : une barre au fond de surface sur une ligne paire
   teintee. *Un fond qui doit se confondre avec ce qu'il recouvre est un fond
   qu'on n'aura jamais juste partout.*

   ON N'EN POSE DONC AUCUN, ET ON MASQUE CE QUI EST DESSOUS : au survol, la piste
   montre les actions A LA PLACE de la date. C'est plus franc a lire — rien ne se
   superpose a rien —, insensible au theme comme au zebrage, et cela emporte le
   badge d'onglet, qui depassait de toute facon sous une barre de 148 px.

   ⚠️ `visibility` ET NON `display` : la date garde sa place, donc la ligne ne
   saute pas d'un pixel quand on la survole. */
.fbx-grid-row .fbx-cell-date > .fbx-actions {
  position: absolute; right: 0; top: 0; margin-top: 0;
  height: var(--fbx-board-cell-h); align-items: center;
}
.fbx-grid-row:hover > .fbx-cell-date > :not(.fbx-actions),
.fbx-grid-row.is-menu-open > .fbx-cell-date > :not(.fbx-actions),
.fbx-cell-date:focus-within > :not(.fbx-actions) { visibility: hidden; }

/* -- LIGNE DENSE : ce que la variante RETIRE ------------------------------
   Une variante dense qui se contenterait de resserrer le rembourrage ne
   gagnerait rien : ce qui plafonnait la hauteur, c'etait le CONTENU empile
   dans la cellule commentaire — commentaire sur deux lignes, page dessous,
   note interne, fil de reponses, barre d'actions. Elle decide donc ce qu'elle
   retire, et chaque retrait est reversible d'un clic.
   ⚠️ TOUT EST PORTE PAR `.fbx-grid-row` : le kanban de `/corrections` reemploie
   `.fbx-comment-box` et `.fbx-cell-comment--clamp` sans etre un board. Ecrites
   sur les classes nues, ces regles auraient resserre des CARTES qui n'ont ni le
   probleme ni la place.

   ⚠️ DEUX LIGNES ET NON UNE (2026-08-31) — MESURE, PAS PREFERENCE. La piste du
   commentaire rend 560 px, soit 89 caracteres par ligne (compte au navigateur,
   police et cesures reelles — jamais une division par une largeur moyenne de
   caractere, les deux methodes divergent beaucoup). Sur les 1 095 retours
   vivants de la production, la mediane fait 66 caracteres et le 3e quartile 132.
   La couverture suit :

     1 ligne   (<=  89 car)   61,0 %      2 lignes (<= 178)   83,7 %
     3 lignes  (<= 267 car)   91,7 %

   Deux lignes rendent donc lisibles **sans un clic** 22,7 points de retours de
   plus. Trois n'en ajouteraient que 8, pour une hauteur en plus sur toutes les
   lignes longues — et le plancher de cellule n'absorbe rien au-dela de deux.

   ⚠️ CE N'EST PAS UNE HAUTEUR IMPOSEE : `line-clamp` AUTORISE deux lignes, il
   n'en force aucune. Mesure au navigateur : un commentaire de 61 caracteres
   garde une ligne de 59 px, un de 160 en prend 80. Le cout se paie donc
   exactement la ou le gain se prend — sur les lignes qui, aujourd'hui, cachent
   leur texte.

   ⚠️ ET L'ALIGNEMENT DATE / PREMIERE LIGNE TIENT : c'est ce que le rembourrage
   calcule de `.fbx-comment-box` protege, et il fallait le verifier — sur les
   trois longueurs sondees, le texte tombe toujours 11 px sous le haut de la
   cellule de date. */
.fbx-grid-row .fbx-cell-comment--clamp { -webkit-line-clamp: 2; }
/* « Voir plus » sur la MEME ligne que le commentaire : sous la boite en bloc,
   il ajoutait une ligne de 15 px a toute ligne dont le texte deborde — soit la
   plupart, une fois le repli passe a une seule ligne. */
.fbx-grid-row .fbx-comment-box { display: flex; align-items: baseline; gap: var(--fbx-space-6); }
.fbx-grid-row .fbx-comment-box > .fbx-cell-comment { flex: 1 1 auto; min-width: 0; }
.fbx-grid-row .fbx-comment-box > .fbx-comment-more { flex: 0 0 auto; }
/* ⚠️ TOUT AUTRE ENFANT PREND LA BOITE ENTIERE, et la regle est ecrite en
   NEGATIF a dessein : l'edition sur place vide la boite et y injecte son
   editeur, dont la classe appartient a la page. Nommer `.fb-edit-box` ici
   ferait du socle le lecteur d'un nom d'ecran ; decrire « ce qui n'est ni le
   commentaire ni son declencheur » tient sans le savoir. */
.fbx-grid-row .fbx-comment-box > :not(.fbx-cell-comment):not(.fbx-comment-more) {
  flex: 1 1 100%; min-width: 0;
}

/* COMPTEUR DE PIECES / DE FIL / DE NOTE — un jeton qui dit COMBIEN et qui
   deplie. Ce n'est ni un `.fbx-tag` (etiquette libre, qu'on retire d'une croix)
   ni un `.fbx-count` (compteur de section, inerte) : celui-ci est un CONTROLE,
   il a un curseur et un survol. */
.fbx-piece {
  display: inline-flex; align-items: center; gap: var(--fbx-space-3); padding: 1px 5px;
  border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius-xs); background: var(--fbx-surface-sunken);
  color: var(--fbx-text-soft); font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold);
  /* ⚠️ INTERLIGNE NEUTRE (2026-08-31) : avec `--fbx-leading-snug`, un jeton
     PORTANT UN CHIFFRE mesurait 19 px et un jeton d'icone seule 17 — deux
     marqueurs voisins de la meme ligne ne faisaient donc pas la meme hauteur.
     L'interligne ne sert a rien ici, le jeton n'ayant jamais deux lignes. */
  line-height: var(--fbx-leading-none);
  cursor: pointer; transition: background .12s, color .12s, border-color .12s;
}
.fbx-piece:hover { background: var(--fbx-fill); color: var(--fbx-text); border-color: var(--fbx-border-strong); }
.fbx-piece svg { display: block; flex-shrink: 0; }
/* La note interne garde son ambre partout ailleurs dans l'outil : son compteur
   ne peut pas etre gris, il ne se distinguerait pas de celui du fil. */
/* ⚠️ PLUS DE BORDURE COLOREE (2026-08-31) : elle cerclait chaque jeton d'un
   filet vif, si bien que deux marqueurs voisins faisaient deux cadres a lire
   avant leur contenu. La couleur porte deja la distinction — elle est sur
   l'icone, la ou l'oeil va. Le contour gris du jeton, lui, reste : c'est ce qui
   en fait un bouton. */
.fbx-piece--note { color: var(--fbx-warn-fg); }
.fbx-piece--note:hover { color: var(--fbx-warn-fg); background: var(--fbx-warn-bg); }

/* LE COMPTEUR DU FIL PREND LA COULEUR DU FIL QU'IL OUVRE (2026-08-30, P61 (d)).
   C'est le raisonnement de la note juste au-dessus, poussé d'un cran : elle ne
   pouvait pas rester grise pour ne pas se confondre avec le fil ; le fil ne peut
   pas rester gris pour ne pas se confondre avec les PIECES, qui portent le meme
   jeton dans la colonne voisine. Gris = un contenu qu'on s'attend a trouver ;
   ambre = une note d'equipe ; accent = quelqu'un a deja repondu.

   ⚠️ C'EST UN DEFAUT DE CONTRASTE, ET IL SE MESURE — meme famille que le filet
   du fil (point (a) du meme lot). La BORDURE du jeton tenait 1,16 contre son
   propre fond : le texte etait lisible (7,29) mais le CHIP N'AVAIT PAS DE BORD,
   donc rien n'accrochait l'oeil pendant un balayage. Un jeton est un objet
   graphique, son seuil est 3:1 — c'est la bordure qu'il fallait corriger, pas la
   taille ni le libelle.
   ⚠️ AUCUN JETON NEUF : `--fbx-accent-fg` est deja la couleur du filet de gauche
   du fil et de ses liens. Le compteur et ce qu'il ouvre portent donc la meme
   couleur, ce qui est le seul rapport qu'on avait a dire.
   ⚠️ CE QUI A ETE ECARTE, ET LA MESURE L'A TRANCHE — ouvrir le fil D'OFFICE au
   rendu : sur le board reel (CUV2P15, 184 racines, 44 avec fil), la page passe
   de 10 892 px a 15 321 px, soit +40,7 % ; une ligne a UNE reponse passe de
   59 px a 136 px, et la pire a 360 px. On paierait 40 % de hauteur sur TOUTES
   les lignes pour un signal que 24 % d'entre elles ont a donner, et on perdrait
   la ligne uniforme a 58 px sur laquelle le balayage vertical repose — le gain
   du 21/08. Le repli n'est pas remis en cause, c'est ce qu'il masque qu'on
   corrige. */
.fbx-piece--fil { color: var(--fbx-accent-fg); }
.fbx-piece--fil:hover { color: var(--fbx-accent-fg); background: var(--fbx-accent-bg); }

/* UN JETON DE PIECE OUVRE LA VISIONNEUSE, IL NE REPLIE RIEN (2026-08-21).
   Le premier jet repliait les vignettes derriere un compteur : il fallait DEUX
   clics pour voir une image — un pour deplier une vignette de 40 px, un pour
   l'agrandir —, et la vignette intermediaire n'apprenait rien a cette taille.
   ⚠️ PAS DE CURSEUR LOUPE ICI, ET C'EST UN CHOIX (2026-08-21) : `zoom-in` avait
   ete pose pour dire « ceci agrandit », mais un jeton de 25 px n'est pas une
   image qu'on agrandit — c'est un bouton, et il se comporte comme les autres
   jetons de la ligne. Le survol standard de `.fbx-piece` (fond, texte, bordure)
   suffit a dire qu'il est cliquable ; deux grammaires de curseur sur des jetons
   voisins et identiques n'auraient distingue que du bruit. La loupe reste sur
   `.fbx-thumb-img`, ou elle porte sur une vraie vignette. */

/* FIL DE REPONSES ET NOTE INTERNE, MEME PRINCIPE. Ils ne figurent sur aucune
   ligne des captures — ils ne baissent donc pas le plancher de hauteur, ils
   creent l'ECART entre deux lignes voisines, et c'est cet ecart qui rend le
   balayage imprevisible.
   ⚠️ `:not(.is-editing)` EST OBLIGATOIRE : un bloc de note qu'on vient d'ouvrir
   pour l'ecrire ne doit pas se refermer sous la saisie. La barre d'actions pose
   `is-note-open` a l'ouverture, ce `:not` couvre le cas ou elle ne l'a pas
   fait (bloc ouvert par un autre chemin). */
.fbx-cell-comment-wrap:not(.is-fil-open) > .fbx-replies { display: none; }
/* ⚠️ LE CHAMP DE REPONSE SE MASQUE AVEC LE FIL (2026-08-31). Il s'ouvre avec la
   ligne depuis ce lot, mais rien ne le refermait : la ligne repliee gardait
   145 px de formulaire sous un fil masque — 203 px de haut au lieu de 59, et un
   demi-etat de plus, exactement ce que la bascule unique existe pour supprimer.
   ⚠️ AUCUN RISQUE DE PERDRE UNE SAISIE : `basculerLigne` REFUSE de fermer une
   ligne dont le champ porte du texte. Ce qui se masque ici est donc toujours un
   champ vide. */
.fbx-cell-comment-wrap:not(.is-fil-open) > [data-reply-host] { display: none; }
.fbx-cell-comment-wrap:not(.is-note-open) > .fbx-note:not(.is-editing) { display: none; }
/* ── CENTRAGE VERTICAL DES CELLULES (2026-08-21) ───────────────────────────
   `align-items: start` datait des lignes hautes : date sur deux lignes,
   commentaire sur deux lignes, page en dessous — le haut était alors le seul
   repère commun. En ligne dense, la date, le statut et le lien flottaient
   au-dessus du texte qu'ils qualifient.

   ⚠️ MAIS `align-items: center` SUR LA LIGNE EST UNE FAUSSE SOLUTION, ET ELLE A
   ÉTÉ ESSAYÉE. Elle centre chaque cellule dans la hauteur de la LIGNE : au repos
   c'est ce qu'on veut, mais déplier un commentaire porte la ligne à plusieurs
   centaines de pixels et emporte la date, le statut et le lien vers le milieu.
   Un simple clic pour lire un retour faisait donc SAUTER toute la rangée —
   défaut constaté à l'écran, plus visible que celui qu'on corrigeait. La
   conditionner par `:has(.is-expanded)` ne réglait rien : elle déplaçait le saut
   au lieu de le supprimer.

   ⚠️ CE QUI MARCHE : UN PLANCHER, PAS UN ALIGNEMENT. Chaque cellule reçoit la
   hauteur de la ligne au repos (`--fbx-board-cell-h`) et centre son contenu
   DEDANS. Les cellules restent calées en haut de la rangée, si bien qu'une
   ligne dépliée ne bouge plus rien : la seule cellule qui grandit est celle
   qu'on vient d'ouvrir, et toutes les autres gardent leur place au pixel près.

   ⚠️ SANS PLANCHER, IL N'Y A RIEN À CENTRER : une cellule de date fait 16 px de
   haut, la centrer dans ses propres 16 px ne la déplace pas d'un pixel. C'est
   pour cela qu'un `align-items` seul ne pouvait pas suffire.

   Le mode d'affichage est choisi cellule par cellule, et pas uniformément : la
   grille étire son enfant unique en largeur (ce dont le `<select>` de statut et
   la rangée de tags ont besoin), le flex ne le ferait pas. */
.fbx-grid-row > .fbx-cell-date,
.fbx-grid-row > .fbx-author,
.fbx-grid-row > .fbx-cell-select,
.fbx-grid-row > .fbx-cell-media,
.fbx-grid-row > .fbx-cell-fil,
.fbx-grid-row > .fbx-cell-tags { min-height: var(--fbx-board-cell-h); align-items: center; }
.fbx-grid-row > .fbx-cell-media,
.fbx-grid-row > .fbx-cell-fil,
.fbx-grid-row > .fbx-cell-tags { align-content: center; }
/* La date et le badge d'onglet s'empilent : deux fois 16 px et leur ecart font
   36 px, sous le plancher de 42 — la ligne ne grandit pas. Le badge tronque
   plutot que d'elargir la colonne, son nom complet restant en infobulle.

   ⚠️ `justify-content` ET NON `align-items` POUR LE CENTRAGE VERTICAL ICI, et
   l'oubli a coute une regression visible : `align-items` gouverne l'axe
   TRANSVERSAL, donc il centre verticalement dans une rangee — c'est ce que fait
   la regle groupee ci-dessus pour toutes les autres cellules — mais
   HORIZONTALEMENT des qu'on passe en colonne. En reprenant `align-items` pour
   coller la date a gauche, on a donc silencieusement retire le seul centrage
   vertical qu'elle avait, et elle est remontee en haut de sa cellule pendant
   que ses voisines restaient centrees.
   *Une propriete flex ne veut pas dire la meme chose selon la direction : en
   changer une oblige a relire les deux.* */
/* ⚠️ `position: relative` N'EST PAS COSMETIQUE : c'est ce qui fait que la barre
   d'actions se cale sur CETTE cellule et non sur la ligne entiere. Sans lui,
   `right: 0` la renverrait au bord droit du board.
   ⚠️ ET LA DATE PASSE A DROITE avec elle : la barre s'aligne sur le bord droit
   de la piste, une date calee a gauche laisserait entre les deux un vide que
   rien n'explique. Derniere colonne, contenu a droite — c'est ce que fait le
   badge de statut juste avant, et la ligne se termine sur un bord franc. */
/* ⚠️ L'EN-TETE DE LA DATE SUIT SES VALEURS (2026-08-31). La date est calee a
   droite depuis que la barre d'actions occupe sa piste ; son intitule, lui,
   restait a gauche — un ecart de 60 px entre le mot et la colonne qu'il nomme,
   sur la seule colonne du tableau ou les deux ne se repondaient plus. Il n'y a
   qu'un en-tete concerne, mais la regle vise la POSITION : c'est la derniere
   piste, et toute colonne calee a droite qui viendrait s'y ajouter en heriterait.
   ⚠️ `justify-content` et non `text-align` : l'en-tete triable est une boite
   flex (elle porte son marqueur en `::after`), et `text-align` n'y ferait rien. */
.fbx-grid-head > *:last-child {
  display: flex; justify-content: flex-end; align-items: center;
}

/* ⚠️ LA DATE RESPIRE A DROITE, ET LA BARRE NE BOUGE PAS (2026-08-31).
   Derniere colonne, calee a droite, elle finissait a 14 px du bord de la ligne —
   le seul rembourrage etant celui de `.fbx-grid-row`. A l'ecran elle paraissait
   collee, la ou toutes les autres cellules ont une voisine pour les separer du
   bord.
   ⚠️ LE RETRAIT PORTE SUR LE CONTENU, PAS SUR LA CELLULE : la barre d'actions
   est en `position: absolute; right: 0` par rapport a celle-ci, donc un
   `padding-right` sur la cellule l'aurait decalee elle aussi — et elle est deja
   a la bonne place, alignee sur le bord de la piste. Le retrait est donc pose
   sur les enfants qui ne sont PAS la barre, exactement ceux que le survol
   masque. */
.fbx-grid-row > .fbx-cell-date > :not(.fbx-actions) { padding-right: var(--fbx-space-6); }
.fbx-grid-row > .fbx-cell-date {
  position: relative;
  display: flex; flex-direction: column;
  align-items: flex-end;     /* axe horizontal : cale a droite, sous la barre */
  justify-content: center;   /* axe vertical   : centre dans le plancher */
  gap: var(--fbx-space-2);
}
.fbx-grid-row > .fbx-cell-date > .fbx-sheet-badge { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fbx-grid-row > .fbx-cell-status {
  min-height: var(--fbx-board-cell-h); display: grid; align-content: center;
}
/* BLOQUANT (2026-09-21) — l'interrupteur du socle, cale a gauche comme le
   statut, centre dans le plancher de la ligne. Son libelle ne se montre qu'en
   cartes (voir les deux `@container board`) : en tableau, l'en-tete le dit. */
.fbx-grid-row > .fbx-cell-bloquant {
  min-height: var(--fbx-board-cell-h); display: flex; align-items: center;
}
.fbx-cell-bloquant-libelle { display: none; }

/* La case de sélection est seule dans sa cellule : elle se centre AVEC la ligne
   plutôt que de se caler sur la première ligne de texte, qui n'est plus une
   référence depuis que la ligne porte un plancher. */
.fbx-cell-select { display: flex; align-items: center; justify-content: center; }

/* La PAGE du retour, sous son commentaire. Elle portait le titre du bloc quand
   la liste était groupée (P23) ; sans elle, la page n'apparaîtrait nulle part —
   il n'y a pas de colonne Page. Discrète et cliquable : elle situe, elle ne
   dispute pas la lecture au commentaire. */
.fbx-row-page { margin-top: var(--fbx-space-4); display: flex; align-items: center; gap: var(--fbx-space-6); flex-wrap: wrap; }
.fbx-row-page a { font-size: var(--fbx-font-sm); color: var(--fbx-accent-fg); text-decoration: none; word-break: break-all; }
.fbx-row-page a:hover { text-decoration: underline; }
/* Onglet d'origine — affiché SEULEMENT si le projet en a plusieurs (P45).
   AUDIENCE D'UN RETOUR (2026-09-05) — « Interne » sur un retour que seule
   l'équipe voit : MÊME RÈGLE, pas une copie — les deux badges répondent à la
   même question (d'où vient ce retour) et se posent au même endroit, sous la
   date. Une VALEUR va dans un attribut (`data-audience`), comme un statut se
   dit par `data-status` ; la teinte info dit un FAIT sur le retour, pas un
   état à traiter. */
/* ÉTAT D'UN TESTEUR (2026-09-23) — « Archivé ». TROISIÈME MEMBRE DE LA MÊME
   FAMILLE, et il en partage la règle plutôt que d'en recopier les six
   déclarations : une VALEUR va dans un attribut (`data-etat`), comme l'onglet
   d'origine et l'audience.
   ⚠️ LE GRIS DE BASE EST L'ÉTAT ARCHIVÉ, et il n'y a pas d'autre valeur
   aujourd'hui : une personne partie se constate, elle n'appelle aucun geste.
   L'état « à autoriser » viendra avec la liste des testeurs autorisés, avec sa
   teinte d'attention — celui-là attendra quelque chose de nous.
   ⚠️ IL N'Y A PAS D'ÉTAT « AUTORISÉ » : la cellule reste vide. Un badge sur
   chaque ligne ne dirait rien, et c'est son absence qui se remarque — même
   règle que la colonne « À vérifier », vide quand il n'y a rien. */
.fbx-sheet-badge,
.fbx-audience-badge,
.fbx-etat-testeur,
.fbx-erreurs-badge {
  display: inline-block; font-size: var(--fbx-font-2xs); font-weight: var(--fbx-weight-semibold); text-transform: uppercase;
  letter-spacing: .3px; color: var(--fbx-text-soft); background: var(--fbx-fill); padding: var(--fbx-space-2) 7px; border-radius: var(--fbx-radius-xs);
}
.fbx-audience-badge[data-audience="equipe"] { color: var(--fbx-info-fg); background: var(--fbx-info-bg); }
/* LES ERREURS DE LA PAGE (2026-10-02) — quatrième membre de la famille, posé à
   côté de « Interne » sous la date : un fait sur le retour, pas un état à
   traiter. ⚠️ LA TEINTE EST CELLE DES ENCARTS D'ÉCHEC (`--fbx-danger-*`), pas
   celle d'un statut : rien n'est à corriger dans le RETOUR, c'est la PAGE qui a
   signalé des erreurs au moment du dépôt. Aucun jeton neuf. */
.fbx-erreurs-badge { color: var(--fbx-danger-fg); background: var(--fbx-danger-bg); }
/* « À AUTORISER » (2026-09-23) — la teinte d'attention que le commentaire
   ci-dessus annonçait. ⚠️ ELLE SE DISTINGUE DE « Archivé », resté neutre, et
   c'est la différence qui compte : une personne partie se constate, une adresse
   hors liste attend un geste de nous. Deux états sous une même teinte auraient
   rendu le second invisible parmi les premiers. */
.fbx-etat-testeur[data-etat="a-valider"] { color: var(--fbx-warn-fg); background: var(--fbx-warn-bg); }

/* COMMENTAIRE REPLIÉ à deux lignes, déplié au clic. Les deux boards l'appliquent
   désormais : `review` ne repliait pas, si bien qu'un retour de dix lignes y
   poussait la ligne suivante hors de l'écran. Le rendu RICHE (gras, listes) est
   conservé des deux côtés — `feedbacks` l'aplatissait au texte brut. */
.fbx-cell-comment--clamp {
  overflow: hidden; text-overflow: ellipsis;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  cursor: pointer;
}
.fbx-cell-comment--clamp.is-expanded { -webkit-line-clamp: unset; display: block; }
.fbx-comment-more {
  display: none; font-size: var(--fbx-font-xs); color: var(--fbx-accent-fg); cursor: pointer;
  margin-top: var(--fbx-space-2); font-weight: var(--fbx-weight-medium);
}
.fbx-comment-more.is-on { display: inline-block; }
.fbx-comment-more:hover { text-decoration: underline; }

/* FIL DE RÉPONSES — un seul rendu pour les deux boards (P37 manque n°1.7 : cinq
   coexistaient dans l'outil). Retenu : le filet de gauche de `review`, plus
   lisible qu’une suite de cartouches gris qui se disputent la place.

   ⚠️ LE FILET EST EN COULEUR D’ACCENT, ET LA MESURE A CORRIGÉ DEUX RÉFLEXES
   (2026-08-25). Il était en `--fbx-n3`, la bordure courante : 1,23 sur le blanc,
   1,19 sur le zébrage, et 1,12 sur la ligne SURVOLÉE — le pire des trois est
   l’état où l’on est en train de lire, le fond de survol étant lui aussi bleuté.
   Un filet est un objet graphique : son seuil est 3:1.
   ⚠️ PREMIER RÉFLEXE ÉCARTÉ — « un bleu clair » : `--fbx-primary-light` (blue-500,
   la teinte de marque) donne 2,46 au survol, mieux et TOUJOURS sous le seuil.
   `--fbx-primary` tient 5,17 au pire en clair (blue-700) et 8,34 au pire en sombre
   (blue-300) : un seul jeton pour les deux thèmes, et c’est déjà la couleur des
   liens du fil, trois lignes plus bas.
   ⚠️ SECOND RÉFLEXE ÉCARTÉ — épaissir le trait : 3 px qu’on ne distingue pas du
   fond n’en deviennent pas plus lisibles à 4. C’est un défaut de CONTRASTE, pas
   de taille.
   Précédent à suivre plutôt qu’à réinventer : `.fbx-callout` porte déjà un filet
   de gauche en accent. */
.fbx-replies {
  margin-top: var(--fbx-space-8); border-left: var(--fbx-fil-filet) solid var(--fbx-accent-fg);
  padding: var(--fbx-space-6) 0 var(--fbx-space-6) var(--fbx-space-10);
  max-width: var(--fbx-board-lecture-w);
  display: flex; flex-direction: column; gap: var(--fbx-space-10);
}
/* ── UNE REPONSE : L'AVATAR DANS SA COLONNE, TOUT LE RESTE A COTE (2026-09-24)
   Retour FEEDBACK b4ae8197. L'avatar (18 px) vivait dans la rangee du nom, et le
   texte repartait dessous, au ras du filet : on ne voyait plus ou commencait
   une voix. Il passe a 24 px — la taille de base de `.fbx-avatar`, celle que la
   vitrine annoncait deja — et nom, date, texte et vignettes s'alignent sur une
   seule colonne a sa droite. Meme gabarit pour la note interne et pour le
   formulaire de reponse, qui porte l'avatar de celui qui repond.
   ⚠️ `.fbx-reply-avatar` EST POSE PAR LE SOCLE (`avatarFil`), jamais ecrit a la
   main dans une page : trois objets le portent, ils doivent rester alignes. */
.fbx-reply, .fbx-reply-form, .fbx-note:has(> .fbx-reply-avatar) {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--fbx-space-8); align-items: start;
}
.fbx-reply > :not(.fbx-reply-avatar):not(.fbx-actions),
.fbx-reply-form > :not(.fbx-reply-avatar),
.fbx-note:has(> .fbx-reply-avatar) > :not(.fbx-reply-avatar) { grid-column: 2; min-width: 0; }
/* ── LE FILET EST UN SEUL TRAIT, ET IL CHANGE DE COULEUR AVEC LA VOIX ─────
   (2026-09-24, second retour de Stephane sur b4ae8197)
   · LA NOTE INTERNE NE SE DECALE PLUS : dans le fil, elle recule de
     l'epaisseur du filet et de son retrait, si bien que SON filet ambre se pose
     exactement sur le trait bleu. Le fil se lit bleu, orange, bleu ; l'avatar de
     la note tombe dans la colonne de ceux des reponses.
   · LE FORMULAIRE PROLONGE LE TRAIT : il porte le meme filet que le fil et le
     suit sans marge, si bien que le trait descend le long de l'editeur. Seul,
     sans reponse au-dessus, il garde son ecart avec le commentaire.
   ⚠️ SPECIFICITE : `/review` pose `.fb-form { margin-top }` dans sa page, apres
   le socle ; la regle qui colle le formulaire au fil doit l'emporter, d'ou
   l'attribut dans son selecteur. */
.fbx-replies > .fbx-note {
  margin: 0 0 0 calc(-1 * (var(--fbx-fil-filet) + var(--fbx-space-10)));
  border-left-width: var(--fbx-fil-filet); padding-left: var(--fbx-space-10);
}
.fbx-cell-comment-wrap > [data-reply-host] { max-width: var(--fbx-board-lecture-w); }
.fbx-reply-form {
  margin-top: var(--fbx-space-8); padding: var(--fbx-space-4) 0 var(--fbx-space-6) var(--fbx-space-10);
  border-left: var(--fbx-fil-filet) solid var(--fbx-accent-fg);
}
.fbx-replies ~ [data-reply-host] > .fbx-reply-form { margin-top: 0; }
/* De l'air sous l'editeur, avant ses boutons. */
.fbx-reply-form > .fbx-form-actions { margin-top: var(--fbx-space-12); }
/* ⚠️ LE VIDE AU-DESSUS DU FIL : la boite du commentaire garde un rembourrage
   bas qui centre sa premiere ligne dans le plancher de cellule, et le fil y
   ajoutait sa marge. Fil ouvert, ce rembourrage ne sert plus a rien : ce qui
   suit le texte est le fil, pas le bas de la ligne. */
.fbx-grid-row .fbx-cell-comment-wrap.is-fil-open > .fbx-comment-box { padding-bottom: 0; }
/* ── LE DECLENCHEUR DE PIED DE FIL (2026-08-31) ──────────────────────────
   Il ferme le fil, donc il se cale a gauche comme les reponses au lieu de
   s'etirer sur toute la largeur (`.fbx-replies` est une colonne flex, dont les
   enfants s'etirent par defaut — un bouton de 560 px pour trois mots).
   ⚠️ IL S'EFFACE PENDANT LA SAISIE, comme la barre du haut : « on repond, on ne
   fait pas autre chose ». La condition decrit l'ETAT — un hote de reponse qui
   n'est plus vide — et ne nomme pas la classe du formulaire : celle-ci
   appartient a la page (`fb-`, pas `fbx-`), et la citer ici ferait du socle le
   lecteur d'un nom d'ecran. Meme raison que la regle en negatif de
   `.fbx-comment-box`, quinze lignes plus haut. */
.fbx-replies > .fbx-btn { align-self: flex-start; }
.fbx-cell-comment-wrap:has(> [data-reply-host]:not(:empty)) .fbx-replies > .fbx-btn { display: none; }
.fbx-reply-head { display: flex; align-items: baseline; gap: var(--fbx-space-8); }
.fbx-reply-author { font-weight: var(--fbx-weight-semibold); font-size: var(--fbx-font-sm); color: var(--fbx-text); }
.fbx-reply-date { font-size: var(--fbx-font-xs); color: var(--fbx-faint); }
.fbx-reply-comment { font-size: var(--fbx-font-base); color: var(--fbx-text-soft); line-height: 1.45; word-break: break-word; }
.fbx-reply-comment a { color: var(--fbx-accent-fg); word-break: break-all; }
.fbx-reply-thumbs { display: flex; flex-wrap: wrap; gap: var(--fbx-space-6); margin-top: var(--fbx-space-6); }
.fbx-reply-thumbs img {
  width: 56px; height: 56px; object-fit: cover; border-radius: var(--fbx-radius-sm);
  border: 1px solid var(--fbx-border); cursor: zoom-in;
}

/* RÉVÉLATION AU SURVOL — elle vivait dans les deux pages, chacune avec son
   sélecteur de ligne (`tr:hover` ici, `.fbx-grid-row:hover` là). Une seule
   mécanique de ligne : une seule règle. */
.fbx-grid-row .fbx-actions { margin-top: var(--fbx-space-8); }
/* ── LES ACTIONS D'UNE REPONSE NE RESERVENT PLUS DE HAUTEUR (2026-08-31) ────
   Revelees au survol, elles etaient invisibles mais occupaient leur place EN
   FLUX : chaque reponse du fil portait donc une bande vide de 32 px sous son
   texte, en permanence, et un fil de trois reponses en accumulait pres de cent.
   C'est le meme defaut, et le meme remede, que la barre de la ligne — elle
   passe en superposition, en HAUT A DROITE de la reponse, ou elle ne recouvre
   que la marge. La reponse ne grandit plus d'un pixel pour des commandes qu'on
   ne voit pas.
   ⚠️ `padding-right` SUR LA REPONSE, ET NON RIEN DU TOUT : sans lui, un texte
   assez long passerait SOUS les boutons au moment ou on les revele — le defaut
   que la barre du commentaire avait mis un mois a montrer. La reserve est de
   80 px, soit les deux boutons et leur ecart, et elle ne coute aucune hauteur. */
/* Jeton de composant nomme par son role : la reserve exacte des deux boutons
   d'une reponse et de leur ecart. */
.fbx-grid-row { --fbx-reply-actions-w: 80px; }
.fbx-reply { position: relative; padding-right: var(--fbx-reply-actions-w); }
.fbx-reply .fbx-actions {
  position: absolute; top: 0; right: 0; margin-top: 0;
}
/* ── ⚠️ INVISIBLE VEUT DIRE INERTE (2026-08-31) ───────────────────────────
   `opacity: 0` ne retire rien du flux des évènements : la barre du commentaire
   est superposée au texte, donc **au repos elle interceptait les clics d'une
   barre qu'on ne voit pas**. Sondé au navigateur : à 40 % de la largeur du
   texte on touchait « répondre », à 60 % « note interne », à 80 % « modifier »,
   à 95 % « supprimer » — pendant que le texte affichait un curseur de main sur
   toute sa largeur. Selon la place laissée au commentaire, **entre 23 % et 75 %
   de celui-ci ne répondait pas au clic**, et le geste cassé était celui posé le
   2026-08-25 : cliquer le commentaire pour ouvrir son fil.
   ⚠️ `pointer-events` ET NON `visibility: hidden` : celui-ci retirerait aussi
   l'élément du parcours de tabulation, donc `:focus-within` ne pourrait plus
   jamais révéler la barre au clavier — on corrigerait la souris en fermant
   l'accès clavier. `pointer-events: none` laisse le focus passer.
   ⚠️ LA RÉCIPROQUE EST OBLIGATOIRE sur chaque sélecteur qui rend l'opacité,
   `:focus-within` compris : sans elle, une barre révélée au clavier resterait
   inerte à la souris. */
.fbx-cell-date > .fbx-actions,
.fbx-reply .fbx-actions,
.fbx-grid-row .fbx-tag-add { opacity: 0; pointer-events: none; transition: opacity .12s; }
.fbx-grid-row:hover .fbx-cell-date > .fbx-actions,
.fbx-grid-row.is-menu-open .fbx-cell-date > .fbx-actions,
.fbx-grid-row:hover .fbx-tag-add,
.fbx-reply:hover .fbx-actions,
.fbx-actions:focus-within,
.fbx-tag-add:focus {
  opacity: 1;
  pointer-events: auto;
}
/* Sans survol (tactile), rien ne se révèle : les actions restent visibles. */
@media (hover: none) {
  /* ⚠️ `pointer-events` REVIENT AVEC L'OPACITÉ, sans quoi le tactile hériterait
     de l'inertie posée plus haut : les actions seraient visibles en permanence
     et ne répondraient à aucun doigt. */
  .fbx-cell-date > .fbx-actions,
  .fbx-reply .fbx-actions,
  .fbx-grid-row .fbx-tag-add { opacity: 1; pointer-events: auto; }

  /* ── LE FOND DE LA BARRE SUIT L'ÉTAT RÉEL DE LA LIGNE (2026-08-21) ───────
     Ces trois lignes rendues visibles en permanence ci-dessus ont un prix qui
     n'avait pas été payé : la barre du commentaire porte un fond, et ce fond
     était la teinte de SURVOL — un état qu'un doigt ne produit jamais. Elle
     posait donc une plaque colorée sur une ligne au repos, et **elle en fausse
     une sur deux** : sous le zébrage, la vraie couleur n'est pas la même d'une
     ligne à l'autre. C'est justement ce que le survol permettait d'ignorer, sa
     teinte étant unique — mais seulement TANT QU'ON SURVOLE.
     ⚠️ ON POSE UN JETON, ON NE REDÉCLARE PAS LE `background`. Une seconde
     déclaration ici l'emporterait aussi en mode CARTES, où la barre repasse
     dans le flux **sans fond** (bloc `@container` plus bas) : on lui rendrait un
     dégradé dont elle n'a pas besoin, en tactile étroit — c'est-à-dire dans le
     cas le plus fréquent. En passant par la variable, la seule règle qui écrit
     `background` reste celle du board, et l'ordre du fichier tranche comme
     avant.
     ⚠️ `:nth-of-type(odd)` EST LE SÉLECTEUR DU ZÉBRAGE, RECOPIÉ TEL QUEL : le
     réécrire autrement (`even`, `:nth-child`) décalerait d'une ligne, l'en-tête
     étant un `<div>` frère compté dans la série. */
  /* ⚠️ SANS SURVOL, ON N'ECHANGE PAS — ON EMPILE (2026-08-31). La barre est
     visible en permanence ici, et la date n'est masquee qu'au `:hover`, qu'un
     doigt ne produit jamais : superposees, elles se chevaucheraient. Elle
     repasse donc DANS LE FLUX, sous la date, comme en mode cartes.
     ⚠️ LE DISPOSITIF `--fbx-actions-bg` A ETE RETIRE AVEC ELLE : il existait
     pour que la barre, superposee en permanence au doigt, porte le fond REEL de
     sa ligne (surface ou zebrage) au lieu de la teinte de survol. Une barre qui
     ne recouvre plus rien n'a plus besoin de se confondre avec quoi que ce
     soit. *Le probleme a disparu avec sa cause, pas avec son remede.* */
  .fbx-grid-row .fbx-cell-date > .fbx-actions {
    position: static; height: auto; margin-top: var(--fbx-space-6);
  }

  /* LA BULLE DES ENTRÉES DE LA BARRE (2026-09-17) : sans survol, elle n'a pas
     de déclencheur — et un toucher émule un survol juste avant de suivre le
     lien, elle clignoterait. Même arbitrage que le widget. */
  .fbx-bulle { display: none; }

  /* ── CIBLES TACTILES (2026-08-20) ─────────────────────────────────────────
     Relevé au doigt, pas à l'œil : ces quatre-là passaient SOUS le minimum de
     24 × 24 px (WCAG 2.5.8, niveau AA) — croix d'effacement de l'intervalle de
     dates 17 × 17, case de sélection en lot 16 × 16, ancre de page 10 × 16 —
     et les boutons d'action restaient à 28 × 28, au-dessus du minimum mais
     loin des 44 px de confort, alors qu'ils portent *supprimer* à côté de
     *répondre*. Deux gestes voisins de 28 px, c'est la suppression atteinte en
     visant la réponse.

     ⚠️ SOUS `(hover: none)` SEULEMENT, ET C'EST LE POINT. La question n'est pas
     la taille de l'écran mais le POINTEUR : une souris vise au pixel, un doigt
     couvre 9 mm. Écrites sous un `max-width`, ces règles auraient grossi les
     boutons d'une fenêtre étroite sur un poste à souris — où rien ne le
     justifie — et les auraient laissés à 28 px sur une tablette large, où le
     doigt est pourtant le seul pointeur.

     ⚠️ LA CIBLE GROSSIT, PAS LE DESSIN. L'icône garde sa taille : ce qu'on
     agrandit est la surface qui reçoit le doigt. Une interface qui grossit ses
     traits en tactile change d'aspect sans raison ; ici l'aspect ne bouge pas,
     seule la zone atteignable s'étend. */
  .fbx-act-btn { width: 40px; height: 40px; }
  .fbx-daterange-x { padding: var(--fbx-space-6); }
  /* La case de sélection est le seul contrôle d'une action EN LOT : la manquer
     coche la voisine, et l'action porte alors sur le mauvais retour. */
  .fbx-cell-select input[type="checkbox"] { width: 20px; height: 20px; }
  /* L'ancre de page peut n'être qu'un caractère (« /x ») : sans hauteur de
     ligne propre, sa cible vaut celle du glyphe. */
  .fbx-row-page a { display: inline-block; padding: var(--fbx-space-4) 0; }
  /* EN TACTILE, LA PASTILLE MESURE 36 PX ET SE TOUCHE SUR 44 (2026-09-25).
     ⚠️ LA CIBLE GROSSIT, PAS LE DESSIN : à 44 px visibles, une rangée de sept
     pastilles doublerait de hauteur au téléphone. Le `::before` déborde de 4 px
     en haut et en bas ; les 8 px de gouttière entre deux rangées l'absorbent. */
  .fbx-status-chip { position: relative; min-height: var(--fbx-pastille-tactile); }
  .fbx-status-chip::before {
    content: ''; position: absolute; left: 0; right: 0;
    top: calc((var(--fbx-pastille-tactile) - var(--fbx-cible-tactile)) / 2);
    bottom: calc((var(--fbx-pastille-tactile) - var(--fbx-cible-tactile)) / 2);
  }
  /* EN TACTILE, UN CHAMP S'ÉCRIT À 16 PX (2026-09-25). En dessous, Safari sur
     iOS agrandit la page au moment où le champ prend le focus, et ne la rend
     pas à la sortie : l'écran reste zoomé, et la barre du bas sort du cadre.
     ⚠️ L'ÉDITEUR RICHE EST CONCERNÉ AUSSI : un `contenteditable` déclenche le
     même zoom qu'un `<input>`.
     ⚠️ `.fbx-body` EN TÊTE : ce bloc précède la déclaration des champs dans le
     fichier, et à spécificité égale c'est la dernière règle qui gagne. */
  .fbx-body .fbx-input,
  .fbx-body .fbx-textarea,
  .fbx-body .fbx-editor-area,
  .fbx-body .fbx-login-box input { font-size: var(--fbx-font-lg); }
}

/* `.fbx-sr-only` — ajoutée puis RETIRÉE le 2026-08-04, le jour même. Elle devait
   masquer les intitulés Image / Capture / Lien du board ; ces intitulés restent
   affichés. Une classe que plus rien ne consomme est du code mort, y compris
   quand elle vient d'être écrite — c'est la règle que la page *Recette &
   couverture* du design system énonce, elle vaut d'abord pour nous. */

/* Étroit : l'en-tête disparaît et la ligne se met en cartes.

   ⚠️ LE REPLI SE DÉCIDE SUR LA PLACE DISPONIBLE, PAS SUR LA TAILLE DE LA FENÊTRE
   (2026-08-20) — d'où `@container` et non `@media`. La règle précédente valait
   `max-width: 720px`, mesuré sur la FENÊTRE, et elle était fausse deux fois.

   ⚠️ (1) ELLE NE CONNAISSAIT PAS LE RAIL. Entre la barre dépliée (244 px) et le
   rail (64 px), la place réelle change de 180 px sans que la fenêtre bouge d'un
   pixel : un seuil de fenêtre est juste dans un état et faux dans l'autre. Une
   requête de conteneur lit ce qui reste VRAIMENT, donc replier le rail suffit à
   ramener le tableau — le geste est réversible et se découvre tout seul.

   ⚠️ (2) ELLE ÉTAIT CALIBRÉE SUR LA SEULE VARIANTE RELECTEUR (610 px de
   colonnes fixes, soit un seuil juste à 2 px près) et n'a jamais été recalculée
   quand la variante admin a reçu la SÉLECTION EN LOT (44) et les TAGS (160),
   qui portent son besoin à 842 px. Résultat mesuré à 834 px de fenêtre : le
   tableau restait un tableau sans avoir la place de son commentaire, et le
   `1fr` tombait à ZÉRO — colonne principale invisible, page du retour cassée un
   caractère par ligne. Une zone morte de plus de 200 px de large, celle d'un
   iPad en portrait ou d'une fenêtre à moitié d'écran.

   ⚠️ LES DEUX SEUILS SE CALCULENT, ILS NE SE CHOISISSENT PAS : colonnes fixes
   + gouttières + `--fbx-board-comment-min`. Rien d'autre — le rail et le
   padding ne sont plus dans l'équation, c'est tout l'intérêt du conteneur.

   ⚠️ RECALCULÉS LE 2026-08-21 AVEC LES GABARITS, DANS LE MÊME GESTE. Six
   colonnes au lieu de huit (Tags versée dans la cellule commentaire, Image et
   Capture fondues en Pièces), et la date ramenée de 92 à 76 px :
     relecteur : 514 fixes + 6x12 gouttieres + 220 plancher + 28 rembourrage =  834
     admin     : 678 fixes + 8x12 gouttieres + 220 plancher + 28 rembourrage = 1022

   ⚠️ TROISIEME RECALCUL EN DEUX JOURS, ET AUCUN N'ETAIT FACULTATIF : la fusion
   des pieces, le retour de la colonne Tags, puis les 12 px que la PASTILLE du
   menu de statut prend au libelle. *Une colonne qui change de contenu change de
   besoin* — c'est la seule regle a retenir de cette ligne.

   ⚠️ LE REMBOURRAGE DE LA LIGNE ENTRE DANS LE CALCUL, ET IL N'Y ENTRAIT PAS.
   La formule d'origine s'arretait a « colonnes fixes + gouttieres + plancher »
   et oubliait les 2 x 14 px de `.fbx-grid-row` : la requete de conteneur mesure
   la BORDURE de `.fbx-grid--board`, la grille se repartit dans le CONTENU de la
   ligne. Il manquait donc 28 px aux deux seuils, et il restait sous chacun une
   bande de 28 px ou le tableau tenait encore alors que son commentaire etait
   deja sous son plancher — le `minmax()` y produisait une barre de defilement
   au lieu d'une colonne evaporee, ce pour quoi il est la, mais un debordement
   reste un debordement. Verifie par balayage (375, 720, 744, 750, 772, 773,
   802, 810, 830, 831, 954, 1062, 1200, 1600) : plus aucune largeur ne laisse le
   commentaire sous 220 px.

   ⚠️ LE GAIN DU LOT RESTE MINCE SUR LE BOARD D'ADMINISTRATION, ET C'EST UN
   ARBITRAGE, PAS UN OUBLI : 1062 avant le lot, 1022 apres. La fusion des
   colonnes de pieces avait rendu 232 px ; la colonne Tags en a repris 154, la
   pastille de statut 12, la colonne Fil 42. Chacune de ces trois reprises a ete
   demandee apres lecture a l'ecran, et chacune vaut mieux que le seuil qu'elle
   coute. Le board RELECTEUR, lui, garde son gain (830 -> 846 : il repasse en
   cartes un peu plus tot, pour six colonnes devenues sept).
   ⚠️ SI CE SEUIL DEVENAIT GENANT, la piste a suivre n'est pas de rogner des
   largeurs deja serrees mais de fondre Fil et Pieces en UNE colonne
   d'indicateurs — ils portent le meme jeton, seule leur nature differe. C'est
   72 px a reprendre, et le prix est un en-tete qui ne nomme plus exactement son
   contenu.

   ⚠️ CES DEUX SEUILS SONT DÉSORMAIS DÉRIVÉS, PLUS CALCULÉS À LA MAIN
   (2026-08-21) : `boardSeuil('board' | 'admin')` les tire de `BOARD_COLONNES`
   (`widget/feedback-shared.js`) et `test/board-gabarit.test.js` les compare à
   ces deux lignes. C'est la fin de la classe de défaut décrite ci-dessus —
   *une colonne ajoutée à la table refait la formule d'elle-même, et un chiffre
   resté en arrière ne peut plus être fusionné.*

   ⚠️ LA COLONNE « BLOQUANT » (2026-09-21) LES A FAIT MONTER DE 84 PX CHACUN,
   ET C'EST UN ARBITRAGE : 838 -> 922 cote relecteur, 1038 -> 1122 cote admin
   (72 px de colonne plus une gouttiere). Demandee en seance comme une colonne
   au repos, plutot qu'un badge ou une bascule au survol. Le board
   d'administration passe donc en cartes sous ~1 414 px de fenetre barre
   depliee, ~1 234 px en rail. La piste de la ligne ci-dessus (fondre Fil et
   Pieces) reste celle a suivre si ce seuil gene. */
.fbx-grid--board { container-type: inline-size; container-name: board; }

@container board (max-width: 864px) {
  .fbx-grid--board > .fbx-grid-head { display: none; }
  .fbx-grid--board > .fbx-grid-row { grid-template-columns: 1fr; gap: var(--fbx-space-6); }
  /* ⚠️ LE PLANCHER DE CELLULE TOMBE AVEC LE TABLEAU : en cartes, les cellules
     sont empilees les unes sous les autres — leur imposer 42 px chacune
     ajouterait une demi-ligne de vide sept fois par retour. Il n'existe que
     pour aligner des cellules COTE A COTE. */
  .fbx-grid--board > .fbx-grid-row > * { min-height: 0; }
  /* ⚠️ EN CARTES, LA BARRE D'ACTIONS REDEVIENT UN BLOC DU FLUX : superposée,
     elle recouvrirait le commentaire, qui occupe alors toute la largeur. La
     rangée méta repasse sous le texte pour la même raison — il n'y a plus de
     « à côté » quand tout est empilé. */
  /* ⚠️ LA BARRE VIT DANS LA CELLULE DE DATE depuis le 2026-08-31, et ce
     selecteur visait encore l'ancienne : elle restait donc SUPERPOSEE en mode
     cartes, la ou toutes les cellules s'empilent et ou plus rien n'a de « a
     cote ». Releve au navigateur — ni le `grep` ni les tests ne pouvaient le
     voir, la regle existait et son selecteur ne trouvait plus rien. */
  .fbx-grid-row .fbx-cell-date > .fbx-actions {
    position: static; height: auto; padding-left: 0; background: none; margin-top: var(--fbx-space-6);
  }
  .fbx-grid-row .fbx-comment-box { min-height: 0; padding-block: 0; }
  /* Vide, la colonne Fil n'a rien a dire une fois empilee : elle disparait,
     comme les cellules de pieces sans piece. */
  .fbx-cell-media:has(> .fbx-cell-empty:only-child) { display: none; }
  .fbx-cell-tags:empty, .fbx-cell-fil:empty, .fbx-cell-ref:empty { display: none; }
  /* En cartes, le nom reprend sa place à côté de l'avatar : pas d'en-tête, et
     la place ne manque plus. */
  .fbx-grid--board .fbx-grid-row > .fbx-author > .fbx-author-name { position: static; clip-path: none; }
  .fbx-grid--board .fbx-grid-row > .fbx-author { justify-content: flex-start; }
  .fbx-cell-status { max-width: 160px; }
  /* L'en-tete a disparu : l'interrupteur « Bloquant » dit ce qu'il bascule. */
  .fbx-cell-bloquant-libelle { display: inline; }
  /* ⚠️ UNE CELLULE VIDE PERD SON SENS AVEC L'EN-TÊTE (2026-08-20). En tableau,
     le tiret d'une cellule Image ou Capture se lit sous son intitulé : il dit
     « pas d'image ». Empilé en carte, l'en-tête ayant disparu, il ne reste que
     deux tirets seuls au milieu du retour, qui n'annoncent plus rien et
     ajoutent deux lignes à chaque carte. L'absence d'image se lit déjà par
     l'absence d'image ; les cellules REMPLIES, elles, restent — une vignette
     n'a pas besoin qu'on la nomme. */
}
/* Le board d'administration se replie PLUS TÔT parce qu'il porte deux colonnes
   de plus. Bloc séparé et placé APRÈS : à spécificité égale (0,2,0), c'est
   l'ordre du fichier qui tranche dans la bande où les deux s'appliquent. */
@container board (max-width: 1066px) {
  .fbx-grid--board-admin > .fbx-grid-head { display: none; }
  .fbx-grid--board-admin > .fbx-grid-row { grid-template-columns: 1fr; gap: var(--fbx-space-6); }
  .fbx-grid--board-admin > .fbx-grid-row > * { min-height: 0; }
  .fbx-grid--board-admin > .fbx-grid-row .fbx-cell-date > .fbx-actions {
    position: static; height: auto; padding-left: 0; background: none; margin-top: var(--fbx-space-6);
  }
  .fbx-grid--board-admin > .fbx-grid-row .fbx-comment-box { min-height: 0; padding-block: 0; }
  .fbx-grid--board-admin .fbx-cell-media:has(> .fbx-cell-empty:only-child) { display: none; }
  .fbx-grid--board-admin .fbx-cell-fil:empty, .fbx-grid--board-admin .fbx-cell-ref:empty { display: none; }
  .fbx-grid--board-admin .fbx-grid-row > .fbx-author > .fbx-author-name { position: static; clip-path: none; }
  .fbx-grid--board-admin .fbx-grid-row > .fbx-author { justify-content: flex-start; }
  .fbx-grid--board-admin .fbx-cell-status { max-width: 160px; }
  .fbx-grid--board-admin .fbx-cell-bloquant-libelle { display: inline; }
}

/* ⚠️ EMPILEE, LA CELLULE DE DATE REPREND LE FLUX (2026-08-31) : calee a droite,
   elle l'est parce qu'elle ferme la LIGNE et que la barre s'aligne sur son bord.
   En cartes il n'y a plus de derniere colonne — la date et ses actions se
   retrouveraient seules a droite d'une pile alignee a gauche. */
@container board (max-width: 1066px) {
  .fbx-grid-row > .fbx-cell-date { align-items: flex-start; }
}

/* ═══ LA LIGNE COMPACTE — le board au téléphone (2026-09-25) ═══════════════
   Troisième présentation du board, après le tableau et les cartes : une ligne
   façon messagerie. Avatar à gauche ; nom et date ; commentaire sur deux
   lignes ; une ligne d'infos (statut, Bloquant, Interne, tag, page, comptes).
   Un toucher la déplie sur place : texte entier, pièces, « Voir sur la page »,
   résumé du fil, note, puis [Répondre] [statut] [⋯].

   ⚠️ LE SEUIL : 480 PX DE BOARD, MESURÉS LE 25/09 (plan boards-mobile, §9).
   Un téléphone en portrait laisse 342 px (382 au plus large) ; la configuration
   d'ordinateur la plus étroite — 834 px de fenêtre, barre latérale dépliée —
   en laisse 542, qui garde donc ses cartes. `@container`, jamais `@media` :
   c'est la place du board qui compte, pas la fenêtre.
   ⚠️ LE JAVASCRIPT LIT LE MODE ICI (`--fbx-board-mode`, `TPXShared.boardCompact`)
   pour savoir si un toucher déplie la ligne : un seul endroit porte le seuil.
   ⚠️ UN SEUL DOM, CELUI DU TABLEAU. Les cellules nom, commentaire et date
   passent en `display: contents` : leurs enfants deviennent des éléments de la
   grille de la ligne et se placent par zone. Les éléments propres au mode
   (`[data-compact]`) sont masqués partout ailleurs.
   ⚠️ LES SÉLECTEURS COMMENCENT PAR `.fbx-grid--board .fbx-grid-row`, SANS `>` :
   la présentation est la même pour les deux boards, et `board-gabarit` lit
   comme seuil de variante toute règle qui commence par `.fbx-grid--board >` ou
   `.fbx-grid--board-admin`.
   ⚠️ ELLE S'ÉCRIT APRÈS LES CARTES, dont les règles valent aussi sous 480 px :
   à spécificité égale, l'ordre du fichier tranche. */
.fbx-grid-row [data-compact] { display: none; }

/* AU TÉLÉPHONE, LA LISTE VA D'UN BORD À L'AUTRE (2026-09-25). La gouttière de
   24 px et le cadre de carte coûtaient 48 px sur 390 — la place de la rangée
   [Répondre] [statut] [⋯], qui ne tenait plus. Le board reprend la gouttière
   par une marge négative et perd son cadre.
   ⚠️ LA REQUÊTE PORTE SUR LE CONTENEUR `page` (`.fbx-main`), PAS SUR `board` :
   une requête de conteneur ne peut pas styler son propre conteneur.
   ⚠️ 432 = 480 − 2 × 24 : à 432 px de page, le board élargi mesure 480 et reste
   compact. Au-dessus (433 à 480 px de page), il est compact AVEC sa gouttière ;
   il n'existe aucune largeur où il serait en cartes et bord à bord. */
@container page (max-width: 432px) {
  .fbx-grid--board {
    margin-inline: calc(-1 * var(--fbx-space-24));
    border-radius: 0; box-shadow: none;
  }
}

@container board (max-width: 480px) {
  .fbx-grid--board .fbx-grid-row {
    --fbx-board-mode: compact;
    grid-template-columns: var(--fbx-board-avatar-compact) minmax(0, 1fr) auto auto;
    grid-template-areas:
      "avatar nom     nom    date"
      "avatar texte   texte  texte"
      "avatar infos   infos  infos"
      "avatar pieces  pieces pieces"
      "avatar lien    lien   lien"
      "avatar fil     fil    fil"
      "avatar liste   liste  liste"
      "avatar note    note   note"
      "avatar agir    statut plus"
      "avatar reponse reponse reponse";
    column-gap: var(--fbx-space-12); row-gap: 0;
    padding: var(--fbx-space-12) var(--fbx-space-16);
    align-items: start; cursor: pointer;
  }
  /* Le zébrage d'un tableau aide l'œil à suivre une ligne d'une colonne à
     l'autre ; une liste de messages n'a pas de colonne à suivre. */
  .fbx-grid--board .fbx-grid-row:nth-of-type(odd) { background: var(--fbx-surface); }
  .fbx-grid--board .fbx-grid-row.is-deplie { cursor: auto; }
  /* ⚠️ UN TOUCHER LAISSE LA LIGNE EN `:hover` jusqu'au toucher suivant (survol
     « collant » des navigateurs mobiles) : la teinte de survol marquerait la
     dernière ligne touchée comme si elle était sélectionnée. */
  .fbx-grid--board .fbx-grid-row:hover { background: var(--fbx-surface); }

  /* Ce qui n'a pas sa place dans la ligne compacte, dépliée ou non. */
  .fbx-grid--board .fbx-grid-row > .fbx-cell-select,
  .fbx-grid--board .fbx-grid-row > .fbx-cell-ref,
  .fbx-grid--board .fbx-grid-row > .fbx-cell-fil,
  .fbx-grid--board .fbx-grid-row > .fbx-cell-tags,
  .fbx-grid--board .fbx-grid-row > .fbx-cell-bloquant,
  .fbx-grid--board .fbx-grid-row .fbx-comment-more { display: none; }

  /* Avatar et nom : la cellule s'efface, ses deux enfants se placent. */
  .fbx-grid--board .fbx-grid-row > .fbx-author { display: contents; }
  .fbx-grid--board .fbx-grid-row > .fbx-author > .fbx-avatar {
    grid-area: avatar; width: var(--fbx-board-avatar-compact); height: var(--fbx-board-avatar-compact);
  }
  .fbx-grid--board .fbx-grid-row > .fbx-author > .fbx-author-name {
    grid-area: nom; align-self: baseline; min-width: 0;
    font-size: var(--fbx-font-md); font-weight: var(--fbx-weight-semibold);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* La date COURTE seule, en haut à droite (« 10:42 », « Hier », « 23/09 ») :
     dans une liste de messages, elle situe. Badges et barre d'actions
     s'effacent (l'audience passe dans la ligne d'infos, les actions dans la
     rangée de la ligne dépliée). */
  .fbx-grid--board .fbx-grid-row > .fbx-cell-date { display: contents; }
  .fbx-grid--board .fbx-grid-row > .fbx-cell-date > * { display: none; }
  /* ⚠️ `visibility: visible` : sur ordinateur, la date s'efface au survol pour
     laisser la place aux actions — et un toucher laisse la ligne en survol. */
  .fbx-grid--board .fbx-grid-row > .fbx-cell-date > .fbx-date-courte {
    display: block; visibility: visible; grid-area: date; align-self: baseline; justify-self: end; padding-right: 0;
    font-size: var(--fbx-font-sm); color: var(--fbx-text-muted); white-space: nowrap;
  }

  /* Le commentaire et ce que la ligne compacte ajoute. */
  .fbx-grid--board .fbx-grid-row > .fbx-cell-comment-wrap { display: contents; }
  .fbx-grid--board .fbx-grid-row .fbx-comment-box {
    grid-area: texte; min-height: 0; padding-block: 0; margin-top: var(--fbx-space-2);
  }
  .fbx-grid--board .fbx-grid-row .fbx-cell-comment {
    font-size: var(--fbx-font-md); line-height: var(--fbx-leading-snug); cursor: inherit;
  }
  .fbx-grid--board .fbx-grid-row .fbx-resume {
    grid-area: infos; display: flex; align-items: center; gap: var(--fbx-space-6);
    min-width: 0; margin-top: var(--fbx-space-6); white-space: nowrap;
  }
  .fbx-grid--board .fbx-grid-row .fbx-resume > * { flex-shrink: 0; }
  .fbx-grid--board .fbx-grid-row .fbx-resume > .fbx-resume-page {
    flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    font-size: var(--fbx-font-sm); color: var(--fbx-text-muted);
  }
  .fbx-grid--board .fbx-grid-row .fbx-resume-page.is-sans-page { font-style: italic; }
  /* La référence ouvre la ligne d'infos : au téléphone la colonne « Réf. »
     disparaît, et c'est le seul endroit où le numéro reste lisible. Le lien
     vers la page, lui, est dans la ligne dépliée (« Voir sur la page »). */
  .fbx-grid--board .fbx-grid-row .fbx-resume-ref {
    font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold);
    font-variant-numeric: tabular-nums; color: var(--fbx-text-soft);
  }
  .fbx-grid--board .fbx-grid-row .fbx-resume-comptes {
    display: inline-flex; gap: var(--fbx-space-10); margin-left: auto; padding-left: var(--fbx-space-6);
  }
  .fbx-grid--board .fbx-grid-row .fbx-resume-compte {
    display: inline-flex; align-items: center; gap: var(--fbx-space-4);
    font-size: var(--fbx-font-sm); color: var(--fbx-text-muted);
  }

  /* ── REPLIÉE : le commentaire sur deux lignes, rien d'autre ─────────────── */
  .fbx-grid--board .fbx-grid-row:not(.is-deplie) > .fbx-cell-media,
  .fbx-grid--board .fbx-grid-row:not(.is-deplie) > .fbx-cell-status,
  .fbx-grid--board .fbx-grid-row:not(.is-deplie) .fbx-cell-comment-wrap > .fbx-replies,
  .fbx-grid--board .fbx-grid-row:not(.is-deplie) .fbx-cell-comment-wrap > .fbx-note,
  .fbx-grid--board .fbx-grid-row:not(.is-deplie) .fbx-cell-comment-wrap > [data-reply-host] { display: none; }
  .fbx-grid--board .fbx-grid-row:not(.is-deplie) .fbx-cell-comment--clamp {
    display: -webkit-box; -webkit-line-clamp: 2; overflow: hidden;
  }

  /* ── DÉPLIÉE : texte entier, pièces, lien, fil, note, rangée d'actions ──── */
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-comment--clamp {
    display: block; -webkit-line-clamp: unset; overflow: visible;
  }
  .fbx-grid--board .fbx-grid-row.is-deplie > .fbx-cell-media {
    grid-area: pieces; display: flex; flex-wrap: wrap; gap: var(--fbx-space-8);
    min-height: 0; margin-top: var(--fbx-space-12);
  }
  .fbx-grid--board .fbx-grid-row.is-deplie > .fbx-cell-media:has(> .fbx-cell-empty:only-child) { display: none; }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-piece-vignette {
    display: block; width: var(--fbx-board-vignette-w); height: var(--fbx-board-vignette-h);
    object-fit: cover; border-radius: inherit;
  }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-media > .fbx-piece {
    width: var(--fbx-board-vignette-w); height: var(--fbx-board-vignette-h); padding: 0; overflow: hidden;
  }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-media > .fbx-piece > svg { display: none; }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-lien-page {
    grid-area: lien; display: inline-flex; align-items: center; gap: var(--fbx-space-6);
    min-height: var(--fbx-cible-tactile); justify-self: start;
    font-size: var(--fbx-font-md); font-weight: var(--fbx-weight-semibold); color: var(--fbx-accent-fg); text-decoration: none;
  }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-fil-resume {
    grid-area: fil; display: flex; align-items: center; gap: var(--fbx-space-8); justify-self: stretch;
    min-height: var(--fbx-cible-tactile); padding: var(--fbx-space-6) var(--fbx-space-10);
    border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius); background: var(--fbx-surface-sunken);
    font: inherit; font-size: var(--fbx-font-sm); color: var(--fbx-text-soft); text-align: left; cursor: pointer;
  }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-comment-wrap > .fbx-replies { grid-area: liste; margin-top: var(--fbx-space-8); }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-comment-wrap > .fbx-note { grid-area: note; }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-comment-wrap > [data-reply-host] { grid-area: reponse; }
  /* LA NOTE INTERNE SE VOIT DÈS LA LIGNE DÉPLIÉE, AVANT LE FIL : elle vit dans
     la liste des réponses, triée par date ; fil fermé, on ne garde qu'elle. */
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-comment-wrap:not(.is-fil-open) > .fbx-replies:has(> .fbx-note) { display: block; }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-comment-wrap:not(.is-fil-open) > .fbx-replies > :not(.fbx-note) { display: none; }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-cell-comment-wrap:not(.is-fil-open) > .fbx-replies > .fbx-note { display: block; }

  /* La rangée [Répondre] [statut] [⋯] : « Répondre » prend la largeur, le
     statut garde la sienne. Le sélecteur est CELUI DE LA CELLULE. */
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-compact-action {
    display: inline-flex; min-height: var(--fbx-cible-tactile); margin-top: var(--fbx-space-12);
  }
  /* ⚠️ `justify-self: stretch` : un `<button>` ne s'étire pas seul dans une
     cellule de grille, et son icône était alors comprimée à 0 px. */
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-compact-action[data-action="repondre"] { grid-area: agir; justify-self: stretch; justify-content: center; }
  .fbx-grid--board .fbx-grid-row .fbx-compact-action > svg { flex-shrink: 0; }
  .fbx-grid--board .fbx-grid-row.is-deplie .fbx-compact-action[data-action="actions"] {
    grid-area: plus; width: var(--fbx-cible-tactile); justify-content: center; padding: 0;
  }
  .fbx-grid--board .fbx-grid-row.is-deplie > .fbx-cell-status {
    grid-area: statut; display: block; max-width: none; min-height: 0; margin-top: var(--fbx-space-12);
  }
  .fbx-grid--board .fbx-grid-row.is-deplie > .fbx-cell-status .fbx-dd { width: auto; min-height: var(--fbx-cible-tactile); }

  /* LE MODE SÉLECTION (2026-09-25) : la ligne choisie prend la teinte d'accent,
     et une coche remplace son avatar. */
  .fbx-grid--board .fbx-grid-row:has(input[data-action="select"]:checked) { background: var(--fbx-accent-bg); }
  .fbx-grid--board .fbx-grid-row:has(input[data-action="select"]:checked) > .fbx-author > .fbx-avatar { visibility: hidden; }
  .fbx-grid--board .fbx-grid-row:has(input[data-action="select"]:checked) .fbx-coche {
    grid-area: avatar; display: flex; align-items: center; justify-content: center;
    width: var(--fbx-board-avatar-compact); height: var(--fbx-board-avatar-compact);
    border-radius: var(--fbx-radius-full); background: var(--fbx-accent-fill); color: var(--fbx-on-accent);
  }
}

/* ── CELLULES DES BOARDS — le CONTRAT de tableau ────────────────────────────
   `review` et `feedbacks` montrent LE MÊME OBJET : un retour. Une même donnée
   doit donc s'y afficher pareil, quelle que soit la mécanique de mise en page
   (grille pour `review`, <table> pour `feedbacks`). Avant ces classes, les deux
   écrans avaient dérivé sans que rien ne le signale — mesuré le 2026-08-02 :
   date 12px grise contre 13px noire, avatar 28 contre 24, commentaire 14/#333
   contre 13/#555, et une icône de lien grise d'un côté, violette de l'autre.

   RÉFÉRENCE RETENUE : `review`. La date passe au second plan (elle situe, elle
   ne se lit pas), le commentaire est l'information principale.

   Ce qui reste PROPRE à un écran, et c'est la seule liste : la colonne Tags et
   le bouton « Réassigner l'URL », tous deux réservés à `feedbacks` (admin). */
/* Le sélecteur composé n'est pas décoratif : `.fbx-table tbody td` pose déjà
   `font-size: 13px` avec une spécificité de (0,1,2) — une classe seule (0,1,0)
   perdrait, et la date resterait à 13px dans une <table> tout en valant 12px
   dans une grille. Même composant, deux rendus : exactement ce qu'on corrige. */
.fbx-cell-date, .fbx-table tbody td.fbx-cell-date { font-size: var(--fbx-font-sm); color: var(--fbx-text-muted); line-height: 1.35; }
/* Sur un board, la date tient sur UNE ligne depuis la variante dense : sans
   `nowrap`, une colonne de 76 px la couperait au premier `/` et la ligne
   reprendrait la hauteur qu'on vient de lui retirer. Le kanban, lui, affiche une
   date relative de longueur variable et garde le retour a la ligne. */
.fbx-grid-row .fbx-cell-date { white-space: nowrap; }
/* MESURE DE LECTURE — plafond posé le 2026-08-04, avec le retrait du plafond de
   PAGE. Les deux vont ensemble : sans lui, la cellule passait de 482 à 743 px et
   la ligne de 71 à 118 caractères, soit dégrader la lecture au nom de l'espace
   qu'on venait de gagner.

   ⚠️ LE PLAFOND EST SUR LE TEXTE, PAS SUR LA CELLULE. `.fbx-cell-comment-wrap`
   garde toute sa largeur : la page, la note interne, le fil de réponses et la
   barre d'actions en profitent — c'est là que va l'espace récupéré. Seule la
   mesure de lecture est bornée.

   560 px, et NON les ~680 pressentis : mesuré au rendu, en comptant les
   caractères réellement posés sur la première ligne (Range + getBoundingClientRect,
   texte français courant, 14 px system-ui), pas en divisant par une largeur
   moyenne — les deux méthodes donnent des résultats très différents.

     482 px (avant)  →  71 car.      560 px →  82 car.      620 px →  99 car.
     680 px          → 107 car.      743 px (sans plafond) → 118 car.

   La plage confortable est 60–80 caractères, et au-delà de ~90 l'œil ne retrouve
   plus le début de la ligne suivante. **680 px donne 107 caractères** : il ne
   tient pas le critère qu'il était censé servir. 560 px est la valeur qui
   élargit le plus tout en restant dans la plage. */
.fbx-cell-comment {
  font-size: var(--fbx-font-md); color: var(--fbx-text); line-height: var(--fbx-leading-normal); word-wrap: break-word;
  max-width: var(--fbx-board-lecture-w);
}
.fbx-cell-comment a { color: var(--fbx-accent-fg); word-break: break-all; }

/* UNE MENTION RECONNUE (2026-10-05) — le « @Nom » qui a prévenu quelqu'un, en
   bleu et sans soulignement : un lien se souligne, une mention ne mène nulle part.
   Posée par `TPXShared.marquerMentions` à partir des noms que le serveur a
   reconnus — une faute de frappe reste noire. Même bleu que les liens
   (`--fbx-accent-fg`, qui tient dans les deux thèmes), demi-gras pour ne pas se
   confondre avec un lien au repos. Vaut partout où le socle rend un commentaire :
   board, fil de réponses, `/corrections`. */
.fbx-mention { color: var(--fbx-accent-fg); font-weight: var(--fbx-weight-semibold); }

/* ── MISE EN FORME DE BLOC DANS UN COMMENTAIRE (2026-08-21) ────────────────
   `renderRich` autorise treize balises, dont SIX portent des marges par défaut
   du navigateur — `p`, `ul`, `ol`, `blockquote` (1em en haut et en bas, plus
   40px d'indentation pour les trois dernières) et `hr`. Aucune règle ne les
   reprenait : un commentaire écrit en liste se rendait donc avec 14 px de vide
   au-dessus et 40 px de retrait, alors que ses voisins commencent au ras de la
   cellule.

   ⚠️ CE N'EST PAS QU'UN ÉCART D'ALIGNEMENT HORIZONTAL : la marge HAUTE du
   premier bloc descend la première ligne du texte, donc elle défait précisément
   ce que le rembourrage compensatoire de `.fbx-comment-box` vient d'établir —
   la première ligne à la hauteur de la date. Un retour sur deux se serait
   aligné, l'autre non, selon qu'il commence par du texte ou par une liste.

   ⚠️ CORRIGÉ POUR LA FAMILLE, PAS POUR LES SEULES LISTES. Le défaut a été vu
   sur un `<ol>`, mais `<p>` produit exactement le même décalage vertical et
   `<blockquote>` le même retrait : ne traiter que `ul`/`ol` aurait laissé le
   défaut revenir au premier commentaire écrit en paragraphes.

   ⚠️ ET SUR LE FIL DE RÉPONSES AUSSI : c'est le même `renderRich`, donc le même
   contenu. Une règle qui ne vaudrait que pour le retour racine créerait deux
   rendus du même objet — le défaut que ce socle existe pour empêcher. */
.fbx-cell-comment > :first-child, .fbx-reply-comment > :first-child { margin-top: 0; }
.fbx-cell-comment > :last-child,  .fbx-reply-comment > :last-child  { margin-bottom: 0; }
.fbx-cell-comment :is(p, ul, ol, blockquote),
.fbx-reply-comment :is(p, ul, ol, blockquote) { margin-block: .4em; }
/* 1,4em et non les 40px du navigateur : le retrait doit rester lisible comme un
   retrait, pas repousser le texte au tiers de sa colonne. */
.fbx-cell-comment :is(ul, ol), .fbx-reply-comment :is(ul, ol) { padding-left: 1.4em; }
.fbx-cell-comment blockquote, .fbx-reply-comment blockquote { margin-inline: 0; padding-left: .9em; }

/* ── REPLIÉ, UN COMMENTAIRE EST UNE LIGNE DE TEXTE ─────────────────────────
   Tant qu'il est tronqué, sa mise en forme de bloc n'a aucun sens à l'écran :
   on n'en voit qu'une ligne. Les marges verticales tombent — sans quoi la ligne
   grandit et la première ligne se décale — et le retrait avec elles, pour que
   tous les commentaires d'une colonne commencent au même pixel.

   ⚠️ `list-style-position: inside` EST CE QUI PERMET DE RETIRER LE RETRAIT SANS
   PERDRE LA PUCE : hors du flux, un marqueur posé dans un retrait nul sort de
   la boîte et se fait couper. Dedans, « 1. » reste visible et compte comme du
   texte — c'est un indice utile, on ne le supprime pas, on le range.

   Déplié, tout revient : les marges resserrées ci-dessus, le retrait, les
   puces à leur place. */
.fbx-cell-comment--clamp:not(.is-expanded) :is(p, ul, ol, blockquote) { margin-block: 0; }
.fbx-cell-comment--clamp:not(.is-expanded) :is(ul, ol) { padding-left: 0; list-style-position: inside; }
.fbx-cell-comment--clamp:not(.is-expanded) blockquote { padding-left: 0; }
/* Un filet horizontal dans un texte tronqué à une ligne ne montre qu'un trait. */
.fbx-cell-comment--clamp:not(.is-expanded) hr { display: none; }
/* Rangée de pièces d'une ligne de board. ⚠️ ELLE NE PORTE PLUS DE VIGNETTE
   depuis le 2026-08-21 : les deux règles `.fbx-cell-media img` qui vivaient ici
   (40 px, agrandissement au survol) n'avaient plus AUCUN consommateur une fois
   les vignettes remplacées par des jetons, et une règle que rien n'atteint est
   du code mort — la règle vaut d'abord pour ce qu'on vient d'écrire.
   Les vignettes du FIL DE RÉPONSES, elles, ont leur propre taille et leur
   propre règle (`.fbx-reply-thumbs img`, 56 px) : rien n'a été perdu.
   ⚠️ UNE SEULE PISTE DEPUIS LE 2026-08-31 : « Fil », « Tags » et « Pièces »
   occupaient 268 px en permanence pour des marqueurs présents 33 %, 17 % et
   15 % du temps — et 54,4 % des lignes n'en portaient aucun. */
.fbx-cell-media,
.fbx-cell-fil { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fbx-space-3); }
/* ⚠️ LES MARQUES DE LA CELLULE TESTEUR (2026-08-31) — compteur de reponses et
   jeton de note. Elles ne poussent PAS le nom : c'est lui qui se tronque, sinon
   deux marques suffiraient a deborder la piste. */
/* ══════════════════════════════════════════════════════════════════════════
   LE SEUIL — la page qu'on voit avant d'etre entre (2026-08-31)
   ══════════════════════════════════════════════════════════════════════════
   Une colonne etroite centree dans la fenetre, sans barre laterale ni en-tete.
   Aucun ecran de travail n'a cette ossature — tous portent la nav de P44 et un
   bandeau —, et c'est precisement ce qui en fait un seuil : il n'y a rien a
   naviguer, on n'est pas encore entre.

   ⚠️ IL VIT AU SOCLE PARCE QUE DEUX PAGES LE PARTAGENT : l'accueil public et la
   404. Il a d'abord ete ecrit en local dans `accueil.html`, et la 404 du
   2026-08-30 a renonce a le reprendre — le recopier aurait fait monter quatre
   cliquets. La note d'alors concluait que « le monter au socle n'aurait rien
   regle, ce vocabulaire portant lui-meme douze valeurs brutes qu'on aurait
   seulement deplacees ». ⚠️ C'ETAIT VRAI D'UN DEPLACEMENT, PAS D'UNE REPRISE :
   monte ET jetonne, il rend ses douze litteraux au lieu de les deplacer, et la
   page qui le portait en local perd ses cinq classes propres. *Une dette qu'on
   renonce a bouger reste une dette ; c'est en la reecrivant qu'elle se solde.*

   ⚠️ CE QUI N'EST PAS ICI : le glyphe (`.fbx-brand-glyph--xl`), les boutons, les
   couleurs. Le seuil ne pose que sa mise en colonne. */
:root {
  /* Deux jetons de DIMENSION nommes par leur role — pas des crans d'echelle :
     la mesure d'une porte, et la taille du seul titre de l'outil qui n'est pas
     un titre d'ecran (le socle assume ses tailles de titre hors barème). */
  --fbx-seuil-porte-w: 340px;
  --fbx-seuil-titre-size: 26px;
}
/* ⚠️ LA HAUTEUR PLEINE PAGE APPARTIENT A LA PAGE, PAS AU COMPOSANT — et c'est
   un cliquet du design system qui l'a montre. La figure de la vitrine devait
   poser `style="min-height: 0"` pour ne pas faire 100vh dans un catalogue, donc
   un `style=` de mise en forme, donc la regle 5 enfreinte. *Un composant qu'on
   doit neutraliser pour le montrer est un composant qui en fait trop.* La
   condition porte sur l'enfant DIRECT du corps : le seuil est alors l'ecran
   entier, ce qu'il n'est jamais dans une demonstration. */
.fbx-body:has(> .fbx-seuil) { min-height: 100vh; display: flex; }
.fbx-seuil {
  flex: 1; box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--fbx-space-20); padding: var(--fbx-space-20) var(--fbx-space-16);
}
.fbx-seuil-marque { display: flex; flex-direction: column; align-items: center; gap: var(--fbx-space-14); }
.fbx-seuil-titre {
  margin: 0; font-size: var(--fbx-seuil-titre-size); font-weight: var(--fbx-weight-bold); color: var(--fbx-text);
}
.fbx-seuil-sous-titre {
  margin: 0; font-size: var(--fbx-font-md); color: var(--fbx-text-muted); text-align: center;
}

/* La porte. ⚠️ LA LARGEUR EST PORTEE PAR LA LISTE, PAS PAR LE LIEN : en ajouter
   une seconde un jour ne demandera alors aucune reprise de mise en page. */
.fbx-seuil-portes {
  display: flex; flex-direction: column; gap: var(--fbx-space-10);
  width: 100%; max-width: var(--fbx-seuil-porte-w);
}
.fbx-seuil-porte {
  display: flex; align-items: center; justify-content: space-between; gap: var(--fbx-space-12);
  padding: var(--fbx-space-14) var(--fbx-space-16);
  font: inherit; font-size: var(--fbx-font-md); font-weight: var(--fbx-weight-medium);
  color: var(--fbx-text); text-decoration: none;
  background: var(--fbx-surface); border: 1px solid var(--fbx-border);
  border-radius: var(--fbx-radius-sm);
  transition: border-color .15s, box-shadow .15s;
}
.fbx-seuil-porte:hover { border-color: var(--fbx-accent-fg); box-shadow: 0 0 0 3px rgba(var(--fbx-accent-rgb), .08); }
/* ⚠️ `--fbx-text-muted` ET NON `-faint`, QUI N'EST JAMAIS DECLARE : la propriete
   HERITAIT alors du parent, silencieusement (DS-2a). */
.fbx-seuil-porte-chevron { color: var(--fbx-text-muted); flex-shrink: 0; }
.fbx-seuil-porte:hover .fbx-seuil-porte-chevron { color: var(--fbx-accent-fg); }
/* `-soft` et non `-muted` : celui-la tombe a 4,43 sur le canvas, sous AA a 12px. */
.fbx-seuil-pied { font-size: var(--fbx-font-sm); color: var(--fbx-text-soft); }
.fbx-seuil-pied a { color: inherit; text-decoration: none; }
.fbx-seuil-pied a:hover { text-decoration: underline; }

/* Vignette agrandissable — la classe est celle que la visionneuse écoute. */
.fbx-thumb-img { cursor: zoom-in; }

/* VISIONNEUSE — une seule pour les deux boards. `/review` avait `.fb-lightbox`
   (fond profond, croix, Échap) et `/feedbacks` `.img-modal` (fond noir, ni
   croix ni Échap) : même vignette, même geste, deux comportements. */
.fbx-lightbox {
  /* Cible des trois contrôles de l'overlay — croix, précédent, suivant. Un seul
     jeton : ils partagent une grammaire, ils doivent partager leur taille. */
  --fbx-lightbox-ctrl: 40px;
  position: fixed; inset: 0; background: var(--fbx-overlay-strong); display: none;
  align-items: center; justify-content: center; z-index: var(--fbx-z-lightbox); padding: var(--fbx-space-24);
}
.fbx-lightbox.is-on { display: flex; }
.fbx-lightbox img { max-width: 95vw; max-height: 92vh; border-radius: var(--fbx-radius); box-shadow: var(--fbx-shadow-lightbox); }
.fbx-lightbox-x {
  position: fixed; top: var(--fbx-space-16); right: var(--fbx-space-20); border: none;
  width: var(--fbx-lightbox-ctrl); height: var(--fbx-lightbox-ctrl);
  border-radius: var(--fbx-radius-full); background: rgba(var(--fbx-on-accent-rgb), .15); color: var(--fbx-on-accent);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.fbx-lightbox-x:hover { background: rgba(var(--fbx-on-accent-rgb), .28); }
/* Le glyphe d'un bouton d'ajout, pose par `enhanceAll` d'apres `data-icon`.
   `flex` et non `inline` : sans lui le SVG herite d'une ligne de base et
   redescend d'un pixel — c'est exactement ce qu'on corrige. L'ecart au libelle
   vient du `gap` de `.fbx-btn`, il ne se redeclare pas ici. */
.fbx-btn-glyph { display: flex; align-items: center; }

/* Navigation entre les pièces d'un même retour. Même rond, même fond et même
   taille de cible que la croix : trois contrôles d'un même overlay, une seule
   grammaire. ⚠️ `[hidden]` doit battre le `display: flex` — la règle globale est
   scopée `.fbx-body [hidden]` et la visionneuse est posée sur `document.body`,
   donc on le dit ici, par spécificité, sans `!important`. */
.fbx-lightbox-nav {
  position: fixed; top: 50%; transform: translateY(-50%);
  width: var(--fbx-lightbox-ctrl); height: var(--fbx-lightbox-ctrl);
  border: none; border-radius: var(--fbx-radius-full);
  background: rgba(var(--fbx-on-accent-rgb), .15); color: var(--fbx-on-accent);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  font-size: var(--fbx-font-xl); line-height: var(--fbx-leading-none);
}
.fbx-lightbox-nav:hover:not(:disabled) { background: rgba(var(--fbx-on-accent-rgb), .28); }
.fbx-lightbox-nav:disabled { opacity: .35; cursor: default; }
.fbx-lightbox-nav--prev { left: var(--fbx-space-16); }
.fbx-lightbox-nav--next { right: var(--fbx-space-16); }
.fbx-lightbox-nav[hidden] { display: none; }
/* Où l'on en est dans la série. Il n'apparaît qu'à partir de deux pièces :
   « 1 / 1 » ne renseigne personne. */
.fbx-lightbox-compteur {
  position: fixed; bottom: var(--fbx-space-20); left: 50%; transform: translateX(-50%);
  padding: var(--fbx-space-4) var(--fbx-space-12); border-radius: var(--fbx-radius-pill);
  background: rgba(var(--fbx-on-accent-rgb), .15); color: var(--fbx-on-accent);
  font-size: var(--fbx-font-xs); font-variant-numeric: tabular-nums;
}
/* Le « — » d'une cellule sans média : une absence ne se crie pas. */
/* Le tiret d'une cellule sans contenu. ⚠️ `n4` est une couleur de BORDURE, pas
   de texte : 1,55:1 en clair, 1,80 en sombre — mesure, c'est le plus faible
   contraste de tout l'outil, sur ~200 cellules d'un board. Passe a `n6`, le cran
   « texte tres discret » : 3,30 / 4,20.
   ⚠️ ON NE VA PAS JUSQU'A 4,5 EN CLAIR, ET C'EST DELIBERE : un tiret qui dit
   « rien ici » n'est pas du contenu, et le porter au niveau du texte courant le
   ferait concurrencer ce qu'il entoure sur ~200 lignes. On corrige un jeton
   employe hors de son role, on ne promeut pas un signe vide. */
.fbx-cell-empty { color: var(--fbx-text-muted); font-size: var(--fbx-font-xs); }
/* Colonne d'ouverture du lien : l'icône se cale en HAUT de la ligne, sinon elle
   suit la ligne de base du texte et danse d'une ligne à l'autre. */

/* Actions au bout d'une ligne (table, liste). Les boutons-ICÔNES ont déjà leur
   conteneur (`.fbx-actions`, feedback-shared.css) ; celui-ci est pour les
   boutons à libellé — trois pages le recopiaient sous trois noms
   (`actions-cell`, `pages-row-actions`, `row-actions`).
   Pas de `flex-wrap` : deux boutons empilés dans une cellule étroite doublent la
   hauteur de la ligne et cassent la lecture du tableau. C'est `.fbx-table-wrap`
   qui absorbe le manque de place, en faisant défiler la table.
   ⚠️ COMMENTAIRE REFERMÉ LE 2026-08-20 : il se fermait quatre lignes plus haut,
   et ces trois lignes se retrouvaient EN CSS BRUT, suivies d'une fermeture
   orpheline. Le parseur rejette alors le bloc jusqu'à son prochain point de
   reprise — des règles disparaissent **sans qu'aucun outil ne le dise** : ni le
   grep, ni les tests, ni le compteur du design system, qui lisent le FICHIER et
   non la feuille effectivement appliquée. Défaut trouvé en tombant deux fois
   dans le même piège le même jour.
   ⚠️ CONTRÔLE : les ouvertures et les fermetures de commentaire doivent être en
   nombre égal dans ce fichier. Et ne jamais écrire ces deux séquences en toutes
   lettres dans un commentaire — une ouverture citée en ouvre une vraie. */
/* `.fbx-row-actions` — RETIREE : l'index des admins portait deux boutons a
   libelle (« Modifier », « Supprimer ») qu'on ne retrouvait nulle part ailleurs.
   Il emploie desormais `.fbx-actions` / `.fbx-act-btn`, comme les trois boards. */

.fbx-mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: var(--fbx-font-xs); color: var(--fbx-text-muted); }
/* Cellule de texte long : une ligne, coupée proprement. */
.fbx-truncate { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ═══════════════════════════════════════════════════════════════════════════
   Cartes, état vide
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-card {
  background: var(--fbx-surface); border-radius: var(--fbx-radius-lg);
  box-shadow: var(--fbx-shadow-card); padding: 18px;
}
/* L'ESPACEMENT ENTRE DEUX CARTES EMPILEES — IL N'ETAIT DEFINI NULLE PART
   (2026-08-28).
   ⚠️ CONSTATE A L'ECRAN, PAS DEDUIT : sur la page d'une livraison, trois cartes
   se touchaient bord a bord, sans un pixel entre elles. Chaque ecran qui en
   empilait s'en tirait par un `margin-bottom` local — `.fbx-card--retour` en
   porte un depuis P37 — ou ne s'en tirait pas du tout.
   ⚠️ SUR LE VOISINAGE, PAS SUR LA CARTE : `+` ne pose rien sur une carte seule,
   ni sur la premiere d'une pile. Un `margin-bottom` porte par toutes aurait
   ajoute un blanc sous la derniere, que le rembourrage de `.fbx-main` double
   deja.
   ⚠️ UN CRAN DE L'ECHELLE, jamais un litteral : `ds-check` S0 refuse une valeur
   de rythme ecrite en dur, et c'est ce qui empeche une quatrieme mesure
   d'apparaitre au prochain ecran. */
.fbx-main > .fbx-card + .fbx-card { margin-top: var(--fbx-space-16); }
/* ⚠️ `.fbx-main >` A ETE AJOUTE LE 2026-08-30, ET SON ABSENCE ETAIT UNE
   REGRESSION EN PRODUCTION. Ecrite sans le parent, la regle frappait AUSSI les
   cartes cote a cote : les sept cartes KPI du tableau de bord vivent dans une
   grille (`.fbx-cards-row`), et chacune sauf la premiere recevait 16 px de
   marge haute. Resultat, « Total retours » remontait de 16 px au-dessus des
   six autres — mesure, l'ecart valait exactement `--fbx-space-16`.
   ⚠️ CE QUE LE SYMPTOME NE DISAIT PAS : la carte fautive etait celle qui
   paraissait decalee, alors que c'est la SEULE que la regle epargnait. On
   cherche donc ce qu'elle a de plus, quand elle est la seule a n'avoir rien.
   ⚠️ `align-items: stretch` N'Y CHANGE RIEN : une marge n'entre pas dans le
   calcul d'etirement d'une piste de grille. C'est ce qui a fait perdre le plus
   de temps — le remede evident ne produisait aucun effet, ce qui eloignait de
   la vraie cause au lieu d'y ramener.
   L'espacement des cartes COTE A COTE vient du `gap` de leur conteneur ; celui
   des cartes EMPILEES, de cette regle. Deux dispositions, deux mecanismes, et
   un seul ecran empile aujourd'hui (`livraison.html`, ses trois cartes filles
   directes de `.fbx-main`). */

/* ── CARTE DE RETOUR — le pendant, en colonne, d'une ligne de board ──────────
   `/corrections` la posait sous `.kanban-card` : fond de surface, rayon `lg` et
   `--fbx-shadow-card`. C'est-à-dire `.fbx-card`, À LA VALEUR PRÈS — mêmes 10 px
   de rayon, même ombre au caractère près —, plus un rembourrage resserré et une
   marge d'empilement. Ce qui restait vraiment propre à l'écran tenait en trois
   règles, et ces trois-là HABILLAIENT DES CLASSES DU SOCLE (`.fbx-cell-date`,
   `.fbx-comment-box`, `.fbx-row-page`) depuis un `<style>` de page : la page ne
   redéfinissait pas un composant, elle le PLAÇAIT — ce qui revient au même le
   jour où le socle change l'espacement de ces cellules.
   ⚠️ ELLE MONTRE LE MÊME OBJET QU'UNE LIGNE DE BOARD, ELLE N'EN EST PAS UNE :
   les règles de densité du board portent toutes `.fbx-grid-row` (repli à une
   ligne, boîte de commentaire en flex) et ne doivent pas l'atteindre — une carte
   a la place, une ligne ne l'a pas. C'est déjà écrit au-dessus de ces règles-là.
   ⚠️ PAS DE SURVOL D'OMBRE, ET C'EST UN RETRAIT ASSUMÉ : elle en portait un, à
   un cheveu de `--fbx-shadow-card-hover`. Dans ce socle, l'ombre qui se lève au
   survol est la promesse de `.fbx-card--clickable`, donc du CLIC — une carte de
   kanban ne se clique pas, ce sont ses boutons qui agissent. Deux mécaniques
   d'ombre pour un seul geste, dont une hors du socle, disaient la même chose et
   ne la disaient pas pareil. */
.fbx-card--retour { padding: var(--fbx-space-14); margin-bottom: var(--fbx-space-10); }

/* En-tête de carte : QUI, puis QUAND. La date est poussée au bout — même geste
   que la méta d'une note interne, et pour la même raison : elle situe le retour,
   elle ne le titre pas. L'auteur, lui, est `.fbx-author` : la carte réécrivait
   son avatar et son nom à la main, donc sans la troncature du nom long que le
   composant porte. */
.fbx-card-head { display: flex; align-items: center; gap: var(--fbx-space-10); margin-bottom: var(--fbx-space-8); }
.fbx-card-head .fbx-cell-date { margin-left: auto; white-space: nowrap; }
.fbx-card--retour .fbx-comment-box,
.fbx-card--retour .fbx-row-page { margin-bottom: var(--fbx-space-8); }

/* Rangée d'actions en pied de carte.
   ⚠️ L'ACTION DESTRUCTIVE VA AU BOUT, et la règle vit ici plutôt que sous un nom
   d'écran : on enchaîne les changements de statut sur une carte, et la seule
   action irréversible ne doit pas se trouver au milieu de cette rangée, sous le
   curseur. Écrite sur `.danger`, elle vaut pour toute carte qui en porte une —
   il n'y a rien à se rappeler d'ajouter. */
.fbx-card-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fbx-space-6); margin-top: var(--fbx-space-8); }
.fbx-card-actions > .fbx-act-btn.danger { margin-left: auto; }
.fbx-empty {
  background: var(--fbx-surface); border-radius: var(--fbx-radius-lg);
  padding: var(--fbx-space-32); text-align: center; color: var(--fbx-text-muted); font-size: var(--fbx-font-md);
}
/* Un état vide porte souvent un glyphe. Il s'efface : c'est un repère, pas une
   illustration — l'écran est vide, il ne doit pas avoir l'air occupé. */
.fbx-empty svg { margin-bottom: var(--fbx-space-12); opacity: .4; }
.fbx-empty p { margin: 0; }
/* Un etat vide peut porter LA SORTIE (2026-08-21) : `/review` propose d'ouvrir
   la recette entiere quand « Mes retours » est ce qui vide la liste. Aucune
   classe nouvelle — le bouton est un `.fbx-btn` ordinaire —, seulement l'ecart
   qui lui manquait : `.fbx-empty p` est a `margin: 0`, donc sans cette regle le
   bouton se colle a la phrase qu'il repond. */
.fbx-empty .fbx-btn { margin-top: var(--fbx-space-12); }
/* Vide PARCE QU'UNE ERREUR l'a rendu vide (lien de relecture invalide, appel en
   échec). Même bloc, autre cause — et la cause doit se voir. */
.fbx-empty--error { color: var(--fbx-danger-fg); }

/* ═══════════════════════════════════════════════════════════════════════════
   Overlay de connexion — le composant le plus dupliqué du projet
   (`.login-box` : 48 déclarations réparties sur 6 pages, à l'identique).
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-login-overlay {
  position: fixed; inset: 0; background: var(--fbx-overlay);
  display: flex; align-items: center; justify-content: center; z-index: var(--fbx-z-login);
}
.fbx-login-overlay.is-hidden { display: none; }
/* ⚠️ MASQUÉE D'EMBLÉE QUAND LE NAVIGATEUR SAIT DÉJÀ QU'ON EST CONNECTÉ
   (2026-08-30). L'overlay est écrit VISIBLE dans les douze écrans et chaque
   page le masque au retour de `/api/auth/me` : sur un compte connecté, la boîte
   apparaissait donc puis disparaissait à CHAQUE chargement.
   La classe est posée sur la racine par le socle, avant que le corps n'existe —
   même motif que `data-theme`, et pour la même raison : la réponse vient du
   réseau, donc après le premier rendu.
   ⚠️ LE DÉFAUT RESTE « VISIBLE » : sans la classe, la boîte s'affiche. Un JS
   qui échoue laisse un écran de connexion utilisable, jamais une page vide. */
:root.tpx-connecte .fbx-login-overlay { display: none; }
/* ── LE MÊME CENTRAGE, MAIS SUR UNE PAGE QUI N'A RIEN DERRIÈRE (2026-08-24) ──
   L'écran « lien expiré » est bâti sur ce composant plutôt que sur une carte de
   formulaire : c'est la même situation — une boîte, deux contrôles, et rien
   d'autre à l'écran —, et il n'y a aucune raison qu'elle se présente autrement
   que la connexion. *Une même fonction, une même place.*

   ⚠️ CE QUI CHANGE EST LE VOILE, ET RIEN D'AUTRE. L'overlay de connexion
   s'ouvre PAR-DESSUS une page qu'il faut estomper, d'où son fond noir à 50 %.
   Ici l'hôte a été vidé : ce voile s'appliquerait sur du vide et donnerait un
   gris sale, tout en faisant croire à un écran bloqué par-dessus un autre. Il
   prend donc le fond de page — qui suit le thème, comme tout le reste.

   ⚠️ LE BOUTON PASSE EN FLEX ICI SEULEMENT. Celui de la connexion ne porte que
   du texte ; celui-ci porte une icône, qui se calerait sur la ligne de base sans
   cette règle. Scopée au modificateur, elle ne peut pas déplacer d'un pixel la
   boîte de connexion — *on n'élargit pas une règle pour le seul écran qui en a
   besoin.* */
.fbx-login-overlay--page { background: var(--fbx-bg); }
.fbx-login-overlay--page .fbx-login-submit {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--fbx-space-8);
}
.fbx-login-box {
  background: var(--fbx-surface); border-radius: var(--fbx-radius-xl); padding: 40px;
  width: 380px; max-width: calc(100vw - 32px); text-align: center; box-shadow: var(--fbx-shadow-modal);
}
/* P40 — la boîte porte le GLYPHE Toutenpixel, pas une bulle générique. C'était
   la seule surface de l'outil sans identité de marque : une personne qui reçoit
   un digest (glyphe rond, bleu marque) puis clique vers le board arrivait sur un
   écran violet titré « tpx-feedback », et rien ne disait que c'était le même
   produit. Le glyphe est le MÊME fichier que celui joint aux mails
   (`widget/toutenpixel-glyph.png`), déjà servi en statique — aucune ressource
   nouvelle, aucune requête externe. */
.fbx-login-box .fbx-login-icon { margin-bottom: var(--fbx-space-14); line-height: 0; }
.fbx-login-box .fbx-login-icon img {
  width: 64px; height: 64px; border-radius: var(--fbx-radius-full); display: inline-block;
}
.fbx-login-box h2 { font-size: var(--fbx-font-xl); font-weight: var(--fbx-weight-bold); color: var(--fbx-text); margin: 0 0 22px; }
/* Le sous-titre a été RETIRÉ des six écrans, pas restylé : « Connectez-vous pour
   accéder aux retours » n'apprenait rien devant deux champs et un bouton. La
   règle reste pour ne pas laisser un `<p>` oublié casser l'espacement. */
.fbx-login-box p { font-size: var(--fbx-font-base); color: var(--fbx-text-muted); margin: -14px 0 22px; }
/* ⚠️ CE CHAMP N'AVAIT NI FOND NI COULEUR DE TEXTE, et c'est passé inaperçu
   pendant tout le chantier du thème sombre : en clair, le blanc du navigateur
   par défaut tombait juste sur une carte blanche, si bien que rien ne signalait
   la déclaration manquante. En sombre, les deux champs sont restés BLANCS au
   milieu d'une boîte #1C2126 — vu à l'œil sur la capture, invisible au relevé
   des couleurs littérales (il n'y en avait aucune à trouver : le défaut était
   une ABSENCE). Les valeurs choisies reconduisent le rendu clair au pixel. */
.fbx-login-box input {
  width: 100%; padding: var(--fbx-space-10) var(--fbx-space-14); border: 1px solid var(--fbx-border);
  background: var(--fbx-surface); color: var(--fbx-text);
  border-radius: var(--fbx-radius); font-size: var(--fbx-font-md); margin-bottom: var(--fbx-space-8);
  transition: border-color .15s; font-family: inherit;
}
/* Le focus suivait la MARQUE (#2BA8DD). Il suit desormais l'accent du produit,
   pour la meme raison que le bouton ci-dessous — voir son commentaire. Le halo
   ne change pas de teinte : il cesse seulement d'etre une litterale, et lit le
   jeton que TOUS les autres champs du socle lisent deja (`.fbx-input:focus`).
   Un halo est decoratif, sans exigence de contraste ; ce qui comptait ici est
   qu'il ne soit plus la seule valeur en dur d'un composant jetonne. */
.fbx-login-box input:focus {
  outline: none; border-color: var(--fbx-accent-fill);
  box-shadow: 0 0 0 3px rgba(var(--fbx-accent-rgb), .16);
}
/* ⚠️ P40 posait ce bouton aux couleurs de la MARQUE — le meme bleu que le bouton
   d'action des mails (`tpxButton`, mail.js). L'intention etait juste : un mail et
   un ecran de connexion qui divergent, c'est le defaut que P40 corrigeait. Mais
   #2BA8DD porte du texte blanc a 2,71 — le bouton le plus important de l'outil,
   celui sans lequel on n'entre pas, etait le moins lisible. Il lit desormais
   `--fbx-accent-fill` (5,69) et son survol `--tpx-blue-800` (7,96), soit le meme
   bleu que le bouton principal de tous les autres ecrans (2026-08-21).
   ⚠️ `--tpx-brand` n'a PAS bouge : il reste iso avec `TPX.blue` de mail.js. Ce
   sont les mails qui font foi sur la marque, et ils n'ont pas ce probleme — leur
   bouton porte la meme couleur mais n'est pas la porte d'entree du produit.
   ⚠️ Le RAYON de 9px, lui, reste celui de `tpxButton` : c'est la forme qui fait
   la parente avec le mail, pas la teinte. */
.fbx-login-submit {
  width: 100%; padding: 11px; border: none; border-radius: 9px;
  background: var(--fbx-accent-fill); color: var(--fbx-on-accent); font-size: var(--fbx-font-md); font-weight: var(--fbx-weight-semibold);
  cursor: pointer; margin-top: var(--fbx-space-4); transition: background .15s; font-family: inherit;
}
.fbx-login-submit:hover { background: var(--tpx-blue-800); }
.fbx-login-error { color: var(--fbx-danger-fg); font-size: var(--fbx-font-sm); margin-top: var(--fbx-space-8); min-height: 18px; }
.fbx-login-divider { display: flex; align-items: center; gap: var(--fbx-space-12); margin: var(--fbx-space-16) 0; color: var(--fbx-faint); font-size: var(--fbx-font-sm); }
.fbx-login-divider::before, .fbx-login-divider::after { content: ''; flex: 1; height: 1px; background: var(--fbx-border); }

/* Bouton Google et repli « connexion par token (legacy) ».
   §1.3 les donnait « identiques partout » — c'est vrai des VALEURS, pas de
   l'écriture : `corrections`, `dashboard` et `setup` les déclarent en CSS,
   `export-context` et `feedbacks` les posent en attribut `style`. D'où la classe
   `login-legacy-toggle` relevée ORPHELINE sur `/feedbacks` par la recette du
   2026-08-01 : elle y est rendue, et stylée nulle part.
   Le déclencheur est un <button>, pas un <div> cliquable : il ouvre un champ de
   saisie, il doit être atteignable au clavier. Le reset est ici, pas dans la page. */
.fbx-login-google { display: flex; justify-content: center; }
.fbx-login-legacy-toggle {
  font: inherit; font-size: var(--fbx-font-xs); color: var(--fbx-faint); background: none; border: none; padding: 0;
  margin-top: var(--fbx-space-12); cursor: pointer; text-decoration: underline;
}
.fbx-login-legacy-toggle:hover { color: var(--fbx-text-soft); }
.fbx-login-legacy { display: none; margin-top: var(--fbx-space-12); }
.fbx-login-legacy.is-on { display: block; }
/* Le champ hérite de `.fbx-login-box input` : seule la marge basse saute, le
   bouton qui suit portant déjà la sienne. */
.fbx-login-legacy input { margin-bottom: 0; }
.fbx-login-legacy .fbx-btn { margin-top: var(--fbx-space-8); }

/* ═══════════════════════════════════════════════════════════════════════════
   Toast — message transitoire, en bas d'écran.
   Note : le toast INLINE (encart coloré dans le flux) est un autre composant,
   plus bas dans ce fichier. Celui-ci est le toast flottant.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-toast {
  position: fixed; bottom: var(--fbx-space-24); left: 50%; transform: translateX(-50%);
  background: var(--fbx-toast-bg); color: var(--fbx-on-accent); padding: var(--fbx-space-12) var(--fbx-space-20); border-radius: var(--fbx-radius);
  font-size: var(--fbx-font-base); z-index: var(--fbx-z-toast); opacity: 0; transition: opacity .2s;
  pointer-events: none; max-width: 90vw;
}
.fbx-toast.is-on { opacity: 1; }
.fbx-toast--error { background: var(--fbx-danger); }

/* ███████████████████████████████████████████████████████████████████████████
   LOT « SOCLE EXHAUSTIF » — 2026-08-01, P37 §9.
   Tout ce qui suit est NEUF : aucune règle ci-dessus n'a été retouchée
   (corbeille.html en dépend, et c'est le seul écran déjà rebranché).
   Les valeurs sont EXTRAITES des pages en service ; quand deux pages
   divergeaient, l'arbitrage est écrit sur place, jamais sous-entendu.
   ███████████████████████████████████████████████████████████████████████████ */

/* ═══════════════════════════════════════════════════════════════════════════
   §9.1 — CHAMPS DE SAISIE
   Le socle avait `.fbx-select` et aucun champ texte : les 4 pages qui en
   posent un le redéclarent. Deux arbitrages, écrits une fois pour toutes :

   • RAYON 8, pas 6. `feedbacks.html` pose ses champs de filtre en 6px,
     `review.html` en 8px. On retient 8 parce que c'est celui de `.fbx-select`
     et qu'un champ texte posé à CÔTÉ d'un select ne peut pas avoir un autre
     arrondi — la barre de filtres les met précisément côte à côte.
   • `font: inherit`. Sans lui, un <input> prend la police système du
     navigateur (Arial sous Windows) alors que le select, lui, hérite : deux
     polices dans la même barre. `review.html` l'avait déjà, pas `feedbacks`.

   La LARGEUR n'est pas dans la base : c'est de la mise en page, pas du
   composant. `--search` fixe 240px (review) ; feedbacks en posait 260, un
   écart de 20px que rien ne justifiait.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-input {
  font: inherit; font-size: var(--fbx-font-base); line-height: var(--fbx-leading-tight); color: var(--fbx-text);
  padding: 7px var(--fbx-space-10); border: 1px solid var(--fbx-border);
  border-radius: var(--fbx-radius); background: var(--fbx-surface);
  transition: border-color .15s, box-shadow .15s;
}
.fbx-input:hover { border-color: var(--fbx-border-strong); }
.fbx-input:focus {
  outline: none; border-color: var(--fbx-accent-fg);
  box-shadow: 0 0 0 3px rgba(var(--fbx-accent-rgb), .12);
}
.fbx-input::placeholder { color: var(--fbx-faint); }
.fbx-input:disabled { background: var(--fbx-fill); color: var(--fbx-text-muted); cursor: not-allowed; }
.fbx-input--search { width: 240px; max-width: 100%; }
/* Recherche avec loupe — meme mecanique que `.fbx-select-icon` : l'icone est
   posee en absolu, inerte, et le champ se decale d'autant. Dans un bandeau ou
   tous les controles se ressemblent par la forme, l'icone est ce qui dit sur
   quoi chacun agit — ecran, calendrier, etiquette, loupe. */
.fbx-search { position: relative; display: inline-flex; align-items: center; }
.fbx-search-glyph {
  position: absolute; left: var(--fbx-space-10); top: 50%; transform: translateY(-50%);
  pointer-events: none; color: var(--fbx-text-muted); display: flex; z-index: 1;
}
.fbx-search-input { padding-left: 30px; }
/* Le champ date est plus court et plus serré : le calendrier natif occupe déjà
   la droite du champ. 150px et 6px 8px sont IDENTIQUES dans les deux boards. */
/* Pleine largeur — vaut pour TOUT contrôle, `.fbx-select` compris : c'est une
   règle de mise en page, pas une propriété du champ de texte. Sur un select,
   elle suit jusqu'au bouton du dropdown maison, qui hérite de `className`. */
.fbx-input--block { width: 100%; }

/* Zone de texte libre. Alignée sur `.fbx-editor-area` (feedback-shared.css) et
   non sur l'une des trois <textarea> des pages (12 / 13 / 14px, 60 / 60 / 80px
   de hauteur) : c'est le MÊME objet que l'éditeur riche, en version brute.
   Deux techniques pour une saisie de message ne doivent pas avoir deux tailles. */
.fbx-textarea {
  font: inherit; font-size: var(--fbx-font-md); line-height: var(--fbx-leading-normal); color: var(--fbx-text);
  width: 100%; min-height: 62px; padding: 9px 11px; resize: vertical;
  border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius);
  background: var(--fbx-surface); transition: border-color .15s, box-shadow .15s;
}
.fbx-textarea:focus {
  outline: none; border-color: var(--fbx-accent-fg);
  box-shadow: 0 0 0 3px rgba(var(--fbx-accent-rgb), .12);
}
.fbx-textarea::placeholder { color: var(--fbx-faint); }
.fbx-textarea--code {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: var(--fbx-font-sm);
  line-height: 1.55; background: var(--fbx-surface-sunken); min-height: 50vh;
}
/* Le pendant sur une LIGNE : un champ qui porte une valeur technique et non de
   la prose — un jeton, un identifiant de classeur. Ce n'est pas de la
   décoration : une chaîne qu'on doit relire caractère par caractère se lit en
   chasse fixe, où le 1, le l et le I ne se confondent plus.
   ⚠️ IL FALLAIT UN MODIFICATEUR, `.fbx-mono` NE POUVAIT PAS SERVIR : elle est
   déclarée ligne 1918, `.fbx-input` ligne 2102, à spécificité égale (0,1,0) —
   et `.fbx-input` porte `font: inherit`, `font-size` et `color`. Posée sur un
   `.fbx-input`, `.fbx-mono` ne rend donc RIEN, sans que rien ne le signale.
   Troisième occurrence de la même leçon : avant de conclure qu'un jeton est
   faux, vérifier que la règle qui le lit gagne. */
.fbx-input--code {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: var(--fbx-font-sm);
  background: var(--fbx-surface-sunken);
}

/* ── BLOC DE CODE (2026-08-28) ─────────────────────────────────────────────
   Du code qu'on LIT et qu'on COPIE, jamais qu'on saisit : le snippet à coller
   dans le site du client, l'aperçu de ce qui sera écrit dans `projects.yaml`.
   C'est le pendant en LECTURE de `.fbx-textarea--code`.

   ⚠️ IL VIENT D'UNE PAGE, ET IL Y ÉTAIT SOMBRE. `/admin/setup.html` portait un
   `.code-block` local sur un fond #1e1e2e avec quatre couleurs littérales —
   donc un bloc qui restait sombre en thème clair, insensible à la palette, et
   quatre couleurs de plus dans un outil qui en compte ses jetons. Le fond est
   désormais celui des autres surfaces en retrait, et la coloration réemploie
   quatre jetons EXISTANTS : aucun jeton neuf, et le bloc bascule avec le thème.

   ⚠️ LES QUATRE RÔLES SE LISENT SUR LA SURFACE EN RETRAIT, pas sur une plaque :
   ils prennent donc les `-fg`, seuls thémés. La règle est celle de P37 — un
   jeton qui sert deux rôles ne se révèle qu'au thème qui les sépare.

   ⚠️ `white-space: pre` ET NON `pre-wrap` : un snippet coupé au milieu d'un
   attribut se recopie faux. Le débordement se fait défiler, il ne se replie
   pas. */
.fbx-code {
  margin: 0; padding: var(--fbx-space-14);
  background: var(--fbx-surface-sunken); color: var(--fbx-text);
  border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius);
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: var(--fbx-font-sm); line-height: var(--fbx-leading-normal);
  white-space: pre; tab-size: 2;
  overflow: auto; max-height: var(--fbx-code-max-h);
}
.fbx-code-key { color: var(--fbx-accent-fg); }
.fbx-code-val { color: var(--fbx-ok-fg); }
.fbx-code-com { color: var(--fbx-text-muted); }
.fbx-code-tag { color: var(--fbx-warn-fg); }

/* ── CHAMP COULEUR (2026-08-28) ────────────────────────────────────────────
   Un nuancier, sa valeur en clair, et de quoi revenir au calcul automatique.
   Les trois vont ENSEMBLE : le nuancier ne dit pas la valeur qu'on recopiera
   dans une charte, et le champ hexadécimal ne se choisit pas à l'œil.

   ⚠️ IL ÉTAIT ÉCRIT EN `style=` DANS LA PAGE — trois attributs de mise en forme,
   dont deux dimensions et une bordure. C'est le composant qui manquait, pas la
   page qui avait raison. */
.fbx-color-field { display: flex; gap: var(--fbx-space-10); align-items: center; }
.fbx-color-field input[type="color"] {
  width: var(--fbx-color-swatch-w); height: var(--fbx-color-swatch-h);
  padding: var(--fbx-space-2); background: var(--fbx-surface);
  border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius);
}
/* La valeur en clair reste COURTE : un hexadécimal fait sept caractères, et un
   champ pleine largeur à côté d'un nuancier de 48 px se lit comme un champ de
   texte libre. */
.fbx-color-field .fbx-input { width: var(--fbx-color-hex-w); }

/* Tampon de copie — le champ que `TPXShared.copier` pose une fraction de seconde
   quand `navigator.clipboard` n'existe pas (contexte non securise). Il doit etre
   SELECTIONNABLE, donc present dans la page : `display: none` ou `hidden` le
   rendraient inselectionnable et la copie de secours echouerait en silence.
   ⚠️ SA PLACE EST ICI ET NON DANS LE JS : sortir un element de l'ecran est une
   affaire de feuille, et le poser en `style=` ecrirait de la mise en forme hors
   du socle. */
/* ⚠️ NI TAILLE NI `hidden` : sa taille n'a aucune importance — il vit le temps
   d'un `execCommand` —, et le sortir du rendu le rendrait inselectionnable. */
.fbx-copie-tampon { position: fixed; top: 0; left: 0; opacity: 0; pointer-events: none; }

/* Select à icône (dashboard : « Tous les projets », « 30 derniers jours »).
   L'icône est posée par un wrapper, le décalage par un modificateur SUR le
   select. C'est ce qui fait qu'elle survit au dropdown maison : `enhanceSelect`
   recopie `className` du <select> sur le bouton (feedback-shared.js:236,241),
   donc `--icon` — et son padding — suivent. Un `.select-wrap select` n'aurait
   pas suivi : le bouton n'est pas un <select>. */
.fbx-select-icon { position: relative; display: inline-flex; align-items: center; }
.fbx-select-icon > .fbx-select-glyph {
  position: absolute; left: var(--fbx-space-10); top: 50%; transform: translateY(-50%);
  pointer-events: none; color: var(--fbx-text-muted); display: flex; z-index: 1;
}
.fbx-select--icon { padding-left: 30px; }

/* Groupe de formulaire. `admin-users` 16px, `setup` 18px, `export-context`
   20px : on retient 16, seule valeur du pas de 8 déjà employé par le socle
   (`.fbx-section` 32, `.fbx-callout` 20 → 8/16/24/32).
   Le libellé n'est PAS en capitales : 2 pages sur 3 l'écrivent en casse
   normale, et les capitales sont déjà le vocabulaire des en-têtes de table. */
.fbx-form-group { margin-bottom: var(--fbx-space-16); }
.fbx-form-group > label {
  display: block; font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-semibold); color: var(--fbx-text-soft); margin-bottom: var(--fbx-space-6);
}
/* ⚠️ DEFAUT DE SOCLE CORRIGE le 2026-08-04. La regle ci-dessus (0,0,2,1) battait
   `.fbx-check` (0,0,1,0) : une case a cocher posee dans un groupe de formulaire
   — c'est-a-dire la facon normale de batir un formulaire — perdait son
   `inline-flex`, donc son alignement vertical ET l'espace entre la case et son
   libelle, et heritait du gras d'un intitule de champ.
   Le symptome se voyait ; la cause, non : rien ne dit a l'ecran qu'un selecteur
   plus specifique est passe devant. C'est pour ca que l'editeur d'avatar
   portait des styles EN LIGNE a cet endroit — un contournement qui masquait le
   defaut au lieu de le corriger, et que le design system interdit.
   La case garde donc sa mise en forme, ou qu'elle soit posee. */
.fbx-form-group > label.fbx-check {
  display: inline-flex; align-items: center; gap: var(--fbx-space-6);
  font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-normal); color: var(--fbx-text-soft); margin-bottom: 0;
}
/* MEME PIEGE, MEME REMEDE — relevé au navigateur le 2026-08-04, sur le tout
   premier interrupteur monté au socle. Un `<label class="fbx-switch">` dans un
   `.fbx-form-group` est un `<label>` : la règle ci-dessus le reprenait, et il
   perdait `inline-flex` (donc l'alignement vertical de la piste avec son texte,
   ET les 10px de `gap`), héritait du gras, gagnait 6px de marge basse et
   s'étalait sur TOUTE la largeur du groupe — 628px mesurés, dont l'essentiel
   est une cible de clic invisible qui bascule le réglage.
   ⚠️ Trois composants du socle sont donc des `<label>` (case, interrupteur,
   et le champ à tags par `cursor: text`) : TOUT NOUVEAU COMPOSANT BÂTI SUR UN
   `<label>` doit reconduire cette exception, sinon il naîtra cassé dans le seul
   endroit où on l'emploie vraiment. Le symptôme se voit, la cause non : rien à
   l'écran ne dit qu'un sélecteur plus spécifique est passé devant. */
.fbx-form-group > label.fbx-switch {
  display: inline-flex; align-items: center; gap: var(--fbx-space-10);
  font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-normal); color: var(--fbx-text-soft); margin-bottom: 0;
}
.fbx-form-hint { font-size: var(--fbx-font-xs); color: var(--fbx-text-muted); margin-top: var(--fbx-space-3); line-height: var(--fbx-leading-snug); }
.fbx-form-actions { display: flex; gap: var(--fbx-space-10); align-items: center; margin-top: var(--fbx-space-24); }
/* Formulaire OUVERT DANS une ligne (répondre, corriger son commentaire) : 24px
   d'écart y ouvriraient un trou au milieu de la liste. */
.fbx-form-actions--tight { margin-top: var(--fbx-space-8); }
.fbx-required { color: var(--fbx-danger-fg); margin-left: var(--fbx-space-2); }
/* Message d'erreur d'un formulaire. Le socle n'avait que celui de la CONNEXION
   (`.fbx-login-error`), et les autres écrans le recopiaient sous un troisième
   nom (`.modal-error` sur admin-users). Même objet, même réserve de hauteur :
   sans `min-height`, l'apparition du message déplace le bouton sous le curseur. */
.fbx-form-error { color: var(--fbx-danger-fg); font-size: var(--fbx-font-sm); margin-top: var(--fbx-space-8); min-height: 16px; }

/* Case à cocher avec libellé. ✅ **Doublon soldé le 2026-08-03** :
   `.fbx-filter-check` portait EXACTEMENT ce corps sous un nom de contexte, né
   de la barre de filtres des boards. La condition posée par P37 §9.8 — « quand
   `review` et `feedbacks` seront rebranchées » — est remplie depuis le lot 2 :
   le nom de contexte a été retiré, celui-ci reste, parce qu'il ne présuppose
   pas où la case est posée. */
.fbx-check {
  font-size: var(--fbx-font-base); color: var(--fbx-text-soft); display: inline-flex; align-items: center; gap: var(--fbx-space-6);
  cursor: pointer; user-select: none;
}
/* Rangée de cases : un groupe de choix qui va à la ligne (projets autorisés). */
.fbx-check-row { display: flex; flex-wrap: wrap; gap: var(--fbx-space-8) var(--fbx-space-14); margin-top: var(--fbx-space-4); }

/* ═══════════════════════════════════════════════════════════════════════════
   INTERRUPTEUR — un réglage qui BASCULE, distinct de la case qui COCHE.
   Monté au socle le 2026-08-04. Il existait DEUX fois, à deux tailles et sous
   deux vocabulaires : `setup` (`.toggle` / `.toggle-track` / `.toggle-label`,
   40 × 22, rayon 11) et `feedbacks` (`.cdr-toggle` / `.cdr-switch`, 30 × 16,
   rayon 8). Même objet, deux apparences — et rien ne l'avait décidé.

   UNE SEULE TAILLE, 40 × 22 : celle qui est à l'écran aujourd'hui (`setup`).
   Pas de variante `--sm` — le second consommateur est masqué, donc aucune
   contrainte de place réelle ne la justifie, et une variante posée « au cas où »
   est le premier pas vers les deux rendus qu'on vient de fondre.

   ⚠️ LE LIBELLÉ VA *DANS* LE `<label>`, pas à côté. `setup` le posait dans un
   `<span>` frère : cliquer le texte ne basculait rien, alors que c'est la plus
   grande cible de l'interrupteur. Aucune classe pour lui — un nœud de texte est
   un élément de la flexbox, le `gap` s'en occupe.

   ⚠️ La case reste FOCUSABLE : `opacity: 0` et non `display: none`
   (`.cdr-toggle` la masquait, donc l'ôtait du parcours au clavier). C'est ce
   qui permet à `:focus-visible` de rendre un halo sur la piste.

   ⚠️ L'état ne se dit PAS par une classe `is-on` posée en JS : il se lit sur
   `input:checked`. Les deux pages tenaient une classe en parallèle de la case —
   deux vérités pour un seul fait, qui divergent dès qu'un chemin en oublie une.
   `.is-disabled` reste une classe : elle porte sur le libellé et le curseur,
   que `input:disabled` ne peut pas atteindre depuis un frère.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-switch {
  display: inline-flex; align-items: center; gap: var(--fbx-space-10);
  font-size: var(--fbx-font-base); color: var(--fbx-text-soft); cursor: pointer; user-select: none;
}
.fbx-switch input {
  position: absolute; opacity: 0; width: 0; height: 0; margin: 0;
}
.fbx-switch-track {
  position: relative; flex-shrink: 0; width: 40px; height: 22px;
  background: var(--fbx-border-strong); border-radius: 11px; transition: background .2s, box-shadow .15s;
}
.fbx-switch-track::after {
  content: ''; position: absolute; left: var(--fbx-space-2); top: var(--fbx-space-2);
  width: 18px; height: 18px; background: var(--fbx-surface); border-radius: var(--fbx-radius-full);
  box-shadow: var(--fbx-shadow-knob); transition: transform .2s;
}
/* La piste allumée est une PLAQUE, comme le bouton principal : sans quoi les deux
   « états actifs » de la même modale prendraient deux bleus différents en sombre. */
.fbx-switch input:checked + .fbx-switch-track { background: var(--fbx-accent-fill); }
.fbx-switch input:checked + .fbx-switch-track::after { transform: translateX(18px); }
.fbx-switch input:focus-visible + .fbx-switch-track {
  box-shadow: 0 0 0 3px rgba(var(--fbx-accent-rgb), .25);
}
.fbx-switch:hover .fbx-switch-track { background: var(--fbx-faint); }
.fbx-switch:hover input:checked + .fbx-switch-track { background: var(--fbx-accent-fill-hover); }
.fbx-switch.is-disabled { opacity: .45; cursor: not-allowed; }
.fbx-switch.is-disabled:hover .fbx-switch-track,
.fbx-switch.is-disabled input:checked + .fbx-switch-track { background: var(--fbx-border-strong); }

/* Le troisième composant de statut : ni badge (affichage) ni chip (filtre),
   mais un CONTRÔLE teinté par l'état courant. Il existait sur les deux boards
   sous deux noms et deux conventions (`.status-select.active` par classe dans
   feedbacks, `.fb-status-select[data-status]` par attribut dans review).
   Convention retenue : l'ATTRIBUT — c'est déjà celle de `.fbx-status-badge`,
   et une valeur de statut n'est pas un état d'interface (`is-on`, `is-hidden`).
   S'emploie SUR un `.fbx-select`, qui porte l'apparence.
   ⚠️ LE FOND TEINTÉ A ÉTÉ RETIRÉ LE 2026-08-21, ET C'EST UN RENVERSEMENT DE LA
   DÉCISION DU 2026-08-02. Celle-ci disait : « un contrôle qui porte l'état
   courant se repère mieux à l'aplat qu'au seul texte ». C'était vrai d'un
   contrôle isolé ; ça ne l'est plus d'une COLONNE. Sur un board de 180 lignes,
   180 aplats de couleur pleine forment une bande verticale qui pèse plus lourd
   que tout le reste du tableau — le commentaire, qui est l'information, passe
   après. Et le même écran porte, six lignes plus haut, des `<select>` de filtre
   blancs à bord arrondi : le même geste s'y présentait sous deux apparences.

   CE QUI REMPLACE : un `.fbx-select` ORDINAIRE — fond neutre, bordure grise,
   chevron gris — plus une PASTILLE de couleur devant le libellé. La couleur ne
   disparaît pas, elle cesse d'être un aplat : elle redevient ce qu'elle est,
   un repère de lecture rapide.

   ⚠️ LA PASTILLE LIT `--st-*`, LE JETON DONT C'EST EXACTEMENT LE RÔLE — il est
   documenté « pastille » dans la palette, avec ses contrastes calculés pour un
   OBJET GRAPHIQUE (seuil 3:1) : 3,19 à 6,70 en clair, 6,45 à 10,69 en sombre.
   Aucun jeton nouveau, et surtout PAS `data-color` : ce serait la quatrième
   copie des couleurs de statut, celle qui a rendu ce composant insensible au
   thème jusqu'au 2026-08-07.

   ⚠️ ELLE SE POSE SUR LE BOUTON DU DROPDOWN (`.fbx-dd`), PAS SUR LE `<select>`
   NATIF : un `<select>` n'affiche aucun pseudo-élément. Le natif est le rendu
   de repli — visible seulement si le JS n'a pas tourné — et il garde son
   apparence ordinaire plutôt qu'un creux de 24 px sans rien dedans.

   ⚠️ LE CHEVRON REDEVIENT CELUI DE `.fbx-select`. Il était coloré par statut au
   moyen d'un masque, parce qu'un chevron gris paraissait sale sur un aplat
   teinté. Sans aplat, le problème n'existe plus : la règle et son masque sont
   retirés, et le bouton reprend le chevron en image de fond de `.fbx-select` —
   celui des filtres, ce qui est tout l'objet du changement.

   ⚠️ LA BORDURE REVIENT AU REPOS. Elle était transparente parce que le fond
   teinté disait déjà « contrôle » ; c'est elle qui le dit maintenant, et c'est
   elle qui accorde ce menu aux sélecteurs du bandeau. */
.fbx-status-select { font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold); }
/* La place de la pastille est prise sur le rembourrage, jamais sur le texte :
   8 px de pastille + 10 px de marge gauche + 6 px d'écart. Le rembourrage droit
   descend de 32 à 28 px — il reste 7 px entre le libellé et le chevron, et la
   colonne Statut n'a pas eu à s'élargir davantage. */
.fbx-status-select.fbx-dd { position: relative; padding-left: var(--fbx-space-24); padding-right: var(--fbx-space-28); }
.fbx-status-select.fbx-dd::before {
  content: ''; position: absolute; left: var(--fbx-space-10); top: 50%; transform: translateY(-50%);
  width: 8px; height: 8px; border-radius: var(--fbx-radius-full); pointer-events: none;
  background: var(--fbx-puce, var(--fbx-faint));
}
/* Le mapping statut -> pastille, et RIEN D'AUTRE : une seule déclaration par
   statut, la forme étant écrite une fois au-dessus. Ajouter un statut, c'est
   ajouter une ligne ici — pas une règle de plus. */
.fbx-status-select[data-status="active"]         { --fbx-puce: var(--st-active); }
.fbx-status-select[data-status="tocheck"]        { --fbx-puce: var(--st-tocheck); }
.fbx-status-select[data-status="needs-revision"] { --fbx-puce: var(--st-needs-revision); }
.fbx-status-select[data-status="pending"]        { --fbx-puce: var(--st-pending); }
.fbx-status-select[data-status="resolved"]       { --fbx-puce: var(--st-resolved); }
.fbx-status-select[data-status="cancelled"]      { --fbx-puce: var(--st-cancelled); }

/* ═══════════════════════════════════════════════════════════════════════════
   §9.2 — AVATARS
   CINQ tailles en service, aucune échelle déclarée : 18 (réponse),
   24 (ligne de table), 28 (review, kanban), 32 (popup du widget) et
   120 (carte de testeur du dashboard, la seule où l'avatar est le SUJET et
   non un repère — elle manquait au premier relevé). La BASE est 24 parce que
   c'est la taille de la ligne de board, l'emploi majoritaire.
   ⚠️ L'URL, elle, ne se règle pas ici : `TPXShared.buildAvatarUrl` existe déjà
   et cinq implémentations coexistent — dont `corrections.html` en Dicebear 7.x,
   qui rend un AUTRE visage pour la même personne. Écart fonctionnel (P37 §1.7),
   pas un sujet de feuille de style.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-avatar { width: 24px; height: 24px; border-radius: var(--fbx-radius-full); background: var(--fbx-fill-strong); flex-shrink: 0; display: block; }
.fbx-avatar--sm { width: 18px; height: 18px; }
.fbx-avatar--md { width: 28px; height: 28px; }
/* ⚠️ `.fbx-avatar--lg` (32 px) RETIRÉE le 2026-08-04, orpheline depuis sa
   création. Le 32 px de l'inventaire est celui du POPUP DU WIDGET, qui a sa
   propre feuille et ne consomme pas le socle : la taille avait été portée ici
   par symétrie avec un relevé, pas par un besoin.
   Elle reviendra le jour où un écran en aura l'usage — un socle se lit pour
   savoir ce qui existe, et une échelle qui annonce un cran vide fait chercher
   où il sert. */
/* Portrait : l'avatar EST le contenu. Carré fluide plafonné, pas une taille
   fixe — la carte le contraint.
   ⚠️ ELLE A ÉTÉ RETIRÉE PUIS RÉTABLIE LE MÊME JOUR (2026-08-06, P22), et
   l'aller-retour vaut d'être consigné. Retirée quand la carte de testeur du
   dashboard est devenue une rangée de liste : plus aucun écran ne faisait de
   l'avatar son contenu, verdict « morte », opposé à celui de `.fbx-btn--soft`.
   Rétablie deux heures plus tard, quand la GESTION des testeurs a pris son
   propre bloc dans `/admin/users.html` — avec ses cartes, donc ses portraits.
   Ce qui avait changé n'était pas le composant mais la décision produit.
   La leçon n'est pas « ne rien supprimer » : c'est qu'une orpheline se juge
   sur ce qui a cessé de l'employer, et que ce jugement se périme quand
   l'interface bouge. Le commentaire, lui, a survécu aux deux passes.
   ⚠️ ELLE N'EST PLUS FLUIDE, ET PLUS À 120 px (2026-08-06, troisième passe du
   même jour). Fluide plafonnée, elle supposait un conteneur qui la contraigne —
   c'était la carte. Dans une CELLULE DE TABLEAU, « 100 % » n'a pas de sens : la
   colonne prendrait la largeur de l'image, qui prendrait la largeur de la
   colonne. Taille fixe, donc, et **80 px** : la ligne reste haute — c'est voulu,
   l'avatar doit se voir — sans qu'un tableau de vingt-et-une personnes demande
   deux écrans de défilement. */
.fbx-avatar--xl { width: 80px; height: 80px; }

/* ⚠️ LA CARTE DE TESTEUR A VÉCU DEUX HEURES, le 2026-08-06 — et sa mort vaut
   d'être consignée, parce qu'elle ne dit rien du composant et tout de l'écran.
   Portée ici depuis le style local du dashboard, elle donnait à la section
   « Testeurs » d'`/admin/users.html` une grammaire — la GRILLE DE CARTES — que
   la section voisine du MÊME ÉCRAN n'avait pas : les comptes d'administration
   sont un TABLEAU. Deux façons de lister des gens l'une sous l'autre, avec deux
   formes d'action (boutons à libellé d'un côté, icônes de l'autre). C'est
   exactement ce que « une même information, une même place » interdit — appliqué
   à deux blocs séparés de trente pixels.
   Les testeurs sont donc passés en `.fbx-table`, comme les comptes. Ce qui
   restait de spécifique — le portrait — tient désormais dans une taille
   d'avatar, et rien d'autre.
   ⚠️ NE PAS RÉINTRODUIRE une grille de cartes sur cet écran sans traiter d'abord
   le tableau des comptes : c'est la COHABITATION qui est le défaut, pas la
   carte. Six classes retirées : `.fbx-tester-grid`, `-card`, `-portrait`,
   `-name`, `-count`, `-chiffres`. */

.fbx-author { display: flex; align-items: center; gap: var(--fbx-space-6); min-width: 0; }
.fbx-author-name {
  font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-semibold); color: var(--fbx-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ═══════════════════════════════════════════════════════════════════════════
   §9.3 — BARRE SEGMENTÉE, COMPTAGE COLORÉ, LIGNE DE LISTE
   ═══════════════════════════════════════════════════════════════════════════ */

/* Barre découpée par statut (dashboard, « Top pages »). Les segments portent
   `data-status` : plus aucun `#hex` en attribut `style` côté page. */
.fbx-bar {
  width: 120px; height: 8px; border-radius: var(--fbx-radius-xs); background: var(--fbx-fill);
  display: flex; overflow: hidden; flex-shrink: 0;
}
.fbx-bar > span { display: block; height: 100%; }
/* Barre PLEINE LARGEUR — même objet, employé comme jauge d'avancement (le
   tableau de bord du testeur montre où il en est de son travail sur la période).
   ⚠️ ELLE EXISTE POUR QUE LA COLORATION SUIVE : une jauge écrite en local
   n'héritait pas de `.fbx-bar > [data-status]`, et sortait toute grise — les
   segments étaient là, sans couleur, ce qui se lit comme une barre vide. */
.fbx-bar--full { width: 100%; height: 14px; border-radius: 7px; }
/* Barre LARGE — même objet, plus lisible quand la ligne a la place (2026-08-06).
   ⚠️ ET DE LARGEUR CONSTANTE. Elle valait auparavant `(total / max) × 100 %`,
   plafonnée et planchée : sur un jeu où un auteur pèse 58 %, tout le monde
   butait sur l'une des deux bornes et les barres finissaient à 118 px contre
   120 — un écart de deux pixels, visible, qui ne voulait RIEN dire. Une longueur
   qui varie sans mesurer est pire qu'une longueur fixe : elle se lit.
   Le volume est écrit dans la colonne « Total » ; la barre ne dit plus que la
   RÉPARTITION, et pour comparer des répartitions il faut justement une base
   commune. */
.fbx-bar--lg { width: 180px; }

/* Barre d'AVANCEMENT — un seul segment, une seule couleur (2026-08-10).
   ⚠️ CE N'EST PAS UNE BARRE DE RÉPARTITION, et c'est pour cela qu'elle porte un
   nom : `.fbx-bar` découpe un total en statuts, celle-ci dit « où j'en suis vers
   un objectif » (la progression d'un badge). Employée sans modificateur, un
   `<span>` sans `data-status` serait resté TRANSPARENT — la barre aurait paru
   vide, ce qui est précisément le contraire de ce qu'elle mesure.
   ⚠️ `--fbx-accent-fill` et non `--fbx-primary` : c'est un APLAT, pas du texte, et
   les deux rôles divergent en thème sombre. */
/* ⚠️ 10 px ET NON 8 (2026-08-11) : à 8 px sous un emblème de 64 px, la jauge se
   lisait comme un SOULIGNEMENT du texte au-dessus, pas comme une mesure. Le
   repère est `.fbx-bar--full` (14 px), qui traverse toute une carte ; celle-ci
   n'en occupe que 70 % et doit rester lisible à cette échelle. */
.fbx-bar--progres { width: 100%; height: 10px; border-radius: 5px; }
.fbx-bar--progres > span { background: var(--fbx-accent-fill); width: var(--fbx-bar-part, 0%); }
/* JAUGE À NIVEAU (2026-09-15) — la même barre, colorée par ce qu'elle mesure :
   vert tout va bien, orange attention, rouge dernière limite (« État du
   service »). Le niveau se dit par `data-niveau`, et le remplissage par la
   propriété `--fbx-bar-part` posée sur le segment : une valeur calculée par le
   JS se pose en propriété personnalisée, jamais en `width` écrite (règle 5).
   ⚠️ `data-niveau` ET NON `data-status` : celui-ci porte les états d'un RETOUR,
   contrôlés par la règle M ; y ranger « critique » ferait sortir une barre grise
   sans que rien ne le signale.
   ⚠️ LES JETONS `--fbx-ok` / `--fbx-warn` / `--fbx-danger`, pas les couleurs de
   statut : « résolu » et « actif » disent autre chose qu'un seuil franchi. */
.fbx-bar--progres[data-niveau="ok"] > span { background: var(--fbx-ok); }
.fbx-bar--progres[data-niveau="attention"] > span { background: var(--fbx-warn); }
.fbx-bar--progres[data-niveau="critique"] > span { background: var(--fbx-danger); }
/* FRISE D'ÉVÉNEMENTS (2026-09-15) — la même barre, lue comme une PÉRIODE : le
   fond dit « en marche », chaque trait un INSTANT où le service a redémarré,
   posé à sa place par `--fbx-bar-pos` et coloré par sa cause (`data-cause`).
   Carte « Service » d'« État du service », sur sept jours.
   ⚠️ SANS `data-niveau` : la jauge à niveau colore les segments, ici ils portent
   leur cause. Le niveau de la carte se dit dans l'encart, pas dans la frise.
   ⚠️ UN REDÉMARRAGE PRÉVU RESTE NEUTRE (déploiement, relance depuis l'écran) :
   seuls les imprévus portent une couleur d'alerte — orange pour un dépassement
   mémoire, rouge pour un plantage. */
.fbx-bar--frise { position: relative; background: var(--fbx-ok); }
.fbx-bar--frise > [data-cause] {
  position: absolute; top: 0; bottom: 0; left: var(--fbx-bar-pos, 0%);
  width: var(--fbx-frise-trait); background: var(--fbx-fill-strong);
}
.fbx-bar--frise > [data-cause="memoire"] { background: var(--fbx-warn); }
.fbx-bar--frise > [data-cause="plantage"] { background: var(--fbx-danger); }

/* PILE D'AVATARS — qui travaille sur cette recette, en un coup d'œil (2026-08-10).
   ⚠️ UNE PRÉSENCE, PAS UN CLASSEMENT : la pile ne porte ni volume ni date. Un
   compte par personne en ferait un palmarès, ce qu'un testeur n'a pas à lire sur
   ses collègues.
   ⚠️ SOI EN TÊTE, CERCLÉ D'ACCENT. Sans repère, un testeur cherche son propre
   portrait dans la pile à chaque ligne — et l'ordre étant fixe, il le cherche au
   même endroit à chaque fois.
   ⚠️ `row-reverse` + HTML ÉCRIT À L'ENVERS, et c'est la seule combinaison qui
   tienne les deux exigences à la fois. En flux normal, chaque avatar recouvre le
   précédent : le premier — le sien — passe DESSOUS et son cerclage est mangé.
   `row-reverse` renverse l'ordre d'AFFICHAGE sans toucher à l'ordre de PEINTURE,
   qui reste celui du DOM : le dernier écrit se retrouve donc à gauche ET
   au-dessus. L'appelant écrit la pile à l'envers (le « +N » en premier, soi en
   dernier).
   ⚠️ Une première version employait `direction: rtl` : elle réglait bien la
   superposition, mais posait « +2 » À GAUCHE et le portrait de la personne à
   droite — l'inverse de ce qu'on veut, constaté à l'écran. */
.fbx-avatar-pile {
  display: inline-flex; flex-direction: row-reverse; justify-content: flex-end;
  flex-shrink: 0;
}
.fbx-avatar-pile > * + * { margin-right: -8px; }
.fbx-avatar-pile .fbx-avatar {
  border: 2px solid var(--fbx-surface); box-sizing: content-box; background: var(--fbx-surface-sunken);
}
.fbx-avatar-pile .fbx-avatar.is-moi { border-color: var(--fbx-accent-fill); }
/* Le reste de la pile, quand elle dépasse quatre. Même gabarit qu'un avatar :
   une pastille plus petite ferait sauter la ligne de base de la rangée. */
.fbx-avatar-pile-plus {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: var(--fbx-radius-full);
  border: 2px solid var(--fbx-surface); box-sizing: content-box;
  background: var(--fbx-fill); color: var(--fbx-text-soft);
  font-size: var(--fbx-font-2xs); font-weight: var(--fbx-weight-semibold);
}

/* Pastille de comptage COLORÉE PAR STATUT.
   ⚠️ Ce n'est pas `.fbx-count`, qui est un compteur neutre et n'a pas de
   statut. Troisième occurrence du piège badge / chip / compteur : trois objets
   qui se ressemblent, trois noms. */
.fbx-status-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 var(--fbx-space-6); border-radius: var(--fbx-radius-lg);
  font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold); color: var(--fbx-on-accent); background: var(--st-pending-plate);
}
/* ⚠️ DEUX TABLES, ET C'EST LA CORRECTION DU 2026-08-07 : il n'y en avait qu'une,
   « pour les deux — segments et pastilles ». Or un SEGMENT de barre ne porte
   rien (objet graphique, seuil 3:1, la pastille de charte convient), tandis
   qu'une PASTILLE DE COMPTAGE porte un chiffre BLANC (seuil 4,5). Sous la meme
   valeur, le chiffre tombait a 3,19 en clair et a 1,67 en SOMBRE, ou `--st-*`
   devient une teinte vive faite pour se lire SUR le fond, pas dessous.
   Mutualiser deux roles parce qu'ils partagent une couleur, c'est ce que cette
   serie entiere aura corrige — ici la fusion etait meme ecrite en commentaire. */
.fbx-bar > [data-status="active"]         { background: var(--st-active); }
.fbx-bar > [data-status="tocheck"]        { background: var(--st-tocheck); }
.fbx-bar > [data-status="needs-revision"] { background: var(--st-needs-revision); }
.fbx-bar > [data-status="pending"]        { background: var(--st-pending); }
.fbx-bar > [data-status="resolved"]       { background: var(--st-resolved); }
.fbx-bar > [data-status="cancelled"]      { background: var(--st-cancelled); }

.fbx-status-count[data-status="active"]         { background: var(--st-active-plate); }
.fbx-status-count[data-status="tocheck"]        { background: var(--st-tocheck-plate); }
.fbx-status-count[data-status="needs-revision"] { background: var(--st-needs-revision-plate); }
.fbx-status-count[data-status="pending"]        { background: var(--st-pending-plate); }
.fbx-status-count[data-status="resolved"]       { background: var(--st-resolved-plate); }
.fbx-status-count[data-status="cancelled"]      { background: var(--st-cancelled-plate); }

/* Ligne « nom + barre + compteurs + actions ». Le motif est générique même si
   une seule page s'en sert aujourd'hui. Les actions passent par
   `.fbx-actions` / `.fbx-act-btn` (feedback-shared.css) : pas de deuxième
   grammaire de bouton-icône. */
.fbx-list { display: flex; flex-direction: column; }
.fbx-list-row {
  display: flex; align-items: center; gap: var(--fbx-space-12); padding: var(--fbx-space-10) 0;
  border-bottom: 1px solid var(--fbx-border-list); font-size: var(--fbx-font-base);
}
.fbx-list-row:last-child { border-bottom: none; }
/* Ligne ENTIÈREMENT CLIQUABLE (2026-08-10) — la rangée EST le lien, pas un
   libellé cliquable dedans.
   ⚠️ Pendant de `.fbx-card--clickable` pour une rangée : même geste, même
   grammaire. Un lien posé sur le seul nom obligerait à viser quelques dizaines
   de pixels alors que toute la rangée désigne le même objet.
   ⚠️ `color: inherit` et `text-decoration: none` sont OBLIGATOIRES : sans eux,
   le `<a>` teinte en bleu souligné TOUT son contenu, chiffres et pastilles
   compris.
   ⚠️ LE SÉLECTEUR PORTE SUR `a`, ET IL N'Y A PAS DE MODIFICATEUR `--clickable`.
   Un a été écrit puis retiré le jour même, orphelin dès sa naissance : il
   n'aurait servi qu'à rendre cliquable une rangée qui ne soit PAS un lien — donc
   une rangée hors du parcours au clavier. Le pendant `.fbx-card--clickable`
   existe parce qu'une carte peut légitimement être un `<div>` ; une rangée de
   liste qui mène quelque part est un lien, et l'écrire ainsi est la garantie
   qu'elle le reste.
   ⚠️ LE SURVOL RESPIRE, et ce n'est pas une coquetterie. Sans rembourrage
   latéral ni rayon, l'aplat de survol se colle aux deux bords de la carte et
   vient buter contre la vignette du projet : il se lit comme un bandeau qui
   déborde, pas comme une rangée désignée. La marge négative compense le
   rembourrage pour que le NOM reste aligné sur les autres blocs de la carte —
   sans elle, toute la liste se décalerait de 10 px vers la droite. */
a.fbx-list-row {
  color: inherit; text-decoration: none; cursor: pointer;
  padding-left: var(--fbx-space-10); padding-right: var(--fbx-space-10); margin-left: -10px; margin-right: -10px;
  border-radius: var(--fbx-radius);
  transition: background-color .12s;
}
a.fbx-list-row:hover { background: var(--fbx-row-hover); }
/* Rangée DISCRÈTE — elle n'appelle aucun geste (2026-08-10). Elle reste lisible
   et cliquable : ce qui baisse est le POIDS, pas la disponibilité. Un affaiblissement
   plus marqué se lirait comme « désactivé », ce qu'elle n'est pas. */
.fbx-list-row.is-discret .fbx-list-name { font-weight: var(--fbx-weight-normal); color: var(--fbx-text-muted); }
.fbx-list-name {
  flex: 1; min-width: 0; font-weight: var(--fbx-weight-medium); color: var(--fbx-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Mention secondaire posée À CÔTÉ d'un nom de liste — un fait sur la personne,
   pas une partie de son nom (2026-08-09). Elle hérite du gris et de la petite
   taille de `.fbx-hint`, et n'ajoute que l'écart et le poids normal : collée au
   nom en 500, elle se lisait comme un morceau de celui-ci. */
.fbx-list-name .fbx-list-meta { margin-left: var(--fbx-space-8); font-weight: var(--fbx-weight-normal); }
.fbx-list-counts { display: flex; gap: var(--fbx-space-4); flex-shrink: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   FIL D'ACTIVITÉ — le tableau de bord, pour combler l'intervalle entre deux
   digests. Deux classes seulement : le reste de la rangée est du socle
   existant (`.fbx-list-row`, `.fbx-avatar--md`, `.fbx-list-name`,
   `.fbx-hint .fbx-list-meta` pour l'extrait, `.fbx-status-badge`).

   ⚠️ NI LA DATE NI LE STATUT NE CHANGENT DE LARGEUR SELON LEUR VALEUR — même
   règle que les contrôles du bandeau, et **le relevé au navigateur a montré
   qu'il fallait les DEUX** : la date seule calée, les badges restaient en flux
   et se posaient à 1024 px sur une rangée, 1046 px sur la suivante — 22 px
   d'écart, assez pour que l'œil le voie sans savoir nommer ce qui cloche.
   ⚠️ La date EMPRUNTE sa teinte et sa taille à `.fbx-cell-date` (à porter
   avec) : une date de fil et une date de tableau sont la même information,
   elles n'ont aucune raison de diverger. Cette classe n'ajoute que le calage. */
.fbx-feed-quand { width: 92px; flex-shrink: 0; text-align: right; }

/* La colonne du statut. `min-width` et NON `width` : le plus long libellé des
   six mesure 67 px (« En attente », relevé au rendu, pas estimé) — 72 laisse la
   marge. Mais un statut ajouté demain peut être plus long, et une largeur ferme
   le COUPERAIT ; en `min-width` il s'étend, et seule sa rangée perd l'alignement.
   Dégrader l'alignement d'une ligne vaut mieux que rendre un mot illisible.

   ⚠️ ELLE NE S'APPUIE PLUS SUR `.fbx-status-badge` (2026-09-20, retour
   65eec032) : le fil portait une plaque colorée là où l'on ne peut rien
   changer — sur les deux boards, cette apparence est celle d'un CONTRÔLE
   (`.fbx-status-select`). Le mot reste, sa teinte vient de `.fbx-hint` comme
   celle de l'extrait voisin, et cette règle ne fait plus que ce que son
   commentaire annonce : caler la colonne.
   ⚠️ `margin-bottom: 0` PARCE QUE `.fbx-hint` EN POSE UNE : pensée pour une
   mention sous un champ, elle décalerait le mot vers le haut dans une rangée
   centrée. */
.fbx-feed-statut { min-width: 72px; text-align: center; flex-shrink: 0; margin-bottom: 0; }

/* LE REPÈRE DU DIGEST — un filet AVEC un mot, et c'est ce qui le distingue de
   `.fbx-separator` (un filet muet entre deux groupes de champs). Ici le trait
   ne sépare pas deux sujets : il date une frontière. Au-dessus, ce que le mail
   n'a pas encore dit ; en dessous, ce qui est déjà parti.

   ⚠️ IL N'APPARAÎT QUE SI LE COMPTE CONNECTÉ A REÇU UN DIGEST sur ce périmètre
   (cf. `dernierDigestRecu`) : le référent varie PAR PROJET, et un repère tiré
   du digest de quelqu'un d'autre marquerait « déjà lu » ce que ce lecteur-ci
   n'a jamais reçu. Pas de repère vaut mieux qu'un repère faux.
   ⚠️ Le filet est posé en `::before`/`::after` plutôt qu'en bordure de rangée :
   il doit passer DERRIÈRE le libellé et non sous lui, sans quoi le mot flotte
   au-dessus d'un trait qui le traverse. */
.fbx-feed-mark {
  display: flex; align-items: center; gap: var(--fbx-space-10);
  margin: var(--fbx-space-6) 0; font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold);
  color: var(--fbx-text-muted); text-transform: uppercase;
}
.fbx-feed-mark::before,
.fbx-feed-mark::after {
  content: ''; height: 1px; background: var(--fbx-border); flex: 1;
}
/* Le repère ne prend pas la couleur d'une alerte : il ne signale rien
   d'anormal, il situe. Un accent ici ferait lire « attention » là où on veut
   lire « à partir d'ici, vous saviez ». */

/* Comptage EN COLONNES ALIGNÉES — une colonne par statut, occupée ou non
   (2026-08-06). En flux, les pastilles se tassent à droite et une ligne sans
   « à revoir » décale toutes les suivantes : deux lignes voisines n'ont alors
   pas leurs chiffres sous les mêmes couleurs, et il faut relire chaque pastille
   au lieu de balayer une colonne. Le vide EST l'information — il dit « aucun ».
   ⚠️ La largeur de colonne est FIXE, pas `auto` : sur `auto`, une colonne dont
   toutes les valeurs tiennent sur un chiffre serait plus étroite que sa
   voisine à trois chiffres, et l'alignement ne survivrait pas au premier
   passage de 9 à 10.
   ⚠️ L'en-tête (`.fbx-list-head`) reprend LA MÊME grille : c'est ce qui
   remplace la légende de couleurs, qui obligeait à faire la correspondance de
   mémoire, une pastille à la fois. */
/* ⚠️ LA LARGEUR DE COLONNE EST CELLE DE SON INTITULÉ, pas celle de son chiffre.
   À 34 px — la largeur d'une pastille — les libellés se coupaient EN PLEIN MOT
   (« À VÉRIFI / ER », « ANNU / LÉ ») : illisible, et le pire des deux mondes,
   puisqu'on avait élargi le bloc pour gagner en lisibilité. 64 px laissent
   « VÉRIFIER » tenir sur une ligne sous son « À ». */
/* ⚠️ LE NOMBRE DE COLONNES EST UN PARAMÈTRE (`--fbx-list-cols`, 2026-08-10), et
   il vaut 7 par défaut — le dashboard d'administration (Total + les six statuts)
   n'a rien à déclarer et ne change pas d'un pixel. L'espace testeur n'en compte
   que quatre : sans ce réglage, il aurait fallu soit lui écrire une grille
   locale — donc un deuxième rendu de la même chose, qui aurait divergé au premier
   ajustement —, soit lui laisser trois colonnes vides. */
.fbx-list-counts--cols {
  display: grid; grid-template-columns: repeat(var(--fbx-list-cols, 7), 104px); gap: 0;
  align-items: stretch;
  /* ⚠️ `flex: 1` — LA GRILLE ABSORBE LE BLANC. À largeur fixe, tout l'espace
     libre s'accumulait entre le nom et la barre : sur un écran large, un vide
     de plusieurs centaines de pixels au milieu de la rangée, pendant que les
     colonnes de chiffres restaient serrées à droite. Les colonnes se partagent
     désormais cet espace (`1fr` chacune), avec un plancher qui garde les
     intitulés lisibles.
     ⚠️ `align-self: stretch` + marge négative : C'EST CE QUI REND LE FILET
     CONTINU. La rangée est un flex `align-items: center` — la grille y était
     donc CENTRÉE, haute de son seul contenu, et ses filets s'arrêtaient avant
     le haut et le bas de la rangée : une colonne de tirets. `stretch` lui donne
     toute la hauteur, la marge négative lui fait couvrir le `padding: 10px 0`
     de la rangée. */
  /* ⚠️ `flex: 0 0 auto` — LA GRILLE NE S'ÉTIRE PAS, C'EST LE NOM QUI CÈDE.
     Mesuré : en `flex: 1`, elle recevait une part de l'espace libre EN PLUS de
     sa largeur intrinsèque et débordait de 110 px, la dernière colonne sortant
     du bloc ; en `flex: 1 1 0` avec `minmax()`, le plancher des colonnes
     l'emportait sur la base zéro et produisait le même débordement.
     Colonnes à 104 px : assez larges pour que les intitulés respirent, et le
     nom — seul élément vraiment élastique de la rangée — prend ce qui reste. */
  flex: 0 0 auto; align-self: stretch; margin: -10px 0;
}
/* L'en-tête n'a pas le même rembourrage que les rangées (`0 0 6px`) : sans
   cette remise à zéro, ses filets dépasseraient sous le trait de séparation. */
.fbx-list-head .fbx-list-counts--cols { margin: 0 0 -6px; }
/* ⚠️ UN FILET ENTRE LES COLONNES, et il n'est pas décoratif : sans lui, sept
   colonnes de chiffres alignées dans du blanc se lisent comme une seule zone
   floue, et l'œil doit remonter jusqu'à l'en-tête pour savoir laquelle il lit.
   Sur la PREMIÈRE, il sépare les comptes du nom — c'est la même fonction.
   `align-items: stretch` + flex interne : le filet doit courir sur toute la
   hauteur de la rangée, donc les cellules la remplissent et centrent leur
   contenu elles-mêmes. Centrées par la grille, elles n'auraient eu que la
   hauteur d'une pastille et les filets auraient été des tirets. */
/* ⚠️ `margin` NÉGATIVE + `padding` ÉGALE : c'est ce qui rend le filet CONTINU.
   La rangée porte `padding: 10px 0` ; une cellule qui s'arrête au contenu
   laisse donc dix pixels de blanc en haut et en bas, et la colonne se lit
   comme une suite de tirets au lieu d'un trait. En débordant du padding puis
   en le recréant à l'intérieur, la cellule couvre toute la hauteur de la
   rangée sans déplacer son contenu d'un pixel. */
/* COMPTEUR CLIQUABLE — il mène à la liste qu'il compte (2026-08-06).
   Elle ne décrit pas une apparence mais un FAIT : ce chiffre-là a une
   destination. Le socle ne peut pas le deviner — sur le tableau de bord elle
   n'existe que si un projet est choisi, sur `/admin/testeurs.html` elle existe
   toujours. Posée sur un `<span>` sans `href`, elle donnerait la main du
   curseur et un survol pour rien : une promesse plus trompeuse qu'un lien mort.
   Elle vivait en local sur le dashboard sous le nom `.tester-lien` ; deux
   écrans l'emploient, elle monte au socle. */
.fbx-count-link { text-decoration: none; cursor: pointer; }
.fbx-count-link:hover { filter: brightness(.94); }

/* LIEN DE TEXTE — le nom d'une ligne qui ouvre SA page (2026-08-28, P63).
   ⚠️ CE N'EST PAS `.fbx-count-link`, ET LA DISTINCTION EST CELLE QUE LE SOCLE
   FAIT DEJA POUR LES STATUTS : celui-la habille un CHIFFRE qui mene a la liste
   qu'il compte — il n'a ni couleur ni graisse, parce qu'il se pose sur une
   pastille qui en a deja. Celui-ci est un lien de TEXTE, et sans lui un nom
   cliquable arrivait bleu souligne du navigateur, au milieu d'un tableau ou
   plus rien d'autre ne l'est.
   ⚠️ L'OUTIL N'AVAIT AUCUN LIEN DE TEXTE, et ce n'etait pas un oubli : rien
   ne menait nulle part. C'est ce que P63 change — le nom d'un projet ouvre son
   ecran, celui d'une livraison ouvre le sien. Deux consommateurs des le premier
   jour, d'ou sa place au socle. */
.fbx-lien { color: var(--fbx-accent-fg); font-weight: var(--fbx-weight-semibold); text-decoration: none; }
.fbx-lien:hover { text-decoration: underline; }

.fbx-list-col {
  display: flex; align-items: center; justify-content: center;
  border-left: 1px solid var(--fbx-border-list);
}

/* ⚠️ LA CELLULE ET LE COMPTEUR SONT DEUX ELEMENTS, et il le faut : poser le
   `display: flex` directement sur la pastille l'etirait sur les 74 px de sa
   colonne — un « 1 » dans un ovale de la largeur d'un bouton. La cellule tient
   la place et porte le filet ; la pastille garde sa forme et se centre dedans. */
.fbx-list-head {
  display: flex; align-items: flex-end; gap: var(--fbx-space-12); padding: 0 0 var(--fbx-space-6);
  border-bottom: 1px solid var(--fbx-border, var(--fbx-border));
  font-size: var(--fbx-font-2xs); font-weight: var(--fbx-weight-semibold); letter-spacing: .3px; text-transform: uppercase;
  color: var(--fbx-text-muted);
}
.fbx-list-head .fbx-list-name { font: inherit; color: inherit; }
/* ⚠️ `word-break: normal` — un intitulé se coupe ENTRE LES MOTS, jamais dedans.
   `break-word` produisait « À VÉRIFI / ER » : le mot le plus important de la
   colonne, rendu méconnaissable pour gagner six pixels. */
.fbx-list-head .fbx-list-counts--cols > span {
  text-align: center; line-height: 1.15; word-break: normal; hyphens: none;
}

/* ── REPLI DES COMPTES QUAND LA RANGÉE N'A PLUS LA PLACE (2026-08-20) ────────
   La grille est en largeur FIXE et `flex: 0 0 auto` : 7 × 104 = 728 px qui ne
   cèdent jamais. Le raisonnement tient sur un écran large — l'alignement ne
   survivrait pas à des colonnes `auto` — mais il n'avait AUCUNE SORTIE BASSE, et
   le composant poussait alors la page entière. Mesuré : +703 px de défilement
   horizontal sur le tableau de bord à 375 px, +116 px sur celui du testeur
   (4 colonnes), et surtout **+69 px dès 1024 × 768** : ce n'était donc pas un
   défaut mobile mais un défaut d'iPad en paysage et de fenêtre à moitié d'écran.
   Une page d'administration qui défile latéralement est un défaut, pas une
   adaptation.

   ⚠️ ON REPLIE LA RANGÉE, ON NE DÉSALIGNE PAS LES COLONNES. Le premier réflexe
   — repasser les comptes en flux de pastilles — aurait tué la seule chose que ce
   composant apporte : le commentaire plus haut le dit, en flux « une ligne sans
   à-revoir décale toutes les suivantes », et il faut relire chaque pastille au
   lieu de balayer une colonne. En passant la RANGÉE en colonne (nom au-dessus,
   comptes dessous sur toute la largeur), les pistes restent identiques d'une
   rangée à l'autre : le balayage vertical est intact, le vide dit toujours
   « aucun ». C'est le repli qui coûte le moins de sens.

   ⚠️ PLANCHER À 64 px, ET CE CHIFFRE EST DÉJÀ ARBITRÉ PLUS HAUT : c'est la
   largeur où « VÉRIFIER » tient sur une ligne sous son « À ». En dessous, les
   intitulés se coupent en plein mot — le défaut que la largeur de 104 px avait
   justement corrigé. L'en-tête est donc CONSERVÉ, pas masqué : sans lui, les
   colonnes redeviennent une légende de couleurs à retenir de mémoire.

   ⚠️ CE BLOC EST PLACÉ APRÈS `.fbx-list-head`, ET C'EST OBLIGATOIRE. Écrit plus
   haut, il perdait contre `align-items: flex-end` de cet en-tête : même
   spécificité (0,1,0), donc c'est l'ordre du fichier qui tranche — et une règle
   sous `@container` ne gagne aucun bonus. Symptôme constaté : la grille repliée
   restait large de 64 px, une seule colonne au lieu de sept.

   ⚠️ LES MARGES NÉGATIVES SONT ANNULÉES. Elles font courir le filet vertical sur
   toute la hauteur d'une rangée en flex ; la rangée repliée n'a plus cette
   géométrie, et les garder décalerait la grille hors de son bloc. */
.fbx-list { container-type: inline-size; container-name: liste; }

@container liste (max-width: 870px) {
  /* ⚠️ `:has()` — LE REPLI NE VISE QUE LES RANGÉES QUI PORTENT DES COMPTES.
     Écrit sur `.fbx-list-row` tout court, il attrapait aussi le FIL D'ACTIVITÉ
     du tableau de bord, qui emploie la même rangée sans avoir le problème :
     ses badges de statut s'étiraient alors sur toute la largeur du bloc. Une
     règle de repli doit viser la cause (la grille fixe), pas la famille. */
  .fbx-list-row:has(.fbx-list-counts--cols),
  .fbx-list-head:has(.fbx-list-counts--cols) {
    flex-direction: column; align-items: stretch; gap: var(--fbx-space-8);
  }
  /* ⚠️ `row-gap` — SANS LUI LES RANGS SE LISENT COMME UNE SEULE LIGNE. Sept
     colonnes qui n'en tiennent que trois produisent trois rangs ; collés, les
     intitulés de l'en-tête s'enchaînaient en « TOTAL À REVOIR ANNULÉ », lu comme
     un libellé unique de trois mots au lieu de trois colonnes distinctes. Le
     `gap: 0` d'origine suppose une rangée unique. */
  .fbx-list-counts--cols,
  .fbx-list-head .fbx-list-counts--cols {
    grid-template-columns: repeat(auto-fit, minmax(64px, 1fr));
    margin: 0; align-self: stretch; row-gap: var(--fbx-space-8);
  }
  /* Le filet de gauche sépare une colonne de sa voisine ; sur la première d'un
     rang il n'a plus rien à séparer, la rangée commençant au bord du bloc. */
  .fbx-list-counts--cols > .fbx-list-col:first-child { border-left: 0; }
}
.fbx-show-all {
  display: block; width: 100%; text-align: center; padding: var(--fbx-space-10) 0 var(--fbx-space-2);
  font: inherit; font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-medium); color: var(--fbx-accent-fg);
  background: none; border: none; cursor: pointer;
}
.fbx-show-all:hover { text-decoration: underline; }

/* ═══════════════════════════════════════════════════════════════════════════
   §9.4 — CARTES DE KPI ET LÉGENDE DE GRAPHE
   ═══════════════════════════════════════════════════════════════════════════ */

/* Grille de cartes. 8 colonnes : une par statut plus le total — la rangée du
   dashboard doit couvrir TOUS les statuts pour que Total = somme (règle de
   CLAUDE.md). Les paliers sont ceux de la page, repris tels quels. */
.fbx-cards-row { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--fbx-space-14); margin-bottom: var(--fbx-space-24); }
/* Rangée à NOMBRE VARIABLE de cartes — elles se partagent toute la largeur
   (2026-08-06). Les huit colonnes ci-dessus sont justes POUR LE DASHBOARD : il
   a huit cartes de statut, et elles doivent s'aligner d'un projet à l'autre même
   quand l'une vaut zéro. Ailleurs, ce gabarit réserve les colonnes manquantes et
   laisse un vide à droite : « État du service » a CINQ tuiles de santé, donc
   trois huitièmes de page blanche, avec des tuiles trop étroites pour ce
   qu'elles portent (« Dernière synchro » passait sur deux lignes). */
.fbx-cards-row--fit { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
@media (max-width: 1280px) { .fbx-cards-row { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 1024px) { .fbx-cards-row { grid-template-columns: repeat(3, 1fr); } }
@media (max-width:  768px) { .fbx-cards-row { grid-template-columns: repeat(2, 1fr); } }
/* QUATRE COLONNES, RANGÉES DE MÊME HAUTEUR (2026-09-15) — « État du service ».
   Huit cartes en deux rangées de quatre : `--fit` en posait sept et une sur une
   page large. `grid-auto-rows: 1fr` donne à TOUTES les rangées la hauteur de la
   plus haute — des cartes de même taille, demandé par Stéphane —, et la carte en
   colonne cale sa jauge en pied : les jauges d'une rangée s'alignent quelle que
   soit la longueur du détail qui les précède.
   ⚠️ APRÈS LES PALIERS CI-DESSUS, ET C'EST VOULU : même spécificité, l'ordre
   tranche. Posée avant, la variante perdait sous 1280 px.
   ⚠️ LE REPLI SE DÉCIDE SUR LA PLACE DE LA PAGE (`@container page`), pas sur la
   fenêtre : replier la barre latérale change la place sans changer la fenêtre.
   700 px : au-dessous, « EN MARCHE DEPUIS » ne tient plus sur une ligne dans un
   quart de largeur. */
.fbx-cards-row--quatre { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: 1fr; }
.fbx-cards-row--quatre > .fbx-card { display: flex; flex-direction: column; }
.fbx-cards-row--quatre > .fbx-card > .fbx-bar { margin-top: auto; }
/* Un peu d'air entre le chiffre et ce qui le situe (2026-09-15, relevé par
   Stéphane : « il y a 42 j » collait à sa date). */
.fbx-cards-row--quatre > .fbx-card > .fbx-kpi-value + .fbx-hint { margin-top: var(--fbx-space-6); }
@container page (max-width: 700px) { .fbx-cards-row--quatre { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container page (max-width: 360px) { .fbx-cards-row--quatre { grid-template-columns: minmax(0, 1fr); } }

/* Carte à chiffre. S'emploie DANS un `.fbx-card` : le socle avait le conteneur,
   pas son contenu chiffré. */
.fbx-kpi-value { font-size: 32px; font-weight: var(--fbx-weight-bold); margin: var(--fbx-space-8) 0 var(--fbx-space-4); line-height: 1.1; }
.fbx-kpi-label { font-size: var(--fbx-font-sm); color: var(--fbx-text-soft); text-transform: uppercase; letter-spacing: .5px; }

/* Chiffre RÉDUIT — le même KPI posé *dans* une carte au lieu de la rangée du
   haut. Monté au socle le 2026-08-04 : `dashboard` l'écrivait sous
   `.mail-kpi-value` (24 px) à côté de `.fbx-kpi-value` (32 px) — même rôle,
   deux grandeurs, et le libellé était redéclaré à 11 px pour aller avec.
   Une variante, pas une classe locale : c'est la même chose en plus petit. */
.fbx-kpi-value--sm { font-size: 24px; margin: 0; }
.fbx-kpi-label--sm { font-size: var(--fbx-font-xs); letter-spacing: .4px; margin-top: var(--fbx-space-4); }

/* Tuile à chiffre — une mini-carte posée SUR une carte. Ce n'est pas un
   `.fbx-card` (fond blanc, ombre portée) : posée sur du blanc, une carte y
   serait invisible ; c'est un aplat gris cerné d'un filet. Rangée fluide, les
   tuiles se partagent la largeur et passent à la ligne.
   `.fbx-kpi-tile--accent` marque CELLE QUI TOTALISE : bande à la marque à
   GAUCHE et chiffre teinté — c'est le seul rang de la rangée qui n'est pas une
   part. */
.fbx-kpi-tiles { display: flex; flex-wrap: wrap; gap: var(--fbx-space-12); margin-bottom: 18px; }
.fbx-kpi-tile {
  flex: 1 1 120px; padding: var(--fbx-space-12) var(--fbx-space-14);
  background: var(--fbx-surface-sunken); border: 1px solid var(--fbx-border-list); border-radius: var(--fbx-radius);
  /* Porte la bande de `--accent`. Déclaré sur la tuile et non sur le
     modificateur : c'est l'ancrage du pseudo-élément, il ne dépend pas de
     l'état. */
  position: relative; overflow: hidden;
}
/* ⚠️ UNE BANDE À GAUCHE, EN PSEUDO-ÉLÉMENT — LA MÊME QUE `.fbx-card--rail`, et
   c'est une correction (2026-08-11). Cette tuile portait un `border-top: 3px`,
   soit le SEUL endroit de l'outil où « ce chiffre-ci totalise » se disait par le
   haut : les sept cartes KPI du dashboard, les cartes de recette et « État du
   service » le disent toutes par une bande à GAUCHE. Deux grammaires pour un
   seul rôle, sur un écran qui porte les deux à quinze centimètres d'écart.
   ⚠️ ET CE N'EST PAS QU'UNE AFFAIRE DE CÔTÉ : une `border` épouse le rayon des
   angles, donc s'amincit à ses extrémités et paraît déformée. Le `::before`
   rectangulaire, coupé net par `overflow: hidden`, reste un trait droit — c'est
   la raison écrite au-dessus de `.fbx-card--rail`, elle vaut ici mot pour mot.
   ⚠️ MÊME LARGEUR (5 px) QUE LA GRANDE CARTE, délibérément : la tuile est « la
   même chose en plus petit » (cf. `.fbx-kpi-value--sm`), et une bande à 3 px se
   lirait comme un autre signal plutôt que comme le même, plus discret. */
.fbx-kpi-tile--accent::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  background: var(--fbx-accent-fg);
}
.fbx-kpi-tile--accent .fbx-kpi-value { color: var(--fbx-accent-fg); }

/* ⚠️ `text-decoration: none` ET `color: inherit` : une carte cliquable peut etre
   un <a> (le tableau de bord du testeur en pose), et un <a> se souligne et se
   colore par defaut. Le soulignement disait « lien » sur certaines tuiles et pas
   sur d'autres, selon qu'elles portaient un href — deux apparences pour le meme
   objet, dans la meme rangee. Ce qui signale le cliquable, c'est le SURVOL. */
.fbx-card--clickable { cursor: pointer; transition: box-shadow .15s, transform .15s;
                       text-decoration: none; color: inherit; }
.fbx-card--clickable:hover { box-shadow: var(--fbx-shadow-card-hover); transform: translateY(-1px); }

/* Filet de statut en tête de carte. Piloté par `data-status`, donc par `--st-*` :
   la page écrivait `#6b7280` et `#78716c` EN DUR pour `pending` et `cancelled`,
   les deux seuls statuts sans token `--fb-*`. Ils en ont un ici. */
/* ⚠️ BANDE À GAUCHE, ET NON EN TÊTE — arbitré le 2026-08-09, pour TOUT l'outil.
   Le filet était en `border-top`. Une bande de tête se perd dès que le bloc
   dépasse deux ou trois lignes : elle marque le haut, pas le bloc. Une bande
   verticale, elle, court sur toute la hauteur — donc elle tient aussi bien sur
   une tuile de KPI que sur un bloc haut, et c'est le même objet qui sert dans
   les deux cas. */
/* ⚠️ LA BANDE EST UN PSEUDO-ÉLÉMENT, PAS UNE BORDURE — et ce n'est pas un détail
   de mise en œuvre. Une `border-left` sur une carte à coins arrondis **épouse la
   courbe** : la bande s'amincit en haut et en bas, et le trait de couleur paraît
   déformé. Rendue en `::before` rectangulaire, coupé net par le rayon de la
   carte (`overflow: hidden`), elle reste un trait droit, de largeur constante,
   dans un bloc qui garde ses angles arrondis.
   ⚠️ `overflow: hidden` : rien ne doit sortir d'une carte à bande — pas de menu
   déroulant ni d'infobulle qui déborde. Si un composant en avait besoin, il ne
   prendrait pas ce modificateur. */
.fbx-card--rail { position: relative; overflow: hidden; }
.fbx-card--rail::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  /* ⚠️ `--rail-couleur` permet de poser une couleur qui N'EST PAS un statut —
     celle d'un PROJET, par exemple, pour qu'une recette se reconnaisse à sa
     bande. Sans elle il aurait fallu un second composant, qui aurait diverge
     du premier au premier ajustement. Le defaut reste le statut. */
  background: var(--rail-couleur, var(--st-pending));
}
.fbx-card--rail[data-status="active"]::before         { background: var(--st-active); }
.fbx-card--rail[data-status="tocheck"]::before        { background: var(--st-tocheck); }
.fbx-card--rail[data-status="needs-revision"]::before { background: var(--st-needs-revision); }
.fbx-card--rail[data-status="pending"]::before        { background: var(--st-pending); }
.fbx-card--rail[data-status="resolved"]::before       { background: var(--st-resolved); }
.fbx-card--rail[data-status="cancelled"]::before      { background: var(--st-cancelled); }
/* Le total n'est pas un statut — il porte la couleur de marque. */
.fbx-card--rail[data-status="total"]::before          { background: var(--fbx-accent-fg); }

/* Légende de graphe. QUATRIÈME objet à pastille, et il fallait le nommer :
   ce n'est pas un badge (il n'affiche pas l'état d'un élément), pas un chip
   (il ne se clique pas), pas un compteur (il porte un libellé). C'est une
   légende — elle explique une couleur. */
.fbx-legend { display: flex; justify-content: center; flex-wrap: wrap; gap: var(--fbx-space-16); margin-top: var(--fbx-space-24); font-size: var(--fbx-font-sm); color: var(--fbx-text-soft); }
.fbx-legend-item { display: flex; align-items: center; gap: 5px; }
.fbx-legend-dot { width: 8px; height: 8px; border-radius: var(--fbx-radius-full); flex-shrink: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   §9.5 — LE RESTE, DÉJÀ CHIFFRÉ EN §1
   ═══════════════════════════════════════════════════════════════════════════ */

/* Largeurs de contenu — UNE PAR TYPE D'ÉCRAN (P41).
   Les modificateurs précédents (`--narrow` 700, `--md` 900, `--wide` 1400)
   nommaient une TAILLE : rien ne disait quel écran devait prendre laquelle, et
   trois pages n'en portaient aucune — une quatrième largeur, par omission.
   Ceux-ci nomment un USAGE, et la question « laquelle ? » se répond alors
   toute seule : la page est-elle un board, un tableau de bord, un formulaire ?

     --board  1400  liste large de retours    feedbacks · corrections · review · corbeille
     --dash   1200  cartes et tableaux        dashboard · admin-users
     --form    800  saisie en colonne         setup · export-context

   800 plutôt que les 700 d'export-context : à 700, les deux boutons de pied de
   `/admin/users.html` passaient à la ligne — c'est le constat qui a ouvert P41. */
/* ⚠️ LES TROIS LARGEURS ONT ETE FONDUES EN UNE, le 2026-08-03.

   P41 avait pose une largeur par TYPE d'ecran (board / dash / form), puis on les
   avait resserrees — deux fois, et deux fois le meme reproche est revenu : « on
   oscille encore trop ». Le defaut n'etait pas dans les valeurs, il etait dans
   l'idee : **une page n'a pas de type, c'est son CONTENU qui en a un.** Un ecran
   comme `setup` porte a la fois un index de projets (large) et un formulaire
   (etroit) ; aucune largeur de page ne pouvait convenir aux deux.

   Desormais : **une seule largeur de page**, et c'est le BLOC qui se contraint.
   Un formulaire prend `.fbx-card--form`, une table prend toute la place. Le
   cadre ne bouge plus d'un ecran a l'autre — seule la mesure de lecture change,
   la ou elle doit changer. */
/* ⚠️ ET LA DERNIERE LARGEUR A SAUTE A SON TOUR, le 2026-08-04 : il n'y a plus
   AUCUN plafond de page. C'est la CONCLUSION de la regle ci-dessus, pas une
   entorse — si le bloc contraint deja ce qui doit etre etroit, le plafond de
   page est redondant.

   1400 n'a jamais ete une decision de lisibilite : c'etait un VESTIGE DE FUSION,
   le compromis entre trois ecrans qui n'avaient pas le meme besoin — et le board
   y avait perdu 200 px (`--board` valait 1600). Mesure a 1920 px : la page
   s'arretait a 1400 et laissait **276 px de vide a droite**, qui n'etaient pas
   une respiration voulue mais **deux marges automatiques**.

   Ce que le retrait rend : la cellule de commentaire passe de **482 a 743 px**
   (+54 %), et c'est la colonne qui porte le fil de reponses, la note interne et
   les actions.

   ⚠️ `--fbx-page-w` A ETE SUPPRIMEE : `.fbx-main` en etait le seul lecteur, un
   jeton que plus personne ne lit est du code mort — meme regle que pour une
   classe orpheline, et la page *Recette & couverture* l'enonce.

   ⚠️ LE `padding: 24px` FAIT LES QUATRE COTES, bas compris : ne pas le
   remplacer par des marges laterales en retirant le plafond. */
/* ⚠️ CONTENEUR DECLARE (2026-08-28) : c'est la PLACE DISPONIBLE qui decide d'un
   repli, jamais la taille de la fenetre — entre la barre depliee (244 px) et le
   rail (64 px), elle change de 180 px sans qu'un seul pixel de fenetre bouge.
   Meme raison que `.fbx-grid--board`, et meme effet utile : replier le rail
   ramene la seconde colonne. */
.fbx-main { padding: var(--fbx-space-24); container-type: inline-size; container-name: page; }

/* LA GOUTTIERE DU WIDGET POSE SUR L'OUTIL (2026-09-11).
   Quand le superadmin est connecte, le serveur injecte le widget feedback dans
   la page et pose `data-widget-outil` sur `<body>`. Le rail du widget est fixe,
   a droite : sans cette reserve, il recouvrait la derniere colonne des boards
   (« Lien », 44 px) et les actions de fin de ligne.
   ⚠️ LE DECLENCHEUR EST L'ATTRIBUT POSE PAR LE SERVEUR, JAMAIS LA PRESENCE DU
   WIDGET : le rail se monte au DOMContentLoaded, apres le premier rendu — une
   regle qui l'attendrait ferait sauter la page de 68 px a chaque chargement.
   ⚠️ LA RESERVE S'AJOUTE AU REMBOURRAGE, ELLE NE LE REMPLACE PAS : le contenu
   garde ses 24 px d'air avant le rail, comme avant le bord. */
.fbx-body[data-widget-outil] .fbx-main {
  padding-right: calc(var(--fbx-space-24) + var(--fbx-widget-outil-reserve));
}
/* ⚠️ SOUS 720 px, LE WIDGET N'EST PAS OFFERT. La barre de navigation de
   l'outil et le rail du widget y deviennent TOUS DEUX une barre basse, au meme
   seuil et au meme endroit : le rail, pose apres, recouvrait la navigation.
   Les empiler aurait pris 116 px de hauteur sur l'axe qu'on venait d'epargner,
   pour un usage qui se fait au poste de travail. On masque le rail — l'unique
   point d'entree du widget —, et la reserve tombe avec lui.
   ⚠️ `#feedback-panel` EST UN ELEMENT DU WIDGET, pas du socle : c'est la seule
   regle d'ici qui le vise, et elle ne le vise que sur l'outil. */
@media (max-width: 720px) {
  .fbx-body[data-widget-outil] .fbx-main { padding-right: var(--fbx-space-24); }
  .fbx-body[data-widget-outil] #feedback-panel { display: none; }
}
/* ⚠️ À L'IMPRESSION, LA RÉSERVE TOMBE AVEC LE RAIL (2026-09-18) : le widget ne
   s'imprime pas (`feedback.css`, section « Impression »), et la gouttière
   laisserait sur le papier une bande blanche là où il se trouvait. */
@media print {
  .fbx-body[data-widget-outil] .fbx-main { padding-right: var(--fbx-space-24); }
}

/* DEUX COLONNES — LA SAISIE A GAUCHE, CE QU'ON EMPORTE A DROITE (2026-08-28).
   ⚠️ IL N'Y AVAIT AUCUNE GRILLE DE PAGE AU SOCLE, et chaque ecran qui en voulait
   une la redeclarait sous un nom local (`.layout`). Celle-ci est nommee par son
   ROLE : une colonne de travail, une colonne de sortie.
   ⚠️ LA COLONNE DE GAUCHE PORTE LA MESURE DE SAISIE (`--fbx-form-w`), la droite
   prend ce qui reste : le snippet est du code, il gagne a etre large, et il n'a
   pas de mesure de lecture a respecter.
   ⚠️ `minmax(0, …)` DES DEUX COTES : sans le zero, un `<pre>` long impose sa
   largeur a la grille et pousse la page — un bloc de code ne se replie pas tout
   seul. */
.fbx-colonnes {
  display: grid;
  grid-template-columns: minmax(0, var(--fbx-form-w)) minmax(0, 1fr);
  gap: var(--fbx-space-16);
  align-items: start;
}
@container page (max-width: 1100px) {
  /* Sous ce seuil, la colonne de droite descend : deux colonnes dont l'une est
     trop etroite pour son contenu ne valent pas mieux qu'une. */
  .fbx-colonnes { grid-template-columns: minmax(0, 1fr); }
}

/* Carte de SAISIE. 760px ≈ 90 caracteres : au-dela, un champ de texte devient
   penible a relire, et un formulaire etale sur 1400px n'a plus d'axe. */
.fbx-card--form { max-width: var(--fbx-form-w); }

/* Trois copies identiques (dashboard, setup). */
.fbx-footer { text-align: center; padding: var(--fbx-space-24); font-size: var(--fbx-font-sm); color: var(--fbx-text-muted); }

/* Pied de page en DEUX BLOCS (2026-08-05) : ce qu'on CONSULTE à gauche, où
   ALLER à droite. Le dashboard empilait les deux, centrés — d'où un lien perdu
   sous une ligne de chiffres, qui ne se lisait ni comme une donnée ni comme une
   destination.
   ⚠️ MODIFICATEUR et non changement de la base : `.fbx-footer` reste centrée
   pour `setup` et pour le design system, qui n'y mettent qu'une ligne de texte —
   un `space-between` sur un enfant unique le collerait à gauche, sans que rien
   ne le signale. */
.fbx-footer--split {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--fbx-space-16); flex-wrap: wrap; text-align: left;
}

/* ═══ LE BANDEAU DES BOARDS AU TÉLÉPHONE (2026-09-25) ═══════════════════════
   Sous 528 px de bandeau, il suit la maquette : le périmètre sur une rangée
   (projet et période, ou projet et livraison), la recherche et le bouton
   « Filtres », puis « N retours · Actualisé à HH:MM · [Actualiser] » ; les
   pastilles de statut défilent au lieu de passer à la ligne. Les autres
   filtres (tag, audience, testeur) passent dans le panneau « Filtres ».
   ⚠️ 528 = 480 + 2 × 24 : le bandeau est HORS de `.fbx-main`, dont la
   gouttière fait les 48 px d'écart avec le board. Liste et bandeau basculent
   ensemble (mesuré, plan boards-mobile §9).
   ⚠️ UN CONTRÔLE QUI PASSE AU PANNEAU SE DÉCLARE DANS LA PAGE
   (`data-filtre-panneau` sur le `<select>`) ; le socle masque l'enveloppe que
   `enhanceSelect` a posée autour, par `:has()`. */
.fbx-board-sticky { container-type: inline-size; container-name: bandeau; }
.fbx-recherche-filtres { display: contents; }
.fbx-filtres-btn { display: none; }
.fbx-status-chip[data-filtre="miens"] { display: none; }

@container bandeau (max-width: 528px) {
  .fbx-board-sticky .fbx-page-header {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--fbx-space-8); padding: var(--fbx-space-10) var(--fbx-space-16);
  }
  .fbx-board-sticky .fbx-page-header > * { width: auto; }
  .fbx-board-sticky .fbx-page-header .fbx-select-icon:has(> [data-filtre-panneau]),
  .fbx-board-sticky .fbx-page-header [data-filtre-panneau] { display: none; }
  .fbx-board-sticky .fbx-recherche-filtres {
    grid-column: 1 / -1; display: flex; align-items: center; gap: var(--fbx-space-8);
  }
  .fbx-board-sticky .fbx-recherche-filtres > .fbx-search { flex: 1; min-width: 0; display: flex; }
  .fbx-board-sticky .fbx-recherche-filtres .fbx-search-input { flex: 1; width: auto; min-width: 0; }
  /* L'intervalle de dates tient dans sa demi-rangée : il relâche le gabarit à
     deux dates et tronque, plutôt que de pousser la page. */
  .fbx-board-sticky .fbx-daterange { min-width: 0; }
  .fbx-board-sticky .fbx-page-header .fbx-select-icon { min-width: 0; }
  .fbx-board-sticky .fbx-page-header .fbx-dd { min-width: 0; overflow: hidden; }
  .fbx-board-sticky .fbx-daterange-btn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .fbx-board-sticky .fbx-header-actions { grid-column: 1 / -1; }
  .fbx-board-sticky .fbx-filtres-btn {
    position: relative; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: var(--fbx-cible-tactile); height: var(--fbx-cible-tactile); padding: 0;
    border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius-full);
    background: var(--fbx-surface); color: var(--fbx-text); cursor: pointer;
  }
  .fbx-board-sticky .fbx-status-chip[data-filtre="miens"] { display: inline-flex; }
  .fbx-board-sticky .fbx-chips--band {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-inline: var(--fbx-space-16);
  }
  .fbx-board-sticky .fbx-chips--band::-webkit-scrollbar { display: none; }
  .fbx-board-sticky .fbx-chips--band > * { flex-shrink: 0; }
}
/* ── LA BARRE D'ACTIONS EN LOT (P23, montée au socle le 2026-09-25) ─────────
   Elle vivait dans la page (`.fb-bulk-bar`), « tant qu'une deuxième page ne
   l'emploie pas » ; le téléphone en fait un second usage — une barre fixée au
   bas de l'écran —, d'où sa place ici. Ce qui s'y range se déclare par
   `data-lot` : compte, tout, statut, supprimer, vider. */
.fbx-barre-lot { background: var(--fbx-surface-sunken); border-bottom-color: var(--fbx-border); }
.fbx-barre-lot-compte { font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-semibold); color: var(--fbx-accent-fg); }
.fbx-barre-lot [data-compact] { display: none; }
/* LE BOUTON FLOTTANT — `.fbx-fab` (2026-09-25, arbitrage A7). Sur ordinateur,
   un bouton comme un autre, à sa place dans le bandeau ; au téléphone, il
   flotte au-dessus de la barre de navigation, là où le pouce l'atteint. */
@container bandeau (max-width: 528px) {
  .fbx-board-sticky .fbx-fab {
    position: fixed; z-index: var(--fbx-z-flottant);
    right: var(--fbx-space-16); bottom: calc(var(--fbx-navbar-h) + var(--fbx-space-16) + env(safe-area-inset-bottom, 0px));
    min-height: var(--fbx-cible-tactile-lg); padding: 0 var(--fbx-space-20);
    border-radius: var(--fbx-radius-xl); box-shadow: var(--fbx-shadow-pop); font-size: var(--fbx-font-md);
    /* ⚠️ Sous 720 px, les boutons des marqueurs deviennent des carrés de
       32 px sans mot : le bouton flottant, posé parmi eux, garde le sien. */
    width: auto; white-space: nowrap; gap: var(--fbx-space-6);
  }
  /* La barre de lot se fixe au bas de l'écran, sur deux rangées : ce qu'on a
     choisi (et comment en sortir), puis ce qu'on en fait. */
  .fbx-board-sticky .fbx-barre-lot:not([hidden]) {
    position: fixed; z-index: var(--fbx-z-flottant); left: 0; right: 0;
    bottom: calc(var(--fbx-navbar-h) + env(safe-area-inset-bottom, 0px));
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "compte tout" "statut supprimer" "vider vider";
    gap: var(--fbx-space-8); padding: var(--fbx-space-12) var(--fbx-space-16);
    border-top: 1px solid var(--fbx-border); box-shadow: var(--fbx-shadow-pop);
  }
  .fbx-board-sticky .fbx-barre-lot [data-compact] { display: inline-flex; }
  .fbx-board-sticky .fbx-barre-lot [data-lot="compte"] { grid-area: compte; align-self: center; }
  .fbx-board-sticky .fbx-barre-lot [data-lot="tout"] { grid-area: tout; }
  .fbx-board-sticky .fbx-barre-lot [data-lot="statut"],
  .fbx-board-sticky .fbx-barre-lot [data-lot="statut"] + .fbx-dd { grid-area: statut; }
  .fbx-board-sticky .fbx-barre-lot [data-lot="supprimer"] { grid-area: supprimer; }
  .fbx-board-sticky .fbx-barre-lot [data-lot="vider"] { grid-area: vider; }
  .fbx-board-sticky .fbx-barre-lot .fbx-btn,
  .fbx-board-sticky .fbx-barre-lot .fbx-dd { min-height: var(--fbx-cible-tactile); justify-content: center; }
}
/* LA DERNIÈRE LIGNE NE RESTE PAS SOUS UN ÉLÉMENT FLOTTANT. ⚠️ `@media` et non
   `@container` : `.fbx-main` n'est pas dans le conteneur du bandeau. Au-dessus
   du seuil compact, la réserve reste sans effet visible (rien ne flotte). */
@media (max-width: 720px) {
  .fbx-body:has(.fbx-fab) .fbx-main,
  .fbx-body:has(.fbx-barre-lot:not([hidden])) .fbx-main { padding-bottom: var(--fbx-reserve-flottant); }
}
/* Le nombre de filtres actifs, en pastille sur le bouton. */
.fbx-filtres-compte {
  position: absolute; top: calc(-1 * var(--fbx-space-2)); right: calc(-1 * var(--fbx-space-2));
  min-width: var(--fbx-space-16); height: var(--fbx-space-16); padding: 0 var(--fbx-space-4); box-sizing: border-box;
  border-radius: var(--fbx-radius-pill); background: var(--fbx-accent-fill); color: var(--fbx-on-accent);
  font-size: var(--fbx-font-2xs); font-weight: var(--fbx-weight-bold); line-height: var(--fbx-space-16); text-align: center;
}
.fbx-filtres-compte[hidden] { display: none; }
/* Bandeau collant : en-tête + filtres restent visibles au défilement.
   Identique au commentaire près dans feedbacks et review. */
.fbx-board-sticky { position: sticky; top: 0; z-index: var(--fbx-z-sticky); box-shadow: var(--fbx-shadow-sticky); }

/* ⚠️ EN MOBILE, IL DÉFILE AVEC LA PAGE (2026-08-20). Mesuré sur `/feedbacks` à
   375 × 844 : le bandeau occupe **356 px collants**, soit 42 % de la hauteur,
   pendant qu'une ligne de board repliée en carte en fait 344 à 369. Il restait
   donc 488 px pour le contenu, c'est-à-dire **1,3 retour visible à la fois** sur
   un board qui en compte 189 — et cela ne s'améliorait jamais en défilant,
   puisque c'est précisément ce que « collant » veut dire.

   ⚠️ CE QU'ON PERD EST RÉEL ET C'EST L'ARBITRAGE : les filtres et le compteur ne
   sont plus sous la main en permanence, il faut remonter. Sur un écran large, un
   bandeau qui tient sur deux rangées coûte peu et sert à chaque instant ; sur un
   téléphone, il coûte la moitié de l'écran pour des contrôles qu'on emploie une
   fois — on cadre, PUIS on lit. Remonter est un geste, relire un board par la
   fente de 488 px en est un par retour.

   ⚠️ SUR LE SEUL AXE OÙ CE N'EST PAS GRAVE. La règle de la barre de navigation,
   plus haut, arbitre dans l'autre sens : la largeur est rare sur un téléphone,
   pas la hauteur. Ici on rend de la hauteur au prix d'un défilement ; là on rend
   de la largeur, qui ne se récupère par aucun geste.

   ⚠️ LE SECOND CRITÈRE EST UNE HAUTEUR, ET IL CORRIGE UNE ERREUR DE LA PREMIÈRE
   VERSION DE CETTE RÈGLE : écrite sous le seul `max-width: 720px`, elle ne
   voyait pas le **téléphone en paysage**. À 844 × 390, la largeur passe le
   seuil — donc le bandeau restait collant — alors que c'est précisément là que
   la hauteur manque le plus : 148 px de bandeau sur 390, pour des lignes de
   302 px, soit **moins d'une ligne visible**. *Un repli se décide sur l'axe qui
   manque* : le bandeau coûte de la HAUTEUR, son critère est donc une hauteur. */
@media (max-width: 720px), (max-height: 600px) {
  .fbx-board-sticky { position: static; box-shadow: none; }
}

/* Barre de filtres en BANDEAU. La `.fbx-filter-bar` du socle est nue (elle
   vivait dans le flux d'une page) ; les trois barres réelles sont des bandeaux
   pleine largeur sous l'en-tête. Modificateur, pour ne pas toucher la base.
   Filet : `--fbx-border` (#e5e7eb, review) et non #f0f0f0 (corrections,
   feedbacks) — #f0f0f0 n'existe nulle part ailleurs dans le socle. */
.fbx-filter-bar--band {
  background: var(--fbx-surface); border-bottom: 1px solid var(--fbx-border);
  padding: var(--fbx-space-12) var(--fbx-space-24);
}
/* Largeur FIXE des selects dans un bandeau : sans elle, la barre « danse » à
   chaque changement de filtre, la largeur du select suivant l'option choisie.
   Le libellé est déjà coupé proprement par `.fbx-dd-label`. */
.fbx-filter-bar--band .fbx-select { width: 165px; flex: 0 0 auto; }
/* Les pastilles de statut sont un SECOND bandeau de filtres, sous le premier :
   même fond, même gouttière, même filet. Sans ce modificateur elles étaient
   posées nues et centrées sur la largeur du tableau — donc décalées par rapport
   à la barre au-dessus, et transparentes : le tableau défilait AU TRAVERS. */
.fbx-chips--band {
  background: var(--fbx-surface); border-bottom: 1px solid var(--fbx-border);
  padding: var(--fbx-space-10) var(--fbx-space-24);
}

/* Enveloppe de la liste d'un board.
   ⚠️ L'EN-TÊTE de groupe repliable (`-head`, `-name`, `-toggle`, `.fbx-chevron`)
   a été RETIRÉ le 2026-08-02 avec les blocs par page (P23) : les deux boards
   rendent désormais une liste à plat, plus personne ne l'employait. Un composant
   que personne n'emploie n'est pas un composant, c'est une intention — et une
   intention se retrouve dans git, pas dans une feuille de style.
   S'il fallait un jour regrouper à nouveau : commit `7ef3119`. */
.fbx-page-group { margin-bottom: var(--fbx-space-24); }

/* `.fbx-link-sheets` — RETIRÉE en P45. C'était le lien direct vers le classeur
   Google Sheets, en trois copies identiques (corrections, dashboard, feedbacks),
   mutualisé par P37 puis supprimé des trois écrans : il n'apportait qu'une
   entrée de plus dans un bandeau qu'on allégeait. Une classe sans consommateur
   ne reste pas au socle — c'est la règle que le compteur de la vitrine tient. */

/* Bouton VERT. ✅ **Doublon soldé le 2026-08-03** : `.fbx-btn--save` portait le
   MÊME vert plus trois états, et P37 §9.8 prévoyait de fondre les deux « en un
   composant à modificateur d'état » — c'est fait.

   Le nom retenu est `--success`, pas `--save`, et ce n'est pas indifférent : il
   décrit une APPARENCE, que « ✓ Résolu » sur une carte de kanban porte aussi
   bien qu'un bouton d'enregistrement. `--save` aurait menti sur le premier, qui
   n'enregistre rien. Les trois états restent disponibles pour qui en a besoin,
   et ne coûtent rien à qui ne les emploie pas.

   Vert = prêt, ambre = en cours, rouge = échec : les mêmes trois couleurs que
   partout ailleurs dans l'outil. */
.fbx-btn--success { background: var(--fbx-ok); color: var(--fbx-on-accent); border-color: transparent; }
.fbx-btn--success:hover { background: var(--fbx-ok-hover); }
.fbx-btn--success.is-saving { background: var(--fbx-warn); }
.fbx-btn--success.is-error  { background: var(--fbx-danger); }

/* Bouton d'appel DISCRET : contour et texte à la marque, fond clair. Relevé par
   la recette du 2026-08-01, absent du backlog initial — `dashboard` (« Inviter »)
   et `feedbacks` (« Répondre ») le portent sous deux noms. Ni `--primary` (trop
   affirmé pour une action secondaire) ni `--outline` (gris, il ne dit pas que
   l'action est celle de la marque). */
.fbx-btn--soft {
  background: var(--fbx-accent-bg); color: var(--fbx-accent-fg);
  border-color: transparent;
}
.fbx-btn--soft:hover { background: rgba(var(--fbx-accent-rgb), .08); }

/* Même geste que `--soft`, mais pour une action destructrice SECONDAIRE, posée
   au bout d'une ligne de table. `--danger` (rouge plein) est un bouton d'appel :
   sept lignes de comptes en donneraient sept, et l'écran n'appelle à rien.
   Relevé sur `admin-users` (« Supprimer » d'une ligne), qui l'écrivait sous le
   même nom `.btn-danger` que le rouge plein des autres pages — faux doublon. */
.fbx-btn--danger-soft { background: var(--fbx-surface); color: var(--fbx-danger-fg); border-color: var(--fbx-danger-bd); }
.fbx-btn--danger-soft:hover { background: var(--fbx-danger-bg); border-color: var(--fbx-danger-fg); }

/* État de chargement d'un bouton — UNE mécanique, là où il y en avait deux :
   `dashboard` échangeait une icône contre un `.spinner`, `feedbacks` coupait
   les événements avec `.loading`. On fait les deux, sous un seul nom. */
.fbx-btn.is-loading { opacity: .7; pointer-events: none; }
.fbx-btn .fbx-spinner { display: none; border-color: rgba(var(--fbx-on-accent-rgb), .35); border-top-color: var(--fbx-on-accent); }
.fbx-btn.is-loading .fbx-spinner { display: inline-block; }
.fbx-btn.is-loading .fbx-btn-icon { display: none; }

/* Roue d'attente. `corrections`/`feedbacks` : 32px, bordure 3, `margin:80px
   auto`. `review` : 40px, bordure 4, centrée en flex. La marge est de la MISE
   EN PAGE, pas du composant → elle sort dans `--block`. */
.fbx-spinner {
  width: 32px; height: 32px; border: 3px solid var(--fbx-border);
  border-top-color: var(--fbx-accent-fg); border-radius: var(--fbx-radius-full);
  animation: fbx-spin .8s linear infinite; box-sizing: border-box;
}
.fbx-spinner--lg { width: 40px; height: 40px; border-width: 4px; }
.fbx-spinner--sm { width: 14px; height: 14px; border-width: 2px; }
.fbx-spinner--block { margin: 80px auto; }
@keyframes fbx-spin { to { transform: rotate(360deg); } }

/* Modale. Le socle n'en avait aucune ; les pages en portent cinq (export ×2,
   invitation, éditeur d'avatar, gestion des comptes) avec deux structures.
   Retenue : celle en TROIS ZONES (dashboard) — un pied qui ne défile pas est
   la seule qui tienne quand le corps est long. Rayon 16 = `--fbx-radius-xl`,
   celui de la boîte de connexion : une surface flottante, un rayon. */
.fbx-modal-overlay {
  position: fixed; inset: 0; background: var(--fbx-overlay); z-index: var(--fbx-z-modal);
  display: flex; align-items: center; justify-content: center; padding: var(--fbx-space-16);
  opacity: 0; pointer-events: none; transition: opacity .2s;
}
.fbx-modal-overlay.is-on { opacity: 1; pointer-events: auto; }
.fbx-modal {
  background: var(--fbx-surface); border-radius: var(--fbx-radius-xl);
  width: 100%; max-width: 440px; max-height: 88vh;
  display: flex; flex-direction: column; box-shadow: var(--fbx-shadow-modal);
}
.fbx-modal--md   { max-width: 520px; }
.fbx-modal--wide { max-width: 820px; }
/* `--xl` (2026-08-09) — pour une modale qui montre une GALERIE, pas un
   formulaire : l'éditeur d'avatar aligne jusqu'à 65 vignettes par trait. À
   820 px, la modale des réglages restait visible tout autour d'elle, si bien
   qu'on ne savait plus laquelle des deux répondait au clavier.
   ⚠️ Une valeur en `min()` et non 1100 px sec : sur un portable à 1280 px, un
   plafond fixe laisserait 90 px de chaque côté, ce qui rend la marge plus
   voyante que le contenu. */
.fbx-modal--xl   { max-width: min(1100px, 94vw); }
.fbx-modal-header {
  display: flex; align-items: center; gap: var(--fbx-space-8); padding: var(--fbx-space-16) var(--fbx-space-20);
  border-bottom: 1px solid var(--fbx-border); flex-shrink: 0;
}
.fbx-modal-header h3 { margin: 0; font-size: 15px; font-weight: var(--fbx-weight-bold); color: var(--fbx-text); flex: 1; }
.fbx-modal-close {
  background: none; border: none; font-size: 20px; line-height: var(--fbx-leading-none); padding: 0;
  color: var(--fbx-text-muted); cursor: pointer; width: 28px; height: 28px;
  border-radius: var(--fbx-radius-sm); display: flex; align-items: center; justify-content: center;
}
.fbx-modal-close:hover { background: var(--fbx-fill); color: var(--fbx-text); }
.fbx-modal-body { padding: var(--fbx-space-16) var(--fbx-space-20); overflow-y: auto; flex: 1; min-height: 0; }
.fbx-modal-footer {
  padding: var(--fbx-space-12) var(--fbx-space-20); border-top: 1px solid var(--fbx-border); flex-shrink: 0;
  display: flex; justify-content: flex-end; align-items: center; gap: var(--fbx-space-8);
  font-size: var(--fbx-font-sm); color: var(--fbx-text-soft);
}
/* ── PANNEAU BAS — `openModal({variante: 'sheet'})` (2026-09-25) ────────────
   La modale du téléphone : elle monte du bas, sur toute la largeur, arrondie
   en haut seulement, avec une poignée qui dit qu'elle se referme vers le bas.
   Même voile, même ombre, même fermeture (croix, voile, Échap) que la modale
   centrée : c'est la même modale, posée autrement. */
.fbx-modal-overlay--sheet { align-items: flex-end; padding: 0; }
.fbx-modal--sheet {
  max-width: none; max-height: var(--fbx-sheet-max-h);
  border-radius: var(--fbx-radius-xl) var(--fbx-radius-xl) 0 0;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.fbx-modal-poignee {
  width: var(--fbx-poignee-w); height: var(--fbx-poignee-h); flex-shrink: 0;
  margin: var(--fbx-space-8) auto 0; border-radius: var(--fbx-radius-pill); background: var(--fbx-border-strong);
}
.fbx-modal--sheet .fbx-modal-header { border-bottom: 0; padding-bottom: var(--fbx-space-4); }
.fbx-modal--sheet .fbx-modal-close { width: var(--fbx-cible-tactile); height: var(--fbx-cible-tactile); }
.fbx-modal--sheet .fbx-modal-body { padding: 0 0 var(--fbx-space-8); }

/* ── PANNEAU D'ACTIONS D'UNE LIGNE (2026-09-25) ─────────────────────────────
   Le retour visé en tête, puis les gestes de `BOARD_ACTIONS` par groupe :
   principaux, secondaires, puis la suppression, seule et en rouge. */
.fbx-panneau-entete { padding: 0 var(--fbx-space-20) var(--fbx-space-12); border-bottom: 1px solid var(--fbx-border); }
.fbx-panneau-meta {
  display: flex; align-items: center; gap: var(--fbx-space-8); min-width: 0;
  font-size: var(--fbx-font-sm); color: var(--fbx-text-muted);
}
.fbx-panneau-meta > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fbx-panneau-texte {
  margin: var(--fbx-space-6) 0 0; font-size: var(--fbx-font-md); line-height: var(--fbx-leading-snug); color: var(--fbx-text);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.fbx-panneau-groupe { display: grid; padding: var(--fbx-space-4) 0; }
.fbx-panneau-groupe + .fbx-panneau-groupe { border-top: 1px solid var(--fbx-border); }
.fbx-panneau-entree {
  display: flex; align-items: center; gap: var(--fbx-space-16); box-sizing: border-box;
  min-height: var(--fbx-cible-tactile-lg); padding: 0 var(--fbx-space-20);
  border: 0; background: none; font: inherit; font-size: var(--fbx-font-lg); color: var(--fbx-text);
  text-align: left; cursor: pointer; justify-self: stretch;
}
.fbx-panneau-entree > svg { flex-shrink: 0; color: var(--fbx-text-muted); }
.fbx-panneau-entree:hover, .fbx-panneau-entree:focus-visible { background: var(--fbx-fill); outline: none; }
.fbx-panneau-entree.is-danger, .fbx-panneau-entree.is-danger > svg { color: var(--fbx-danger-fg); }
.fbx-panneau-entree.is-on { font-weight: var(--fbx-weight-semibold); }
.fbx-panneau-libelle { flex: 1; min-width: 0; }
/* ── PANNEAU « RÉPONDRE » (2026-09-25) ──────────────────────────────────────
   Le retour en entier, ses dernières réponses, le statut à poser, l'éditeur. */
.fbx-panneau-texte.is-complet { display: block; -webkit-line-clamp: unset; overflow: visible; }
.fbx-panneau-fil { padding: var(--fbx-space-8) var(--fbx-space-20) 0; }
.fbx-panneau-plus {
  min-height: var(--fbx-cible-tactile); padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold); color: var(--fbx-accent-fg);
}
.fbx-panneau-section { padding: var(--fbx-space-12) var(--fbx-space-20) 0; }
.fbx-panneau-section > .fbx-panneau-entree { padding-inline: 0; }
.fbx-panneau-section-titre {
  display: block; margin-bottom: var(--fbx-space-8);
  font-size: var(--fbx-font-sm); font-weight: var(--fbx-weight-semibold); color: var(--fbx-text-muted);
}
.fbx-panneau-editeur { padding: var(--fbx-space-12) var(--fbx-space-20) 0; }
.fbx-modal--sheet .fbx-modal-footer { padding: var(--fbx-space-12) var(--fbx-space-20); }
.fbx-panneau-envoyer { flex: 1; justify-content: center; min-height: var(--fbx-cible-tactile-lg); }
/* Le pied du panneau Filtres : « Réinitialiser » et « Afficher N retours ». */
.fbx-panneau-pied-btn { flex: 1; justify-content: center; min-height: var(--fbx-cible-tactile-lg); }
/* ── SEGMENTS — un choix exclusif, en rangée (2026-09-25) ───────────────────
   L'audience du panneau Filtres : trois valeurs qui s'excluent, toutes visibles.
   Une pastille dit « j'ajoute ce filtre », un segment « je choisis cette vue ». */
.fbx-segments {
  display: flex; padding: var(--fbx-space-2); gap: var(--fbx-space-2);
  border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius); background: var(--fbx-surface-sunken);
}
.fbx-segment {
  flex: 1; min-height: var(--fbx-cible-tactile); border: 0; border-radius: var(--fbx-radius-sm);
  background: none; font: inherit; font-size: var(--fbx-font-md); color: var(--fbx-text-soft); cursor: pointer;
}
.fbx-segment[aria-checked="true"] {
  background: var(--fbx-surface); color: var(--fbx-text); font-weight: var(--fbx-weight-semibold); box-shadow: var(--fbx-shadow-card);
}

/* ── DICTÉE DANS L'ÉDITEUR DU SOCLE (2026-09-25) ────────────────────────────
   Le micro de la barre, allumé pendant l'écoute ; sous la saisie, un point qui
   bat et le segment provisoire. Portage de la ligne d'état du widget. */
.fbx-ed-btn.is-active { background: var(--fbx-danger-bg); color: var(--fbx-danger-fg); }
.fbx-dictee-etat {
  display: flex; align-items: center; gap: var(--fbx-space-6); padding: var(--fbx-space-4) var(--fbx-space-10);
  font-size: var(--fbx-font-sm); color: var(--fbx-text-muted);
}
.fbx-dictee-etat[hidden] { display: none; }
.fbx-dictee-point {
  width: var(--fbx-space-8); height: var(--fbx-space-8); border-radius: var(--fbx-radius-full);
  background: var(--fbx-danger-fg); animation: fbx-dictee-bat 1s ease-in-out infinite;
}
@keyframes fbx-dictee-bat { 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) { .fbx-dictee-point { animation: none; } }
.fbx-dictee-interim { font-style: italic; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fbx-panneau-valeur {
  flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fbx-font-sm); color: var(--fbx-text-muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   TOASTS — quatre niveaux, et un second composant qui n'en est pas un
   Relevé du 2026-08-01 : le socle n'offrait que « neutre » et « erreur ». Or
   l'outil en émet QUATRE. Le widget client, notamment, confirme la demande de
   lien magique par un toast BLEU — un niveau que le socle ne savait pas rendre,
   donc que personne n'aurait pu réutiliser.
   ⚠️ Le toast du widget (`FeedbackWidget._showToast`) garde SA PROPRE TABLE DE
   COULEURS en dur (`#e53e3e` / `#dd6b20` / `#38a169` / `#3182ce`) — une
   cinquième source, distincte de `--fbx-danger`/`--fbx-warning`/`--fbx-success`/
   `--fbx-info`. On ne l'importe pas : on aligne le socle sur SES tokens et on
   nomme l'écart dans /admin/design-system.html §Toast.
   ⚠️ MISE À JOUR DU 2026-08-11 — « aucune règle CSS » n'est plus vrai : la
   FORME du toast du widget vit désormais dans `feedback.css` (`.fb-toast`), et
   seul le FOND reste posé par le JS, parce qu'il dépend du niveau du message.
   Le déménagement n'était pas cosmétique : l'apparition dépendait d'un
   `requestAnimationFrame` et le message restait invisible là où cette frame ne
   vient pas. **L'écart de COULEURS, lui, demeure entier** — ne pas lire cette
   ligne comme sa résolution.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-toast--success { background: var(--fbx-ok); }
.fbx-toast--warning { background: var(--fbx-warn); color: var(--fbx-on-warn); }
.fbx-toast--info    { background: var(--fbx-info); }

/* Toast EN LIGNE — autre composant, malgré le nom. Il ne flotte pas : il prend
   sa place dans le flux, à côté du bouton qui vient d'agir (l écran de lien
   expiré, `TPXShared.mountExpiredAccess`).
   Un message qui doit rester sous les yeux pendant qu'on relit le formulaire
   n'est pas un message transitoire. */
.fbx-toast-inline {
  display: none; font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-medium);
  padding: var(--fbx-space-8) var(--fbx-space-14); border-radius: var(--fbx-radius);
}
.fbx-toast-inline.is-on { display: inline-block; }
.fbx-toast-inline--success { background: rgba(var(--fbx-ok-rgb), .1); color: var(--fbx-ok-fg); }
.fbx-toast-inline--error   { background: rgba(var(--fbx-danger-rgb), .1); color: var(--fbx-danger-fg); }
.fbx-toast-inline--info    { background: rgba(var(--fbx-info-rgb), .1); color: var(--fbx-info-fg); }

/* ═══════════════════════════════════════════════════════════════════════════
   NOTE INTERNE (P21) — l'annotation de travail de l'équipe
   Une note par retour, visible des seuls écrans d'administration. Elle n'est
   PAS une réponse : la réponse s'adresse au contributeur (widget, /review,
   digest), la note ne sort jamais de l'espace admin. C'est tout l'objet de
   cette phase, et c'est ce que le rendu doit dire au premier coup d'œil —
   d'où l'ambre (`--fbx-warning`), la seule couleur du socle qui ne soit prise
   par aucun statut affiché sur les boards ET qui se lise comme un avertissement
   plutôt que comme un état du retour. Le libellé « Interne » est écrit dans le
   bloc, pas seulement suggéré par la couleur : une couleur seule ne se lit pas
   sur une capture en noir et blanc, ni pour un daltonien.

   Trois états, trois classes, jamais un `style` posé à la main :
     `.fbx-note`            — le bloc, note posée
     `.fbx-note--empty`     — aucune note : déclencheur discret, pas un bloc vide
     `.fbx-note.is-editing`  — la saisie a remplacé le corps
   ═══════════════════════════════════════════════════════════════════════════ */
/* ⚠️ PLUS DE FOND AMBRE (2026-08-31) : le filet de gauche suffit a dire qu'on
   change d'objet, et il le dit sans peser. L'aplat, lui, faisait de la note le
   bloc le plus lourd d'un fil ou elle est le moins frequent — zero enregistrement
   en production au 31/08. Le survol garde sa teinte : c'est la seule chose qui
   dise « ceci se clique ». */
.fbx-note {
  margin-top: var(--fbx-space-8); padding: 7px var(--fbx-space-10);
  border-left: 3px solid var(--fbx-warn);
  border-radius: 0 var(--fbx-radius-sm) var(--fbx-radius-sm) 0;
  font-size: var(--fbx-font-base); line-height: var(--fbx-leading-normal); color: var(--fbx-text);
  cursor: text;
}
.fbx-note:hover { background: rgba(var(--fbx-warn-rgb), .16); }
/* Pas de note ⇒ RIEN dans le flux. C'est la seconde correction du 2026-08-03,
   et elle va plus loin que la première.
     1er jet  : le déclencheur s'affichait sur toutes les lignes — bruyant.
     2e jet   : révélé au survol — mieux, mais il restait un objet d'un genre
                unique, un lien textuel, là où les trois autres actions de la
                ligne sont des icônes dans une barre.
     Retenu   : **la note interne devient la QUATRIÈME icône** de la barre
                d'actions (`.fbx-act-btn`, comme répondre / modifier /
                supprimer). Quatre actions du même rang, quatre boutons
                identiques — et le curseur, la taille de cible et la révélation
                au survol viennent alors du composant existant, sans règle
                propre.
   Le bloc reste rendu (il porte le formulaire d'édition), simplement invisible
   tant qu'il n'y a rien à lire. `.is-on` le montre quand même — la vitrine du
   design system s'en sert pour documenter l'état vide. */
.fbx-note--empty { display: none; }
.fbx-note--empty.is-on {
  display: block; border-left-color: transparent; background: none;
  padding: var(--fbx-space-3) 0; color: var(--fbx-text-muted); font-size: var(--fbx-font-xs);
}
/* En édition, un bloc « vide » redevient un bloc à part entière. */
.fbx-note--empty.is-editing { display: block; }
/* L'icône s'allume quand une note existe : la barre dit alors qu'il y a
   quelque chose à lire, sans qu'on ait à parcourir la ligne. */
.fbx-act-btn.has-note { color: var(--fbx-warn-fg); }
.fbx-act-btn.has-note:hover { background: rgba(var(--fbx-warn-rgb), .16); color: var(--fbx-warn-fg); }

/* ── UNE NOTE SE LIT COMME UNE REPONSE (2026-08-31) ────────────────────────
   `.fbx-note-head` A ETE RETIREE : la note reemploie `.fbx-reply-head`, donc son
   avatar, son nom et sa date relative. Elle portait avant son propre en-tete —
   un glyphe et le mot « INTERNE » en capitales a gauche, l'auteur relegue en
   petit a droite —, si bien que la premiere chose lue etait ce qu'un fond ambre
   disait deja, pendant que QUI a ecrit et QUAND se cherchait au bout de la ligne.
   ⚠️ CE QUI SUBSISTE EST LE RAPPEL, ET IL A CHANGE DE PLACE : « Note interne »
   pousse a droite de l'en-tete, la ou une reponse met sa date. Il faut qu'il
   reste — ecrire pour l'equipe en croyant ecrire au contributeur serait la faute
   que ce bloc existe pour empecher — mais il n'a pas a occuper la place de
   l'identite.
   ⚠️ `nowrap` + le `flex-wrap` de `.fbx-reply-head` : dans la colonne Commentaire
   d'un board la largeur descend sous 130 px, et le libelle se coupait EN PLEIN
   MILIEU. Il passe entier a la ligne suivante. */
.fbx-note .fbx-reply-head { flex-wrap: wrap; }
.fbx-note-meta {
  margin-left: auto; white-space: nowrap;
  font-size: var(--fbx-font-2xs); font-weight: var(--fbx-weight-bold);
  letter-spacing: .04em; text-transform: uppercase; color: var(--fbx-warn-fg);
}
/* Le texte de la note. `pre-wrap` : elle est saisie dans une <textarea>, donc
   les retours à la ligne sont ceux que l'auteur a tapés — les perdre changerait
   une liste de points en un pavé. */
.fbx-note-body { white-space: pre-wrap; word-wrap: break-word; }
.fbx-note-form { display: none; }
.fbx-note.is-editing { cursor: default; }
.fbx-note.is-editing .fbx-note-body, .fbx-note.is-editing .fbx-note-empty-label { display: none; }
.fbx-note.is-editing .fbx-note-form { display: block; }
/* La saisie hérite de `.fbx-textarea` ; seule la hauteur descend, une note
   tenant en deux lignes là où une réponse en fait dix. */
.fbx-note-form .fbx-textarea { min-height: 52px; font-size: var(--fbx-font-base); }

/* ═══════════════════════════════════════════════════════════════════════════
   Réglages — UNE modale, une colonne d'onglets à gauche, un panneau à droite.

   Le premier jet ouvrait un menu flottant (Mes informations / Préférences /
   Déconnexion), chacun menant à SA propre modale : trois surfaces pour un même
   sujet — ce qui me concerne, moi — et un menu qui ne sert qu'à en ouvrir un
   autre est un clic qui n'apprend rien. Une seule surface, donc : ajouter une
   section (le thème, demain) n'en crée pas une de plus.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-modal.fbx-reglages {
  display: flex; flex-direction: row; padding: 0; overflow: hidden;
  width: min(860px, 94vw); max-width: none; height: min(560px, 88vh); max-height: 88vh;
}
.fbx-reglages-rail {
  width: 216px; flex-shrink: 0; padding: var(--fbx-space-16) var(--fbx-space-10) 0;
  background: var(--fbx-bg); border-right: 1px solid var(--fbx-border);
  display: flex; flex-direction: column; gap: var(--fbx-space-2);
}
.fbx-reglages-rail-titre {
  font-size: 10.5px; font-weight: var(--fbx-weight-bold); text-transform: uppercase; letter-spacing: .07em;
  color: var(--fbx-text-muted); padding: 0 var(--fbx-space-10) var(--fbx-space-10);
}
.fbx-reglages-rail-pied {
  margin-top: auto;
  min-height: 61px; box-sizing: border-box; display: flex; align-items: center;
  margin-left: -10px; margin-right: -10px; padding: 0 var(--fbx-space-10);
}
.fbx-reglages-rail-pied .fbx-reglages-tab { margin: 0; }
.fbx-reglages-tab {
  display: flex; align-items: center; gap: var(--fbx-space-10); width: 100%;
  padding: 9px var(--fbx-space-10); border: 0; border-radius: var(--fbx-radius);
  background: none; font: inherit; font-size: 13.5px; font-weight: var(--fbx-weight-medium); text-align: left;
  color: var(--fbx-text-soft); cursor: pointer; white-space: nowrap;
  transition: background .12s, color .12s;
}
.fbx-reglages-tab svg { flex-shrink: 0; color: var(--fbx-text-muted); }
.fbx-reglages-tab:hover { background: var(--fbx-surface); color: var(--fbx-text); }
.fbx-reglages-tab.is-current {
  background: rgba(var(--fbx-accent-rgb), .09); color: var(--fbx-accent-fg); font-weight: var(--fbx-weight-semibold);
}
.fbx-reglages-tab.is-current svg { color: var(--fbx-accent-fg); }
.fbx-reglages-out { color: var(--fbx-danger-fg); }
.fbx-reglages-out svg { color: var(--fbx-danger-fg); }

/* Le corps est une COLONNE : le panneau defile, le pied reste. Sans quoi le
   bouton d'enregistrement se placait a la suite du contenu — donc a une hauteur
   differente selon l'onglet, et jamais en face de « Deconnexion ». */
.fbx-reglages-corps { flex: 1; min-width: 0; display: flex; flex-direction: column; position: relative; }
.fbx-reglages-corps .fbx-modal-close { position: absolute; top: var(--fbx-space-16); right: 18px; z-index: 1; }
.fbx-reglages-panneaux { flex: 1; overflow-y: auto; padding: var(--fbx-space-24) var(--fbx-space-28); }
.fbx-reglages-titre { font-size: var(--fbx-font-lg); font-weight: var(--fbx-weight-bold); margin: 0 0 var(--fbx-space-20); color: var(--fbx-text); }
/* Meme hauteur que le pied du rail : les deux barres se repondent en travers
   de la modale, et l'action principale tombe toujours au meme endroit. */
.fbx-reglages-pied {
  border-top: 1px solid var(--fbx-border); padding: var(--fbx-space-12) var(--fbx-space-28);
  display: flex; justify-content: flex-end; align-items: center; gap: var(--fbx-space-10);
  min-height: 61px; box-sizing: border-box;
}

@media (max-width: 720px) {
  .fbx-modal.fbx-reglages { flex-direction: column; height: min(620px, 92vh); }
  .fbx-reglages-rail {
    width: auto; flex-direction: row; overflow-x: auto; padding: var(--fbx-space-8);
    border-right: 0; border-bottom: 1px solid var(--fbx-border);
  }
  .fbx-reglages-rail-titre { display: none; }
  .fbx-reglages-rail-pied { margin: 0; padding: 0; border: 0; }
}

/* ── Éditeur d'avatar (monté au socle le 2026-08-03) ───────────────────────
   Il vivait dans `dashboard.html` : un administrateur pouvait personnaliser
   l'avatar d'un TESTEUR, mais personne ne pouvait toucher au sien. */
.fbx-ae-apercu { display: flex; justify-content: center; margin-bottom: var(--fbx-space-20); }
.fbx-ae-apercu img {
  width: 240px; height: 240px; border-radius: var(--fbx-radius-full);
  background: var(--fbx-bg); transition: opacity .2s;
}
.fbx-ae-fonds { display: flex; gap: var(--fbx-space-8); flex-wrap: wrap; }
.fbx-ae-fond {
  width: 32px; height: 32px; padding: 0; border-radius: var(--fbx-radius-full); cursor: pointer;
  border: 3px solid transparent; transition: border-color .15s, transform .15s;
}
.fbx-ae-fond:hover { transform: scale(1.1); }
.fbx-ae-fond.is-on { border-color: var(--fbx-accent-fg); transform: scale(1.15); }
/* `.fbx-ae-grille` — RETIRÉE le 2026-08-08. Elle rangeait sur deux colonnes les
   huit `<select>` de traits (« variant07 »…), remplacés par des vignettes : on
   choisit désormais ce qu'on VOIT, et un trait par onglet plutôt que huit listes
   côte à côte. */

/* GALERIE DE TRAITS (2026-08-08). Chaque vignette est l'avatar ENTIER avec ce
   seul trait forcé — un nez ne se juge pas hors du visage qui le porte, et
   DiceBear ne rend pas un composant isolé de toute façon.
   ⚠️ Hauteur PLAFONNÉE et défilement interne : l'onglet Cheveux compte 64
   entrées, sans quoi le pied de la modale (Enregistrer) sortirait de l'écran.
   ⚠️ La largeur de colonne est FIXE (`repeat(auto-fill, 64px)`) : une grille en
   `1fr` étirerait les vignettes d'un onglet à l'autre selon leur nombre, et les
   visages changeraient de taille en changeant d'onglet. */
.fbx-ae-panneaux { margin-top: var(--fbx-space-12); }
.fbx-ae-vignettes {
  display: grid; grid-template-columns: repeat(auto-fill, 64px); gap: var(--fbx-space-10);
  max-height: 288px; overflow-y: auto; padding: var(--fbx-space-4) var(--fbx-space-2);
}
.fbx-ae-vignette {
  display: flex; flex-direction: column; align-items: center; gap: var(--fbx-space-2);
  padding: var(--fbx-space-3); border: 2px solid transparent; border-radius: var(--fbx-radius-lg);
  background: none; cursor: pointer; transition: border-color .15s, background .15s;
}
.fbx-ae-vignette img {
  width: 54px; height: 54px; border-radius: var(--fbx-radius-full); background: var(--fbx-bg); display: block;
}
.fbx-ae-vignette span {
  font-size: var(--fbx-font-2xs); color: var(--fbx-text-muted); line-height: 1.2;
  max-width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fbx-ae-vignette:hover { background: var(--fbx-fill); }
.fbx-ae-vignette.is-on { border-color: var(--fbx-accent-fg); }
.fbx-ae-vignette.is-on span { color: var(--fbx-accent-fg); font-weight: var(--fbx-weight-semibold); }

/* « Afficher » : seuls la barbe, les lunettes et le geste sont tirés au hasard
   par DiceBear (probabilité) — les autres traits sont toujours présents. */
.fbx-ae-opt { display: inline-flex; margin-bottom: var(--fbx-space-10); font-size: 11.5px; color: var(--fbx-text-muted); }

/* `.fbx-filter-date-group` et `.fbx-input--date` — RETIREES en P43. Elles
   habillaient le couple « du … au … » : deux champs de date posés a cote d'un
   selecteur de periode qui disait la meme chose. Le controle d'intervalle
   ci-dessous remplace les deux ; leurs classes n'ont plus de consommateur. */

/* ═══════════════════════════════════════════════════════════════════════════
   Intervalle de dates (P43) — UN contrôle là où il y en avait deux.
   Le panneau est un `.fbx-dd-pop` : même popup que le dropdown maison, la
   combobox et les réglages. On n'en crée pas un cinquième.
   ═══════════════════════════════════════════════════════════════════════════ */
.fbx-daterange {
  display: inline-flex; align-items: center; gap: var(--fbx-space-2);
  border: 1px solid var(--fbx-border); border-radius: var(--fbx-radius);
  background: var(--fbx-surface); padding: 0 var(--fbx-space-8) 0 var(--fbx-space-10); height: 33px; box-sizing: border-box;
  transition: border-color .15s, box-shadow .15s;
}
.fbx-daterange:hover { border-color: var(--fbx-border-strong); }
.fbx-daterange:focus-within { border-color: var(--fbx-accent-fg); box-shadow: 0 0 0 3px rgba(var(--fbx-accent-rgb), .12); }
/* `padding-left: 8px` : le meme ecart que `.fbx-select--icon` laisse entre son
   glyphe et son texte, pour que les deux controles s'alignent a l'oeil. */
.fbx-daterange-btn {
  border: 0; background: none; font: inherit; font-size: var(--fbx-font-base); color: var(--fbx-text);
  padding: 0 var(--fbx-space-6) 0 var(--fbx-space-8); height: 100%; cursor: pointer; white-space: nowrap;
  text-align: left;
  /* La largeur du gabarit à deux dates, posée par `mountDateRange`. */
  min-width: var(--fbx-daterange-min, auto);
}
.fbx-daterange.is-vide .fbx-daterange-btn { color: var(--fbx-text-soft); }
.fbx-daterange-x {
  border: 0; background: none; padding: var(--fbx-space-2); cursor: pointer; display: inline-flex;
  color: var(--fbx-text-muted); border-radius: var(--fbx-radius-full);
}
.fbx-daterange-x:hover { background: var(--fbx-bg); color: var(--fbx-text); }
/* Rien a effacer : la croix s'efface, mais garde sa place. */
.fbx-daterange.is-vide .fbx-daterange-x { visibility: hidden; pointer-events: none; }
.fbx-daterange-cal { display: inline-flex; color: var(--fbx-text-muted); flex-shrink: 0; }

.fbx-cal { display: flex; padding: 0; max-height: none; overflow: visible; }
/* Les raccourcis REMPLISSENT l'intervalle : ils ne vivent pas à côté de lui.
   C'est ce qui supprime la contradiction des deux anciens contrôles. */
.fbx-cal-raccourcis {
  display: flex; flex-direction: column; gap: var(--fbx-space-2); padding: var(--fbx-space-10) var(--fbx-space-8);
  border-right: 1px solid var(--fbx-border); background: var(--fbx-bg);
  border-radius: var(--fbx-radius-lg) 0 0 var(--fbx-radius-lg);
}
.fbx-cal-rac {
  border: 0; background: none; font: inherit; font-size: 12.5px; text-align: left;
  padding: 7px var(--fbx-space-12); border-radius: var(--fbx-radius-pill); cursor: pointer; white-space: nowrap;
  color: var(--fbx-text-soft);
}
.fbx-cal-rac:hover { background: var(--fbx-surface); color: var(--fbx-text); }
.fbx-cal-rac.is-on { background: rgba(var(--fbx-accent-rgb), .12); color: var(--fbx-accent-fg); font-weight: var(--fbx-weight-semibold); }

/* L'INTITULE D'UN GROUPE DE RACCOURCIS (2026-08-29) — « Recettes » sous les
   periodes usuelles. Une LIGNE de la meme colonne, pas un second bloc : elle est
   deja etroite et defile en mobile, un conteneur de plus casserait ce repli.
   ⚠️ NI BOUTON NI CIBLE : c'est du texte, il ne se survole pas et ne se clique
   pas — sinon on l'essaie une fois, il ne se passe rien, et on doute des vrais. */
.fbx-cal-rac-titre {
  padding: var(--fbx-space-10) var(--fbx-space-12) var(--fbx-space-4);
  font-size: var(--fbx-font-xs); font-weight: var(--fbx-weight-semibold);
  color: var(--fbx-text-muted); text-transform: uppercase; letter-spacing: .04em;
}
.fbx-cal-corps { padding: var(--fbx-space-10) var(--fbx-space-12) var(--fbx-space-14); position: relative; }
.fbx-cal-nav { position: absolute; top: var(--fbx-space-12); left: var(--fbx-space-12); right: var(--fbx-space-12); display: flex; justify-content: space-between; }
.fbx-cal-fleche {
  border: 0; background: none; font-size: var(--fbx-font-xl); line-height: var(--fbx-leading-none); color: var(--fbx-text-soft);
  width: 26px; height: 26px; border-radius: var(--fbx-radius-full); cursor: pointer;
}
.fbx-cal-fleche:hover { background: var(--fbx-bg); color: var(--fbx-text); }
.fbx-cal-mois-paire { display: flex; gap: 22px; }
.fbx-cal-titre { text-align: center; font-size: var(--fbx-font-base); font-weight: var(--fbx-weight-semibold); margin-bottom: var(--fbx-space-8); }
.fbx-cal-grille { display: grid; grid-template-columns: repeat(7, 30px); gap: 1px; }
.fbx-cal-jsem { font-size: var(--fbx-font-xs); color: var(--fbx-text-muted); text-align: center; padding-bottom: var(--fbx-space-4); }
.fbx-cal-vide { height: 30px; }
.fbx-cal-jour {
  height: 30px; border: 0; background: none; font: inherit; font-size: 12.5px;
  color: var(--fbx-text); cursor: pointer; border-radius: var(--fbx-radius-full);
}
.fbx-cal-jour:hover { background: var(--fbx-bg); }
/* L'intérieur de l'intervalle est un aplat continu ; les bornes sont pleines.
   Rayons carrés à l'intérieur pour que la bande ne se coupe pas entre deux
   cases — un intervalle doit se lire comme UN trait, pas comme n pastilles. */
.fbx-cal-jour.is-entre { background: rgba(var(--fbx-accent-rgb), .12); border-radius: 0; }
.fbx-cal-jour.is-bord { background: var(--fbx-accent-fill); color: var(--fbx-on-accent); font-weight: var(--fbx-weight-semibold); }
.fbx-cal-jour.is-debut { border-radius: var(--fbx-radius-full) 0 0 var(--fbx-radius-full); }
.fbx-cal-jour.is-fin { border-radius: 0 var(--fbx-radius-full) var(--fbx-radius-full) 0; }
.fbx-cal-jour.is-seul { border-radius: var(--fbx-radius-full); }


/* ── Vignette de PROJET ──────────────────────────────────────────────────────
   Reconnaitre une recette d'un coup d'oeil, sans la lire. Meme principe que les
   dossiers de Google Drive : un aplat colore portant les initiales, ou un logo
   quand le projet en declare un.
   ⚠️ La couleur n'est pas decorative : elle est DERIVEE du code projet (ou
   declaree), donc STABLE — un projet garde sa couleur d'un ecran a l'autre et
   d'une session a l'autre. Une couleur tiree au hasard a chaque rendu ne
   permettrait justement pas de reconnaitre quoi que ce soit. */
.fbx-projet-vignette {
  width: 40px; height: 40px; border-radius: var(--fbx-radius-lg); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-weight: var(--fbx-weight-bold); font-size: var(--fbx-font-md); letter-spacing: .5px;
  color: var(--fbx-on-accent); object-fit: cover; overflow: hidden;
}
.fbx-projet-vignette--lg { width: 56px; height: 56px; border-radius: 12px; font-size: var(--fbx-font-xl); }
.fbx-projet-chip { display: flex; align-items: center; gap: var(--fbx-space-12); }
.fbx-projet-nom { font-weight: var(--fbx-weight-bold); font-size: var(--fbx-font-lg); line-height: 1.2; }
.fbx-projet-code { color: var(--fbx-text-muted); font-size: var(--fbx-font-sm); }
/* Variante EN-TÊTE DE BLOC — quand le projet n'accompagne pas une ligne mais
   ouvre une section entière (tableau de bord du testeur, un bloc par recette).
   ⚠️ UNE VARIANTE, PAS UN SECOND COMPOSANT : l'écran avait d'abord réécrit ses
   propres `.tdb-recette-nom` / `-sous`, et le compteur du design system a
   signalé les trois classes du socle devenues orphelines. C'est le même objet
   à une autre grandeur — cela se dit par un modificateur. */
.fbx-projet-chip--lg { gap: var(--fbx-space-16); margin-bottom: 18px; flex-wrap: wrap; }
/* ⚠️ UNE MARGE QUI NE SEPARE DE RIEN N'A PAS LIEU D'ETRE (2026-09-03). Les 18 px
   ci-dessus posent le bandeau AU-DESSUS de ce qui suit — le filet, les colonnes.
   Quand la livraison ne porte ni perimetre, ni document, ni ticket, il n'y a
   rien dessous : la marge devient un blanc en pied de carte, que rien
   n'explique et qu'on lit comme un bloc en attente de chargement.
   ⚠️ MEME RAISONNEMENT QUE LE FILET, qui ne se pose que s'il a quelque chose a
   separer. Ici la condition s'ecrit en CSS — `:last-child` DIT exactement « il
   n'y a rien apres moi » — plutot qu'en JS : une classe conditionnelle poserait
   la meme regle a un second endroit, et il faudrait y penser sur chaque ecran
   qui emploie ce composant. */
.fbx-projet-chip--lg:last-child { margin-bottom: 0; }
.fbx-projet-chip--lg .fbx-projet-nom { font-size: 22px; }
.fbx-projet-chip--lg .fbx-projet-code { font-size: var(--fbx-font-base); margin-top: var(--fbx-space-3); }


/* ══════════════════════════════════════════════════════════════════════════
   CÉLÉBRATION D'UN BADGE — l'éclat qui dit qu'on vient de gagner (2026-08-13)
   ══════════════════════════════════════════════════════════════════════════
   Un badge se gagnait EN SILENCE : `badges-parite.test.js` le dit depuis le
   10/08, et c'était vrai jusqu'ici — le testeur découvrait son badge des jours
   plus tard, s'il pensait à ouvrir son tableau de bord. Cet éclat le lui dit
   au moment où il arrive.

   ⚠️ CE N'EST NI UN TOAST NI UNE MODALE, et il fallait trancher : la règle du
   socle dit « un toast ne demande rien, une modale décide » (P38). Une
   célébration ne décide rien — donc **aucun bouton de validation**, elle se
   retire seule. Mais elle n'est pas non plus un compte rendu qu'on glisse dans
   un coin : c'est le seul moment où l'emblème se regarde.
   ⚠️ ELLE NE PIÈGE PERSONNE : clic, Échap et le délai la ferment tous les
   trois. Un voile dont on ne sait pas sortir serait pire que pas de voile.

   ⚠️ LE CONTENU VA DANS UNE CARTE, il ne flotte pas sur le voile (arbitré à
   l'écran le 2026-08-13). Un premier jet posait le nom et la règle en blanc,
   à même le voile : cela demandait un voile **plus dense** (.72 contre .5)
   pour que le texte tienne son contraste, donc un composant qui assombrissait
   la page plus que toutes les autres surfaces de l'outil, et un texte qui
   n'était plus jetonné. La carte rend au composant la grammaire commune —
   `--fbx-surface`, `--fbx-radius-lg`, l'ombre des modales — et le voile
   revient à la densité de `.fbx-modal-overlay`. **Une même chose, une même
   forme.**

   ⚠️ `prefers-reduced-motion` N'EST PAS UNE OPTION SUR CE COMPOSANT. C'est le
   seul du socle qui projette quatorze objets en mouvement ; pour qui a réglé
   son système là-dessus, la question n'est pas le confort mais le malaise. Le
   badge reste montré — **on retire le mouvement, jamais l'information**. */
.fbx-celebration {
  position: fixed; inset: 0; z-index: var(--fbx-z-celebration);
  display: flex; align-items: center; justify-content: center; padding: var(--fbx-space-24);
  /* La MÊME densité que `.fbx-modal-overlay` : deux voiles dans un même outil
     n'ont aucune raison d'assombrir différemment. */
  background: var(--fbx-overlay);
  /* ⚠️ L'OPACITÉ N'EST PAS ANIMÉE À L'ENTRÉE — leçon du toast du widget
     (2026-08-11), et le piège s'y est retendu deux fois : une animation qui ne
     progresse pas (onglet d'arrière-plan) tient sa PREMIÈRE image, donc
     `from { opacity: 0 }` laisse un écran vide. Une apparition ne doit jamais
     dépendre de ce qui l'anime. */
  animation: fbx-celebration-voile .18s ease-out;
}
@keyframes fbx-celebration-voile { from { background-color: transparent; } }

/* LA CARTE. ⚠️ `overflow` reste VISIBLE : les particules partent de l'emblème
   et sortent volontairement de ses bornes — un `hidden` posé par réflexe les
   couperait net à mi-vol, ce qui se verrait comme un défaut de rendu. */
.fbx-celebration-carte {
  position: relative; text-align: center;
  background: var(--fbx-surface); color: var(--fbx-text);
  border: 1px solid var(--fbx-border);
  border-radius: var(--fbx-radius-lg, 14px);
  box-shadow: var(--fbx-shadow-celebration);
  /* ⚠️ `box-sizing` EXPLICITE : sans lui le padding s'AJOUTE à la largeur —
     mesuré à 402 px pour un `min(340px, 100%)` —, et sur un écran étroit
     « 100 % » devient 100 % + 62 px, donc un débordement horizontal de la
     page. Le socle ne l'applique pas universellement. */
  box-sizing: border-box;
  padding: 26px 30px var(--fbx-space-24); width: min(340px, 100%);
  display: flex; flex-direction: column; align-items: center; gap: var(--fbx-space-10);
  animation: fbx-celebration-carte-entree .26s cubic-bezier(.2, .9, .3, 1);
}
@keyframes fbx-celebration-carte-entree { from { transform: translateY(10px) scale(.97); } }

.fbx-celebration-titre {
  font-size: 10.5px; font-weight: var(--fbx-weight-bold); letter-spacing: .08em; text-transform: uppercase;
  color: var(--fbx-text-muted);
}
/* L'emblème EN GRAND — le même objet que `.tdb-badge-embleme`, à une autre
   grandeur : teinte pilotée par `--badge-teinte` (registre `BADGES` du socle),
   icône blanche, angles de la même famille. */
.fbx-celebration-embleme {
  position: relative;
  width: 96px; height: 96px; border-radius: 28px;
  display: grid; place-items: center; color: var(--fbx-on-accent);
  background: var(--badge-teinte, var(--fbx-border-strong));
  box-shadow: var(--fbx-shadow-embleme);
  animation: fbx-celebration-pop .52s cubic-bezier(.22, 1.4, .36, 1);
}
@keyframes fbx-celebration-pop {
  0%   { transform: scale(.2) rotate(-14deg); }
  62%  { transform: scale(1.14) rotate(3deg); }
  100% { transform: scale(1) rotate(0); }
}
.fbx-celebration-nom   { font-size: 19px; font-weight: var(--fbx-weight-bold); color: var(--fbx-text); }
.fbx-celebration-regle { font-size: var(--fbx-font-base); line-height: 1.45; color: var(--fbx-text-muted); }
/* Le rang dans la file quand plusieurs badges tombent ensemble (296 + 5 points
   décrochent « Beau plumage » ET « Aigle royal » dans le même geste). */
.fbx-celebration-file  { font-size: 11.5px; color: var(--fbx-text-muted); margin-top: var(--fbx-space-2); }

/* LES PARTICULES — l'éclat du « like » de Twitter : une projection radiale,
   brève, qui part de l'emblème.
   ⚠️ CHAQUE PARTICULE PORTE SON ANGLE ET SA DISTANCE EN VARIABLES, posées à la
   génération : c'est la seule façon d'en avoir quatorze différentes sans
   quatorze règles CSS — et la seule qui laisse le CSS piloter le mouvement.
   ⚠️ `pointer-events: none` : elles couvrent l'emblème pendant le vol, et sans
   cela le clic qui ferme atterrirait sur une particule. */
.fbx-celebration-eclat { position: absolute; inset: 0; pointer-events: none; }
.fbx-particule {
  position: absolute; left: 50%; top: 50%;
  width: var(--p-taille, 9px); height: var(--p-taille, 9px);
  margin: calc(var(--p-taille, 9px) / -2) 0 0 calc(var(--p-taille, 9px) / -2);
  border-radius: var(--fbx-radius-full); background: var(--p-couleur, var(--fbx-on-accent));
  /* ⚠️ `linear`, ET L'ACCÉLÉRATION EST ÉCRITE DANS LES IMAGES-CLÉS — mesuré au
     navigateur le 2026-08-13, parce que l'inverse ne se voit pas en lisant le
     code. Une `cubic-bezier(.15, .8, .3, 1)` s'applique ENTRE CHAQUE PAIRE
     d'images : très sortante, elle consommait ~95 % du fondu dans les 250
     premières millisecondes. À 430 ms sur 720, l'opacité relevée valait
     **0,07** et l'échelle **0,21** — l'éclat était fini avant d'avoir été vu,
     et rien dans les valeurs écrites ne le laissait deviner.
     Avec une progression linéaire, un palier d'images dit exactement ce qu'il
     montre, et l'instant se calcule de tête. */
  animation: fbx-particule-vol var(--p-duree, .95s) linear forwards;
  animation-delay: var(--p-retard, 0s);
  /* ⚠️ ELLES NAISSENT INVISIBLES **PAR TRANSFORM**, PAS PAR OPACITÉ (`scale(0)`
     dans l'image à 0 %) : le vol se voit, mais si l'animation ne démarre
     jamais, une particule figée au centre reste un point sous l'emblème — pas
     un écran vide. Le défaut, ici, se cache tout seul. */
  transform: scale(0);
}
/* Jaillir vite, filer, s'éteindre : l'élan est dans l'écart entre les deux
   premières images, la queue tient l'éclat lisible le temps qu'on le voie. */
@keyframes fbx-particule-vol {
  0%   { transform: rotate(var(--p-angle)) translateY(0) scale(0); opacity: 1; }
  12%  { transform: rotate(var(--p-angle)) translateY(calc(var(--p-loin) * .38)) scale(1.1); opacity: 1; }
  30%  { transform: rotate(var(--p-angle)) translateY(calc(var(--p-loin) * .66)) scale(1); opacity: 1; }
  62%  { transform: rotate(var(--p-angle)) translateY(calc(var(--p-loin) * .88)) scale(.8); opacity: .9; }
  100% { transform: rotate(var(--p-angle)) translateY(var(--p-loin)) scale(.25); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .fbx-celebration,
  .fbx-celebration-carte,
  .fbx-celebration-embleme { animation: none; }
  /* ⚠️ ON RETIRE LES PARTICULES, ON NE LES FIGE PAS : sans animation elles
     restent quatorze points empilés au centre de l'emblème — un défaut visible
     là où l'on venait justement d'épargner le mouvement. */
  .fbx-celebration-eclat { display: none; }
}

/* ── LE CALENDRIER EN ÉTROIT — UN MOIS, ET LES RACCOURCIS EN RANGÉE ──────────
   (2026-08-20). Il fait 613 px : une colonne de raccourcis (~135) plus DEUX
   mois de 216 px et leurs marges. Sur 375 px de large, on en voyait **367**,
   soit 60 % — le second mois entier hors de l'écran, et avec lui la flèche
   « mois suivant », qui est calée à droite du corps.

   ⚠️ CE N'ÉTAIT PAS UN DÉFAUT DE POSITIONNEMENT : `placer()` recale déjà le
   panneau à 8 px du bord et faisait son travail (mesuré à x=8). Le panneau est
   simplement PLUS LARGE que l'écran — aucun calcul de position ne peut rattraper
   ça. ⚠️ Et parce qu'il est en position absolue, **il ne pousse pas la page** :
   `scrollWidth === clientWidth`, donc l'audit de débordement le déclarait sain.
   *Un contrôle peut être hors de l'écran sans que rien ne déborde.*

   ⚠️ LA FORME EST CELLE QUI EXISTE DÉJÀ DANS LE SOCLE, pas une invention :
   `.fbx-reglages-rail` bascule de la colonne à la rangée défilante sous ce même
   seuil. Même problème, même geste, même point de rupture — c'est la règle
   « une même fonction, une même place » appliquée à un rail de raccourcis.

   ⚠️ LE SECOND MOIS EST MASQUÉ, PAS RETIRÉ DU RENDU : le JS continue de le
   produire, donc les flèches, la sélection d'une plage à cheval sur deux mois et
   l'état `is-entre` fonctionnent exactement comme avant. On navigue d'un mois à
   l'autre par les flèches, ce qui est le geste attendu sur un téléphone.
   Largeur obtenue : 240 px de corps, qui tient dans 375 avec ses marges. */
/* ⚠️ LA LARGEUR EST CELLE D'UN MOIS, ÉCRITE COMME UN CALCUL ET NON DÉDUITE DU
   VIEWPORT. Deux tentatives ont échoué avant celle-ci, et chacune apprend :
   (1) `max-width: calc(100vw - 16px)` laissait dépasser de **7 px**, parce que
   `100vw` **inclut la barre de défilement** (390 contre 375 de largeur utile) ;
   toute soustraction en dur aurait été un ajustement calé sur une barre dont la
   largeur varie d'un navigateur à l'autre — juste ici, faux ailleurs.
   (2) `width: min-content` donnait **579 px**, pire que le mal : les raccourcis
   en `overflow-x: auto` continuent de peser dans le calcul intrinsèque du
   conteneur, contrairement à ce que la règle « un bloc défilant a une largeur
   minimale nulle » laisse espérer.
   Le calcul ci-dessous EST le gabarit de `.fbx-cal-grille` — sept colonnes de
   30 px, six gouttières de 1 px — plus le rembourrage horizontal de
   `.fbx-cal-corps`. Il est écrit en clair pour qu'une modification de la grille
   se voie ici ; ce n'est pas un nombre magique, c'est la même mesure. */
@media (max-width: 720px) {
  .fbx-cal { flex-direction: column; width: calc(7 * 30px + 6 * 1px + 24px); }
  .fbx-cal-raccourcis {
    flex-direction: row; overflow-x: auto; scrollbar-width: thin;
    border-right: 0; border-bottom: 1px solid var(--fbx-border);
    border-radius: var(--fbx-radius-lg) var(--fbx-radius-lg) 0 0;
  }
  .fbx-cal-raccourcis .fbx-cal-rac { flex: 0 0 auto; }
  .fbx-cal-mois-paire > .fbx-cal-mois + .fbx-cal-mois { display: none; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   VENUES DE `feedback-palette.css` — à plier (R) et à ranger (V, C).   DS-1

   POURQUOI ELLES SONT ICI, ET ICI PLUTÔT QU'AILLEURS DANS CE FICHIER.
   `feedback-palette.css` portait quarante-deux règles ciblant une classe du
   socle, chargées après lui sur les treize écrans. Elle ne remappait pas le
   jeton : elle REPEIGNAIT LA RÈGLE — le motif que `CLAUDE.md` interdit, que ce
   fichier dénonce lui-même en commentaire, et qui a déjà coûté
   `.fbx-btn--primary` (bouton principal illisible en sombre, 1,81:1) et la
   dette d'ordre de `.fbx-callout--warning`. Elle n'était donc pas une feuille de
   jetons mais une seconde définition partielle du socle.

   ⚠️ LA FIN DU FICHIER EST LE SEUL ENDROIT QUI NE CHANGE RIEN. La palette était
   chargée après tout le socle ; la fin du socle est encore après tout le socle.
   L'ordre relatif de ces règles entre elles est conservé, et leur position vis-
   à-vis du reste ne bouge pas : le déplacement est neutre PAR CONSTRUCTION, sans
   avoir à raisonner sur la spécificité — ce qu'aucun outil du dépôt ne sait
   faire.

   ⚠️ LES RANGER PRÈS DE LEUR COMPOSANT LES DÉPLACERAIT DANS LA CASCADE, et rien
   ici ne sait prouver que ce serait neutre. C'est un travail de DS-3, avec un
   navigateur. Jusque-là, elles restent groupées et le disent.

   Ce qui pouvait être PLIÉ dans la règle du composant l'a été (DS-1, bloc 3) :
   ne restent ici que les sélecteurs qui n'ont pas de jumeau exact au socle.
   ═══════════════════════════════════════════════════════════════════════════ */
:root,
[data-palette="p37"] {
  background: var(--fbx-bg);
  color: var(--fbx-text);
}

/* Badge de mail — LA FORME SERVIE, relevée dans `tpxBadge` (src/services/mail.js),
   valeur ISO dans les deux thèmes.
   ⚠️ ELLE MONTRAIT LA FORME *PROPOSÉE* JUSQU'AU 2026-08-27 : une pilule EN
   CONTOUR, sans fond, avec les six couleurs d'une proposition du 01/08 qui n'a
   jamais été exécutée. La vitrine mesurait donc un plan et l'affichait comme un
   état — 4,01 annoncés pour des mails qui sortent à 1,99. *Le design system ne
   décrit rien, il consomme et il mesure* : il consomme désormais ce qui part.
   La proposition vit dans `doc/notes/2026-08-01-proposition-pilule-mail.md` ;
   le jour où elle est exécutée, cette règle et les six jetons la suivent.
   ⚠️ LE FOND EST UN VOILE DE LA COULEUR ELLE-MÊME, comme `${color}1A` côté
   mail : `color-mix` avec `transparent` rend exactement la couleur à 10 %, sans
   qu'il faille un compagnon `-rgb` par statut. Et il COÛTE du contraste — c'est
   précisément ce que la proposition corrigeait en passant au contour. */
/* `[data-status]` est nécessaire, pas décoratif : les règles d'interface
   ci-dessus portent le même poids (0,3,0) et gagneraient sinon — le badge
   garderait leur fond teinté et le barré de `cancelled`. */
.fbx-status-badge--mail[data-status] {
  background: color-mix(in srgb, currentColor 10%, transparent); border: none;
  border-radius: var(--fbx-radius-pill); padding: var(--fbx-space-3) 9px; font-weight: var(--fbx-weight-semibold);
  text-decoration: none;   /* le barré de `cancelled` ne s'applique pas ici */
}
.fbx-status-badge--mail[data-status="active"]         { color: var(--st-active-mail); }
.fbx-status-badge--mail[data-status="tocheck"]        { color: var(--st-tocheck-mail); }
.fbx-status-badge--mail[data-status="needs-revision"] { color: var(--st-needs-revision-mail); }
.fbx-status-badge--mail[data-status="pending"]        { color: var(--st-pending-mail); }
.fbx-status-badge--mail[data-status="resolved"]       { color: var(--st-resolved-mail); }
.fbx-status-badge--mail[data-status="cancelled"]      { color: var(--st-cancelled-mail); }

.fbx-card,
.fbx-table { background: var(--fbx-surface); color: var(--fbx-text); }
/* ⚠️ Le FOND de l'en-tête et celui du survol ne sont plus écrits ici : ils
   viennent des jetons `--fbx-thead-bg` et `--fbx-row-hover`, remappés en tête
   de ce fichier. Les redéclarer par sélecteur, c'est repeindre la <table> en
   oubliant la grille — le défaut corrigé le 2026-08-04. */
.fbx-table thead th,
.fbx-grid-head { color: var(--fbx-text-muted); border-bottom-color: var(--fbx-border); }
.fbx-table tbody td,
.fbx-grid-row { border-bottom-color: var(--fbx-border); }

/* ⚠️ POURQUOI `.fbx-btn--primary` N'EST PAS DANS CE BLOC, alors que toutes ses
   voisines y sont : elle a été retirée de la palette le 2026-08-07, seule et
   avant les autres, parce qu'elle avait CASSÉ. Elle écrivait
   `background: var(--fbx-accent-fg); color: #fff` et passait devant la
   déclaration du socle — en sombre, `--ui-accent-text` vaut #7CCBEB, et du blanc
   dessus tombe à 1,81:1 : le bouton principal de chaque formulaire, illisible,
   pendant que `--fbx-accent-fill` résolvait pourtant juste.
   ⚠️ LE SYMPTÔME ACCUSAIT LE JETON ; LE COUPABLE ÉTAIT LA RÈGLE QUI REFUSAIT DE
   LE LIRE. C'est le défaut que DS-1 solde en gros, et celui-là l'avait annoncé. */

.fbx-body,
[data-palette="p37"].fbx-body { background: var(--fbx-bg); color: var(--fbx-text); }

/* Le chemin prend la couleur du TEXTE, pas celle de l'accent : c'est un repère
   de localisation, il n'appelle pas au clic. Le glyphe porte seul la marque. */
.fbx-path,
.fbx-path-name { color: var(--fbx-text); }

/* ⚠️ `.fbx-sync-info` était ABSENTE de cette liste : le compteur suivait la
   palette (#6b7178) et l'indicateur de fraîcheur restait au gris du socle
   (#999) — deux informations de même nature, côte à côte, en deux gris. Le
   genre d'écart qu'aucune règle ne signale : il ne naît pas d'une décision,
   mais d'un oubli dans une énumération. */
/* ⚠️ `.fbx-user-info` A ÉTÉ RETIRÉE de cette liste (DS-1) : la classe n'existe
   plus nulle part — `feedback-admin.css` porte le commentaire qui acte son
   retrait. Une part de sélecteur qui ne vise rien ne se voit pas : elle ne casse
   pas, elle ne peint pas, elle se recopie. Sa règle propre
   (`.fbx-user-info strong`) est partie avec elle. */
.fbx-filter-bar label,
.fbx-sync-info,
.fbx-filter-count { color: var(--fbx-text-muted); }

/* ⚠️ `.fbx-dd-btn` A ÉTÉ RETIRÉE de ces deux groupes (DS-1) : aucune feuille ne
   la définit, aucune source ne la pose. Le bouton du dropdown maison est
   `.fbx-dd > button`, habillé par `feedback-shared.css`. */
.fbx-input,
.fbx-textarea {
  background: var(--fbx-surface); color: var(--fbx-text); border-color: var(--fbx-border);
}
}
.fbx-input:focus,
.fbx-textarea:focus {
  border-color: var(--fbx-accent); box-shadow: 0 0 0 3px var(--fbx-focus);
}
.fbx-dd-pop {
  background: var(--fbx-surface); border-color: var(--fbx-border); color: var(--fbx-text);
}
.fbx-dd-opt:hover,
.fbx-dd-opt.is-active { background: var(--fbx-accent-bg); color: var(--fbx-accent-fg); }

/* ⚠️ LA DETTE D'ORDRE DES DEUX ALERTES EST ÉTEINTE (DS-1), et il faut savoir ce
   qu'elle était pour ne pas la rouvrir.

   `.fbx-callout--warning` et `--error` étaient RECOPIÉES ici, à l'identique de
   leur déclaration au socle, pour une seule raison : `.fbx-callout` de la
   palette passait devant elles, à spécificité égale et dans une feuille chargée
   après. Un avertissement et une erreur s'affichaient donc exactement comme une
   explication neutre, sur les écrans qui portaient la palette. Personne ne
   l'avait vu parce qu'aucun écran n'affichait encore d'alerte — le premier à le
   faire fut la carte de sauvegarde, et le défaut est apparu avec elle
   (2026-08-05). C'était la DEUXIÈME occurrence du motif, après
   `.fbx-table tbody tr:hover` repeinte sans sa jumelle `.fbx-grid-row:hover`.

   ⚠️ CE QUI L'ÉTEINT N'EST PAS UN RÉGLAGE MAIS LA DISPARITION DE LA CAUSE :
   `.fbx-callout` de la palette est PLIÉE dans celle du socle, et les deux
   variantes y sont déclarées APRÈS elle, dans le même fichier. L'ordre suffit.
   Les recopies sont donc parties. Une variante de `.fbx-callout` ajoutée demain
   n'a plus rien à reconduire ici — elle s'écrit au socle, après sa base, et
   c'est tout.

   ⚠️ NE PAS RÉINTRODUIRE UNE RÈGLE DE COMPOSANT DANS LA FEUILLE DE JETONS : la
   règle K de `tools/ds-check.mjs` échoue désormais dessus, et cette dette-là est
   exactement ce qu'elle interdit. */
.fbx-callout--warning strong,
.fbx-callout--error strong { color: inherit; }
