/* Statsmaker v2 — couche sémantique (design §A.7).
 *
 * Ce fichier ne redéfinit AUCUN token de `ds-tokens.css` ni de `sm-theme.css` :
 * il NOMME les ~200 valeurs que la maquette écrivait en dur, en style en ligne.
 * Toute valeur qui apparaît deux fois dans un écran v2 doit vivre ici.
 *
 * Trois corrections de contraste validées y sont appliquées — dans les TOKENS,
 * jamais dans les écrans (design §F.3) :
 *   F-1  le jaune `#c99a2e` échoue en TEXTE en mode clair (2,58:1) : on sépare
 *        le remplissage (`--sm-draw`) du texte (`--sm-draw-text`) ;
 *   F-2  blanc sur `#e23b3b` (« EN DIRECT ») = 4,26:1 : `--sm-live` passe à
 *        `#cf2f2f`, soit 5,25:1, pour un écart visuel imperceptible ;
 *   F-3  `#c0453c` échoue en mode SOMBRE (3,46:1) : le rouge est inversé comme
 *        le reste de la palette (`#f08a83`, ≈ 7,1:1).
 *
 * S'y ajoute F-4 : `--color-divider` (1,31:1) ne peut pas servir de seule
 * bordure de contrôle — 1.4.11 exige 3:1. Les filets purement décoratifs
 * restent, eux, en `--color-divider`.
 *
 * ⚠️ F-4bis — LE CORRECTIF F-4 NE SUFFISAIT PAS, ET LA TABLE DU DESIGN ÉTAIT
 * FAUSSE. `neutral-500` en mode clair (#8f99ab) sur `--color-surface` mesure
 * **2,87:1**, pas les « 3,3:1 » annoncés par la table §6.6 du design : la
 * bordure de TOUS les champs du produit échouait donc à 1.4.11, sur la foi
 * d'un chiffre jamais vérifié. `--sm-border-field` prend `neutral-600` dans
 * les deux modes — un seul cran de rampe, mesuré :
 *
 *     clair   #67707f sur surface #ffffff = 5,00:1 · sur fond #f3f6fb = 4,61:1
 *     sombre  #8b98a9 sur surface #111a28 = 5,96:1 · sur fond #080d16 = 7,54:1
 *
 * 🛑 Ces quatre nombres ne sont pas déclaratifs : ils sont recalculés à chaque
 * exécution de la suite par `football/tests/test_v2_css.py`
 * (`ContrasteDesJetonsTests`), qui LIT ce fichier et résout l'indirection
 * `var()`. C'est le garde-fou qui manquait — celui dont l'absence a laissé une
 * affirmation non vérifiée tenir lieu de mesure.
 *
 * ⚠️ F-4ter — ET CE GARDE-FOU-LÀ NE SUFFISAIT TOUJOURS PAS.
 * Il mesure un JETON. Il ne sait pas dire si un contrôle le CONSOMME : le champ
 * `<input class="input">` prend sa bordure de `ds-tokens.css`, et rien, dans la
 * mesure du jeton, ne dit laquelle des deux règles gagne. La même faute s'est
 * donc rejouée d'un étage — une table qui annonce un contraste que le rendu ne
 * produit pas.
 * `BordureDesControlesTests` mesure désormais la couleur **réellement peinte** :
 * il part des règles CSS qui posent une bordure sur un contrôle, résout la
 * cascade (spécificité, puis ordre de chargement des cinq feuilles) et calcule
 * le contraste de la règle gagnante — `color-mix()` compris. La liste des
 * contrôles corrigés vit dans `sm-shell.css` ; en ajouter un ici sans l'y
 * ajouter fait échouer la suite.
 */
:root {
  /* ---- États sémantiques (hex trouvés en dur dans la maquette) ---- */
  --sm-live:        #cf2f2f;   /* était #e23b3b — corrigé pour le contraste (F-2) */
  --sm-live-on:     #ffffff;
  --sm-loss:        #c0453c;   /* défaite, carton rouge, erreur de formulaire, buts contre */
  --sm-loss-text:   #9c332b;   /* variante TEXTE sur fond clair */
  --sm-draw:        #c99a2e;   /* nul, carton jaune — REMPLISSAGE seulement */
  --sm-draw-text:   #8a6410;   /* variante TEXTE : #c99a2e ne passe pas en clair (F-1) */
  --sm-win:         var(--color-accent-2-600);
  --sm-win-text:    var(--color-accent-2-700);
  --sm-card-yellow: var(--sm-draw);
  --sm-card-red:    var(--sm-loss);

  /* ---- Verrou (variante 3 : pastille ; variantes 1/2/4 : bloc) ---- */
  --sm-lock-bg:     var(--color-accent-100);
  --sm-lock-fg:     var(--color-accent-800);
  --sm-lock-icon:   var(--color-accent-700);
  --sm-lock-border: var(--color-accent-300);

  /* ---- Périmètre (toujours vert : « voici sur quoi je calcule ») ---- */
  --sm-scope-bg:     var(--color-accent-2-100);
  --sm-scope-border: var(--color-accent-2-300);
  --sm-scope-fg:     var(--color-accent-2-900);

  /* ---- Bordure de contrôle (F-4 + F-4bis, voir l'en-tête) ----
         --color-divider = 1,31:1 et neutral-500 = 2,87:1 : les deux échouent à
         WCAG 1.4.11. neutral-600 = 5,00:1. ---- */
  --sm-border-field: var(--color-neutral-600);
  /* ---- F-4quater : la bordure SURVOLÉE. `ds-tokens.css` la pose à
         `color-mix(--color-text 45%)`, soit 2,95:1 : tant que le repos valait
         1,31:1 le survol AMÉLIORAIT la limite ; depuis que le repos tient
         5,00:1, il la DÉGRADE sous le seuil de 1.4.11. Un état ne doit jamais
         affaiblir ce que le repos garantit. neutral-700 = 8,14:1 sur carte,
         7,51:1 sur la page : le survol renforce, comme il doit. ---- */
  --sm-border-field-hover: var(--color-neutral-700);

  /* ---- Espacements réellement écrits ---- */
  --sp-1:4px; --sp-2:6px; --sp-3:8px; --sp-4:10px; --sp-5:12px;
  --sp-6:14px; --sp-7:16px; --sp-8:18px; --sp-9:22px; --sp-10:26px;

  /* ---- Rayons réellement écrits ---- */
  --r-pill:999px; --r-hero:24px; --r-portrait:20px; --r-card:16px;
  --r-card-in:14px; --r-sm:12px; --r-icon:10px; --r-xs:8px; --r-chip:6px; --r-round:50%;

  /* ---- Typographie ---- */
  --fs-hero:31px; --fs-page:28px; --fs-score:32px; --fs-h2:26px; --fs-h2-sec:22px;
  --fs-h3:20px; --fs-h4:18px; --fs-num-lg:26px; --fs-num-md:17px;
  --fs-body:14px; --fs-body-sm:13.5px; --fs-ui:13px; --fs-meta:12.5px;
  --fs-cap:12px; --fs-micro:11.5px;

  /* Interligne du corps de texte. Il vivait en dur dans `ds-tokens.css`, où il
     n'était qu'une valeur de confort — jusqu'à ce qu'une hauteur collante
     doive s'en DÉRIVER (`sm-match.css`). Une grandeur dont dépend une
     géométrie n'est plus un confort : c'est un jeton. */
  --sm-lh-body:1.55;

  /* ---- Divers ---- */
  --sm-tabbar-h:74px;     /* barre d'onglets mobile ; main a 90px de padding-bottom */
  --sm-tap-min:44px;      /* cible tactile minimale (§F.6) */
  --sm-pile-gap:14px;     /* R-P : l'écart entre deux cartes d'une pile */

  /* ── Hauteur de l'en-tête collant : CALCULÉE, jamais recopiée ──────────
   *
   * 🛑 CE QUE COÛTAIT LE CHIFFRE EN DUR. Trois valeurs contradictoires
   * coexistaient pour une seule grandeur : `--sm-shell-h: 58px` ici, un
   * commentaire qui annonçait « 70px », et `top: 52px` recopié de la maquette
   * dans `v2/glossaire.html`. La vraie hauteur, MESURÉE au navigateur
   * (Playwright, `tools/captures`), est **67 px** — et **63 px** sous 640 px.
   * Résultat servi : la barre de catégories du glossaire passait 15 px SOUS
   * l'en-tête translucide et apparaissait rognée et floutée au défilement
   * (`backdrop-filter`) ; la ligne d'en-tête de la table de comparaison,
   * 9 px. C'est le défaut que le client a vu.
   *
   * La cause n'est pas le mauvais chiffre : c'est qu'il y avait un chiffre.
   * L'en-tête a grandi quand les contrôles sont passés à 44 px (§F.6) et
   * personne n'a répercuté la conséquence géométrique — parce qu'elle vivait
   * à trois endroits dont aucun ne dépendait de la cause.
   *
   * Elle est donc DÉRIVÉE de ce qui la produit réellement, dans `.sm-hd-bar` :
   * deux paddings verticaux, l'élément le plus haut de la barre (une icône de
   * 44 px = `--sm-tap-min`), et le filet du bas. Repasser les contrôles à
   * 40 px met à jour les offsets collants tout seul.
   *
   * ⚠️ La hauteur DIFFÈRE selon la largeur, et on ne la moyenne pas : sous
   * 640 px le padding tombe à 9 px (voir la requête média en bas de fichier),
   * ce qui donne 63 px. Un seul jeton change, `--sm-shell-h` suit.
   */
  --sm-hd-pad-y:11px;     /* padding vertical de `.sm-hd-bar` */
  --sm-hd-filet:1px;      /* `border-bottom` de `.sm-hd` */
  --sm-shell-h:calc(var(--sm-hd-pad-y) * 2 + var(--sm-tap-min) + var(--sm-hd-filet));

  /* ── R-V — UN `top:` COLLANT NE CONTIENT JAMAIS UN NOMBRE. NI AU PREMIER
   *          ÉTAGE, NI AU SECOND. ────────────────────────────────────────────
   *
   * 🛑 CE QUE `--sm-shell-h` NE SUFFISAIT PAS À FERMER. Le jeton ci-dessus a
   * bien supprimé les hauteurs d'en-tête recopiées — mais il ne dit rien du
   * SECOND étage, celui des écrans qui posent leur propre barre collante sous
   * la coquille. Faute de jeton, chaque écran a écrit sa constante à la main :
   * `calc(var(--sm-shell-h) + 12px)` dans Match ET dans Scouting, `+ 53px`
   * dans le Glossaire. Trois nombres, trois occasions de mentir.
   *
   * Et l'un mentait, MESURÉ au navigateur sur `/fr/match/…?onglet=composition` :
   * `.sm-mat-collant` (score + onglets, fond opaque, `z-index: 4`) colle à
   * 67 px et mesure 92 px — elle occupe donc 67 → 159. `.sm-mat-fiche` collait
   * à 67 + 12 = 79 px, soit **80 px de fiche masqués derrière la barre**,
   * pendant tout le défilement. Le `+12` n'était pas une gouttière : c'était
   * l'oubli de la seconde barre.
   *
   * La cause n'est pas le mauvais nombre, c'est qu'il y avait un nombre. Un
   * écran qui gagne une barre collante devait PENSER à relire les `top:` de
   * tous ses autres collants. Désormais il déclare `--sm-collant-h` une fois,
   * et ses collants de troisième rang descendent ensemble.
   *
   *   .mon-ecran      { --sm-collant-h: calc(<hauteur DÉRIVÉE de la barre>); }
   *   .ma-barre       { top: var(--sm-shell-h); }    ← 2ᵉ étage : sous la coquille
   *   .mon-collant    { top: var(--sm-collant-top); } ← 3ᵉ étage : sous les deux
   *
   * ⚠️ `--sm-collant-h` se DÉRIVE des composants de la barre (paddings, tailles
   * de police, filets), jamais du `92px` relevé : un nombre mesuré cesse d'être
   * vrai à la première retouche de la barre, et c'est exactement le défaut
   * qu'on ferme ici.
   *
   * 🔒 DEUX RANGS, ET ILS NE SE MÉLANGENT PAS. `--sm-shell-h` et
   * `--sm-collant-top` ne valent que pour un élément qui colle à la PAGE. Dès
   * qu'un élément colle à une BOÎTE (un conteneur `overflow: auto`), son offset
   * correct est `0` — il est relatif à cette boîte — et la boîte doit porter
   * une BORNE DE HAUTEUR, sans quoi elle ne défile pas et le collage n'opère
   * jamais. Compare confondait les deux : un `top` de page dans une boîte sans
   * borne, donc un en-tête qui ne collait JAMAIS (mesuré : `scrollHeight ===
   * clientHeight`). Le motif juste est celui de l'effectif d'Équipe —
   * `--sm-cadre-h` + `overflow: auto` sur le cadre, `top: 0` sur l'en-tête.
   *
   * `football/tests/test_socle_collant.py` rend les trois règles opposables.
   */
  --sm-collant-gap:12px;   /* gouttière sous la coquille ou sous une barre */
  --sm-collant-h:0px;      /* hauteur du 2ᵉ étage ; REDÉCLARÉE par le seul
                              écran qui en porte un, et dérivée de ses
                              composants (voir `sm-match.css`) */
  --sm-collant-top:calc(var(--sm-shell-h) + var(--sm-collant-h) + var(--sm-collant-gap));

  /* Borne de hauteur d'un CADRE à défilement propre (le motif ci-dessus).
     Écrite quatre fois dans le produit — effectif d'Équipe, `.sm-roster`, et
     maintenant Compare — donc une seule fois ici. */
  --sm-cadre-h:72vh;
  --sm-cadre-h-court:62vh;  /* le même cadre sur un écran bas (≤ 760 px) */
  /* Borne d'un PANNEAU collant, lui : ce qui reste de la fenêtre sous son
     point de collage. Un panneau plus haut que cela a un bas inatteignable —
     mesuré sur la fiche de Scouting et ses 31 critères, dont le bouton
     « Fiche complète » sortait de l'écran. */
  --sm-collant-panneau-h:calc(100vh - var(--sm-collant-top) - var(--sm-collant-gap));

  /* Hauteur VISIBLE d'un champ de saisie de la maquette (une ligne de
     `--fs-body`). Sert à `.sm-touch-champ` ci-dessous : c'est la hauteur que
     le champ doit CONSERVER une fois sa cible tactile agrandie. */
  --sm-champ-h:17px;
  --sm-page-max:1120px;
  /* Affordance des cases À COCHER : rond = « choisissez-en un », carré =
     « cochez-en autant que vous voulez ». `ds-tokens.css` peint TOUT en rond
     (`.radio .dot`, 50 %) — sur la grille de douze compétitions du premier
     écran d'un client, c'est un contresens. Un rayon franc, et pas
     `--radius-sm` (8px sur une boîte de 16px : re-rond). */
  --sm-case-radius:4px;
  --sm-case-marque:2px;   /* épaisseur du trait de la coche */

  /* ── Trois géométries d'écran, et elles sont IDENTIQUES dans les deux
     thèmes : ce sont des mesures, pas des couleurs. Rien à ajouter dans le
     bloc `html[data-sm-theme="dark"]`. ── */
  --sm-aside-w:320px;     /* colonne latérale de `.sm-grid`. La maquette écrit
                             320 | 340 | 380 selon l'écran ; on fige la valeur
                             de l'accueil et de Player, les deux seules
                             mesurées. */
  --sm-pastille:24px;     /* pastille d'initiales d'équipe. Ce composant sert
                             SEPT écrans : la taille ne peut pas vivre dans la
                             feuille de l'un d'eux. */
  --sm-panneau-h:292px;   /* 🛑 hauteur PLANCHER du panneau vedette, et c'est un
                             jeton parce que les TROIS états la partagent :
                             direct lisible, direct verrouillé, prochain
                             rendez-vous. Une page qui saute selon l'heure est
                             un défaut, pas un état.

                             ⚠️ VALEUR MESURÉE AU NAVIGATEUR, pas estimée. Le
                             design annonçait 264 px en supposant l'explication
                             du verrou sur DEUX lignes ; relevée, elle en prend
                             trois à 1120 px et quatre à 390 px, et l'état
                             verrouillé montait à 291 / 357 px. Un plancher
                             sous le plus grand des trois états ne les égalise
                             pas : il laisse l'écart entier. Le plancher vaut
                             donc l'état le plus haut. Écart résiduel mesuré :
                             1 px sur bureau, 9 px sur téléphone. */
}

html[data-sm-theme="dark"] {
  --sm-live:        #ff6b6b;
  --sm-live-on:     #1a0606;
  --sm-loss:        #f08a83;   /* F-3 : le rouge est inversé comme le reste */
  --sm-loss-text:   #f08a83;
  --sm-draw:        #e8b84b;
  --sm-draw-text:   #e8b84b;   /* le jaune passe en sombre : texte = remplissage */
  /* Même cran de rampe qu'en clair (F-4bis) : 5,96:1 sur surface. Le mode
     sombre passait déjà (3,07:1), mais de justesse et par accident — deux
     modes qui divergent d'un cran divergeront d'un ton à la retouche suivante. */
  --sm-border-field: var(--color-neutral-600);
  /* F-4quater : même cran que le clair — 9,38:1 sur carte, 10,44:1 sur page. */
  --sm-border-field-hover: var(--color-neutral-700);
}

/* ---- Animations déclarées (design §A.8) : centralisées ici, plus jamais
       redéclarées dans le <style> d'un écran. ---- */
@keyframes sm-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .45; transform: scale(.82); }
}
@keyframes sm-bar  { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes sm-in   { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes sm-spin { to { transform: rotate(360deg); } }

/* Chargeur circulaire — le seul motif de chargement du produit. */
.sm-loader {
  width: 48px; height: 48px; border-radius: var(--r-round);
  border: 4px solid var(--color-accent-200);
  border-top-color: var(--color-accent);
  animation: sm-spin .9s linear infinite;
}

/* Barre de défilement fine de tout conteneur à défilement horizontal. */
.sm-scroll { scrollbar-width: thin; }
.sm-scroll::-webkit-scrollbar { height: 6px; width: 6px; }
.sm-scroll::-webkit-scrollbar-thumb {
  background: var(--color-neutral-300); border-radius: var(--r-pill);
}

/* ── R-T — UNE CIBLE TACTILE S'AGRANDIT VERS L'INTÉRIEUR, JAMAIS VERS
      L'EXTÉRIEUR ────────────────────────────────────────────────────────────
 *
 * Agrandit la zone sensible SANS changer un pixel visible (§F.6) : la densité
 * voulue par le client est préservée, la cible tactile passe à 44 px.
 *
 * 🛑 **Ce que la classe fait vraiment, et qu'on oublie en la posant.** Le
 * pseudo-élément fait `max(100%, 44px)` dans les DEUX axes et il est CENTRÉ sur
 * l'élément : sur une pilule de 28 px, il déborde de 8 px au-dessus **et** de
 * 8 px au-dessous. Ce débord ne pousse rien — il se superpose. Deux pilules
 * séparées par un `gap: 8px` ont donc des zones cliquables qui se MORDENT :
 * on vise une puce, on en active une autre. Mesuré au navigateur sur les puces
 * de rang de Discipline et sur la pagination de Communauté (lot B).
 *
 * ⚠️ La classe n'est donc licite que si le CONTENEUR absorbe le débord —
 * l'absorption se pose sur le conteneur, jamais sur l'élément :
 *
 *     .mon-rail {
 *       --sm-touch-h: 28px;             ← hauteur VISIBLE du membre, mesurée
 *       --sm-touch-membre: mon-jeton;   ← la classe qui porte `.sm-touch`
 *       gap: max(var(--sp-3), calc(var(--sm-tap-min) - var(--sm-touch-h)));
 *     }
 *
 * `--sm-touch-membre` n'est pas de la décoration : c'est le lien que
 * `test_socle_cibles_tactiles.py` suit pour aller du gabarit à la règle qui
 * absorbe. Sans lui, la garde ne pourrait vérifier que ce qu'on a pensé à lui
 * dire, et on retomberait sur une liste tenue à la main.
 *
 * 🔒 Deux corollaires, et ils se lisent comme des interdits :
 *   1. sur un élément qui fait DÉJÀ 44 px, la classe est du bruit — elle laisse
 *      croire que la cible dépend d'elle. On la retire.
 *   2. dans une pile sans `gap` (une liste de liens), il n'y a rien à absorber :
 *      c'est l'élément lui-même qui monte à `min-height: var(--sm-tap-min)`.
 *      C'est plus cher en flux, et c'est le bon prix — un lien qui ouvre la
 *      mauvaise fiche est pire qu'un lien difficile à viser. */
.sm-touch { position: relative; }
.sm-touch::after {
  content: ''; position: absolute; inset: 50% auto auto 50%;
  width: max(100%, var(--sm-tap-min)); height: max(100%, var(--sm-tap-min));
  transform: translate(-50%, -50%);
}

/* Pendant de `.sm-touch` pour les CHAMPS DE SAISIE.
 *
 * ⚠️ POURQUOI `.sm-touch` ne suffit pas ici, et pourquoi c'est un piège. Un
 * `<input>` est un élément REMPLACÉ : il n'a **pas** de `::after`, donc
 * `.sm-touch` posé dessus ne fait strictement rien — silencieusement. Et le
 * poser sur la pilule qui l'entoure serait pire : le pseudo-élément
 * recouvrirait le champ et intercepterait les clics, si bien que le champ ne
 * prendrait plus le focus à la souris. Un « correctif » d'accessibilité qui
 * casse la saisie à la souris.
 *
 * On agrandit donc la boîte RÉELLE du champ — invisible, puisqu'il n'a ni fond
 * ni bordure propres, ceux-ci appartenant à la pilule — puis on annule la
 * hauteur ajoutée par une marge négative. La pilule garde exactement sa
 * hauteur de maquette, et la cible tactile fait 44 px. C'est la règle du §F.6
 * (partie 3, l. 320-328) appliquée au cas que `.sm-touch` ne sait pas traiter.
 *
 * `line-height` est POSÉ et non hérité : sans lui, la hauteur de contenu
 * dépendrait du `line-height: normal` de la police, donc du rendu, et la marge
 * négative ne compenserait plus exactement.
 *
 * 🛑 **DEUX CONDITIONS, ET LEUR VIOLATION A ÉTÉ MESURÉE AU NAVIGATEUR.**
 *
 * La marge négative ne compense l'agrandissement qu'à deux conditions :
 *
 *   (1) **le champ est NU** — aucune bordure, aucun fond, aucun rayon PROPRES.
 *       Sa seule étendue visible est sa boîte de ligne, sinon la marge négative
 *       ne cache rien : elle tire une pilule VISIBLE de 13,5 px par-dessus ses
 *       voisines. Mesuré sur Scouting (`/fr/evaluations/`) : 14 champs sur 16
 *       portaient `margin-top: -13,5px` avec `min-height: 44px` — les pilules
 *       passaient de 31 à 44 px (la densité arbitrée par le client, perdue) et
 *       débordaient de 13,5 px de chaque côté sur leurs voisines.
 *
 *   (2) **personne ne réécrit `min-height`.** Le socle de filtres pose 32 px sur
 *       les contrôles (décision du client, lot 13) dans une feuille d'écran,
 *       donc APRÈS celle-ci : `min-height` est écrasée, `margin-block` ne l'est
 *       pas. Il reste une marge négative ORPHELINE — 27 px de flux qui
 *       disparaissent par rangée, sans un pixel de cible gagné. Mesuré sur
 *       Discipline : `label.sm-disc-lab` recouvrait son `<select>` de 9 px sur
 *       109 px de large. C'est la « card superposée » signalée par le client.
 *
 * ⚠️ **Un champ qui porte sa peau ne prend donc JAMAIS cette classe.** Il a deux
 * sorties honnêtes, et deux seulement : monter sa propre pilule à
 * `min-height: var(--sm-tap-min)` et l'assumer visuellement, ou assumer les
 * 32 px arbitrés par le client. La classe, elle, est réservée au cas pour
 * lequel elle a été écrite — un `<input>` transparent dans une pilule qui n'est
 * pas lui. À ce jour, `.sm-gloss-q` (Glossaire) est son SEUL emploi.
 *
 * `test_socle_cibles_tactiles.py` rend les deux conditions opposables.
 */
.sm-touch-champ {
  line-height: var(--sm-champ-h);
  min-height: var(--sm-tap-min);
  margin-block: calc((var(--sm-champ-h) - var(--sm-tap-min)) / 2);
}


/* ── `hidden` est une propriété du DOM, pas une suggestion ──────────────────
 *
 * ⚠️ La règle qui manquait, et le défaut qu'elle ferme DÉFINITIVEMENT.
 *
 * `[hidden] { display: none }` vient de la feuille du NAVIGATEUR. En CSS, une
 * déclaration d'auteur bat toujours une déclaration du navigateur, quelle que
 * soit sa spécificité : n'importe quelle règle de composant qui pose un
 * `display` — `.btn { display: inline-flex }`, `.sm-field-err { display: block }`
 * — rend donc visible un élément que le serveur a explicitement masqué. Le
 * balisage est juste, le comportement est faux, et rien ne le signale.
 *
 * Trois éléments du front v2 en vivaient : les messages d'erreur du formulaire
 * de contact et de la connexion (affichés en rouge sur une page vierge), et le
 * bouton « Télécharger en PDF » du glossaire (visible sans JavaScript, alors
 * qu'il ne fait rien). Les composants qui s'en étaient sortis avaient chacun
 * écrit sa garde à la main (`.sm-hd-menu[hidden]`, `.sm-cmp tbody[hidden]`,
 * `.sm-info-panneau[hidden]`, `.sm-auth-oeil[hidden]`) : une garde par
 * composant, donc un oubli par composant neuf.
 *
 * `!important` est ici l'outil juste, et le seul : il rend la neutralisation
 * INCONDITIONNELLE, y compris pour une feuille d'écran chargée après celle-ci.
 * La spécificité, elle, ne suffirait pas — l'ordre des feuilles rouvrirait le
 * trou au prochain écran.
 *
 * Les deux dérogations volontaires restent possibles et le sont : à
 * l'impression, `sm-info.css` déplie les panneaux masqués par
 * `.sm-info-panneau[hidden]` (0,2,0), qui bat ce sélecteur (0,1,0) à égalité
 * d'`!important`. Une dérogation doit être NOMMÉE — c'est exactement la
 * propriété recherchée.
 *
 * `test_v2_css.HiddenTests` dérive la règle des GABARITS : tout élément qui
 * porte `hidden` dans `templates/v2/` est confronté aux `display` des feuilles.
 */
[hidden] { display: none !important; }

/* Motif de champ du produit (design partie 7 §3.4, arbitrage Q-7).
   Il vivait dans `sm-info.css`, où il n'a rien à faire : le formulaire de
   contact, l'écran de connexion et les quatre écrans de réinitialisation
   emploient le MÊME motif. Le redéclarer par feuille aurait produit deux
   définitions de la même chose, qui divergent à la première retouche.
   `.sm-field-err` porte `--sm-loss-text` (7,19:1 en clair, ≈ 7,1:1 en sombre)
   et jamais `--sm-loss`, qui échoue en texte sur fond clair (F-1/F-3). */
.sm-field { margin: 0; position: relative; }
.sm-field .input { width: 100%; box-sizing: border-box; }
.sm-field-err { display: block; margin-top: var(--sp-1); font-size: var(--fs-cap); color: var(--sm-loss-text); }

/* ── L'ICÔNE, primitive partagée par tous les écrans ──────────────────────
 * 🛑 Une icône est un ORNEMENT DE LIBELLÉ, jamais le libellé : elle est
 * `aria-hidden` et le contrôle qui la porte écrit son nom en toutes lettres
 * (visible, ou en `.sm-sr-only` juste en dessous). Le tracé vit dans
 * `statsmaker/icones.py` ; cette règle ne fait que le poser correctement.
 *
 * ⚠️ `flex: none` : dans un bouton en `inline-flex`, une icône sans cette
 * déclaration se fait écraser par un libellé long — elle devient un ovale.
 * `currentColor` est porté par le SVG lui-même : l'icône prend donc la couleur
 * de son bouton, y compris à l'état actif, sans une seule règle de plus. */
.sm-icone {
  flex: none;
  /* L'alignement optique sur une ligne de texte : le carré du SVG déborde de
     la hauteur des minuscules, et `baseline` le poserait trop haut. */
  vertical-align: -2px;
}

/* ══════════════════════════════════════════════════════════════════════════
   RÈGLE R-P — L'ESPACEMENT D'UNE PILE APPARTIENT À LA PILE
   ══════════════════════════════════════════════════════════════════════════

   🔴 **LE DÉFAUT QUE CECI FERME : « les cartes sont superposées ».** Quatre
   écrans empilaient leurs cartes en enfants DIRECTS de `<main>` — Arbitre,
   Entraîneur, Match et Communauté. Or toutes les familles de cartes du front
   déclarent, en toutes lettres et dans leur propre en-tête, qu'elles ne portent
   **aucune marge** : « l'espacement d'une pile appartient à la pile, pas à ses
   éléments ». La doctrine était juste ; il manquait la pile.

   Résultat : deux cartes blanches à coins arrondis se touchaient exactement,
   leurs deux ombres portées se recouvrant sur la couture. Ce n'est pas un
   chevauchement au sens strict — c'est ce qu'on voit, et c'est ce que le client
   a décrit. `sm-arbitre.css` portait même la trace de la tentative avortée :
   un sélecteur `.sm-arb-carte + .sm-arb-carte,` **suivi d'une ligne vide et
   d'une virgule pendante**, qui le raccrochait silencieusement à la règle du
   TITRE de carte — la deuxième carte d'une pile héritait donc d'un
   `font-size: 16px` que personne n'avait demandé.

   🛑 **POURQUOI LA RÈGLE EST ICI ET PAS DANS LES QUATRE FEUILLES.** C'est une
   règle de COQUILLE : elle ne parle d'aucun écran, elle parle de ce qui arrive
   quand deux cartes se suivent au premier niveau de la page. Écrite quatre
   fois, elle aurait quatre valeurs d'écart au premier ajustement — et un
   cinquième écran l'oublierait, exactement comme les quatre premiers.

   ⚠️ **`.sm-main > …` — L'ENFANT DIRECT, ET C'EST TOUT LE SOIN DE LA RÈGLE.**
   Les mêmes classes de carte servent aussi de lignes de liste
   (`<li class="sm-com-carte">` dans un `.sm-com-liste` à `gap: 14px`) et de
   tuiles de grille. Sans le `>`, chacune recevrait une marge EN PLUS du `gap`
   de son conteneur : 28 px au lieu de 14, sur toutes les listes du produit. Une
   pile qui a déjà son écart n'en veut pas un second.

   ⚠️ **La liste des familles est ÉNUMÉRÉE, jamais devinée.** Un
   `[class*="-carte"]` attraperait `…-carte-titre`, `…-carte-entete`,
   `…-carte-lien` — des enfants, pas des cartes. Le garde-fou
   `test_socle_pile.py` DÉRIVE des gabarits les cartes réellement adjacentes au
   premier niveau et exige qu'elles soient couvertes ici : un cinquième écran
   qui empile fera rougir la suite le jour où il est écrit. */
:where(.sm-main) > .sm-carte + .sm-carte,
:where(.sm-main) > .sm-arb-carte + .sm-arb-carte,
:where(.sm-main) > .sm-agt-carte + .sm-agt-carte,
:where(.sm-main) > .sm-com-carte + .sm-com-carte,
:where(.sm-main) > .sm-ent-carte + .sm-ent-carte,
:where(.sm-main) > .sm-equ-carte + .sm-equ-carte,
:where(.sm-main) > .sm-jou-carte + .sm-jou-carte,
:where(.sm-main) > .sm-mat-carte + .sm-mat-carte,
:where(.sm-main) > .sm-sco-carte + .sm-sco-carte,
:where(.sm-main) > .sm-ab-carte + .sm-ab-carte {
  margin-top: var(--sm-pile-gap);
}

/* ══════════════════════════════════════════════════════════════════════════
   LE TERRAIN — une seule écriture, deux écrans (règle R-T2)
   ══════════════════════════════════════════════════════════════════════════

   🔴 **« Le problème avait été corrigé dans Équipe mais oublié ici, tu vois
   l'importance du DRY ? »** Le terrain était dessiné DEUX FOIS —
   `.sm-equ-terrain` sur la fiche Équipe, `.sm-mat-champ` sur la page Match —
   avec deux jeux de règles qui avaient divergé point par point : `overflow`,
   surface de réparation, ligne médiane, et jusqu'au SENS du terrain. Un
   correctif appliqué d'un côté ne pouvait pas atteindre l'autre.

   🛑 **`overflow: hidden` N'EST PAS COSMÉTIQUE.** Les pastilles sont posées en
   `position: absolute` avec `translate(-50%, -50%)` : celles des ailes (à 15 %
   et 85 %) et leur étiquette de nom débordent du rectangle par construction.
   Sans lui, elles flottent sur le fond de la carte, à côté de la pelouse.

   ⚠️ La pelouse est le seul fond sombre du produit, dans les DEUX thèmes : un
   terrain clair ne se lit pas comme un terrain. Les jetons y sont donc écrits
   en blanc en dur, et non avec un jeton de texte qui s'inverserait.

   ⚠️ `aspect-ratio: 3/4` et `max-width` : c'est la mesure de la maquette. Un
   terrain à 50 % de la largeur d'un écran de bureau deviendrait illisible en
   hauteur. */
.sm-terrain {
  position: relative;
  aspect-ratio: 3 / 4;
  max-width: 420px;
  margin: 0 auto;
  border: 1px solid rgba(255, 255, 255, .25);
  border-radius: 14px;
  background-image: linear-gradient(rgba(255, 255, 255, .07) 1px, transparent 1px);
  background-size: 100% 12.5%;
  overflow: hidden;
}

/* La surface de réparation et la ligne médiane. Sans elles, « un rectangle rayé
 * n'est pas un terrain » — et surtout, rien ne dit de quel côté est le but,
 * donc dans quel sens se lit la composition. */
.sm-terrain-surface {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 42%;
  height: 12%;
  border: 1px solid rgba(255, 255, 255, .25);
  border-top: none;
  border-radius: 0 0 10px 10px;
}

.sm-terrain-mediane {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 1px;
  background: rgba(255, 255, 255, .2);
}

/* 🛑 `top` et `left` sont écrits EN LIGNE par le gabarit, depuis des valeurs
 * calculées en Python : onze positions par système, quinze systèmes, aucune
 * règle CSS ne les tiendrait. */
.sm-terrain-place {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  text-decoration: none;
  color: #fff;
}

.sm-terrain-num {
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 50%;
  background: var(--color-accent);
  border: 2px solid rgba(255, 255, 255, .85);
  color: #fff;
  display: grid;
  place-items: center;
  font-family: var(--font-heading);
  font-size: 13px;
}

/* 🔴 **LA PASTILLE DONT LA FICHE EST OUVERTE.** Le panneau de droite parlait
 * d'un joueur que rien ne désignait sur la pelouse.
 *
 * ⚠️ **Trois marques, pas une teinte.** Un anneau, une pastille agrandie et un
 * nom opaque : la couleur seule ne dirait rien à qui ne la distingue pas
 * (WCAG 1.4.1), et sur un fond vert imprimé en niveaux de gris elle disparaît.
 * ⚠️ Le sélecteur lit `aria-current`, l'attribut que le serveur pose et que
 * l'îlot recopie — pas une classe en plus, qui serait une seconde vérité.
 * ⚠️ `transform` cumule le centrage de la place : l'oublier ferait SAUTER la
 * pastille d'un quart de sa largeur au moment où on la sélectionne. */
.sm-terrain-place[aria-current] { z-index: 2; }

.sm-terrain-place[aria-current] .sm-terrain-num {
  background: #fff;
  color: var(--color-accent);
  border-color: var(--color-accent);
  box-shadow: 0 0 0 4px rgba(255, 255, 255, .45);
  transform: scale(1.12);
}

.sm-terrain-place[aria-current] .sm-terrain-nom {
  background: #fff;
  color: var(--color-text);
  font-weight: 600;
}

/* ⚠️ `nowrap` tient parce que le serveur n'y met qu'un PATRONYME : un mot ne se
 * coupe pas, et onze pastilles cessent de se recouvrir. `max-width` borne le cas
 * du patronyme composé. */
.sm-terrain-nom {
  max-width: 92px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .45);
  font-size: 12px;
  color: #fff;
}

/* La mention du repli de disposition. ⚠️ Sur la pelouse, donc en blanc atténué :
 * c'est une précision sur ce qu'on regarde, pas un avertissement. */
.sm-terrain-note {
  margin: 10px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: rgba(255, 255, 255, .82);
  text-wrap: pretty;
}

/* La carte qui porte le terrain : la pelouse, son titre et son pied. */
.sm-terrain-carte {
  background: var(--sm-pitch-a);
  border-radius: 16px;
  padding: 16px;
  color: #fff;
  height: max-content;
}

.sm-terrain-tete {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 10px;
}

.sm-terrain-tete h2,
.sm-terrain-eq { margin: 0; font-family: var(--font-heading); font-size: 16px; color: #fff; }
.sm-terrain-meta { font-size: 12.5px; opacity: .9; }
.sm-terrain-meta a { color: #fff; }

/* ══════════════════════════════════════════════════════════════════════════
   LES GROUPES — filtrer SANS recharger et SANS JavaScript (règle R-G)
   ══════════════════════════════════════════════════════════════════════════

   🔴 **« Pourquoi la page se rechargerait pour aller vers le reste des
   boutons ??? »** Les familles de statistiques d'un match (Tout, Attaque,
   Passes & duels, Défense & discipline) et les groupes de colonnes de la
   feuille étaient des LIENS : chaque clic repartait au serveur, rechargeait la
   page entière et redescendait au même endroit. Or le serveur a déjà tout
   calculé — c'est le même relevé, regardé sous un autre angle.

   🛑 **DES BOUTONS RADIO ET `:has()`, PAS UN ÎLOT.** Un îlot aurait marché,
   mais il aurait fallu que les boutons fassent quelque chose sans lui (règle
   D-5) — donc rester des liens, donc recharger pour qui n'a pas de script.
   Ici, **rien ne recharge et rien ne dépend de JavaScript** : le choix est un
   `<input type="radio">`, et `:has()` demande à l'ancêtre de masquer ce qui
   n'appartient pas au groupe coché. Le navigateur fait tout.

   ⚠️ **Le repli est le bon sens.** Un moteur qui ignore `:has()` n'applique
   aucune règle de masquage : le lecteur voit TOUT, ce qui est précisément
   l'état « Tout » — jamais un écran vide.

   ⚠️ **Le radio est déplacé hors de l'écran, jamais `display: none`** : un
   contrôle en `display: none` sort de l'ordre de tabulation, et le groupe
   cesserait d'être utilisable au clavier.

   Contrat : un ancêtre `.sm-cols`, des `<input type="radio" id="sm-cols-X">`
   dans un `.sm-groupes`, et des éléments filtrés portant `data-sm-fam="…"` où
   chaque lettre est un code de famille. */
.sm-groupes {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0;
  padding: 0;
  border: 0;
}

.sm-groupes legend {
  padding: 0 8px 0 0;
  font-size: 12px;
  color: var(--color-neutral-700);
}

.sm-groupes input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.sm-groupes label {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 5px 13px;
  border: 1px solid var(--sm-border-field);
  border-radius: var(--r-pill);
  font-size: var(--fs-meta);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

.sm-groupes input:checked + label {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #fff;
}

/* 🛑 Le repère de focus vit sur le LIBELLÉ, puisque le contrôle est hors de
 * l'écran : sans cette règle, la navigation au clavier serait invisible. */
.sm-groupes input:focus-visible + label {
  outline: var(--sm-focus-w, 2px) solid var(--color-accent);
  outline-offset: 2px;
}

@media (pointer: coarse) {
  .sm-groupes label { min-height: var(--sm-tap-min); }
}

/* ── Le masquage : par RANG, et c'est ce qui le rend réutilisable ─────────
 *
 * 🛑 **LE RANG, PAS LE NOM DE LA FAMILLE.** Un premier jet écrivait une règle
 * par code (`#sm-cols-g`, `#sm-cols-o`…) — ce qui ne marchait que pour les
 * quatre groupes du tableau de match, dont les codes sont écrits en dur. Les
 * familles de statistiques, elles, sont **dérivées du relevé** : leurs clés
 * changent avec les données, et aucune feuille de style ne peut les connaître.
 *
 * Le rang, lui, est une position : premier bouton, deuxième, troisième. Il ne
 * dépend d'aucun vocabulaire, donc le même jeu de règles sert n'importe quel
 * groupe de n'importe quel écran.
 *
 * ⚠️ **`~=` ET NON `*=`** : un élément peut appartenir à PLUSIEURS groupes
 * (« Minutes » est dans les quatre). `data-sm-rang="2 3"` est une liste de
 * mots, et `~=` teste l'appartenance à cette liste. `*=` aurait fait matcher
 * « 2 » dans « 12 » au douzième groupe.
 *
 * ⚠️ **Le premier rang ne filtre RIEN** : c'est « Tout », et c'est l'état
 * d'ouverture. Il n'a donc pas de règle.
 *
 * ⚠️ Huit rangs : au-delà, un rang de boutons cesse d'être un choix et devient
 * une liste. Aucun écran du produit n'en propose plus de cinq. */
.sm-cols:has(.sm-groupes input:nth-of-type(2):checked) [data-sm-rang]:not([data-sm-rang~="2"]) { display: none; }
.sm-cols:has(.sm-groupes input:nth-of-type(3):checked) [data-sm-rang]:not([data-sm-rang~="3"]) { display: none; }
.sm-cols:has(.sm-groupes input:nth-of-type(4):checked) [data-sm-rang]:not([data-sm-rang~="4"]) { display: none; }
.sm-cols:has(.sm-groupes input:nth-of-type(5):checked) [data-sm-rang]:not([data-sm-rang~="5"]) { display: none; }
.sm-cols:has(.sm-groupes input:nth-of-type(6):checked) [data-sm-rang]:not([data-sm-rang~="6"]) { display: none; }
.sm-cols:has(.sm-groupes input:nth-of-type(7):checked) [data-sm-rang]:not([data-sm-rang~="7"]) { display: none; }
.sm-cols:has(.sm-groupes input:nth-of-type(8):checked) [data-sm-rang]:not([data-sm-rang~="8"]) { display: none; }
.sm-cols:has(.sm-groupes input:nth-of-type(9):checked) [data-sm-rang]:not([data-sm-rang~="9"]) { display: none; }

/* ══════════════════════════════════════════════════════════════════════════
   LE TABLEAU — une écriture, cinq écrans, et des colonnes qui se mesurent
   (règle R-TB)
   ══════════════════════════════════════════════════════════════════════════

   🔴 **CE QUE CECI FERME.** Cinq tableaux — Arbitre, Agent, Entraîneur,
   Scouting, Match — déclaraient chacun les MÊMES règles : `width: 100%`,
   `border-collapse`, `font-size: 13px`, un filet `border-top`, un en-tête en
   petites capitales grises. Cinq écritures du même objet, qui avaient déjà
   commencé à diverger sur le rembourrage et l'alignement.

   🔴 **ET LES COLONNES PRENAIENT LA PLACE QU'ELLES N'UTILISAIENT PAS.** Un
   `<table width="100%">` en disposition automatique répartit le surplus de
   largeur **sur toutes les colonnes** : sur une page de 1120 px, six colonnes
   de nombres à un chiffre recevaient chacune près de 180 px pour en employer
   trente. C'est l'« espace plus grand que ce qu'il ne faut » signalé par le
   client, et il grandit avec l'écran — donc il est invisible sur un portable.

   🛑 **`width: 1%` N'EST PAS UNE LARGEUR, C'EST UNE INSTRUCTION.** En
   disposition automatique, une largeur spécifiée est un vœu que le navigateur
   ne peut pas exaucer en dessous du contenu : une colonne à `1 %` reçoit donc
   exactement **la largeur de son plus long contenu**, jamais moins. C'est la
   demande du client — « dynamique en fonction de la taille du plus long » — et
   c'est le navigateur qui mesure, pas nous.

   ⚠️ **Une colonne doit absorber le reste**, sinon les colonnes se tassent à
   gauche et la table laisse un vide à droite. C'est la colonne d'IDENTITÉ (le
   nom, le match, la compétition) : la seule dont le contenu est du texte, donc
   la seule à qui de la place sert à quelque chose.

   ⚠️ Ce que la règle NE prend PAS : le rembourrage et l'alignement des
   nombres. Ils varient d'un écran à l'autre pour de bonnes raisons — une liste
   d'évaluations se lit plus aérée qu'un bilan de six colonnes — et chaque
   feuille garde les siens. */
.sm-tab {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.sm-tab th,
.sm-tab td {
  padding: 9px 8px;
  border-top: 1px solid var(--color-divider);
  font-weight: 400;
}

/* ── La MESURE, séparée de la forme ───────────────────────────────────────
 * 🛑 **DEUX CLASSES, PARCE QUE DEUX BESOINS.** `.sm-tab` porte la forme
 * commune aux cinq tableaux qui se ressemblent ; `.sm-tab-mesure` ne porte que
 * la largeur de colonne. Les tableaux de la fiche Joueur et de l'écran Compte
 * ont leur propre dessin (filet du bas, en-tête en police de titre) et n'ont
 * aucune raison d'adopter celui des cinq autres — mais ils ont exactement le
 * même défaut de largeur. Leur imposer la forme pour obtenir la mesure aurait
 * repeint deux écrans pour corriger un espacement.
 *
 * ⚠️ `nowrap` est ce qui rend la largeur MESURABLE : sans lui, le navigateur
 * peut couper, et la colonne n'a plus de largeur minimale à respecter. */
:is(.sm-tab, .sm-tab-mesure) th,
:is(.sm-tab, .sm-tab-mesure) td {
  width: 1%;
  white-space: nowrap;
}

.sm-tab thead th {
  border-top: none;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--color-neutral-700);
}

/* 🛑 **LA COLONNE D'IDENTITÉ ABSORBE LE RESTE**, et elle est la seule à
 * pouvoir se couper : c'est du texte, et un nom long vaut mieux sur deux
 * lignes qu'en débordement. Le sélecteur vise `scope="row"` plutôt qu'une
 * classe : c'est déjà ce que le balisage dit de cette cellule, et les cinq
 * tableaux le posent — il n'y a rien à ajouter aux gabarits. */
:is(.sm-tab, .sm-tab-mesure) thead th:first-child,
:is(.sm-tab, .sm-tab-mesure) th[scope="row"],
:is(.sm-tab, .sm-tab-mesure) .sm-tab-libre {
  width: auto;
  text-align: left;
  white-space: normal;
}

/* ⚠️ **`.sm-tab-libre` EST LA PORTE DE SORTIE**, et elle sert un cas réel : un
 * tableau dont aucune cellule ne porte `scope="row"` n'a pas de colonne
 * d'identité déclarée — ses colonnes se tasseraient toutes à gauche en laissant
 * un vide à droite. L'historique des paiements est dans ce cas : ses quatre
 * colonnes sont des `<td>`, et c'est la formule qui doit absorber. */

/* Contenu réservé aux technologies d'assistance (libellés de champ réels). */
.sm-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}


/* ── La hauteur de l'en-tête sous 640 px ───────────────────────────────────
 * `.sm-hd-bar` y passe à `padding: 9px 14px` (`sm-shell.css`). UN seul jeton
 * change ici ; `--sm-shell-h` étant un `calc()`, tous les offsets collants
 * suivent — c'est exactement ce que le `top: 52px` en dur ne faisait pas.
 * Mesuré au navigateur : 63 px à 360 px de large.
 */
@media (max-width: 640px) {
  :root { --sm-hd-pad-y: 9px; }
  /* Le panneau vedette respire moins large : l'explication du verrou passe de
     trois lignes à quatre, et la boîte doit suivre pour que les trois états
     restent à la même hauteur. Mesuré : 357 px pour l'état verrouillé. */
  :root { --sm-panneau-h: 348px; }
}

/* ══ LE CHAMP DE FORMULAIRE ════════════════════════════════════════════════
 *
 * 🔴 **IL N'Y EN AVAIT PAS.** Les formulaires du v2 rendaient `form.as_p` avec
 * les classes Bootstrap du formulaire v1 (`form-control`, `form-select`) — des
 * classes dont AUCUNE feuille du v2 ne porte la règle. La carte « Identité »
 * était donc peinte par la feuille du navigateur : des champs de largeur
 * variable, sans hauteur commune, sans état de focus. C'est le « style cassé »
 * que le client a signalé.
 *
 * ⚠️ Le socle, et non `sm-compte.css` : l'inscription, la connexion et le
 * contact ont exactement le même besoin. Une seconde écriture par écran est ce
 * que ce chantier passe son temps à retirer.
 */
.sm-champ {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.sm-champ-lib {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text-muted);
}

/* ⚠️ L'astérisque n'est pas une couleur seule : `aria-hidden` le retire de la
 * voix, et `required` sur le champ le dit à sa place. */
.sm-champ-requis { color: var(--color-danger); margin-left: 2px; }

/* 🛑 `width: 100%` + `box-sizing` : sans les deux, un `<input>` prend sa
 * largeur NATURELLE (la taille du contenu attendu, ~20 caractères), et une
 * grille de champs devient une grille de champs de largeurs différentes. */
.sm-champ-saisie {
  width: 100%;
  box-sizing: border-box;
  /* R-T : 44 px de cible tactile, comme tout ce qui se touche dans ce produit. */
  min-height: 44px;
  padding: 10px 12px;
  border: 1px solid var(--color-border);
  border-radius: 10px;
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: 14px;
}

.sm-champ-saisie:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
  border-color: var(--color-accent);
}

/* Une case à cocher n'est pas une saisie : elle ne prend ni la largeur ni la
 * hauteur d'un champ de texte. */
.sm-champ-saisie[type="checkbox"] {
  width: auto;
  min-height: 0;
  padding: 0;
}

.sm-champ-aide { font-size: 11.5px; color: var(--color-text-muted); }

/* ⚠️ L'erreur est SOUS le champ, et elle est écrite. `as_p` la plaçait au-dessus
 * de l'étiquette : on lisait le reproche avant de savoir de quoi il parlait. */
.sm-champ-erreur { font-size: 11.5px; color: var(--color-danger); }

.sm-champ--faux .sm-champ-saisie { border-color: var(--color-danger); }

/* ══ SUIVRE ════════════════════════════════════════════════════════════════
 *
 * 🔴 Le bouton n'existait sur AUCUNE fiche d'entité. Il vit dans le socle parce
 * que trois écrans le rendent — et six le rendront.
 *
 * ⚠️ L'état est porté par le LIBELLÉ (« Suivre » / « Suivi »), jamais par la
 * seule teinte : WCAG 1.4.1. La couleur ne fait que confirmer.
 */
.sm-suivre-form { display: inline-flex; }

.sm-suivre {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* R-T : la cible fait 44 px de haut, comme partout ailleurs. */
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.sm-suivre:hover { border-color: var(--color-accent); color: var(--color-accent); }

.sm-suivre:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.sm-suivre--actif {
  border-color: var(--color-accent);
  background: var(--color-accent);
  color: #fff;
}

.sm-suivre--actif:hover { background: var(--color-accent-700); color: #fff; }
