/* =====================================================================
   Ez Average — feuille de style de base
   UNE SEULE COPIE, partagée par toutes les fédérations.

   Tout est bâti sur des variables CSS. Une fédération ne redéfinit que
   des valeurs — jamais des règles — via ez_federationweb.fThemeConfig,
   injecté par le gabarit dans un :root{} après ce fichier.

   La conséquence est celle qu'on cherchait : une correction ici profite
   à tout le monde d'un coup. Si chaque fédération avait sa copie, il
   faudrait la répéter autant de fois qu'il y a de fédérations — le
   défaut qu'on a justement corrigé sur les rôles.
   ===================================================================== */

:root {
  --ez-primary:      #003a70;
  --ez-primary-text: #ffffff;
  --ez-accent:       #d4af37;
  --ez-surface:      #ffffff;
  --ez-text:         #1a1a1a;
  --ez-logo-height:  28px;
  /* La couleur du fond du logo, quand la fédération en demande un.
     ⚠ Elle est SANS EFFET tant qu'aucun fond n'est choisi : c'est le
       choix (plaque ou halo) qui déclenche son emploi, pas elle. */
  --ez-logo-back:    #ffffff;

  /* Valeurs de structure : pas surchargeables par une fédération.
     Elles tiennent la cohérence de l'interface d'un bout à l'autre. */
  --ez-bg:           #f4f5f7;
  --ez-muted:        #6b7280;
  --ez-border:       #d9dde3;
  /**
   * ⚠ LE ROUGE DE L'ERREUR NE VIENT PAS DE LA FÉDÉRATION. Swiss Bowling
   *   est rouge : un champ fautif teinté de la couleur du site ne se
   *   distinguerait de rien. Ces trois valeurs sont donc écrites en dur,
   *   comme celles d'« .alert--error », et ne suivent aucun thème.
   */
  --ez-err:          #b42318;
  --ez-err-clair:    #f3c7c3;
  --ez-radius:       6px;
  --ez-gap:          16px;

  /* ⚠ L'ÉCHELLE DES CONTRÔLES — TROIS HAUTEURS, PAS SEPT.
   *
   *   Relevé avant : « Enregistrer » 26 px, les lettres A–Z 26, la
   *   pagination 31, une liste déroulante 38, le filtre 39, le champ de
   *   recherche 42, « Chercher » 44. Quatre tailles de police. Sur la
   *   barre de recherche, trois contrôles côte à côte faisaient 39, 42 et
   *   44 px : ils ne s'alignaient pas.
   *
   *   Il n'y avait pas d'échelle, juste des valeurs posées au fil de
   *   l'eau. TOUT CE QUI SE CLIQUE TOMBE MAINTENANT DANS L'UNE DES TROIS.
   *   Une quatrième hauteur qui apparaît, c'est le début du retour en
   *   arrière. */
  --ez-h-l:  40px;  --ez-p-l:  15px;   /* action principale, champs, listes  */
  --ez-h-m:  32px;  --ez-p-m:  14px;   /* action secondaire, pages, A–Z      */
  --ez-h-s:  26px;  --ez-p-s:  13px;   /* action discrète, pastilles         */
  --ez-r:     8px;                     /* un seul rayon pour tout ça         */
  --ez-font:         system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --ez-mono:         ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

*, *::before, *::after { box-sizing: border-box; }

/* ⚠ « hidden » DOIT GAGNER. Le style par défaut du navigateur pose
   « [hidden] { display: none } » — une règle à SPÉCIFICITÉ FAIBLE, que
   « .btn { display: inline-flex } » écrase sans effort. Le bouton
   « Appliquer », masqué par le script quand il a pris la main, restait
   donc affiché. C'est le genre de défaut qu'on met une heure à voir
   parce qu'on cherche du côté du script. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: var(--ez-font);
  font-size: 16px;
  line-height: 1.5;
  color: var(--ez-text);
  background: var(--ez-bg);
}

/* ==================================================================== l'écran
   LE RAIL À GAUCHE, LA VUE À DROITE.

   Il y avait deux bandeaux empilés en haut — service, puis menu — et,
   dans la configuration, un troisième menu à gauche dans la page. Trois
   niveaux de navigation à trois endroits, et la couleur de la fédération
   réduite à 48 px de bandeau : « ça fait ambiance année 2000 ».

   ⚠ TOUT EST DANS LE RAIL désormais : identité, rubriques, sections,
     langue, compte. Un seul endroit où l'on navigue, et la couleur de la
     fédération tient toute la hauteur — c'est elle qui donne son identité
     à l'écran avant qu'on ait lu un mot.
   ==================================================================== */

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

/* ------------------------------------------------------------- le rail */
.ez-rail {
  width: 248px; flex: 0 0 248px;
  /* ⚠ LE DÉGRADÉ EST CALCULÉ À PARTIR DE LA COULEUR DE LA FÉDÉRATION, pas
     posé en dur : color-mix garde la teinte choisie dans Apparence et ne
     fait que l'assombrir et l'éclaircir. Une fédération en vert obtient
     un rail vert, pas un rail bleu. */
  background: var(--ez-primary);
  background: linear-gradient(180deg,
              color-mix(in srgb, var(--ez-primary) 88%, #000) 0%,
              var(--ez-primary) 42%,
              color-mix(in srgb, var(--ez-primary) 88%, #fff) 100%);
  color: var(--ez-primary-text);
  padding: 18px 14px 22px;
  position: sticky; top: 0; align-self: flex-start;
  height: 100vh; overflow-y: auto;
}

.ez-rail__haut { display: flex; align-items: center; gap: 10px; padding: 0 6px 18px; }
.ez-rail__marque {
  display: flex; align-items: center; gap: 10px;
  text-decoration: none; color: inherit; min-width: 0; flex: 1;
}
.ez-rail__initiales {
  width: 38px; height: 38px; flex: 0 0 38px; border-radius: 10px;
  background: var(--ez-accent); color: #2a1f00;
  display: grid; place-items: center;
  font-weight: 800; font-size: 16px; letter-spacing: -.02em;
}
.ez-rail__ident { min-width: 0; }
.ez-rail__nom {
  display: block; font-size: 15px; font-weight: 650; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ez-rail__sous { display: block; font-size: 12.5px; opacity: .62; }

/* ⚠ LA BASCULE N'EXISTE QUE SUR TÉLÉPHONE. Au-dessus de 900 px elle est
   retirée du flux — pas seulement masquée — pour qu'elle ne laisse ni
   espace ni cible de clic invisible. */
.ez-rail__bascule {
  display: none;
  width: var(--ez-h-m); height: var(--ez-h-m); flex: 0 0 var(--ez-h-m);
  border-radius: var(--ez-r); cursor: pointer;
  place-items: center; font-size: 17px;
  background: rgba(255, 255, 255, .12);
}
.ez-nav-coche { position: absolute; opacity: 0; pointer-events: none; }
.ez-visuel-seul {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.ez-rail__bloc { margin-bottom: 18px; }
.ez-rail__titre {
  font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
  /* ⚠ LA TEINTE QUASI BLANCHE, PAS LA MOYENNE. Le dégradé du rail
     s'éclaircit vers le bas : une teinte moyenne y tombe à 3,0 de
     contraste en suisse. Celle-ci tient 4,6 — contre 2,60 pour le blanc
     à 58 % d'opacité qu'elle remplace. Et l'opacité repasse à 1 : à .58,
     une couleur se délave et son contraste mesuré ne veut plus rien dire. */
  color: var(--ez-g-rail); opacity: 1;
  padding: 0 10px 6px; font-weight: 650;
}
.ez-rail__lien {
  display: flex; align-items: center;
  height: var(--ez-h-m); padding: 0 10px 0 22px; margin-bottom: 1px;
  border-radius: var(--ez-r); text-decoration: none;
  font-size: var(--ez-p-m); color: inherit; opacity: .82; position: relative;
}
.ez-rail__lien:hover, .ez-rail__lien:focus-visible {
  background: rgba(255, 255, 255, .10); opacity: 1;
}
.ez-rail__lien.is-active {
  background: rgba(255, 255, 255, .15); opacity: 1; font-weight: 600;
  box-shadow: inset 3px 0 0 var(--ez-g-clair, transparent);
}
/* ⚠ L'OR MARQUE CE QUI EST OUVERT, et c'est sa seule fonction dans le
   rail : une couleur d'accent qui sert à deux choses n'en signale plus
   aucune. */
.ez-rail__lien.is-active::before {
  content: ""; position: absolute; left: -14px; top: 6px; bottom: 6px;
  width: 3px; border-radius: 0 3px 3px 0; background: var(--ez-accent);
}
/* ⚠ GRISÉ, PAS ABSENT : la rubrique existe dans le plan et n'est pas
   encore faite. La masquer laisserait croire qu'elle n'existera jamais. */
.ez-rail__lien.is-todo { opacity: .42; cursor: default; }
.ez-rail__lien.is-todo:hover { background: none; opacity: .42; }

/* ⚠ SEULS LES BLOCS QUI ONT UN TITRE PORTENT LE FILET. Le premier bloc du
   rail — Accueil / Gestion / Configuration / État — n'en a pas : un filet
   vertical qui ne tient aucun titre ne tiendrait rien du tout. C'est la
   classe « ez-groupe », posée par config/_menu.php, qui fait le tri. */
.ez-rail__bloc.ez-groupe::before {
  content: ""; position: absolute; left: 11px; top: 24px; bottom: 1px;
  width: 2px; border-radius: 2px; background: var(--ez-g-clair);
}

/* ==================================================== barre de service
   LANGUES, COMPTE, SITE OFFICIEL — EN HAUT, À DROITE DU RAIL.

   ⚠ ILS ÉTAIENT AU PIED DU RAIL. Le rail fait la hauteur de l'écran :
     son pied se retrouvait collé en bas, à des centaines de pixels du
     dernier lien du menu. Relevé avant le déplacement : le sélecteur de
     langue s'affichait à y = 903 sur un écran de 900 de haut — hors de
     vue. Il est maintenant à y = 253.

   ⚠ LE FOND EST BLANC, ET C'EST MESURÉ. Sous la barre commence le gris
     de page (--ez-bg, #f4f5f7) ; la fiche, elle, est blanche. Écart
     perceptuel relevé au pixel (ΔE — sous 1,5 l'œil ne distingue plus) :

                          vs gris de page   vs fiche blanche
       barre blanche            3,6               0,0
       barre au gris de page    0,0               3,6
       barre gris franc         4,6               8,2

     Une barre grise se serait FONDUE dans la page au lieu de s'en
     détacher — l'inverse de l'effet cherché. Le blanc s'en écarte, et le
     filet du bas marque la limite avec le contenu.

   ⚠ ET ELLE N'EST PAS DANS LE RAIL. Sous 900 px, « ez-rail__contenu » se
     replie derrière la case à cocher du menu ; une barre placée dedans
     disparaîtrait avec lui.
   ==================================================================== */
.ez-barre {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 0 22px; min-height: 46px;
  background: var(--ez-surface);
  border-bottom: 1px solid var(--ez-border);
}
.ez-barre__g, .ez-barre__d { display: flex; align-items: center; gap: 16px; }
.ez-barre__d { margin-left: auto; }

/* ⚠ LES TROIS COMPOSANTS VIENNENT DU RAIL, OÙ LE FOND EST SOMBRE : ils
   héritaient d'un blanc et se survolaient en rgba(255,255,255,…). Sur
   fond blanc, tout cela est invisible. On les rhabille ici plutôt que de
   les réécrire — le rail n'existe plus qu'ici, mais l'habillage
   classique, lui, a ses propres classes. */
.ez-barre .ez-lang { padding: 0; }
.ez-barre .ez-lang a { color: var(--ez-muted); opacity: 1; }
.ez-barre .ez-lang a:hover, .ez-barre .ez-lang a:focus {
  background: var(--ez-bg); color: var(--ez-text);
}
.ez-barre .ez-lang__current { background: var(--ez-primary); color: var(--ez-primary-text); opacity: 1; }
.ez-barre .ez-compte { padding: 0; display: flex; align-items: baseline; gap: 10px; }
.ez-barre .ez-compte__nom { display: inline; opacity: 1; color: var(--ez-text); font-weight: 600; }
.ez-barre .ez-compte__out { color: var(--ez-muted); opacity: 1; }
.ez-barre .ez-compte__out:hover, .ez-barre .ez-compte__out:focus { color: var(--ez-text); }
.ez-barre .ez-compte__off { padding: 0; color: var(--ez-muted); opacity: 1; }
.ez-barre .ez-rail__externe { padding: 0; color: var(--ez-muted); opacity: 1; }
.ez-barre .ez-rail__externe:hover { color: var(--ez-text); }
.ez-rail__externe {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--ez-p-s); color: inherit; opacity: .7; text-decoration: none;
  padding: 0 10px;
}
.ez-rail__externe:hover { opacity: 1; text-decoration: underline; }

.ez-lang { display: flex; flex-wrap: wrap; gap: 3px; list-style: none; margin: 0; padding: 0 6px; }
.ez-lang a, .ez-lang__current {
  display: grid; place-items: center;
  min-width: var(--ez-h-s); height: var(--ez-h-s); padding: 0 6px;
  border-radius: 6px; font-size: var(--ez-p-s); font-weight: 600;
  color: inherit; text-decoration: none; opacity: .6;
}
.ez-lang a:hover, .ez-lang a:focus { opacity: 1; background: rgba(255,255,255,.15); }
.ez-lang__current { opacity: 1; background: rgba(255,255,255,.22); }

.ez-compte { padding: 0 10px; font-size: var(--ez-p-s); line-height: 1.4; }
/* ⚠ LE NOM EST DEVENU UN LIEN (vers « Mon compte »), et un lien sans
     règle prend le bleu souligné du navigateur : mesuré rgb(0,0,238)
     sur le bandeau sombre, illisible et hors charte. Il garde donc
     exactement l'apparence qu'il avait en tant que texte, et ne se
     souligne qu'au survol — c'est ce qui dit qu'on peut cliquer. */
.ez-compte__nom { display: block; opacity: .9; color: inherit; text-decoration: none; }
.ez-compte__nom:hover, .ez-compte__nom:focus { opacity: 1; text-decoration: underline; }
.ez-compte__out { color: inherit; opacity: .65; text-decoration: none; }
.ez-compte__out:hover, .ez-compte__out:focus { opacity: 1; text-decoration: underline; }
.ez-compte__off  { padding: 0 10px; font-size: var(--ez-p-s); opacity: .5; font-style: italic; }

.ez-menu__ext { font-size: .85em; opacity: .75; }

/* Le logo dans la barre de menu, à gauche du lien « Site officiel ».
 *
 * ⚠ PAR DÉFAUT AUCUN FOND : le logo est posé directement sur la couleur
 *   de la barre, et un logo à traits sombres y sera peu lisible. La
 *   fédération choisit dans Apparence entre rien, une plaque, ou un halo
 *   qui épouse le trait — avec la couleur de son choix.
 *
 * ⚠ LES QUATRE VALEURS DE REPLI CI-DESSOUS SONT LE MODE « AUCUN FOND ».
 *   Service\Theme n'écrit ses variables QUE si la fédération a demandé
 *   un fond ; sans elles, une fédération qui n'a rien réglé recevrait
 *   « background: ; » et le navigateur jetterait la règle entière.
 *
 * ⚠ max-height PLAFONNE --ez-logo-height : sans lui, une fédération qui
 *   réglerait une hauteur trop grande ferait grandir la barre de menu
 *   chez elle seulement. */
/* ⚠ LE PLAFOND DE 43 px A ÉTÉ MESURÉ SUR L'ANCIENNE BARRE DE MENU, qui
   n'existe plus : dans le rail, la place ne manque pas. Il est donc
   devenu PRUDENT au lieu d'être contraignant — et on n'y touche pas
   sans remesurer, parce que Theme::PX_MAX doit rester égal à cette
   valeur (bin/selftest.php le vérifie) et qu'une valeur stockée
   au-dessus du plafond serait silencieusement ramenée. Le relever est
   possible ; c'est une décision, pas un ajustement. */
.ez-menu__logo {
  height: var(--ez-logo-height);
  background: var(--ez-logo-plate, transparent);
  padding: var(--ez-logo-plate-pad, 0);
  border-radius: var(--ez-logo-plate-radius, 0);
  filter: var(--ez-logo-halo, none);
  /* ⚠ content-box, ET C'EST VOULU : la marge de la plaque S'AJOUTE à la
   *   hauteur au lieu de rogner l'image, qui garderait sinon deux
   *   tailles différentes selon qu'on a mis un fond ou non. 34 px de
   *   plafond + 6 px de marge = 40 px, toujours sous les 49 px de la
   *   barre : le fond ne la fait donc pas grandir non plus. */
  box-sizing: content-box;
  /* ⚠ 43 px, ET C'EST UN PLAFOND DUR — MESURÉ, PAS ESTIMÉ.
   *
   *   La barre de menu mesure 49 px, fixée par la hauteur des libellés.
   *   Relevé au navigateur, hauteur de barre à chaque pas :
   *
   *       sans fond     jusqu'à 49 px → barre 49    50 px → barre 50
   *       avec plaque   jusqu'à 43 px → barre 49    44 px → barre 50
   *
   *   La plaque ajoute ses 6 px de marge (content-box, voir plus haut),
   *   d'où l'écart. 43 est donc la plus grande valeur qui tienne DANS LES
   *   DEUX CAS : la barre a exactement la même hauteur sur toutes les
   *   fédérations, quel que soit le réglage de chacune, qu'elle ait un
   *   logo ou non, un fond ou non. C'est ce plafond qui le garantit, pas
   *   la discipline de celui qui règle.
   *
   * ⚠ IL VALAIT 34, ET C'ÉTAIT FAUX — posé au jugé. Les deux fédérations
   *   avaient 44 en base : Service\Theme refusait la valeur, n'écrivait
   *   aucune variable, et le logo retombait sur les 28 px du :root
   *   ci-dessus. Le plafond CSS et celui de Theme::PX_MAX doivent rester
   *   ÉGAUX — c'est leur écart qui rend ce genre de perte invisible. */
  max-height: 43px;
  width: auto;
  display: block;
  flex: 0 0 auto;
  align-self: center;
}

/* -------------------------------------------------------------- la vue */
.ez-vue { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* ⚠ 1500 px, ET C'EST LA VALEUR DE LA FÉDÉRATION — Xavier l'a portée de
   1100 à 1500 à la main. Elle borne le CONTENU, pas la fenêtre : sur un
   très grand écran, une ligne de tableau large de 2 500 px ne se lit
   plus. */
.ez-main { max-width: 1500px; width: 100%; margin: 0 auto; padding: 24px 28px 40px; }
.ez-footer {
  margin-top: auto; padding: 16px 28px 22px;
  color: var(--ez-muted); font-size: 13px;
}

/* ⚠ SOUS 900 px LE RAIL SE REPLIE, et c'est une case à cocher qui le
   commande — aucun script. Le rail redevient un bandeau horizontal, la
   marque reste visible, et le reste s'ouvre au bouton. */
@media (max-width: 900px) {
  .ez-shell { flex-direction: column; }
  .ez-rail {
    width: 100%; flex: none; height: auto; position: static;
    padding: 12px 14px;
  }
  .ez-rail__haut { padding-bottom: 0; }
  .ez-rail__bascule { display: grid; }
  .ez-rail__contenu { display: none; padding-top: 14px; }
  .ez-nav-coche:checked ~ .ez-shell .ez-rail__contenu { display: block; }
  .ez-rail__lien.is-active::before { left: -8px; }
  /* ⚠ LA BARRE VIENT JUSTE SOUS LE RAIL REPLIÉ. Sur un téléphone, les
   *   langues, le nom, « Déconnexion » et « Site officiel » ne tiennent
   *   pas sur une ligne de 390 px : elle passe sur deux, et « ez-barre__d »
   *   cesse d'être poussé à droite — sinon il s'y colle tout seul sur sa
   *   propre ligne, à l'opposé des langues. */
  .ez-barre { padding: 8px 16px; gap: 8px 14px; }
  .ez-barre__d { margin-left: 0; flex-wrap: wrap; gap: 12px; }
  .ez-main { padding: 18px 16px 32px; }
  .ez-footer { padding: 14px 16px 20px; }
}

/* ------------------------------------------------------------------ panneau */
.panel {
  background: var(--ez-surface);
  border: 1px solid var(--ez-border);
  border-radius: var(--ez-radius);
  padding: 20px 24px;
  margin-bottom: var(--ez-gap);
}
.panel--center { text-align: center; }
.panel--narrow { max-width: 420px; margin-left: auto; margin-right: auto; }
.panel__title { margin: 0 0 12px; font-size: 20px; font-weight: 600; }
.panel__lead  { margin: 0 0 16px; color: var(--ez-muted); }

/* ------------------------------------------------------------------ chiffres */
.stats { display: flex; gap: 32px; flex-wrap: wrap; }
.stat { display: flex; flex-direction: column; }
.stat__value { font-size: 32px; font-weight: 600; color: var(--ez-primary); line-height: 1.1; }
.stat__label { font-size: 14px; color: var(--ez-muted); }

/* ------------------------------------------------------------------ tableau */
.table { width: 100%; border-collapse: collapse; font-size: 15px; }
/* ⚠ « middle », PAS « top » — ET C'ÉTAIT UN VRAI DÉFAUT, MESURÉ.
 *
 *   Avec « vertical-align: top », le nom du club et son nombre de
 *   licences se calaient en HAUT d'une ligne de 58 px, pendant que les
 *   listes déroulantes de la colonne voisine, elles, se centraient. Écart
 *   relevé au navigateur : le texte passait 9 px au-dessus du centre de
 *   sa propre ligne. D'un bout à l'autre du tableau, rien n'était sur la
 *   même ligne d'œil.
 *
 *   « top » ne se justifie que pour une cellule qui porte un paragraphe à
 *   côté d'une cellule courte. Dans une liste de données, il n'y en a
 *   pas : on centre. */
.table th, .table td {
  text-align: left;
  padding: 8px 12px;
  border-bottom: 1px solid var(--ez-border);
  vertical-align: middle;
}
/* ⚠ PAS DE MAJUSCULES FORCÉES. « NOM ▲ LICENCES RÉGION » criait, et
   c'est une des trois choses qui datent un écran. La maquette validée les
   montrait en minuscules — la feuille avait gardé l'ancienne règle. */
.table thead th {
  font-size: 13px;
  font-weight: 650;
  letter-spacing: 0;
  color: var(--ez-muted);
  border-bottom-width: 1px;
}
.table tbody tr:last-child th, .table tbody tr:last-child td { border-bottom: 0; }
/* ⚠ LE SURVOL N'EST PAS DÉCORATIF sur 465 lignes larges de 1 400 px :
   c'est ce qui relie le nom à gauche au bouton à droite. */
.ez-list__table tbody tr:hover { background: color-mix(in srgb, var(--ez-primary) 4%, transparent); }
.table tbody th { font-weight: 500; color: var(--ez-muted); width: 34%; }

/* Un tableau de licenciés est large : il défile dans son cadre, la page
   elle-même ne défile jamais horizontalement. */
.table-wrap { overflow-x: auto; }

/* ------------------------------------------------------------------ champs */
.field { margin-bottom: var(--ez-gap); }
.field__label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.field__hint  { display: block; font-size: 13px; color: var(--ez-muted); margin-top: 4px; }

/**
 * ⚠ CES DEUX CLASSES ÉTAIENT POSÉES PAR DIX GABARITS ET N'AVAIENT
 *   JAMAIS EU UNE SEULE RÈGLE.
 *
 *   Le bandeau « Corrigez les champs signalés » s'affichait, et rien
 *   n'était signalé : le message d'erreur sortait en texte noir de 16 px,
 *   en ligne, impossible à distinguer d'une aide. Mesuré sur le rendu —
 *   « rgb(26,26,26) », « display: inline » — avant correction.
 *
 * ⚠ ET LA BORDURE ROUGE NE S'APPLIQUE PAS AUX MÊMES CHOSES SELON LE
 *   PORTEUR. Sur un « .field », un seul contrôle est en cause : c'est lui
 *   qu'on souligne. Sur un « .fieldset », la faute porte sur l'ENSEMBLE —
 *   « au moins un libellé », « les plages se chevauchent » — et rougir
 *   les neuf cases accuserait huit innocentes. On borde le groupe.
 */
.field__error {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--ez-err);
  margin-top: 4px;
}

.field.field--error > .field__input,
.field.field--error > .field__select,
.field.field--error .ez-cherche .field__input {
  border-color: var(--ez-err);
}
/* ⚠ Le halo de focus reste celui de la fédération : il dit « vous êtes
   ici », pas « c'est faux ». Seule la bordure porte l'erreur. */
.field.field--error > .field__input:focus,
.field.field--error > .field__select:focus { border-color: var(--ez-err); }

.fieldset.field--error { border-color: var(--ez-err); }
.fieldset.field--error > .fieldset__legend { color: var(--ez-err); font-weight: 600; }

/**
 * ⚠ « field__input--mono » ÉTAIT ORPHELINE, ELLE AUSSI — posée par trois
 *   gabarits sur les champs de code, sans une règle. Un code court se lit
 *   caractère par caractère : « I », « l » et « 1 » doivent se
 *   distinguer.
 */
/**
 * ⚠ « .field__input.field__input--mono » ET NON « .field__input--mono »
 *   SEUL. « .field__input » pose « font: … » — la forme raccourcie, qui
 *   RÉINITIALISE font-family — et, à spécificité égale, c'est la règle
 *   écrite en dernier qui gagne. Mesuré sur le rendu : la police restait
 *   « system-ui » alors que la règle existait. Deux classes sur le même
 *   élément suffisent à passer devant.
 *
 * ⚠ PAS DE « text-transform: uppercase » ICI. Il afficherait « PO » et
 *   enregistrerait « po » : l'écran mentirait sur ce qui est en base.
 *   Si les codes doivent être en majuscules, c'est à l'écriture que ça
 *   se décide, pas dans la feuille.
 */
.field__input.field__input--mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: .04em;
}
/* ⚠ CHAMPS ET LISTES SONT EN TAILLE L, comme l'action principale : ils
   se posent sur la même ligne qu'elle, et c'est là que le désalignement
   se voyait. La flèche de la liste est une image en ligne — pas de
   fichier, pas de requête — parce que « appearance: none » retire aussi
   celle du système. */
.field__input, .field__select {
  width: 100%;
  height: var(--ez-h-l);
  padding: 0 12px;
  font: var(--ez-p-l)/1 var(--ez-font);
  color: var(--ez-text);
  background: var(--ez-surface);
  border: 1px solid var(--ez-border);
  border-radius: var(--ez-r);
}
.field__input:focus, .field__select:focus {
  outline: 2px solid color-mix(in srgb, var(--ez-primary) 30%, transparent);
  outline-offset: 0; border-color: var(--ez-primary);
}
.field__select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 32px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%236b7280' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center;
}
/* Un champ multiligne n'a pas de hauteur d'échelle : c'est sa raison d'être. */
textarea.field__input { height: auto; padding: 10px 12px; line-height: 1.5; }

/* ------------------------------------------------------------------ boutons */
/* ⚠ TROIS NIVEAUX, UNE SEULE FORME. « btn » plein pour l'action
   principale, « btn--ghost » en contour pour la secondaire. La TAILLE ne
   dit rien du niveau : c'est la couleur qui le dit, la taille ne dit que
   la place disponible. Confondre les deux, c'est ce qui donnait un
   « Enregistrer » de 26 px à côté d'un « Chercher » de 44. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: var(--ez-h-m); padding: 0 14px;
  border: 1px solid transparent; border-radius: var(--ez-r);
  background: var(--ez-primary); color: var(--ez-primary-text);
  font: 600 var(--ez-p-m)/1 var(--ez-font);
  text-decoration: none; white-space: nowrap; cursor: pointer;
  transition: background .12s, border-color .12s, filter .12s;
}
.btn:hover, .btn:focus { filter: brightness(1.1); }
.btn:focus-visible { outline: 2px solid var(--ez-accent); outline-offset: 2px; }
.btn--large { height: var(--ez-h-l); padding: 0 18px; font-size: var(--ez-p-l); }
/**
 * UN BOUTON DONT LE LIBELLÉ PEUT PASSER À LA LIGNE.
 *
 * ⚠ « .btn » PORTE « white-space: nowrap » ET UNE HAUTEUR FIXE, et c'est
 *   juste pour 99 % des boutons : « Enregistrer » ne doit pas se couper
 *   en deux. Mais « Reprendre les 865 valeurs sans ambiguïté » fait
 *   310 px — mesuré — et poussait la liste des clubs de 71 px à 320 px
 *   de large. Le libellé porte un nombre : il grandit avec les données,
 *   et aucune traduction ne peut garantir sa longueur.
 *
 * ⚠ C'EST UN MODIFICATEUR, PAS UNE CORRECTION DE « .btn ». Autoriser
 *   TOUS les boutons du site à se replier changerait la mise en page de
 *   chaque écran pour régler le cas d'un seul.
 *
 * ⚠ ET « height: auto » VA AVEC. Sans lui, le texte passe bien sur deux
 *   lignes et déborde du bouton, qui garde sa hauteur d'origine.
 */
.btn--wrap {
  white-space: normal;
  height: auto;
  min-height: var(--ez-h-m);
  padding-top: 6px; padding-bottom: 6px;
  text-align: center;
  max-width: 100%;
}

.btn--small { height: var(--ez-h-s); padding: 0 10px; font-size: var(--ez-p-s); }
.btn--ghost {
  background: var(--ez-surface); color: var(--ez-text);
  border-color: var(--ez-border); font-weight: 500;
}
.btn--ghost:hover, .btn--ghost:focus { filter: none; border-color: #b9c0c9; background: #fbfcfd; }
.btn--danger { background: #b42318; border-color: #b42318; color: #fff; }

/* ------------------------------------------------------------ configuration
   ⚠ LE MENU LATÉRAL A DISPARU D'ICI : il est dans le rail, avec le reste
     de la navigation. « .ez-config » ne sert plus qu'à envelopper le
     contenu — les gabarits la portent encore, et la retirer des quinze
     obligerait à tous les rouvrir pour rien. */
.ez-config { display: block; }
.ez-config__body { min-width: 0; }

/* ------------------------------------------------------------------ formulaires */
/**
 * ⚠ LA LIGNE SE REPLIE. Sans « flex-wrap », une ligne trop chargée ne
 *   rétrécit pas : elle déborde, et c'est la PAGE ENTIÈRE qui prend une
 *   barre horizontale — le même défaut que le calque des catégories, sous
 *   un autre visage. Trois champs sur la ligne d'identité d'une catégorie
 *   sortaient de 90 px à 390 px de large ; la ligne des niveaux de jeu,
 *   qui n'en porte que deux, sortait déjà de 46 px à 320 px.
 *
 *   Le repli est déjà la réponse retenue pour « .ez-list__table
 *   .field__row » plus bas : on ne fait ici que la généraliser.
 */
.field__row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/**
 * DEUX CHAMPS COMPLETS CÔTE À CÔTE — étiquette, saisie, aide, erreur.
 *
 * ⚠ C'EST UN MODIFICATEUR, ET IL LE FAUT. « .field__row » sert déjà à
 *   poser des morceaux de champ sur une ligne (une case et son bouton,
 *   un code et son numéro) avec « align-items: center » et la marge
 *   basse de chaque champ. Changer ces deux points sur la classe de base
 *   déplacerait la mise en page de sept écrans qui vont très bien —
 *   dont l'invitation d'un utilisateur et la fiche d'une valeur typée,
 *   vérifiés.
 *
 * ⚠ « flex-start » ET NON « center » : deux champs dont un seul porte une
 *   ligne d'aide n'ont pas la même hauteur. Centrés, leurs saisies se
 *   décalent l'une par rapport à l'autre — l'œil voit un défaut
 *   d'alignement sans savoir d'où il vient.
 */
.field__row--paire { align-items: flex-start; gap: 6px 12px; }
.field__row--paire > .field { margin-bottom: 0; }
/**
 * ⚠ « LE CHAMP QUI PREND LA PLACE QUI RESTE ». Sans lui, « Nom » et
 *   « Nom court » se partagent la largeur moitié-moitié : vingt
 *   caractères ont alors autant de place que cinquante, et le nom du
 *   club est coupé pendant que son abréviation nage dans le vide.
 *
 * ⚠ « min-width: 0 » VA AVEC « flex: 1 », TOUJOURS. Un élément flexible
 *   refuse par défaut de descendre sous la largeur de son contenu : sans
 *   cette ligne, la règle n'élargit rien et déborde à l'étroit. Même
 *   défaut que sur .fieldset, pour la même raison.
 */
.field__row--paire > .field--grow { flex: 1 1 14em; min-width: 0; }
.field__input--small { width: 5em; flex: 0 0 auto; }
/**
 * ⚠ « width: auto » SEUL NE BORNE RIEN : une liste déroulante se
 *   dimensionne sur son option la plus longue, et « — aucun
 *   rattachement — » dépasse la largeur d'un téléphone à lui seul. Le
 *   couple max-width / min-width est ce qui l'autorise à rétrécir.
 */
.field__row .field__select { width: auto; max-width: 100%; min-width: 0; }

.fieldset {
  /**
   * ⚠ « min-width: 0 » N'EST PAS DÉCORATIF SUR UN FIELDSET. Le
   *   navigateur lui donne « min-width: min-content » d'office — une
   *   règle du standard, propre à cet élément — et il refuse alors de
   *   descendre sous la largeur de son contenu. Conséquence mesurée :
   *   le tableau du découpage, pourtant enfermé dans une enveloppe à
   *   « overflow-x: auto », poussait la PAGE de 139 px à 320 px. Le
   *   défilement interne ne peut pas fonctionner tant que le cadre qui
   *   le contient ne sait pas rétrécir.
   */
  min-width: 0;
  border: 1px solid var(--ez-border);
  border-radius: var(--ez-radius);
  padding: 12px 16px;
  margin: 0 0 var(--ez-gap);
}
.fieldset__legend { font-size: 14px; font-weight: 500; padding: 0 6px; color: var(--ez-muted); }
.check { display: flex; align-items: center; gap: 8px; padding: 4px 0; cursor: pointer; }

/* Réglages avancés repliés : ils existent, sans encombrer l'écran de
   celui qui ne les cherche pas. <details> le fait sans une ligne de JS. */
.details { margin: 8px 0 0; border-top: 1px solid var(--ez-border); padding-top: 8px; }
.details__summary {
  cursor: pointer;
  font-size: 14px;
  color: var(--ez-primary);
  padding: 4px 0;
  list-style: revert;
}
.details__summary:focus-visible { outline: 2px solid var(--ez-primary); outline-offset: 2px; }
.details[open] .details__summary { margin-bottom: 8px; }

/* ------------------------------------------------------------------ alertes */
.alert {
  padding: 10px 14px;
  border-radius: var(--ez-radius);
  border: 1px solid transparent;
  margin: 0 0 var(--ez-gap);
  font-size: 15px;
}
.alert--error  { background: #fdeceb; border-color: #f3c7c3; color: #8a1c14; }
.alert--info   { background: #eef4fb; border-color: #cddef2; color: #1c4e8a; }
.alert--ok     { background: #e7f4ec; border-color: #bfe0cd; color: #17603a; }

/* ============================================================ la fiche
   UNE FICHE SE LIT D'UN ÉCRAN, PAS DE TROIS.

   ⚠ Mesuré avant refonte : 1 800 px de haut pour vingt-sept faits, huit
     panneaux empilés, chaque fait seul sur une ligne de 1 126 px. Après :
     810 px, et huit informations de PLUS. Le gain ne vient pas d'avoir
     retiré des données — il vient d'avoir cessé d'en mettre une par
     ligne sur toute la largeur.

   ⓘ Ces classes servent la fiche d'un licencié ; elles sont écrites pour
     resservir telles quelles à toute fiche de lecture. */

/* --- l'en-tête d'identité --- */
.fic__nom { font-size: 26px; font-weight: 700; line-height: 1.15; margin: 0 0 2px; }
.fic__num {
  font-family: var(--ez-mono); font-size: 13px; background: var(--ez-bg);
  border-radius: 4px; padding: 2px 7px; color: var(--ez-muted);
}
/* Les six clés : la valeur en gros, son nom dessous. C'est ce qu'on
   vient chercher en ouvrant une fiche. */
.fic__cles { display: flex; gap: 22px; flex-wrap: wrap; margin: 14px 0 0; }
.fic__cle  { min-width: 0; }
.fic__cle b    { display: block; font-size: 15px; font-weight: 600; line-height: 1.3; }
.fic__cle span { display: block; font-size: 12px; color: var(--ez-muted); }

/* --- la grille de faits ---
   ⚠ « auto-fit » ET NON « auto-fill » : à deux faits dans un cadre
     large, auto-fill laisserait des colonnes vides et le tout collerait
     à gauche. auto-fit répartit ce qu'il y a. */
.fx { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
      gap: 2px 20px; margin: 0; }
.fx > div { padding: 5px 0; min-width: 0; }
.fx dt { font-size: 12px; color: var(--ez-muted); line-height: 1.3; }
.fx dd { margin: 0; font-size: 14px; line-height: 1.4; overflow-wrap: anywhere; }
/* Une adresse tient rarement dans une demi-colonne. */
.fx .large { grid-column: 1 / -1; }
/* ⚠ « quatre informations non renseignées » — on masque les champs
     vides, on ne les cache pas. La nuance tient dans cette ligne. */
.fx__vides { margin: 4px 0 0; font-size: 12px; color: var(--ez-muted); }

/* --- deux colonnes, qui retombent en une --- */
.col2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
@media (max-width: 820px) { .col2 { grid-template-columns: 1fr; } }

/* --- un cadre plus sobre que « .panel » ---
   ⓘ Le titre d'un bloc n'est pas un titre de page : petites capitales,
     gris, il sépare sans crier. Trois de ces titres dans un même cadre
     restent lisibles ; trois « .panel__title » se battraient. */
.bloc { border: 1px solid var(--ez-border); border-radius: var(--ez-radius);
        background: var(--ez-surface, #fff); padding: 14px 16px; margin: 0 0 16px; }
.bloc__t { font-size: 12px; font-weight: 700; letter-spacing: .06em;
           text-transform: uppercase; color: var(--ez-muted); margin: 0 0 8px; }
.bloc .fx + .bloc__t, .bloc .fx__vides + .bloc__t { margin-top: 14px; }

/* --- une rangée de oui/non --- */
.oui { display: flex; gap: 8px; flex-wrap: wrap; }
.oui .badge { font-weight: 500; }

/* ------------------------------------------------------------------ badges */
.badge {
  display: inline-block;
  padding: 2px 8px;
  font-size: 12px;
  font-weight: 600;
  border-radius: 999px;
}
.badge--ok { background: #e7f4ec; color: #17603a; }
.badge--ko { background: #fdeceb; color: #b42318; }
.badge--mirror { background: #eef0f3; color: var(--ez-muted); font-weight: 500; }
/* « fédération » contre « général » : on doit voir d'un coup d'œil quelle
   valeur a été choisie ici et laquelle est simplement héritée. */
.badge--own { background: #eef4fb; color: #1c4e8a; font-weight: 500; }
/* ⚠ L'ORANGE DU RETARD — « la licence n'est plus active, et voici depuis
     quand ». Ni vert ni rouge : ce n'est pas une réussite, ce n'est pas
     une faute, c'est un écart qui appelle un geste (renouveler).

   ⓘ Contraste mesuré : #7a4a05 sur #fdf3e3 rend 7,4 — au-dessus des 4,5
     exigés, et lisible aussi pour un daltonisme rouge-vert, où seule la
     LUMINOSITÉ distingue. C'est pour ça que la pastille porte AUSSI un
     texte : l'année. La couleur ne porte jamais l'information seule. */
.badge--late { background: #fdf3e3; color: #7a4a05; }

/* ------------------------------------------------------------------ à recopier */
/* Un bloc destiné à être sélectionné et collé tel quel : police fixe,
   fond contrasté, et retour à la ligne autorisé — une clé de 64 signes
   ne doit pas forcer un défilement horizontal sur un portable. */
.pre {
  font-family: var(--ez-mono);
  font-size: 14px;
  background: #1d2733;
  color: #e8eef5;
  padding: 10px 14px;
  border-radius: var(--ez-radius);
  margin: 8px 0;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-all;
  user-select: all;
}
.pre code { background: none; color: inherit; padding: 0; font-size: inherit; }
.steps { margin: 8px 0 8px 20px; padding: 0; }
.steps li { margin: 3px 0; }

/* ------------------------------------------------------ barre de recherche
 *
 * REFAITE DE ZÉRO. Les trois versions précédentes réutilisaient
 * .field__input et échouaient toutes pour la même raison :
 *
 * ⚠ .field__input EST FAIT POUR UN FORMULAIRE EMPILÉ. Un champ par
 *   ligne, chacun sur toute la largeur — d'où son « width: 100% ». Dans
 *   une barre horizontale, cette largeur devient la base du calcul flex
 *   et le champ RÉCLAME toute la ligne, quoi qu'on écrive ensuite.
 *   Le défaire après coup, c'est se battre contre sa propre feuille.
 *
 * La barre a donc SES PROPRES CLASSES, sans rien hériter des champs de
 * formulaire. Aucune règle « width » nulle part.
 *
 * ⚠ ET ELLE MARCHE MÊME SANS CSS. Un <input>, un <button> et un <a> sont
 *   des éléments EN LIGNE par nature : ils se rangent d'eux-mêmes côte à
 *   côte. C'était le « width: 100% » qui cassait ce comportement, pas
 *   son absence. Le pire cas — feuille non chargée — donne donc une
 *   barre laide mais sur une seule ligne.
 */
.searchbar {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 12px;
  margin: 0 0 var(--ez-gap);
}

/* ⚠ flex: 1 1 0% — la base est ZÉRO, pas « auto ».
 *   Le champ ne réclame donc AUCUNE largeur propre : il se contente de
 *   ce qui reste une fois les boutons placés. Avec « auto », la base
 *   serait allée chercher une largeur, et c'est là que tout partait de
 *   travers. Il ne peut plus rien repousser.
 */
.searchbar__field {
  flex: 1 1 0%;
  min-width: 0;
  height: var(--ez-h-l);
  padding: 0 12px;
  font: var(--ez-p-l)/1 var(--ez-font);
  color: var(--ez-text);
  background: var(--ez-surface);
  border: 1px solid var(--ez-border);
  border-radius: var(--ez-r);
}
.searchbar__field:focus {
  outline: 2px solid color-mix(in srgb, var(--ez-primary) 30%, transparent);
  outline-offset: 0;
  border-color: var(--ez-primary);
}
/* ⚠ LE BOUTON DE LA BARRE PASSE EN TAILLE L. C'est là que le
   désalignement crevait les yeux : champ 42, filtre 39, bouton 44. */
.searchbar .btn, .searchbar__btn.btn { height: var(--ez-h-l); padding: 0 18px;
                                       font-size: var(--ez-p-l); }

/* Les boutons gardent leur largeur : c'est le texte qui la fixe, et on
   ne coupe pas un libellé de bouton. */
.searchbar__btn { flex: 0 0 auto; white-space: nowrap; }

/* Sur un téléphone, comprimer n'a plus de sens : le champ prend la
   première ligne, les boutons la seconde, avec l'écart du « gap ». */
@media (max-width: 560px) {
  .searchbar { flex-wrap: wrap; }
  .searchbar__field { flex: 1 1 100%; }
}

/* Rangée de boutons simple, sans champ — bas de formulaire. */
.toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin: 0 0 var(--ez-gap); }

/* Une case à cocher qui occupe sa ligne, pour un choix décrit sur deux
   lignes — le libellé et son explication. */
.check--block { align-items: flex-start; padding: 8px 0; }
.check--block input { margin-top: 3px; }

/* Les trois listes de langues sur une ligne.
 *
 * ⚠ LES CASES À COCHER N'Y SONT PAS : elles sont empilées en dessous.
 *   Une case en bout de rangée se lit mal — l'œil cherche le carré au
 *   même endroit d'une ligne à l'autre.
 *
 * ⚠ minmax(0, 1fr) et non 1fr : une piste de grille refuse par défaut de
 *   descendre sous la largeur de son contenu, et une liste déroulante au
 *   long libellé ferait déborder la ligne.
 */
.langrow {
  display: grid;
  /**
   * ⚠ AUTO-FIT, PAS UN NOMBRE DE COLONNES. Trois colonnes fixes
   *   renvoyaient la quatrième langue à la ligne toute seule (Suisse :
   *   FR·DE·IT puis EN). Un « repeat(4) » aurait réglé ce cas-là et cassé
   *   celui d'une fédération à deux langues — deux cases étroites et la
   *   moitié de la ligne vide.
   *
   *   On déclare la largeur MINIMALE d'un champ, et la grille compte la
   *   place réelle : quatre langues côte à côte dès qu'elles tiennent,
   *   moins sinon. Elle sait aussi ce que la fenêtre ignore — que le rail
   *   latéral prend sa part, et que l'habillage classique n'en prend pas.
   *
   * ⚠ « min(150px, 100%) » ET NON « 150px » : une piste de 150 px dans un
   *   conteneur de 120 px déborde. La forme min() la laisse rétrécir sous
   *   son propre minimum quand il n'y a plus le choix — c'est ce qui tient
   *   l'écran à 320 px.
   */
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  gap: 12px;
  align-items: end;
  margin: 4px 0 10px;
}
/**
 * ⚠ LES PLAGES D'UN RÉFÉRENTIEL, CÔTE À CÔTE. Trois colonnes fixes :
 *   « les deux », « hommes », « femmes » sont un seul découpage, et le
 *   nombre est borné à trois par la nature de la chose. Empilées, il
 *   fallait descendre l'oeil pour vérifier que les bornes se suivaient.
 *
 * ⚠ L'ESPACEMENT EST PLUS LARGE ENTRE LES GROUPES (24 px) QU'À
 *   L'INTÉRIEUR (6 px, hérité de .field__row). Sans cet écart, les neuf
 *   cases se lisent comme une seule suite et on ne voit plus où finit
 *   une plage.
 */
.plages {
  display: grid;
  /**
   * ⚠ « auto-fit » ET NON « repeat(3) » — LA LEÇON A COÛTÉ UNE MESURE.
   *   Trois colonnes fixes tenaient à 1280 px et à 1500 px, mais à
   *   1024 px elles tombaient à 206 px et CHAQUE PLAGE se repliait à
   *   l'intérieur : la ligne faisait 86 px de haut au lieu de 40. Le
   *   point de rupture ne peut pas être écrit à la main, parce que la
   *   largeur utile ne dépend pas que de la fenêtre — le rail latéral en
   *   prend sa part, et l'habillage classique n'en prend pas.
   *
   *   Avec « auto-fit », c'est la grille qui compte la place réelle :
   *   trois plages quand elles tiennent, deux sinon, une à la fin. On
   *   déclare la largeur MINIMALE d'une plage, pas le nombre de
   *   colonnes.
   */
  grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr));
  gap: 10px 18px;
  margin-bottom: 6px;
}

/**
 * ⚠ UN CHIFFRE DANS UNE COLONNE ALIGNÉE À DROITE. « tabular-nums » parce
 *   que sans lui « 1 » et « 8 » n'ont pas la même largeur : les unités
 *   dansent d'une ligne à l'autre et la colonne cesse de se lire d'un
 *   coup d'oeil.
 */
.ez-chiffre { font-variant-numeric: tabular-nums; }

/**
 * UNE CARTE PAR GROUPE — les valeurs typées
 *
 * ⚠ CE N'EST PAS UN CADRE DANS UN CADRE. La première version posait
 *   neuf « fieldset » à filet gris DANS le grand panneau blanc de la
 *   page : neuf boîtes blanches sur fond blanc, séparées par un trait
 *   d'un pixel. Elles se relisaient comme un seul bloc — c'est ce que
 *   Xavier appelait « fouillis », et le filet n'y pouvait rien.
 *
 *   Le panneau de la page est donc RETIRÉ sur cet écran, et chaque
 *   groupe devient une carte posée sur le gris. C'est le gris entre les
 *   cartes qui sépare, pas une bordure de plus.
 *
 * ⚠ ET LE TITRE EST UN BANDEAU, pas une légende. Une légende de
 *   « fieldset » flotte sur la bordure et ne pèse rien ; un bandeau qui
 *   tient toute la largeur donne au groupe une tête, et c'est elle qu'on
 *   compte du regard.
 */
/**
 * ⚠ LE TITRE ET LE CHAPÔ D'UN ÉCRAN SANS PANNEAU. Posés à même le gris,
 *   ils ont besoin d'une marge latérale — sinon ils collent au bord,
 *   alors que les cartes, elles, ont leur propre bordure pour les tenir.
 */
.ez-entete { padding: 0 4px; }

/**
 * ⚠ UNE VALEUR QU'ON MONTRE SANS POUVOIR LA CHANGER. Elle occupe la
 *   place d'un champ, dans une ligne de champs : sans cette hauteur, la
 *   ligne se désaligne dès qu'un contrôle voisin fait 40 px. On ne la
 *   grise pas — ce n'est pas un champ désactivé, c'est un fait.
 */
.ez-fige {
  margin: 0; display: flex; align-items: center;
  height: var(--ez-h-l); font-size: var(--ez-p-m);
}

.ez-carte {
  background: var(--ez-surface);
  border: 1px solid var(--ez-border);
  border-radius: var(--ez-radius);
  margin: 0 0 var(--ez-gap);
  /* ⚠ « overflow: hidden » : sans lui, le bandeau déborde des coins
     arrondis et la carte a deux angles carrés en haut. */
  overflow: hidden;
}
.ez-carte__titre {
  margin: 0;
  padding: 10px 20px;
  font-size: 15px; font-weight: 650;
  background: #f7f8fa;
  border-bottom: 1px solid var(--ez-border);
}
.ez-carte__corps { padding: 12px 20px 16px; }

/**
 * ⚠ LE BANDEAU D'UN GROUPE TECHNIQUE EST PLUS SOURD, et son titre porte
 *   la mention. Ça remplace le titre de section qui les regroupait : une
 *   carte dit elle-même ce qu'elle est, sans qu'on ait à retenir sous
 *   quel intertitre on se trouve.
 */
.ez-carte--tech > .ez-carte__titre { background: var(--ez-bg); color: var(--ez-muted); }
.ez-carte__mention { font-weight: 400; color: var(--ez-muted); }

/**
 * ⚠ DES LARGEURS FIXES, IDENTIQUES D'UNE CARTE À L'AUTRE — c'est le
 *   second défaut, et il est indépendant du premier. Sans
 *   « table-layout: fixed », chaque tableau calcule ses colonnes sur SON
 *   contenu : « Licence internationale » et « SUBSCRIBE » n'ont pas la
 *   même longueur, et « Code » tombait à 335 px dans un groupe, 310 dans
 *   le suivant, 425 dans le troisième. Rien ne s'alignait.
 */
.ez-tv-table { table-layout: fixed; min-width: 520px; }
/* ⚠ 120 px ET NON 96 : « Numéro interne » se coupait en deux lignes,
   et l'en-tête faisait alors deux fois la hauteur des autres. Mesuré au
   rendu. */
.ez-tv-table .tv-c1 { width: 120px; }
.ez-tv-table .tv-c2 { width: 150px; }
/* ⚠ « auto » EST DÉCLARÉ, pas sous-entendu : c'est la seule colonne qui
   absorbe la place restante, et l'écrire évite qu'on lui donne une
   largeur « pour faire propre » le jour où on relira ce bloc. */
.ez-tv-table .tv-c3 { width: auto; }
.ez-tv-table .tv-c4 { width: 76px; }
.ez-tv-table .tv-c5 { width: 112px; }
.ez-droite   { text-align: right; }

/**
 * LE DÉCOUPAGE D'UNE RÉGION, DANS SA FICHE
 *
 * ⚠ CE N'EST PAS « .ez-list__table », ET C'EST DÉLIBÉRÉ. Celle-là porte
 *   la recherche, le tri, la pagination et un repli en cartes sur
 *   téléphone — tout ce qu'une liste de cinq lignes n'a pas à porter.
 *   Mesuré : de 1 à 15 lignes par région, 5,1 en moyenne en France.
 *
 * ⚠ ELLE DÉFILE DANS SON PROPRE CADRE. Cinq colonnes dont un bouton ne
 *   tiennent pas dans 320 px : sans « overflow-x » sur l'enveloppe,
 *   c'est la PAGE ENTIÈRE qui prend une barre horizontale — le défaut
 *   qu'on a déjà corrigé deux fois ailleurs.
 */
.ez-sous-cadre { overflow-x: auto; }
.ez-sous-table {
  width: 100%; border-collapse: collapse;
  font-size: var(--ez-p-m); min-width: 380px;
}
/**
 * ⚠ UNE COLONNE ALIGNÉE À DROITE GARDE SA GOUTTIÈRE. Sans elle, « 1 » et
 *   « CHE » se touchent et se lisent « 1CHE ». Vu dans ma propre
 *   maquette, en corrigeant le défaut d'à côté.
 */
.ez-sous-table th:last-child,
.ez-sous-table td:last-child { padding-right: 0; }

.ez-sous-table th {
  text-align: left; padding: 0 16px 7px 0; white-space: nowrap;
  font-size: 13px; font-weight: 500; color: var(--ez-muted);
  border-bottom: 1px solid var(--ez-border);
}
.ez-sous-table td {
  padding: 8px 16px 8px 0; border-bottom: 1px solid var(--ez-border);
  vertical-align: middle;
}
.ez-sous-table tr:last-child td { border-bottom: 0; }
/* ⚠ Une ligne éteinte se lit encore : on la pâlit, on ne l'efface pas. */
.ez-sous-table tr.is-off td { color: var(--ez-muted); }

.langrow__item { min-width: 0; }
.langrow .field__select { width: 100%; }



/* ------------------------------------------------------ tableau de bord
 * Les chiffres à gauche, le logo poussé à droite et centré sur la
 * hauteur. align-items: center fait le centrage vertical ; margin-left:
 * auto sur le logo absorbe l'espace libre, sans avoir à savoir combien
 * de chiffres le précèdent. */
/* ⚠ C'EST LE CADRE LUI-MÊME qui est la rangée : titre et chiffres dans
 *   une colonne à gauche, logo à droite. Centrer le logo sur les seuls
 *   chiffres le ferait descendre de la hauteur du titre — les écarts au
 *   bord haut et au bord bas cessent alors d'être égaux, et ça se voit. */
.dash { display: flex; align-items: center; gap: 24px; }
.dash__main { flex: 1 1 auto; min-width: 0; }
/* Le titre ne doit pas ajouter d'écart en bas de sa colonne : il fausserait
   le centrage de la colonne, donc celui du logo en face. */
.dash__main .panel__title { margin-top: 0; }
.dash__logo {
  flex: 0 0 auto;
  margin-left: auto;
  align-self: center;
  max-height: 96px;
  max-width: 40%;
  width: auto;
  /* La marge demandée, pour qu'il ne colle pas au bord du cadre. */
  margin-right: 8px;
  object-fit: contain;
}
/* Sur un écran étroit, le logo passe sous les chiffres plutôt que de les
   écraser — un logo réduit à 60 px de large ne sert plus à rien. */
@media (max-width: 620px) {
  .dash { flex-direction: column; align-items: flex-start; gap: 16px; }
  .dash__logo { margin-left: 0; align-self: flex-start; max-width: 60%; }
}

/* Le logo dans l'écran Apparence, avec son nom de fichier à côté. */
.logobox { display: flex; align-items: center; gap: 16px; margin: 4px 0 12px; }
.logobox__img {
  max-height: 72px; max-width: 220px; width: auto;
  border: 1px solid var(--ez-border); border-radius: var(--ez-radius);
  padding: 6px; background: var(--ez-surface);
}
.colorpick {
  width: 56px; height: 34px; padding: 2px; flex: 0 0 auto;
  border: 1px solid var(--ez-border); border-radius: var(--ez-radius);
  background: var(--ez-surface); cursor: pointer;
}
/* La couleur du fond du logo n'a de sens que si un fond est demandé : le
 * champ disparaît quand le sélecteur retombe sur « aucun ».
 *
 * ⚠ EN CSS PUR, SANS UNE LIGNE DE JAVASCRIPT, et c'est imposé : la page
 *   est servie sous Content-Security-Policy « default-src 'self' », qui
 *   refuse tout script écrit dans la page. « :has » lit la sélection
 *   courante — pas celle du chargement — donc le champ suit le
 *   sélecteur immédiatement, sans aller-retour au serveur.
 *
 * ⚠ Un navigateur trop ancien pour « :has » ignore la règle et affiche
 *   toujours le champ. C'est LE bon sens de dégradation : un champ de
 *   trop, jamais un champ manquant — et le contrôleur écarte de toute
 *   façon la couleur quand aucun fond n'est posé. */
.fieldset:has(#t_logoBack option[value="0"]:checked) .field--logoback {
  display: none;
}

/**
 * ⚠ LES POINTS DE RUPTURE PASSENT AVANT LE NOMBRE DE LANGUES, et c'est
 *   pour ça qu'ils visent « .langrow » et non les modificateurs : à
 *   spécificité égale, c'est la règle écrite en dernier qui l'emporte.
 *   Quatre colonnes sur un téléphone donneraient quatre champs de 60 px.
 *
 *   Le seuil est monté de 780 à 900 px : quatre colonnes ont besoin de
 *   plus de place que trois.
 */
/**
 * ⚠ PLUS AUCUN POINT DE RUPTURE POUR « .langrow » NI « .plages ».
 *   Il y en avait deux, à 780 et 520 px, et ils ne pouvaient pas dire la
 *   vérité : la largeur utile ne dépend pas que de la fenêtre — le rail
 *   latéral en prend sa part. Mesuré : à 1024 px de fenêtre, une colonne
 *   de plage tombait à 206 px et se repliait, alors que la règle écrite à
 *   la main la croyait au large. « auto-fit » compte la place réelle ;
 *   un seuil écrit à la main le contredirait.
 */

.empty { color: var(--ez-muted); padding: 18px 0; }
.muted { color: var(--ez-muted); }

/* Les pages sans menu latéral : acceptation d'invitation, connexion. */
.ez-narrow { max-width: 520px; margin: 0 auto; }
/* Un tableau posé DANS une alerte : sans fond ni marge propres, il
   redécoupe le bloc et casse la lecture. */
.table--tight { margin: 6px 0; background: none; }
.table--tight th, .table--tight td { padding: 4px 8px; font-size: 14px; }

/* ------------------------------------------------------------------ divers */
code { font-family: var(--ez-mono); font-size: .92em; background: var(--ez-bg); padding: 1px 5px; border-radius: 3px; }



@media (max-width: 640px) {
  .table tbody th { width: auto; }
  .stats { gap: 20px; }
}

.detail {
  font-family: var(--ez-mono);
  font-size: 13px;
  text-align: left;
  background: var(--ez-bg);
  border: 1px solid var(--ez-border);
  border-radius: var(--ez-radius);
  padding: 12px;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

/* ------------------------------------------------------------------
   La fiche d'un utilisateur
   ------------------------------------------------------------------ */

/* Le retour à la liste, au-dessus du titre. */
.crumb { margin: 0 0 8px; font-size: 13px; }
.crumb a { color: var(--ez-muted); text-decoration: none; }
.crumb a:hover { color: var(--ez-primary); text-decoration: underline; }

.panel__sub { margin: 20px 0 8px; font-size: 15px; font-weight: 600; }

/* Deux colonnes : intitulé, valeur. Passe à une seule sur mobile. */
.facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 12px 0 0; }
.facts dt { color: var(--ez-muted); font-size: 13px; }
.facts dd { margin: 0; font-size: 14px; }
@media (max-width: 520px) {
  .facts { grid-template-columns: 1fr; }
  .facts dd { margin-bottom: 8px; }
}

/* (l'ancienne redéfinition de .btn--small est retirée : elle annulait
   l'échelle posée plus haut.) */

/* ⚠ Un champ de date a besoin de sa largeur. « field__input--small »
 *   convient à un nombre, pas à « jj/mm/aaaa » suivi de l'icône du
 *   calendrier : le champ affichait « mm/ » et rien d'autre. */
/* ⚠ « flex: 1 1 0 » ET « min-width: 0 », les deux ensemble.
 *
 *   Un champ de date a une largeur INTRINSÈQUE — environ 172 px dans
 *   Chromium — et « width: auto » la lui laisse : deux dates côte à côte
 *   débordaient alors de leur colonne. flex-basis à 0 les fait partager
 *   la place disponible, et min-width à 0 lève le plancher que Flexbox
 *   applique par défaut (« min-width: auto », c'est-à-dire la taille
 *   intrinsèque) — sans lui, rien ne se comprime et le débordement
 *   revient.
 *
 * ⚠ 145 PIXELS, ET LE CHIFFRE EST MESURÉ, PAS ESTIMÉ. En dessous, le
 *   champ rogne son propre gabarit — « jj/mm/aaa », puis « jj/mm/ », et
 *   l'icône du calendrier finit par recouvrir l'année. Le navigateur ne
 *   le signale d'aucune façon : scrollWidth reste égal à clientWidth,
 *   c'est le rendu interne du champ qui coupe. Ça ne se voit qu'à l'œil,
 *   d'où la mesure faite à la capture d'écran.
 *
 *   C'est ce plancher qui fixe la largeur de la troisième colonne de la
 *   carte : deux dates de 145 px, plus la flèche entre elles. */
.field__row input[type="date"] { flex: 1 1 0; min-width: 145px; width: auto; }

/* Une habilitation retirée : lisible, mais visiblement éteinte. */
.is-off td { opacity: .55; }

/* Le périmètre suit LE PROFIL, et ne se restreint que vers le bas.
 *
 * ⚠ LE PÉRIMÈTRE N'EST PAS UN CHOIX LIBRE : le profil porte son type
 *   dans ez_role.fScopeTypeId — 2 pour une région, 4 pour un club, tout
 *   le reste pour la fédération. Un sélecteur de périmètre libre à côté
 *   du profil faisait doublon, et permettait surtout « responsable de
 *   club sur toute la fédération » : un administrateur qui ne dit pas
 *   son nom.
 *
 *   Ce qui reste possible, c'est de RESTREINDRE un profil fédéral à une
 *   région ou à un club. Le périmètre obtenu est alors toujours inclus
 *   dans celui du profil — une restriction ne peut pas être une
 *   élévation.
 *
 * ⚠ EN CSS PUR : la page est servie sous Content-Security-Policy
 *   « default-src 'self' », qui refuse tout script écrit dans la page —
 *   sans aucune erreur visible pour le signaler. « :has » lit le
 *   data-scope du profil sélectionné, puis la restriction choisie.
 *
 * ⚠ MASQUER NE DISPENSE PAS DE SÉPARER LES NOMS. Un champ en
 *   display:none est quand même ENVOYÉ : les deux listes portent donc
 *   « scopeid_region » et « scopeid_club », et le contrôleur lit celle
 *   qui correspond au périmètre retenu. Avec un nom commun, la dernière
 *   gagnerait.
 *
 * Deux sélecteurs par règle : la fiche utilise une liste déroulante pour
 * le profil, l'invitation des boutons radio.
 *
 * Un navigateur sans « :has » affiche tout : gênant, jamais bloquant —
 * le contrôleur ne lit que ce qui correspond au profil retenu. */
.scope--narrow, .scope--region, .scope--club { display: none; }

/* La restriction ne s'offre qu'aux profils fédéraux (1) ou sans
   périmètre propre (0) : un profil déjà régional n'a rien à restreindre. */
.fieldset:has(option[data-scope="1"]:checked) .scope--narrow,
.fieldset:has(option[data-scope="0"]:checked) .scope--narrow,
.fieldset:has(input[data-scope="1"]:checked)  .scope--narrow,
.fieldset:has(input[data-scope="0"]:checked)  .scope--narrow { display: block; }

/* La liste des régions : profil régional, ou profil fédéral restreint. */
.fieldset:has(option[data-scope="2"]:checked) .scope--region,
.fieldset:has(input[data-scope="2"]:checked)  .scope--region,
.fieldset:has(.scope--narrow option[value="2"]:checked):has(option[data-scope="1"]:checked) .scope--region,
.fieldset:has(.scope--narrow option[value="2"]:checked):has(option[data-scope="0"]:checked) .scope--region,
.fieldset:has(.scope--narrow option[value="2"]:checked):has(input[data-scope="1"]:checked)  .scope--region,
.fieldset:has(.scope--narrow option[value="2"]:checked):has(input[data-scope="0"]:checked)  .scope--region { display: block; }

/* La liste des clubs : profil de club, ou profil fédéral restreint. */
.fieldset:has(option[data-scope="4"]:checked) .scope--club,
.fieldset:has(input[data-scope="4"]:checked)  .scope--club,
.fieldset:has(.scope--narrow option[value="4"]:checked):has(option[data-scope="1"]:checked) .scope--club,
.fieldset:has(.scope--narrow option[value="4"]:checked):has(option[data-scope="0"]:checked) .scope--club,
.fieldset:has(.scope--narrow option[value="4"]:checked):has(input[data-scope="1"]:checked)  .scope--club,
.fieldset:has(.scope--narrow option[value="4"]:checked):has(input[data-scope="0"]:checked)  .scope--club { display: block; }

/* Le panneau des gestes qui ne se défont pas.
 *
 * ⚠ LA COULEUR N'EST PAS DÉCORATIVE : c'est le seul signal avant un geste
 *   irréversible. La confirmation est un second envoi, pas une fenêtre du
 *   navigateur — « confirm() » passerait par JavaScript, que la
 *   Content-Security-Policy du site refuse, et sans erreur visible.
 *
 * ⚠ Rouge FIXE, hors du thème de la fédération : une fédération qui
 *   choisirait un rouge comme couleur principale ferait sinon ressembler
 *   « Supprimer » à n'importe quel autre bouton.
 *
 * .btn--danger existe déjà plus haut — on ne le redéfinit pas. */
.panel--danger { border-color: #e3b8b8; }
.panel--danger .panel__title { color: #b42318; }

/* Le périmètre écrit dans le badge du profil : plus discret que le
   profil lui-même, mais lisible — c'est lui qui distingue deux personnes
   au même profil et aux pouvoirs très différents. */
.badge__scope { opacity: .75; font-weight: 400; }

/* Une habilitation retirée, dans la liste : présente mais éteinte. */
.badge--off {
  background: var(--ez-bg);
  color: var(--ez-muted);
  border: 1px solid var(--ez-border);
  text-decoration: line-through;
}

/* ------------------------------------------------------------------
   Les habilitations, une carte chacune
   ------------------------------------------------------------------ */

/* ⚠ UNE SEULE CARTE PAR LIGNE. Deux de front paraissaient tenir sur un
 *   grand écran, mais les libellés y étaient à l'étroit dès qu'un nom de
 *   club était long, et la lecture sautait de gauche à droite pour des
 *   blocs qui n'ont rien à voir entre eux. */
.cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--ez-gap);
  margin: 12px 0 16px;
}
.card { margin: 0; }

/* Les champs se répartissent DANS la carte, sur DEUX colonnes.
 *
 *   Ligne 1 :  Profil                 |  Mandat (les deux dates)
 *   Ligne 2 :  Restreindre le périmètre |  la région ou le club
 *
 * ⚠ PROFIL ET MANDAT SONT PLACÉS EXPLICITEMENT, le reste se place tout
 *   seul à la suite. C'est ce qui fait tenir la mise en page dans les
 *   trois cas : un profil régional n'a pas de sélecteur de restriction,
 *   un profil fédéral non restreint n'a ni région ni club, et un profil
 *   fédéral restreint a les deux. Les champs masqués ne réservant
 *   aucune place en grille, la deuxième ligne se recompose d'elle-même.
 *
 * ⚠ La colonne de droite est la plus large : elle porte soit les deux
 *   champs de date, soit un nom de club long. Celle de gauche n'a que
 *   des libellés courts.
 *
 * En dessous de 900 px, tout s'empile : deux colonnes sur un écran
 * étroit donneraient des listes illisibles. */
.card__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 var(--ez-gap);
  align-items: start;
}
@media (min-width: 900px) {
  .card__grid { grid-template-columns: minmax(230px, 1fr) minmax(330px, 1.1fr); }
  .card__grid .field--role { grid-column: 1; grid-row: 1; }
  .card__grid .field--term { grid-column: 2; grid-row: 1; }
}

/* La carte vide se distingue de celles qui existent déjà. */
.card--new { border-style: dashed; }

.card__head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--ez-border);
  font-size: 15px;
}

/* Supprimer reste un bouton discret DANS la carte : il n'efface qu'une
 * habilitation, pas un compte. Le rouge plein est réservé à ce qui
 * emporte tout — voir .btn--danger. */
.btn--danger-ghost { color: #b42318; border-color: #e3b8b8; }
.btn--danger-ghost:hover, .btn--danger-ghost:focus {
  background: #fdf3f2;
  border-color: #b42318;
}

/* La confirmation avant de supprimer une habilitation.
 *
 * ⚠ UN <details> NATIF, PAS UN confirm(). La page est servie sous
 *   Content-Security-Policy « default-src 'self' » : un script écrit
 *   dans la page n'est jamais exécuté, et rien ne le signale. <details>
 *   s'ouvre sans script, sans recharger la page — donc sans perdre ce
 *   qui est en cours de saisie dans les autres cartes.
 *
 * ⚠ Le marqueur du <summary> est retiré des DEUX façons : « list-style »
 *   pour les navigateurs récents, « ::-webkit-details-marker » pour les
 *   Safari et Chrome plus anciens. L'une sans l'autre laisse une flèche
 *   au milieu du bouton. */
.confirm { display: inline-block; position: relative; }
.confirm > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}
.confirm > summary::-webkit-details-marker { display: none; }
.confirm > summary::marker { content: ""; }

/* ⚠ JUSTE À DROITE DU BOUTON CLIQUÉ, et en surimpression.
 *
 *   À droite : la question se lit dans le prolongement du geste, sans
 *   quitter la ligne des yeux — et il n'y a que du vide à cet endroit
 *   de la carte.
 *
 *   En surimpression : ouvert dans le flux, le repli pousserait le bas
 *   de la carte et ferait sauter la page sous le curseur — le bouton
 *   « Oui » se retrouverait ailleurs qu'à l'endroit visé. */
.confirm__box {
  position: absolute;
  z-index: 5;
  left: calc(100% + 8px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  white-space: nowrap;
  background: var(--ez-surface);
  border: 1px solid #e3b8b8;
  border-radius: var(--ez-radius);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .12);
  font-size: 14px;
}

/* Le repli d'une carte doit passer AU-DESSUS de la carte suivante. */
.card:has(.confirm[open]) { position: relative; z-index: 6; }

/* ⚠ SUR TÉLÉPHONE, « à droite du bouton » N'EXISTE PAS.
 *
 *   « left: calc(100% + 8px) » avec « white-space: nowrap » donne un
 *   repli de 470 px posé à droite du bouton : sur un écran de 390 px il
 *   part hors champ, et la question — donc le bouton « Oui » — devient
 *   inatteignable. Vu sur Swiss Bowling / Clubs, où l'encart propose de
 *   reprendre les régions depuis les licenciés.
 *
 *   Sous 700 px le repli redescend DANS LE FLUX, sous le bouton, sur
 *   toute la largeur. On perd la surimpression — et donc la promesse que
 *   le bas de la carte ne bouge pas — mais un bouton qui saute de
 *   quelques pixels reste cliquable, un bouton hors de l'écran non. */
@media (max-width: 700px) {
  .confirm { display: block; }
  .confirm__box {
    position: static;
    transform: none;
    margin-top: 8px;
    white-space: normal;
    flex-wrap: wrap;
  }
}

/* ==================================================================== liste
   LE COMPOSANT DE LISTE — app/View/_list.php.

   Une seule feuille pour les six écrans qui affichent une liste. Avant,
   chacun avait sa pagination, sa recherche, son tableau : trois styles
   pour la même chose, et rien de triable nulle part.

   ⚠ TOUT EST EN LIENS, PAS EN SCRIPT. Le tri, la lettre, la page et la
     taille de page sont des <a> ; la recherche est un <form method=get>.
     La page se met en favori, revient avec « précédent », s'imprime, et
     ne dépend d'aucun JavaScript — que la Content-Security-Policy
     refuserait en ligne de toute façon.
   ==================================================================== */

/* ⚠ IL RÉUTILISE « .searchbar », IL NE LA REFAIT PAS. Une seconde barre
     de recherche avec ses propres règles, c'était deux barres à corriger
     — et la seconde était déjà fausse : un <select> de filtre y prenait
     « width: 100% » et poussait le bouton sur la ligne suivante. */
.ez-list__search { flex-wrap: wrap; }
.ez-list__search .field__select { width: auto; max-width: 100%; }
.ez-list__count  { color: var(--ez-muted); font-size: 14px; margin: 0 0 6px; }

/* --- la barre A–Z ------------------------------------------------- */
.ez-alpha {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin: 0 0 var(--ez-gap);
}
.ez-alpha__l {
  display: grid; place-items: center;
  min-width: var(--ez-h-m); height: var(--ez-h-m); padding: 0 8px;
  font-size: var(--ez-p-m);
  border: 1px solid transparent;
  border-radius: var(--ez-r);
  text-decoration: none;
  color: var(--ez-muted);
}
.ez-alpha__l:hover, .ez-alpha__l:focus { background: var(--ez-bg); color: var(--ez-text); }
.ez-alpha__l.is-on {
  background: var(--ez-primary);
  color: var(--ez-primary-text);
  border-color: var(--ez-primary);
}
/* ⚠ UNE LETTRE SANS RÉSULTAT RESTE VISIBLE MAIS N'EST PAS UN LIEN.
     La masquer ferait sauter l'alphabet d'un cran et on chercherait
     du regard une lettre qui a bougé. */
.ez-alpha__l.is-empty { color: #d5dae1; cursor: default; }
.ez-alpha__l.is-empty:hover { background: none; color: #d5dae1; }

/* --- l'en-tête triable --------------------------------------------- */
.ez-sort { color: inherit; text-decoration: none; white-space: nowrap; }
.ez-sort:hover, .ez-sort:focus { color: var(--ez-primary); text-decoration: underline; }
.ez-sort.is-on { color: var(--ez-text); }
.ez-sort__a { font-size: 10px; margin-left: 4px; vertical-align: 1px; }
.table td.is-right, .table th.is-right { text-align: right; }

/* --- le pied : pages, taille, export -------------------------------- */
.ez-list__foot {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  margin-top: var(--ez-gap);
}
.ez-list__tools { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }

.ez-pager { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.ez-pager__l {
  display: grid; place-items: center;
  min-width: var(--ez-h-m); height: var(--ez-h-m); padding: 0 9px;
  font-size: var(--ez-p-m);
  border: 1px solid transparent;
  border-radius: var(--ez-r);
  text-decoration: none;
  color: var(--ez-muted);
  background: transparent;
}
.ez-pager__l:hover, .ez-pager__l:focus { background: var(--ez-bg); color: var(--ez-text); }
.ez-pager__l.is-on {
  background: var(--ez-primary);
  color: var(--ez-primary-text);
  border-color: var(--ez-primary);
}
.ez-pager__gap { color: var(--ez-muted); padding: 0 2px; }

.ez-per { display: inline-flex; gap: 6px; align-items: center; font-size: 14px; }
.ez-per__n { text-decoration: none; color: var(--ez-primary); padding: 0 2px; }
.ez-per__n.is-on { color: var(--ez-text); font-weight: 600; text-decoration: underline; }

/* --- le téléphone : chaque ligne devient une carte ------------------
   ⚠ SOUS 700 px, UN TABLEAU DE SIX COLONNES NE TIENT PAS. Il défilait
     latéralement dans .table-wrap : on voyait le nom, jamais l'état, et
     il fallait pousser le tableau de côté pour chaque ligne.

   ⚠ L'ÉTIQUETTE VIENT DE data-label, POSÉ PAR LE GABARIT. Sans elle, la
     carte serait une pile de valeurs nues : « 146 » sans « Licences »
     ne veut rien dire. C'est pour ça que _list.php le pose sur CHAQUE
     cellule, même quand le tableau est visible.
   ------------------------------------------------------------------- */
@media (max-width: 700px) {
  .ez-list__table thead { position: absolute; left: -9999px; }
  .ez-list__table, .ez-list__table tbody, .ez-list__table tr, .ez-list__table td {
    display: block;
    width: 100%;
  }
  .ez-list__table tr {
    border: 1px solid var(--ez-border);
    border-radius: var(--ez-radius);
    margin-bottom: 10px;
    padding: 4px 0;
    background: var(--ez-surface);
  }
  .ez-list__table td {
    border-bottom: 0;
    padding: 5px 12px;
    text-align: left;
  }
  .ez-list__table td.is-right { text-align: left; }
  .ez-list__table td.is-title { font-weight: 600; font-size: 16px; padding-top: 8px; }
  .ez-list__table td.is-title::before { display: none; }
  .ez-list__table td::before {
    content: attr(data-label);
    display: block;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--ez-muted);
  }
  /* Une cellule vide n'occupe pas une ligne d'étiquette pour rien. */
  .ez-list__table td:empty { display: none; }
  .ez-list__foot { justify-content: flex-start; }
}

/* ⚠ UN <select> SE DIMENSIONNE SUR SON OPTION LA PLUS LONGUE, et dans un
     conteneur flex sa largeur minimale vaut « auto », donc cette largeur-
     là. La liste des cent un départements poussait donc la page à 690 px
     sur un téléphone de 390 : tout le site défilait latéralement, en-tête
     compris, et « width: 100% » n'y pouvait rien — c'est le MINIMUM qui
     débordait, pas la largeur demandée. D'où « min-width: 0 », qui rend
     le rétrécissement possible. */
.ez-list__table .field__row { flex-wrap: wrap; }
.ez-list__table .field__select,
.ez-list__table .field__input { min-width: 0; max-width: 100%; }

/* Sur un écran large, on borne ces listes déroulantes : sans borne, deux
   <select> de cent une lignes occupent toute la largeur et repoussent le
   bouton « Enregistrer » sur une seconde ligne, ce qui double la hauteur
   de chaque club. */
@media (min-width: 900px) {
  /* ⚠ 270 px, MESURÉ SUR LE PLUS LONG LIBELLÉ : « Bourgogne-Franche-Comté »
     était coupé à 240. Une borne existe pour que deux <select> de cent une
     lignes ne mangent pas l'écran — encore faut-il qu'elle laisse passer
     le libellé le plus long du référentiel. */
  .ez-list__table .field__select { max-width: 270px; }
}

/* ==================================================================== classique
   L'ANCIEN HABILLAGE, RENDU DISPONIBLE — pas ressuscité par nostalgie.

   Xavier : « la seule chose dont je me plaignais, c'était les listes,
   seulement les listes ; sur les menus et la barre en haut tout était
   parfait pour moi ». Le rail lui plaît aussi, d'où le choix laissé dans
   Configuration → Général, case « Menu latéral ».

   ⚠ SEUL L'HABILLAGE BASCULE. La liste refaite, l'échelle des contrôles
     et l'alignement vertical du texte restent dans LES DEUX modes :
     c'étaient ses griefs, pas des choix de présentation. Ressusciter les
     sept hauteurs de boutons et le texte décalé de 9 px sous prétexte de
     « rendre l'ancien aspect » serait rendre les défauts avec.

   ⚠ CE BLOC NE S'APPLIQUE QUE SOUS « body.ez-classique » quand le
     sélecteur désigne un élément qui existe AUSSI dans le rail. Les
     autres — .ez-topbar, .ez-menu, .ez-side — n'existent que dans cet
     habillage-ci : les scoper n'apporterait rien.
   ==================================================================== */
.ez-header { background: var(--ez-primary); color: var(--ez-primary-text); }

.ez-header { background: var(--ez-primary); color: var(--ez-primary-text); }

.ez-topbar {
  background: linear-gradient(rgba(0,0,0,.22), rgba(0,0,0,.22));
  font-size: 13px;
}
/* ⚠ GRILLE 1fr / auto / 1fr, ET PAS « space-between ».
 *   Avec space-between, le bloc central n'est centré que si les deux
 *   côtés ont la même largeur — ce qui n'arrive jamais. Le nom de la
 *   fédération se décalait donc dès que le logo apparaissait, ou qu'une
 *   langue de plus s'ajoutait au sélecteur. Avec deux colonnes
 *   souples de part et d'autre, le centre est le centre. */
.ez-topbar__inner {
  max-width: 1500px;
  margin: 0 auto;
  padding: 6px 20px;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--ez-gap);
}
/* Le logo puis les langues, tout à gauche de la page. */
.ez-topbar__left {
  display: flex;
  align-items: center;
  gap: 14px;
  justify-self: start;
  min-width: 0;
}
.ez-site    { justify-self: center; }
.ez-account { justify-self: end; }

.ez-site { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ez-site__name {
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Logo et lien groupés, poussés à droite ensemble. */
.ez-menu__right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 16px;
}

.ez-account { display: flex; align-items: center; gap: 12px; white-space: nowrap; }
/* ⚠ Même raison que « .ez-compte__nom » : c'est un lien depuis le 26/08. */
.ez-account__name { opacity: .85; color: inherit; text-decoration: none; }
.ez-account__name:hover, .ez-account__name:focus { opacity: 1; text-decoration: underline; }
.ez-account__out  { color: inherit; text-decoration: none; opacity: .85; }
.ez-account__out:hover, .ez-account__out:focus { opacity: 1; text-decoration: underline; }
.ez-account__off  { opacity: .55; font-style: italic; }

/* --- sélecteur de langue --- */
.ez-lang { display: flex; gap: 4px; list-style: none; margin: 0; padding: 0; }
.ez-lang a, .ez-lang__current {
  display: block;
  padding: 2px 6px;
  border-radius: var(--ez-radius);
  text-decoration: none;
  color: inherit;
  opacity: .7;
}
.ez-lang a:hover, .ez-lang a:focus { opacity: 1; background: rgba(255,255,255,.15); }
.ez-lang__current { opacity: 1; font-weight: 600; background: rgba(255,255,255,.22); }

/* --- barre de menu --- */
.ez-menu__inner {
  max-width: 1500px;
  margin: 0 auto;
  padding: 0 20px;
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}
.ez-menu__item {
  display: block;
  padding: 11px 14px;
  color: inherit;
  text-decoration: none;
  border-bottom: 3px solid transparent;
  opacity: .85;
}
.ez-menu__item:hover, .ez-menu__item:focus { opacity: 1; background: rgba(255,255,255,.10); }
.ez-menu__item.is-active {
  opacity: 1;
  font-weight: 600;
  border-bottom-color: var(--ez-accent);
}




/* ⚠ PORTÉES PAR LE CORPS : « .ez-main » et « .ez-footer » existent dans les
   deux habillages, et les règles du rail sont plus haut dans la feuille. */
.ez-classique .ez-main { max-width: 1500px; margin: 0 auto; padding: 24px 20px 48px; }
.ez-classique .ez-footer {
  max-width: 1500px; margin: 0 auto;
  padding: 0 20px 28px;
  color: var(--ez-muted); font-size: 13px;
}
.ez-classique .ez-lang {
  display: flex; gap: 4px; list-style: none; margin: 0; padding: 0;
}
.ez-classique .ez-lang a, .ez-classique .ez-lang__current {
  min-width: auto; height: auto; padding: 3px 7px; border-radius: 4px;
  font-size: 13px; font-weight: 600; display: inline-block;
}
.ez-classique .ez-menu__ext { margin-left: 3px; }

/* --- le menu de configuration, de retour DANS la page ---------------
   ⚠ CE N'EST PLUS LE GABARIT DE L'ÉCRAN QUI LE POSE. Il était appelé
     par quinze gabarits ; il est maintenant rendu par layout.php, qui
     l'insère ici en habillage classique et dans le rail sinon. Les
     quinze gabarits n'ont plus à s'en occuper — et n'ont plus à
     connaître leur propre « slug ». */
.ez-page { display: flex; gap: 24px; align-items: flex-start; }
.ez-page__body { flex: 1; min-width: 0; }

.ez-side { width: 210px; flex: 0 0 210px; }
.ez-side__block { margin-bottom: 20px; }
.ez-side__title {
  font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ez-g, var(--ez-muted)); margin: 0 0 6px; padding: 0 10px; font-weight: 600;
}
.ez-side__item {
  display: block; padding: 7px 10px 7px 22px; border-radius: var(--ez-radius);
  text-decoration: none; color: var(--ez-text); font-size: 14px;
}
.ez-side__item:hover, .ez-side__item:focus { background: rgba(0, 0, 0, .05); }
.ez-side__item.is-active {
  background: var(--ez-primary); color: var(--ez-primary-text); font-weight: 600;
  /* ⚠ LE LISERÉ EST DANS LA TEINTE CLAIRE, PAS LA SOMBRE : il se pose SUR
   *   la pastille, qui porte la couleur de la fédération. La teinte sombre
   *   y disparaîtrait. */
  box-shadow: inset 3px 0 0 var(--ez-g-clair, transparent);
}
.ez-side__item.is-todo { color: var(--ez-muted); opacity: .6; cursor: default; }
.ez-side__item.is-todo:hover { background: none; }

/* ==================================================== groupes du menu
   UN FILET ET UNE TEINTE PAR GROUPE.

   ⚠ POURQUOI UNE COULEUR ICI, ALORS QUE LA RÈGLE DU SITE DIT « LA
     COULEUR MARQUE L'EXCEPTION »

     C'est cette règle qui a vidé les listes de leurs pastilles : sur
     465 clubs, une couleur qui revient à chaque ligne ne signale plus
     rien. Un menu n'est pas une liste. Deux ou trois groupes, toujours
     les mêmes, toujours au même endroit : la couleur n'y crie pas une
     alerte, elle sert de REPÈRE — on retrouve « Les référentiels » à sa
     teinte avant d'avoir lu le titre.

     ⚠ LA FRONTIÈRE EST À TENIR : le menu doit rester le SEUL endroit du
       site où la couleur soit décorative. Partout ailleurs, elle marque
       toujours l'exception.

   ⚠ LES TEINTES SONT MESURÉES SUR LE PIXEL RENDU, PAS CALCULÉES.

     Première tentative : trois teintes moyennes, contrastes calculés
     contre un rouge « suisse » choisi de mémoire. Elles annonçaient 4,9
     à 5,9 partout. Photographiées sur le vrai site, elles tombaient à
     3,0 sur le rail rouge.

     ⚠ LA CAUSE : le rail n'a pas de couleur de fond, il a un DÉGRADÉ —
       et il s'ÉCLAIRCIT vers le bas, jusqu'à rgb(207,45,72) en suisse.
       « getComputedStyle().backgroundColor » rend « transparent » sur un
       dégradé : la sonde remontait alors jusqu'au blanc de la page et
       mesurait un contraste qui n'existait nulle part. Il a fallu
       photographier la page et lire le pixel.

   ⚠ TROIS VALEURS PAR TEINTE, ET IL EN FAUT TROIS :

       --ez-g         sombre     titre et filet du menu classique, sur gris clair
       --ez-g-clair   moyenne    filet du rail, liseré de la pastille
       --ez-g-rail    quasi blanche  titre du rail, sur le dégradé

     Le titre du rail doit être quasi blanc parce que le point le plus
     clair du dégradé rouge ne laisse aucune marge : même une teinte
     pâle y plafonne à 4,2. Contrastes relevés sur le rendu :

                     menu clair   rail bleu   rail rouge
       titre         4,5 à 5,1     7,3+        4,6 à 4,7
       filet         idem          4,3         2,7   (décoratif, jamais seul)

     ⚠ POUR COMPARAISON, CE QUI EST EN LIGNE AUJOURD'HUI : le titre du
       rail est blanc à 58 % d'opacité, soit 3,89 sur le bleu et 2,60 sur
       le rouge. La teinte quasi blanche n'est donc pas un compromis :
       elle DOUBLE presque le contraste sur le rouge.

     Le jour où un espace aura quatre groupes, il faudra MESURER une
     quatrième teinte sur le rendu — pas en choisir une.

   ⚠ LA TEINTE VIENT DE LA CLÉ DU BLOC, JAMAIS DE SON RANG. Le rang
     change dès qu'une permission masque un bloc : « Les référentiels »
     devient le premier pour qui n'a pas FEDERATION_CONFIG, et changerait
     de couleur d'un utilisateur à l'autre.
   ==================================================================== */
.ez-groupe {
  position: relative;
  /* Repli neutre : une clé sans teinte déclarée garde un filet gris
     plutôt que de perdre son filet. bin/selftest.php vérifie qu'aucune
     clé de Sections n'en est là. */
  --ez-g: #5b6577;
  --ez-g-clair: rgba(255, 255, 255, .55);
  --ez-g-rail: #f2f4f7;
}
.ez-groupe--sport,
.ez-groupe--federation,
.ez-groupe--installation { --ez-g: #4a6b8a; --ez-g-clair: #9dc0de; --ez-g-rail: #eef4fa; }

.ez-groupe--licences,
.ez-groupe--referentiels,
.ez-groupe--trace        { --ez-g: #4e7a5c; --ez-g-clair: #96c9a6; --ez-g-rail: #eff8f2; }

.ez-groupe--comptes,
.ez-groupe--droits       { --ez-g: #8a6535; --ez-g-clair: #dbb887; --ez-g-rail: #fcf5ea; }

/* Le filet — il part SOUS le titre et descend jusqu'au dernier lien. */
.ez-side__block.ez-groupe::before {
  content: ""; position: absolute; left: 11px; top: 26px; bottom: 2px;
  width: 2px; border-radius: 2px; background: var(--ez-g);
}

@media (max-width: 900px) {
  /* ⚠ « align-items: stretch » N'EST PAS DÉCORATIF, IL EMPÊCHE UN
   *   DÉBORDEMENT. En colonne, l'axe transversal devient l'HORIZONTALE :
   *   le « align-items: flex-start » de la règle de base cesse d'aligner
   *   en haut et se met à dimensionner les enfants sur leur contenu.
   *   .ez-page__body prenait alors 402 px dans une page de 350 — et
   *   « min-width: 0 » n'y peut rien, il ne borne que l'axe principal.
   *   Vu sur Swiss Bowling / Clubs : un encart d'alerte à 352 px de
   *   largeur minimale poussait toute la page hors de l'écran à 390 px.
   *   Le symptôme est trompeur — on accuse le tableau, jamais l'encart. */
  .ez-page { flex-direction: column; gap: 16px; align-items: stretch; }
  .ez-side { width: 100%; flex: none; }
  /* ⚠ LE DÉCALAGE ET LE FILET N'ONT PLUS DE SENS ICI. Le bloc se replie en
   *   lignes côte à côte et le titre passe sur toute la largeur : le filet
   *   vertical traverserait des rubriques posées à l'horizontale, et le
   *   décalage de 12 px décalerait la première d'une ligne sans raison. */
  .ez-side__block { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
  .ez-side__title { width: 100%; padding: 0; }
  .ez-side__block.ez-groupe::before { display: none; }
  .ez-side__item { padding-left: 10px; }
}
@media (max-width: 640px) {
  .ez-topbar__inner { flex-wrap: wrap; justify-content: flex-start; gap: 8px 14px; }
  .ez-site { order: -1; width: 100%; }
}

/* ⚠ 800 px, ET PLUS 640. LA BARRE A GAGNÉ UNE ENTRÉE.
 *
 *   « Accueil · Configuration · État » tenait partout. « Accueil ·
 *   Gestion · Configuration · État » occupe 411 px, et la barre porte
 *   AUSSI le logo et « Site officiel », calés à droite. Mesuré au
 *   navigateur : entre 650 et 690 px de large, les deux blocs se
 *   RECOUVRENT — le menu passe sous « Site officiel ».
 *
 *   Le seuil de défilement horizontal remonte donc à 800 px, ce qui
 *   couvre la bande avec de la marge. En dessous, la barre défile au
 *   doigt au lieu de se chevaucher.
 *
 *   ⚠ Une entrée de plus un jour — « Sécurité », « Compétitions » —
 *     déplacera cette bande vers le haut. Le contrôle du selftest compte
 *     les entrées et rappelle de remesurer. */
@media (max-width: 800px) {
  .ez-menu__inner { flex-wrap: nowrap; overflow-x: auto; }
  .ez-menu__item { white-space: nowrap; }
}

/* ==================================================================== volet
   LA LISTE SE LIT, ON MODIFIE À CÔTÉ.

   ⚠ CE QUI FAISAIT « SAISIE EN MASSE » PLUTÔT QUE « LISTE », c'est que
     chaque ligne portait deux listes déroulantes et un bouton : vingt-
     cinq lignes, cinquante contrôles de formulaire dans la grille.
     Aucune peinture ne rattrape ça — il faut les sortir de là.

   ⚠ MAIS ON NE LES SUPPRIME PAS. Rattacher trente clubs à la suite avec
     un volet, c'est trente ouvertures. L'interrupteur « édition rapide »
     les rend, à la demande.
   ==================================================================== */

/* La rangée qui porte la barre A–Z et l'interrupteur de mode. */
.ez-list__rang {
  display: flex; flex-wrap: wrap; gap: 12px;
  align-items: center; justify-content: space-between;
  margin: 0 0 var(--ez-gap);
}
.ez-list__rang .ez-alpha { margin: 0; }

/* Les deux valeurs d'une ligne, en lecture. */
.ez-val { display: inline-block; margin-right: 10px; }
.ez-val--2 { min-width: 120px; }

/* ⚠ « Modifier » NE SE MONTRE QU'AU SURVOL — ou au clavier. Vingt-cinq
   boutons alignés en permanence, c'est le bruit qu'on vient de retirer ;
   mais « :focus-within » le fait réapparaître dès qu'on tabule dessus,
   sinon il serait inatteignable sans souris. */
/* ⚠ LE BOUTON NE SE CACHE QUE SI LA LIGNE S'OUVRE. Sur l'écran des
   catégories, TOUTES les lignes viennent du référentiel maître : aucune
   ne se modifie, donc aucune n'est cliquable — et le bouton « Désactiver »
   y est la seule action possible. Le masquer au repos rendrait l'écran
   entièrement inerte au premier regard. */
.ez-list__table tbody tr.is-clic .ez-survol { opacity: 0; transition: opacity .12s ease; }
.ez-list__table tbody tr.is-clic:hover .ez-survol,
.ez-list__table tbody tr.is-clic:focus-within .ez-survol { opacity: 1; }
@media (hover: none) {
  /* Sur un écran tactile il n'y a pas de survol : le bouton reste là. */
  .ez-list__table tbody tr.is-clic .ez-survol { opacity: 1; }
}

/* En édition rapide, la rangée se serre : taille M, pas L. */
.field__row--serree { gap: 6px; align-items: center; }
.field__row--serree .field__select { height: var(--ez-h-m); font-size: var(--ez-p-m); }

/* --- l'interrupteur -------------------------------------------------- */
.ez-bascule-form { display: flex; align-items: center; gap: 10px; }
.ez-bascule {
  display: inline-flex; align-items: center; gap: 9px; cursor: pointer;
  font-size: var(--ez-p-m); color: var(--ez-muted); user-select: none;
}
.ez-bascule input { position: absolute; opacity: 0; pointer-events: none; }
.ez-bascule__piste {
  width: 38px; height: 22px; flex: 0 0 38px; border-radius: 999px;
  background: #d0d5dd; position: relative; transition: background .15s;
}
.ez-bascule__piste::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 16px; height: 16px; border-radius: 50%; background: #fff;
  transition: transform .15s; box-shadow: 0 1px 2px rgba(16, 24, 40, .2);
}
.ez-bascule input:checked + .ez-bascule__piste { background: var(--ez-primary); }
.ez-bascule input:checked + .ez-bascule__piste::after { transform: translateX(16px); }
.ez-bascule input:focus-visible + .ez-bascule__piste {
  outline: 2px solid var(--ez-accent); outline-offset: 2px;
}

/* --- il y avait un volet ici, et il n'y en a plus -------------------
   ⚠ LA FICHE D'UN CLUB EST UNE PAGE, comme sur tous les autres écrans.
     Elle a été, un temps, une boîte posée par-dessus la liste. Clubs
     était alors le seul écran du site à superposer quelque chose, et le
     seul à s'être cassé quatre fois de suite :

       - un calque « inset: 0 » sans ancêtre positionné, qui s'est calé
         sur la FENÊTRE et a bloqué tous les clics de la page ;
       - un <select> masqué par « position: absolute; opacity: 0 » mais
         jamais borné : 386 px de large, les champs coupés, une barre de
         défilement horizontale dans la boîte ;
       - un voile resté en travers APRÈS la fermeture, parce que les
         règles qui devaient le masquer visaient des VOISINS QUI SUIVENT
         alors que le voile était écrit avant.

     Les trois sont la même leçon, et elle est écrite ici pour qu'on ne
     recommence pas : un élément sorti du flux garde une taille et une
     place si on ne lui en donne pas, et le rendre invisible ne le rend
     pas inoffensif. Une page qui remplace la page n'a pas ce problème —
     il n'y a plus de « derrière ».

     Si un volet devait revenir un jour, il faudrait : un voile qui soit
     un vrai lien, une fermeture marquée sur <body> (jamais entre
     voisins), et les champs bornés à la largeur de la boîte. */

/* --- la liste déroulante cherchable ----------------------------------
   ⚠ LE <select> D'ORIGINE RESTE DANS LE DOCUMENT : c'est lui qui porte le
     nom du champ et part avec le formulaire. On le masque, on pose un
     champ texte devant. Si le script échoue, c'est le <select> qui
     s'affiche — pas un formulaire amputé de sa valeur. */
.ez-cherche { position: relative; }

/* ⚠ LE <select> D'ORIGINE EST MASQUÉ, MAIS IL PREND ENCORE DE LA PLACE
 *   S'IL N'EST PAS BORNÉ — et c'est la TROISIÈME fois que la même faute
 *   se paie sous un autre visage.
 *
 *   « position: absolute » SANS « left »/« top » laisse l'élément à sa
 *   position statique, et « opacity: 0 » ne le retire pas du calcul du
 *   débordement. Le <select> des cent un départements fait 386 px de
 *   large (il se dimensionne sur son option la plus longue) : le corps
 *   du volet passait de 430 à 794 px de contenu, une barre de défilement
 *   horizontale apparaissait, et les deux champs se retrouvaient coupés
 *   sur la droite.
 *
 *   Il faut donc le BORNER : ancré en haut à gauche, 1 px sur 1 px,
 *   rogné. Il reste dans le document — il porte le nom du champ et part
 *   avec le formulaire, c'est tout l'intérêt de doubler le <select>
 *   plutôt que de le remplacer — mais il ne mesure plus rien. */
.ez-cherche__vrai {
  position: absolute; left: 0; top: 0;
  width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
  overflow: hidden; clip-path: inset(50%);
}
.ez-cherche__menu {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 5;
  background: var(--ez-surface); border: 1px solid var(--ez-border);
  border-radius: var(--ez-r); box-shadow: 0 8px 24px rgba(16, 24, 40, .12);
  max-height: 240px; overflow-y: auto; display: none;
}
.ez-cherche.is-open .ez-cherche__menu { display: block; }
.ez-cherche__opt { padding: 9px 12px; font-size: var(--ez-p-m); cursor: pointer; }
.ez-cherche__opt:hover { background: var(--ez-bg); }
.ez-cherche__vide { padding: 12px; font-size: var(--ez-p-m); color: var(--ez-muted); }

/* ==================================================================== ligne
   LA LIGNE ENTIÈRE S'OUVRE — et le reste s'efface.

   Xavier, capture des Régions à l'appui : « tu as les textes, pastilles,
   boutons, liens, c'est lourd ». Sur chaque ligne il y avait UN LIEN
   bleu souligné, UNE PASTILLE verte « actif » et UN BOUTON
   « Désactiver » — trois façons d'attirer l'œil en même temps, sur cent
   vingt et une lignes.

   ⚠ QUATRE RÈGLES, ET ELLES VALENT PARTOUT :
     1. pas de lien coloré sur le nom — c'est la LIGNE qui s'ouvre ;
     2. pas de bouton par ligne — l'action passe dans la fiche ;
     3. la couleur marque l'EXCEPTION : rien pour « actif », une marque
        discrète pour « désactivé » ;
     4. on fusionne ce qui est une seule idée — « Niveau » et
        « Rattachement » disent tous deux OÙ la ligne se situe.
   ==================================================================== */

/* ⚠ « relative » SUR LE <tr> : c'est ce qui permet au lien du nom de
   s'étirer sur toute la ligne. Si un navigateur ne l'honore pas, le lien
   couvre sa seule cellule — on clique le nom, comme avant. */
.ez-list__table tbody tr.is-clic { position: relative; cursor: pointer; }

/* ⚠ LE LIEN NE RESSEMBLE PLUS À UN LIEN. Il garde tout ce qui fait un
   lien — clavier, nouvel onglet, lecteur d'écran — et perd le bleu
   souligné qui faisait crier chaque ligne. */
.ez-titre {
  color: inherit; text-decoration: none; font-weight: 620;
}

/* ⚠ LE CALQUE NE S'ÉTIRE QUE SUR UNE LIGNE CLIQUABLE, ET SEULEMENT DEPUIS
 *   UN VRAI <a>. La règle était écrite « .ez-titre::after », sans portée,
 *   et le défaut a été SÉVÈRE :
 *
 *   Sur l'écran des catégories, les onze lignes viennent du référentiel
 *   maître : aucune n'est cliquable, donc aucune ne porte
 *   « position: relative », et le nom est rendu en <span>, pas en <a>.
 *   « inset: 0 » n'avait alors AUCUN ancêtre positionné : il se calait sur
 *   le bloc conteneur initial, c'est-à-dire LA FENÊTRE. Onze calques
 *   invisibles couvraient la page entière — plus un seul clic ne passait
 *   nulle part, ni sur le menu, ni sur les boutons, ni sur les liens.
 *
 *   Deux conditions, donc, et il faut les deux :
 *     tr.is-clic   la ligne est bien positionnée, le calque a une borne ;
 *     a.ez-titre   un <span> n'ouvre rien, il n'a rien à recouvrir.
 */
.ez-list__table tbody tr.is-clic a.ez-titre::after {
  content: ""; position: absolute; inset: 0;
}
.is-clic:hover .ez-titre { text-decoration: underline; text-underline-offset: 2px; }
.ez-titre:focus-visible { outline: 2px solid var(--ez-accent); outline-offset: 2px; }

/* Ce qui reste cliquable DANS la ligne doit passer au-dessus du lien
   étiré, sinon on ouvrirait la fiche en visant une liste déroulante.

   ⚠ CETTE LISTE EST À TENIR À JOUR, ET ON L'OUBLIE. Le compte du
     découpage — « .ez-nb », un lien nu dans une cellule — a été ajouté
     sans y penser : le calque le recouvrait entièrement, et cliquer
     dessus ouvrait la fiche de la région au lieu d'aller à son
     découpage. Rien ne le signalait à l'écran ; c'est le navigateur
     piloté qui l'a dit, en refusant le clic pendant trente secondes.

     La règle : tout élément cliquable posé dans une ligne cliquable doit
     figurer ici.

     ⚠ « .ez-nb » A DISPARU AVEC LE BOUTON. Le compte du découpage n'est
       plus un lien : la ligne entière mène à la fiche de la région, et
       c'est là qu'est la liste. Un lien de plus vers la même page ne
       servait qu'à se faire recouvrir. */
/* ⚠⚠ « .ez-dessus » ET « .ez-actions a » S'AJOUTENT À CETTE LISTE, et
     c'est la même raison qui les y met : le lien du nom s'étire en
     ABSOLU sur toute la rangée pour la rendre cliquable, et il recouvre
     tout ce qui ne se relève pas. Sans eux, cocher une case ou cliquer
     « Licence » ouvrirait la fiche — un clic qui fait autre chose que
     ce qu'on visait, et personne ne comprend pourquoi. */
.ez-list__table tbody tr.is-clic .field__row,
.ez-list__table tbody tr.is-clic form,
.ez-list__table tbody tr.is-clic .btn,
.ez-list__table tbody tr.is-clic .ez-dessus,
.ez-list__table tbody tr.is-clic .ez-actions a,
.ez-list__table tbody tr.is-clic select { position: relative; z-index: 1; }

/* Les deux liens d'action ne se coupent pas en deux. */
.ez-actions { white-space: nowrap; }

/* Le confort de sélection — « Tout · Rien · Inverser » et le compteur.
 *
 * ⚠ IL EST DANS la barre d'outils, PAS À CÔTÉ. Une seconde « .toolbar »
 *   imbriquée apporterait sa marge basse et décalerait les boutons
 *   d'action d'une demi-ligne ; ce span-ci ne fait que reprendre la
 *   disposition, sans marge.
 *
 * ⓘ Il naît caché : `app.js` le montre s'il a trouvé le formulaire. Les
 *   boutons d'action, eux, sont dehors et restent visibles sans script.
 */
.ez-selbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin: 0; }

/* Un champ éteint DOIT SE VOIR ÉTEINT.
 *
 * ⚠ Sans cette règle il garde le fond blanc et le texte noir d'un champ
 *   ordinaire : mesuré sur « Numéro » de l'écran Licence, rien à l'œil
 *   ne distinguait un numéro de licence — qui ne se change pas ici — du
 *   nom du club juste à côté. On clique, il ne se passe rien, et on
 *   croit l'écran cassé.
 *
 * ⓘ Le fond suffit : ni barré, ni pâli au point d'être illisible. Un
 *   champ non modifiable reste une information à LIRE.
 */
.field__input:disabled,
.field__select:disabled,
.field__input[readonly] {
  background: var(--ez-bg);
  color: var(--ez-muted);
  cursor: default;
}
.ez-actions .muted { padding: 0 4px; }

.ez-chev-col { width: 34px; text-align: right; padding-left: 0 !important; }
.ez-chev { color: var(--ez-muted); font-size: 20px; line-height: 1;
           opacity: 0; transition: opacity .12s; }
.is-clic:hover .ez-chev, .is-clic:focus-within .ez-chev { opacity: 1; }
/* Sur un écran tactile il n'y a pas de survol : le chevron reste là. */
@media (hover: none) { .ez-chev { opacity: 1; } }

/* ⚠ LA SOUS-LIGNE PORTE CE QU'ON A FUSIONNÉ. « Département de
   Auvergne-Rhône-Alpes » remplace deux colonnes ; en gris et en plus
   petit, elle se lit sans peser. */
.ez-sous { display: block; font-size: 12.5px; color: var(--ez-muted); font-weight: 400; }
.ez-duo { display: block; }
.ez-duo + .ez-sous { margin-top: 1px; }

/* ⚠ RIEN POUR « ACTIF ». Une marque sur chaque ligne ne marque rien. */
.ez-off {
  display: inline-flex; align-items: center; height: var(--ez-h-s); padding: 0 9px;
  border-radius: 999px; font-size: var(--ez-p-s); font-weight: 600;
  background: var(--ez-bg); color: var(--ez-muted);
}
/* Sept pixels, une seule couleur, et seulement là où il y a un problème. */
.ez-pt { display: inline-block; width: 7px; height: 7px; border-radius: 50%;
         background: #f79009; margin-right: 8px; vertical-align: 1px; }
.ez-alerte { color: #b54708; }

/* ⚠ UN LIBELLÉ DE BOUTON PEUT ÊTRE PLUS LARGE QU'UN TÉLÉPHONE.
   « Nouvelle valeur dans « Mouvements d'un licencié » » fait 357 px, et
   « .btn » porte « white-space: nowrap » — le bouton ne pouvait donc pas
   rétrécir, et c'est LUI qui poussait toute la page au-delà des 390 px.
   Sous 700 px on l'autorise à passer à la ligne ; la hauteur d'échelle
   devient un minimum au lieu d'une valeur fixe. */
@media (max-width: 700px) {
  .toolbar .btn {
    white-space: normal;
    height: auto;
    min-height: var(--ez-h-m);
    padding: 6px 12px;
    text-align: center;
  }
  .toolbar .btn--large { min-height: var(--ez-h-l); }
}
