/*
  Bibliothèque de composants.
  ============================================================================

  Le vocabulaire que les écrans suivants assemblent : champ, libellé, boutons,
  carte, badge d'état, bandeau, tableau repliable, résumé d'erreurs. Aucun
  écran métier ici — le formulaire d'inscription est le récit 1.6, l'espace
  organisateur le 1.5 et le 2.1.

  Trois règles s'appliquent à tout ce qui est ajouté ici :

  1. Aucune couleur, aucune taille, aucun rayon en clair : tout vient de
     tokens.css. Un test refuse une couleur écrite en dur dans ce fichier.
  2. Tout état se lit. Un badge porte toujours son mot, un bandeau toujours sa
     phrase : la couleur double l'information, elle ne la porte jamais seule.
  3. Toute cible tactile mesure au moins --touch-target-min, et le focus reste
     visible. Ne jamais poser `outline: none`.

  Rendu serveur, sans JavaScript : ces composants sont du HTML et du CSS.
  Les points de bascule apparaissent en clair, une media query n'acceptant pas
  de variable — 640px et 1024px, les mêmes que --breakpoint-md et
  --breakpoint-lg.
*/

/* ==========================================================================
   Champ de saisie et son libellé
   ==========================================================================

   Balisage attendu :

     <div class="field">
       <label class="field__label" for="prenom">Prénom
         <span class="field__required" aria-hidden="true">*</span>
       </label>
       <input class="field__control" id="prenom" name="prenom" type="text">
     </div>

   Le `for` du libellé et le `id` du champ sont obligatoires : sans eux, le
   libellé n'est pas annoncé et le toucher du libellé ne donne pas le focus.

   **L'obligation se porte dans le balisage**, par l'attribut `required` sur le
   champ. L'astérisque est un doublon visuel, marqué `aria-hidden` : seul
   `required` la fait annoncer par une technologie d'assistance. Un champ
   obligatoire sans `required` est un défaut, pas un choix.

   **Jetons `autocomplete` attendus** sur les champs d'identité et de
   coordonnées, faute de quoi le parent ressaisit à la main ce que son
   téléphone connaît :

     prénom du joueur           autocomplete="given-name"
     nom du joueur              autocomplete="family-name"
     nom du responsable         autocomplete="section-responsable family-name"
     courriel du responsable    autocomplete="email"
     téléphone du responsable   autocomplete="tel"

   Les jetons `section-*` séparent le joueur de son responsable, sans quoi le
   navigateur propose le même nom pour les deux. `autocomplete="off"` est
   réservé aux champs qu'aucun jeton ne décrit — un numéro de licence, une
   année de naissance.
*/

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-half);
  min-width: 0;
}

.field__label {
  font-size: var(--text-label-field-size);
  font-weight: var(--text-label-field-weight);
  line-height: var(--text-label-field-leading);
  color: var(--color-on-surface);
}

/*
  Astérisque de l'obligatoire. Volontairement PAS rouge : le rouge ne dit que
  l'erreur. Un champ obligatoire non rempli n'est pas encore une erreur.
*/
.field__required {
  color: var(--color-on-surface-variant);
}

/* Mention « Facultatif » des champs qui ne le sont pas. */
.field__optional {
  font-size: var(--text-caption-size);
  font-weight: var(--text-caption-weight);
  line-height: var(--text-caption-leading);
  color: var(--color-on-surface-variant);
}

.field__control {
  width: 100%;
  min-width: 0;
  height: var(--field-height);
  padding-inline: var(--space-2);
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-default);
  background-color: var(--color-surface);
  color: var(--color-on-surface);
  /* 16px exactement : en deçà, Safari sur iPhone zoome sur le champ au focus
     et l'utilisateur se retrouve avec une page décalée. */
  font-size: var(--text-body-md-size);
  line-height: var(--text-body-md-leading);
  /* La bordure change au focus et à l'erreur : la faire glisser plutôt que
     sauter rend le changement perceptible sans le rendre bruyant. */
  transition: border-color var(--motion-quick) var(--motion-ease);
}

textarea.field__control {
  height: auto;
  min-height: calc(2 * var(--field-height));
  padding-block: var(--space-1);
  resize: vertical;
}

select.field__control {
  /* Une liste déroulante native garde sa flèche système ; rien à dessiner. */
  cursor: pointer;
}

.field__control::placeholder {
  color: var(--color-on-surface-variant);
}

/*
  Champ verrouillé — readonly, jamais disabled (disabled empêcherait sa
  soumission) — récit 3.4 : identité fédérale figée. Fond légèrement grisé,
  pour qu'un parent distingue au coup d'œil un champ qu'il ne peut plus
  modifier d'un champ ordinaire, sans jamais recourir à une couleur en dur.
*/
.field__control[readonly] {
  background-color: var(--color-surface-container);
  color: var(--color-on-surface-variant);
}

/*
  Focus : bordure primaire et anneau de 2 px.

  `.field--focus` fige cette apparence sans que le champ ait réellement le
  focus. Elle n'existe que pour la galerie de composants, qui doit montrer
  l'état sur une page statique. Ne pas l'employer dans un écran.
*/
.field__control:focus-visible,
.field--focus .field__control {
  border-color: var(--color-primary);
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: 1px;
}

.field--error .field__control {
  border-color: var(--color-error);
}

/*
  Sur un champ invalide, le focus reprend la main. Sans cette règle, celle de
  l'erreur, de même spécificité et écrite après, écrase la bordure de focus et
  la rend inatteignable — le seul champ où l'on est sûr que l'utilisateur
  reviendra perdrait son repère visuel.
*/
.field--error .field__control:focus-visible,
.field--error.field--focus .field__control {
  border-color: var(--color-primary);
}

/*
  Message d'erreur du champ. C'est du texte : la bordure rouge seule ne dit
  rien à qui ne distingue pas les couleurs. Le relier au champ par
  `aria-describedby`, et poser `aria-invalid="true"` sur le champ.
*/
.field__error {
  display: flex;
  gap: var(--space-half);
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-leading);
  font-weight: var(--text-label-field-weight);
  color: var(--color-error);
}

/* Depuis le 13 septembre 2026 (AD-22), ce même nœud sert aussi de région
   `aria-live` à la validation en direct : vide au repos (aucune erreur), il
   ne doit alors occuper aucun espace — sans changer le rendu d'un champ
   valide. */
.field__error:empty {
  display: none;
}

.field__hint {
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-leading);
  color: var(--color-on-surface-variant);
}

/* Même geste que .field__error:empty ci-dessus, pour l'annonce du filtrage
   des résultats fédéraux (AD-22) qui réutilise .field__hint. */
.field__hint:empty {
  display: none;
}

/* ==========================================================================
   Case à cocher et son libellé
   ==========================================================================
   Le motif de .field ne convient pas à une case à cocher : sa mise en page
   en colonne empile libellé puis contrôle, pensée pour un champ de texte.
   Ici, la case se pose à côté de son texte plutôt qu'au-dessus.

   Balisage attendu :

     <div class="check-field">
       <input type="checkbox" id="consent" name="consent">
       <label for="consent">Texte du consentement…</label>
     </div>

     <div class="check-field check-field--error">
       <input type="checkbox" id="consent" name="consent" aria-invalid="true"
              aria-describedby="consent-erreur">
       <label for="consent">Texte du consentement…</label>
     </div>
     <p class="field__error" id="consent-erreur">Message d'erreur</p>

   Comme tout composant de cette bibliothèque, la cible tactile mesure au
   moins --touch-target-min — posée sur la case elle-même, pas seulement sur
   son conteneur : min-height sur .check-field ne rendrait cliquable qu'une
   bande verticale, la case native restant à sa taille par défaut, bien en
   deçà des 44 px. Le focus de la case reste celui, natif, posé une fois pour
   toutes sur :focus-visible dans base.css — rien à y ajouter pour l'état au
   repos ; l'état en erreur doit seulement lui laisser la main au focus,
   même raisonnement que .field--error .field__control:focus-visible.
*/

.check-field {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  min-height: var(--touch-target-min);
  padding-block: var(--space-1);
}

.check-field input[type='checkbox'] {
  width: var(--touch-target-min);
  height: var(--touch-target-min);
  margin: 0;
  flex: none;
}

.check-field--error input[type='checkbox'] {
  outline: var(--focus-ring-width) solid var(--color-error);
  outline-offset: 1px;
}

/* Sur la case en erreur, le focus reprend la main — sans cette règle,
   celle de l'erreur écraserait l'anneau de focus natif et rendrait le seul
   champ où l'utilisateur revient sûrement dépourvu de son repère visuel. */
.check-field--error input[type='checkbox']:focus-visible {
  outline-color: var(--color-primary);
}

/* ==========================================================================
   Boutons
   ==========================================================================
   Pleine largeur sur téléphone — le pouce vise mal —, largeur du contenu
   au-delà du point de bascule. La hauteur dépasse déjà les 44 px exigés.
*/

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  width: 100%;
  min-height: var(--button-height-primary);
  padding-inline: var(--space-3);
  border: 1px solid transparent;
  border-radius: var(--radius-default);
  font-size: var(--text-body-md-size);
  font-weight: var(--text-label-field-weight);
  line-height: var(--text-body-md-leading);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  /* Retour immédiat à l'action — EXPERIENCE.md, « Bouton de validation » :
     une légère élévation au survol et au focus signale que le bouton est
     actionnable. Court et discret, jamais un rebond. La préférence de
     mouvement réduit ramène ces durées à zéro (fin de base.css). */
  transition:
    background-color var(--motion-quick) var(--motion-ease),
    border-color var(--motion-quick) var(--motion-ease),
    color var(--motion-quick) var(--motion-ease),
    transform var(--motion-quick) var(--motion-ease);
}

.button--primary {
  background-color: var(--color-primary);
  color: var(--color-on-primary);
  box-shadow: var(--shadow-raised);
}

.button--primary:hover {
  background-color: var(--color-on-primary-container);
  color: var(--color-on-primary);
}

.button--secondary {
  background-color: transparent;
  border-color: var(--color-outline);
  color: var(--color-on-surface);
}

.button--secondary:hover {
  background-color: var(--color-surface-container);
  color: var(--color-on-surface);
}

/*
  Les deux années candidates, à côté du champ d'année de naissance — 13
  septembre 2026. La fédération ne publie aucune année de naissance, mais sa
  catégorie d'âge (Pup, Ben…) n'en laisse que deux possibles. Deux pastilles
  à portée de pouce remplacent quatre chiffres à taper sur un téléphone.

  Amélioration progressive stricte : `x-cloak` les tient invisibles tant
  qu'Alpine n'a pas démarré, et sans JavaScript elles ne s'affichent jamais —
  l'indication reste alors dans le libellé d'aide, en toutes lettres, et le
  parent saisit comme avant. Ce sont des `type="button"`, jamais des boutons
  de soumission, et elles n'imposent rien : elles écrivent dans le champ, qui
  reste libre — une tranche décalée d'une saison resterait donc corrigeable.
*/
.annees {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.annees .button {
  width: auto;
  min-width: var(--touch-target-min);
  min-height: var(--touch-target-min);
  padding-inline: var(--space-2);
}

/*
  L'élévation d'un pixel, au survol comme au focus clavier. Un pixel : de
  quoi sentir que l'élément répond, jamais de quoi déplacer la mise en page
  ni faire sursauter le texte voisin. Exclut le bouton désactivé, qui ne
  répond précisément à rien.
*/
.button:hover:not([disabled]):not([aria-disabled='true']),
.button:focus-visible {
  transform: translateY(-1px);
}

.button:active:not([disabled]):not([aria-disabled='true']) {
  transform: translateY(0);
}

.button[disabled],
.button[aria-disabled='true'] {
  background-color: var(--color-surface-container-high);
  border-color: var(--color-outline-variant);
  color: var(--color-on-surface-variant);
  box-shadow: none;
  cursor: not-allowed;
}

.button-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

@media (min-width: 640px) {
  .button {
    width: auto;
  }

  .button-row {
    flex-direction: row;
    align-items: center;
  }
}

/* ==========================================================================
   Carte
   ==========================================================================
   Fond de surface, rayon large, ombre douce, sans bordure. La profondeur
   vient de l'ombre, jamais d'un trait marqué ni d'un aplat coloré.

   Une carte ne porte jamais le logo : son fond peut être teinté un jour, et
   le pavé blanc du fichier apparaîtrait.
*/

.card {
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: var(--space-3);
  min-width: 0;
}

/*
  Carte principale — amplifiée, 13 septembre 2026 (DESIGN.md, Elevation &
  Depth / Shapes). Réservée aux écrans clés eux-mêmes : le bandeau du
  tournoi, le formulaire de recherche/d'inscription, l'écran d'entrée
  organisateur — jamais une ligne de tableau ni un encart secondaire, qui
  restent de simples .card.

  Dégradé très diffus vers la surface, jamais entre deux teintes de marque
  saturées — DESIGN.md l'exige. --color-primary-container est un jeton
  fonctionnel déjà autorisé en fond, jamais un --color-brand-* : ces
  derniers ne passent aucun seuil de contraste et ne servent que le logo,
  les filets et les liserés.

  La forme décorative est un ::after, jamais un élément réel : hors de
  l'arbre d'accessibilité par construction, sans qu'aucun aria-hidden ne
  soit nécessaire — plus sûr qu'un élément qu'il faudrait masquer. Sa
  couleur reprend --color-primary, jamais un jeton de marque, pour la même
  raison que le dégradé ci-dessus.
*/
/*
  --card-accent/--card-accent-container pilotent la teinte de la carte
  principale — bleu primary par défaut (pages publiques). .card--principale--organisateur
  (13 septembre 2026) les substitue par l'accent vert olive de l'espace
  organisateur, sans dupliquer aucune des règles ci-dessous : même dégradé,
  même ombre, même forme décorative, seule la teinte change.
*/
.card--principale {
  --card-accent: var(--color-primary);
  --card-accent-container: var(--color-primary-container);
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-deep);
  background-image: linear-gradient(155deg, var(--card-accent-container) 0%, var(--color-surface) 65%);
}

/* Accent de l'espace organisateur — 13 septembre 2026 : même richesse que les
   pages publiques, teinte distincte pour signaler le contexte. Jamais le
   jeton brand-lime lui-même, voir DESIGN.md, Colors. */
.card--principale--organisateur {
  --card-accent: var(--color-organisateur-accent);
  --card-accent-container: var(--color-organisateur-accent-container);
}

/*
  Issue d'une inscription — 13 septembre 2026. La confirmation, l'attente et
  l'annulation doivent se distinguer au premier coup d'œil : la teinte de la
  carte reprend l'emploi unique déjà fixé par DESIGN.md — vert pour une place
  obtenue, jaune pour une attente. L'annulation n'en reçoit aucune et garde
  la teinte neutre : le rouge ne dit que l'erreur, et une annulation n'en est
  pas une.

  La teinte ne porte jamais l'information seule : le badge et la phrase qui
  l'accompagnent donnent le mot dans les trois cas.
*/
.card--principale--succes {
  --card-accent: var(--color-success);
  --card-accent-container: var(--color-success-container);
}

.card--principale--attente {
  --card-accent: var(--color-warning);
  --card-accent-container: var(--color-warning-container);
}

/*
  Le surtitre de l'espace organisateur repasse au gris de texte, et ce n'est
  pas un oubli : --color-organisateur-accent est mesuré à 3,7:1 sur blanc,
  sous le seuil AA des 4,5:1 exigés pour du texte de cette taille. DESIGN.md
  l'annonce déjà — « usage décoratif uniquement, jamais destiné à porter du
  texte ». L'anis reste donc au dégradé, au cercle et au damier, qui ne
  portent aucun mot ; le texte, lui, garde un jeton mesuré pour lui.
*/
.card--principale--organisateur .surtitre {
  --surtitre-accent: var(--color-on-surface-variant);
}

/*
  Halo, et non plus disque — 13 septembre 2026. Le bord net d'un aplat
  circulaire se voyait comme un défaut de rendu là où DESIGN.md demande « un
  cercle ou un dégradé radial très doux ». Le dégradé radial s'éteint de
  lui-même avant son bord : plus large, plus présent, et pourtant plus
  discret qu'un disque de 12 %.
*/
.card--principale::after {
  content: '';
  position: absolute;
  right: -3.5rem;
  bottom: -4rem;
  width: 13rem;
  height: 13rem;
  border-radius: var(--radius-full);
  background-image: radial-gradient(closest-side, var(--card-accent), transparent);
  opacity: 0.22;
}

/* Le contenu reste au-dessus de la forme décorative, jamais recouvert. */
.card--principale > * {
  position: relative;
}

/* La remise à zéro annule la marge des paragraphes : les composants qui en
   empilent plusieurs la rétablissent au rythme. */
.card p + p,
.banner p + p,
.error-summary p + p {
  margin-top: var(--space-1);
}

.card__title {
  font-size: var(--text-headline-sm-size);
  font-weight: var(--text-headline-sm-weight);
  line-height: var(--text-headline-sm-leading);
  margin-bottom: var(--space-1);
}

/* ==========================================================================
   Affiche
   ==========================================================================
   Nouveau le 13 septembre 2026. Le tournoi n'avait jusqu'ici aucune présence
   propre : son nom était un .card__title de même poids que « Aucun joueur
   trouvé », et sa date se perdait dans un paragraphe de texte courant. Une
   page d'inscription à une rencontre associative se lit comme une affiche —
   qui, quand, où, et l'état des inscriptions, saisis d'un coup d'œil.

   L'affiche n'est pas un nouveau fond : elle se pose SUR la carte principale
   (`class="card card--principale affiche"`) et n'en change ni le dégradé, ni
   l'ombre, ni la forme décorative. Elle ajoute deux choses, et deux
   seulement : la composition en deux colonnes au-delà du point de bascule, et
   la frise en damier de son pied.

   Balisage attendu :

     <div class="card card--principale affiche">
       <div class="affiche__titre stack stack--serre">
         <p class="surtitre">Inscriptions ouvertes</p>
         <h1>…</h1>
       </div>
       <dl class="fiche">…</dl>
     </div>
*/

.affiche {
  /*
    Le voile de la maquette par-dessus le dégradé de la carte principale :
    l'ordre compte, les couches se peignent de la première à la dernière et
    la teinte d'accent doit rester dessous. Les deux foyers radiaux sont
    ancrés aux coins hauts — c'est ce qui donne au bloc son air de papier
    imprimé plutôt que d'aplat uni.
  */
  background-image:
    var(--voile-affiche),
    linear-gradient(155deg, var(--card-accent-container) 0%, var(--color-surface) 65%);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* Le contenu ne descend jamais jusqu'à la frise : elle borde l'affiche,
     elle ne la traverse pas. */
  padding-bottom: var(--space-4);
}

/*
  Frise en damier — la signature « échecs » du site, et sa seule occurrence
  par page. Géométrie pure, jamais une illustration : DESIGN.md n'autorise
  hors du logo que des formes abstraites, et cette règle tient toujours —
  aucune pièce, aucun personnage, aucun dessin. Deux rangées de huit pixels,
  très peu opaques, dans l'accent de la carte : bleu sur les pages publiques,
  anis dans l'espace organisateur, sans qu'une seule déclaration ne change.

  Un ::before, jamais un élément réel : hors de l'arbre d'accessibilité par
  construction, comme la forme décorative de .card--principale. Le cercle de
  celle-ci est un ::after, peint par-dessus ; à 12 % d'opacité sur 22 %, leur
  recouvrement au coin bas-droit ne se distingue pas.
*/
.affiche::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--space-2);
  background-image: conic-gradient(
    var(--card-accent, var(--color-primary)) 0 25%,
    transparent 0 50%,
    var(--card-accent, var(--color-primary)) 0 75%,
    transparent 0);
  background-size: var(--space-2) var(--space-2);
  opacity: 0.22;
  /* Au-dessus du cercle décoratif de .card--principale, qui est un ::after
     et se peindrait sinon par-dessus la frise. Sans risque pour le texte :
     le padding bas de l'affiche le tient à distance de cette bande. */
  z-index: 1;
}

/*
  Une seule figure décorative par bloc. L'affiche en porte déjà deux — le
  voile de ses coins hauts et la frise de son pied — et le cercle de la
  carte principale y faisait une troisième tache, terne là où le voile est
  lumineux. Il reste sur les cartes principales qui ne sont pas des affiches
  (le formulaire d'inscription, la confirmation d'annulation), où il est la
  seule.
*/
.affiche::after {
  content: none;
}

/*
  Affiche illustrée — 15 septembre 2026, DESIGN.md « Illustration du tournoi ».
  Une variante de .affiche, et non un composant de plus : elle n'ajoute que
  l'image et la colonne qui l'accueille, tout le reste — voile, frise, fiche —
  reste ce qu'il était. Portée par la seule page d'accueil, où le tournoi est
  le sujet ; les affiches des autres pages (formulaire, confirmation, espace
  organisateur) ne la reçoivent pas et ne changent pas d'un pixel.

  Sur téléphone, l'image est un bandeau : l'illustration livrée est un
  portrait 2:3, qui occuperait sinon un écran entier avant que le premier mot
  ne paraisse. object-position la cadre sur son tiers bas, où se tiennent les
  pièces — un recadrage centré n'aurait montré que le mur du fond.
*/
/* Sous le titre, l'image et les faits. Empilés sur téléphone, côte à côte
   dès 640 px — voir plus bas. */
.affiche__media {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.affiche__image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: center 62%;
  border-radius: var(--radius-lg);
  /* La même ombre que les cartes ordinaires : l'image est posée SUR
     l'affiche, elle n'est pas l'affiche. */
  box-shadow: var(--shadow-card);
}


@media (min-width: 640px) {
  /*
    Composition asymétrique : le titre occupe la colonne la plus large, la
    fiche des faits la seconde. Déséquilibrée à dessein — deux colonnes
    égales donneraient un tableau, pas une affiche.
  */
  .affiche {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    gap: var(--space-gutter);
    align-items: start;
    padding-block: var(--space-4) var(--space-5);
  }

  /*
    La colonne des faits est la plus étroite des deux : l'étiquette y passe
    au-dessus de sa valeur plutôt qu'à côté. Côte à côte, elle prenait la
    moitié de la largeur disponible et coupait « samedi 17 octobre 2026 » sur
    trois lignes. Sur téléphone, où l'affiche occupe toute la largeur, la
    forme côte à côte reste la bonne : elle tient en deux fois moins de
    hauteur, et le bandeau d'accueil doit rester court (EXPERIENCE.md,
    Responsive & Platform).
  */
  .affiche .fiche {
    gap: var(--space-2);
  }

  .affiche .fiche__ligne {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-half);
  }
}

@media (min-width: 640px) {
  /*
    Deux colonnes : l'image debout dans la première, tout le discours dans la
    seconde. La colonne de l'image est bornée en largeur plutôt que donnée en
    fraction — au-delà, une illustration de 2:3 pousse la fiche des faits
    vers le bas de l'écran et l'affiche cesse de tenir d'un seul regard.

    Cette règle défait la grille titre | fiche que .affiche pose au même
    point de bascule, et ne la rend jamais : le titre est monté au-dessus, en
    pleine largeur — 15 septembre 2026, seconde passe. Le conteneur de la
    page plafonne à une mesure de lecture bien plus étroite que la fenêtre —
    une affiche de 640 px de large sur un écran de 1280 — et y loger trois
    colonnes découpait « Grand Prix Jeunes 45 » en trois lignes dans la
    sienne. Remonté, le même titre tient sur une seule. Un point de bascule
    se lit sur la fenêtre, jamais sur la largeur réellement disponible ici.

    14 rem et non davantage pour la même raison : ce que l'image prend, le
    titre le perd, et c'est le titre qui porte le nom du tournoi.

    Ce bloc vient APRÈS celui de .affiche, et doit y rester : les deux
    sélecteurs ont la même spécificité, c'est donc l'ordre du fichier qui
    les départage. Placé avant, il était silencieusement écrasé et la
    largeur d'image ci-dessous ne s'appliquait pas — le rendu paraissait
    juste, par coïncidence, ce qui est la pire façon d'avoir raison.
  */
  .affiche--illustree {
    /* Le titre reprend toute la largeur : l'affiche redevient une pile, et
       c'est .affiche__media, sous lui, qui porte les deux colonnes. */
    display: flex;
    flex-direction: column;
  }

  .affiche--illustree .affiche__media {
    display: grid;
    grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
    column-gap: var(--space-gutter);
    align-items: start;
  }

  /* L'image reprend son format d'origine dès qu'elle a une colonne à elle :
     le bandeau n'était qu'un pis-aller de la largeur du téléphone. */
  .affiche--illustree .affiche__image {
    aspect-ratio: 2 / 3;
    object-position: center;
  }

  /* Les deux actions de l'affiche — 19 septembre 2026 — restent empilées
     et pleine largeur dans leur colonne, comme sur téléphone : côte à
     côte, « Inscrire un joueur » et « Voir les 12 joueurs inscrits » se
     cassaient chacun sur deux lignes dans la colonne des faits. */
  .affiche--illustree .button-row {
    flex-direction: column;
    align-items: stretch;
  }

  .affiche--illustree .button-row .button {
    width: 100%;
  }
}

/* ==========================================================================
   Fiche de faits
   ==========================================================================
   Les faits d'un tournoi — quand, où, jusqu'à quelle heure — sous leur
   étiquette. Une vraie liste de définitions : le lien entre l'étiquette et
   sa valeur est porté par le balisage, pas seulement par l'alignement.

   Même dessin que la piste étiquetée du tableau replié (voir plus bas,
   `.table tbody td`) : étiquette en petites capitales grises à gauche,
   valeur à droite. Reprendre cet idiome plutôt que d'en inventer un second
   garde les deux formes reconnaissables comme une seule.

     <dl class="fiche">
       <div class="fiche__ligne fiche__ligne--forte">
         <dt>Quand</dt><dd>samedi 17 octobre 2026</dd>
       </div>
     </dl>
*/

.fiche {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  min-width: 0;
}

.fiche__ligne {
  display: grid;
  grid-template-columns: minmax(0, 6.5rem) minmax(0, 1fr);
  gap: var(--space-2);
  align-items: baseline;
  min-width: 0;
}

.fiche dt {
  font-size: var(--text-label-caps-size);
  font-weight: var(--text-label-caps-weight);
  line-height: var(--text-label-caps-leading);
  letter-spacing: var(--text-label-caps-tracking);
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}

.fiche dd {
  margin: 0;
  min-width: 0;
}

/* La ligne qui porte le fait le plus utile — la date, presque toujours.
   Une seule par fiche : deux valeurs affirmées n'en affirment plus aucune. */
.fiche__ligne--forte dd {
  font-size: var(--text-headline-sm-size);
  font-weight: var(--text-headline-sm-weight);
  line-height: var(--text-headline-sm-leading);
}

/* ==========================================================================
   Métadonnées en ligne
   ==========================================================================
   Une suite de faits courts et secondaires sur une seule ligne — club,
   catégorie, classement d'un joueur trouvé. Remplace l'empilement de
   paragraphes de même poids, illisible au balayage.

   Le point médian séparateur est décoratif : il est posé en `content`, donc
   hors du texte lui-même, et n'est jamais la seule marque de séparation —
   les éléments sont des <li> distincts, annoncés comme tels.
*/

.metas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-half) var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-leading);
  color: var(--color-on-surface-variant);
}

/*
  Séparateur porté par l'élément qui précède, jamais par celui qui suit : au
  retour à la ligne, un point médian se retrouve alors en fin de ligne, là où
  il se lit comme une suite — et non en début de ligne, où il ressemblait à
  une puce mal placée.
*/
.metas > *:not(:last-child)::after {
  content: '·';
  margin-inline-start: var(--space-1);
  color: var(--color-outline);
}

/* ==========================================================================
   Compteurs
   ==========================================================================
   Les deux chiffres que l'organisateur vient voir — places prises, liste
   d'attente. Ils étaient noyés dans une phrase ; ils portent ici leur propre
   échelle, avec leur libellé sous eux. Le libellé n'est jamais facultatif :
   un grand nombre sans son mot ne dit rien.
*/

.compteurs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.compteur {
  display: flex;
  flex-direction: column;
  gap: var(--space-half);
  min-width: 0;
}

.compteur__valeur {
  font-size: var(--text-headline-md-size);
  font-weight: var(--text-display-lg-weight);
  line-height: var(--text-display-lg-leading);
  letter-spacing: var(--text-display-lg-tracking);
}

.compteur__libelle {
  font-size: var(--text-label-caps-size);
  font-weight: var(--text-label-caps-weight);
  line-height: var(--text-label-caps-leading);
  letter-spacing: var(--text-label-caps-tracking);
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}

/* ==========================================================================
   Référence d'inscription
   ==========================================================================
   Les huit caractères que la famille recopiera peut-être au téléphone :
   espacés, pour que O et 0, I et 1 se distinguent à la relecture. Occupait
   jusqu'au 13 septembre 2026 le plus grand corps de la page — au-dessus du
   statut, qui est pourtant ce qu'on vient lire.
*/

.reference {
  font-size: var(--text-headline-md-size);
  font-weight: var(--text-display-lg-weight);
  line-height: var(--text-headline-md-leading);
  letter-spacing: 0.06em;
}

/* ==========================================================================
   Barre d'outils
   ==========================================================================
   Les actions d'un écran, réunies sur une ligne plutôt que semées en liens
   nus entre deux paragraphes. Se distingue de .button-row, qui groupe les
   actions d'un formulaire sous ce formulaire : la barre d'outils appartient
   à l'écran, pas à un envoi.
*/

.barre-outils {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* ==========================================================================
   Résultat de recherche
   ==========================================================================
   Un joueur trouvé dans l'annuaire fédéral. Trois registres, jamais six
   paragraphes de même poids : son nom, ses faits secondaires en
   métadonnées, et l'action qui le choisit.

     <article class="card resultat">
       <div class="resultat__identite stack stack--serre">…</div>
       <form class="resultat__action">…</form>
     </article>
*/

.resultat {
  display: grid;
  gap: var(--space-2);
  transition: box-shadow var(--motion-quick) var(--motion-ease);
}

/* Le survol ne porte aucune information — il aide seulement l'œil à tenir
   sa ligne dans une liste longue. `:focus-within` le reprend au clavier,
   pour que la même aide existe sans souris. */
.resultat:hover,
.resultat:focus-within {
  box-shadow: var(--shadow-raised);
}

.resultat__nom {
  font-size: var(--text-headline-sm-size);
  font-weight: var(--text-headline-sm-weight);
  line-height: var(--text-headline-sm-leading);
}

@media (min-width: 640px) {
  .resultat {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-3);
  }
}

/* ==========================================================================
   Groupe de formulaire
   ==========================================================================
   Le formulaire d'inscription tient en une seule page et en deux groupes —
   le joueur, puis son responsable (EXPERIENCE.md, Component Patterns). Le
   numéro rend cette structure visible d'un coup d'œil ; il est `aria-hidden`
   parce qu'il ne fait que répéter l'ordre déjà porté par la suite des
   titres, et qu'un lecteur d'écran annoncerait sinon « 1 Le joueur ».
*/

.groupe__entete {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.groupe__numero {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--space-4);
  height: var(--space-4);
  border-radius: var(--radius-full);
  background-color: var(--color-primary-container);
  color: var(--color-on-primary-container);
  font-size: var(--text-label-field-size);
  font-weight: var(--text-display-lg-weight);
  line-height: 1;
}

/* ==========================================================================
   Grille de champs
   ==========================================================================
   Deux champs côte à côte au-delà du point de bascule, et seulement quand
   la paire se lit mieux ainsi : prénom et nom, année de naissance et
   licence. Jamais un champ long — une adresse électronique, un club —
   qu'une demi-largeur tronquerait à la saisie.
*/

.grille-champs {
  display: grid;
  gap: var(--space-3);
  min-width: 0;
}

@media (min-width: 640px) {
  .grille-champs {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ==========================================================================
   Badge d'état
   ==========================================================================
   Pilule, et toujours accompagné de son mot : « Confirmé »,
   « En attente — rang 3 ». Un badge vide de texte est un défaut, pas un
   style.
*/

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-half);
  padding: var(--space-half) var(--space-2);
  border-radius: var(--radius-full);
  font-size: var(--text-label-caps-size);
  font-weight: var(--text-label-caps-weight);
  line-height: var(--text-label-caps-leading);
  letter-spacing: var(--text-label-caps-tracking);
  text-transform: uppercase;
  white-space: nowrap;
}

.badge--confirmed {
  background-color: var(--color-success-container);
  color: var(--color-on-success-container);
}

.badge--waiting {
  background-color: var(--color-warning-container);
  color: var(--color-on-warning-container);
}

/* Inscription annulée — récit 4.3 : un état terminal, jamais une erreur, mais
   visuellement distinct des deux badges actifs ci-dessus. */
.badge--cancelled {
  background-color: var(--color-error-container);
  color: var(--color-on-error-container);
}

/*
  État sans couleur d'emploi — 13 septembre 2026 : des inscriptions fermées
  ne sont ni un succès, ni une attente, ni une erreur. Elles s'affichaient
  jusqu'ici en texte nu à côté d'un badge « Ouvertes », si bien que les deux
  états d'une même information ne se lisaient pas de la même façon. Jetons
  neutres, contraste mesuré à 5,7:1.
*/
.badge--neutre {
  background-color: var(--color-surface-container-high);
  color: var(--color-on-surface-variant);
}

/* ==========================================================================
   Bandeau
   ==========================================================================
   Trois variantes, et trois seulement : information, tournoi complet, échec.
   Chacune porte un titre en toutes lettres ; la teinte ne fait que le
   répéter.

   Poser `role="status"` sur une information ou une attente, `role="alert"`
   sur un échec.
*/

.banner {
  display: flex;
  flex-direction: column;
  gap: var(--space-half);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xl);
  min-width: 0;
}

.banner__title {
  font-size: var(--text-headline-sm-size);
  font-weight: var(--text-headline-sm-weight);
  line-height: var(--text-headline-sm-leading);
}

/*
  Un lien dans un bandeau est souvent la seule action proposée — l'adresse de
  contact des inscriptions fermées, par exemple. Il doit donc rester une cible
  tactile, d'où la hauteur imposée ; placez-le sur sa propre ligne pour que la
  hauteur ne disloque pas un paragraphe.
*/
.banner a {
  display: inline-block;
  min-height: var(--touch-target-min);
  padding-block: var(--space-1);
  color: inherit;
}

.banner--info {
  background-color: var(--color-primary-container);
  color: var(--color-on-primary-container);
}

/* Tournoi complet : une attente, pas une panne. Jaune, jamais rouge. */
.banner--full {
  background-color: var(--color-warning-container);
  color: var(--color-on-warning-container);
}

.banner--failure {
  background-color: var(--color-error-container);
  color: var(--color-on-error-container);
}

/*
  Bandeau long, fait pour être lu — l'encadré de publication du formulaire
  d'inscription (AD-15), quatre paragraphes que le parent doit avoir lus
  avant de cocher. Son texte est validé et ne change pas ; seule sa
  respiration change : les paragraphes s'écartent au rythme, et la mesure se
  borne, une ligne de plus de 70 caractères se perdant à la relecture.

  Nouveau le 13 septembre 2026, et volontairement un simple modificateur :
  un bandeau reste un bandeau, avec sa teinte, son titre et son rôle ARIA.
*/
.banner--lecture p + p {
  margin-top: var(--space-2);
}

.banner--lecture p {
  max-width: 68ch;
}

/*
  Ici, et seulement ici, un lien redevient inline. La règle générale de
  .banner en fait un bloc de 44 px de haut — juste pour un bandeau court dont
  le lien est la seule action, comme le dit son commentaire (« placez-le sur
  sa propre ligne »), désastreux dans quatre paragraphes de texte suivi : les
  trois adresses de contact citées au fil des phrases creusaient chacune un
  trou de deux interlignes au milieu du paragraphe.

  Le socle d'accessibilité tient toujours : un lien inséré dans une phrase
  est explicitement dispensé de la taille de cible minimale, sa hauteur étant
  contrainte par l'interligne du texte qui l'entoure. La doublure verticale
  est conservée — sur un élément inline elle agrandit la zone cliquable sans
  déplacer la ligne.
*/
.banner--lecture a {
  display: inline;
  min-height: 0;
}

/* ==========================================================================
   Dépliant
   ==========================================================================
   Nouveau le 13 septembre 2026, en réponse à « les pages sont trop
   bavardes ». Ce qui aide à agir reste visible ; ce qui explique seulement
   le pourquoi se replie. Le contenu demeure dans le document — il est lu
   par une recherche dans la page, et compté par les épreuves.

   **Un `<details>` natif, jamais une bulle au survol.** Le visiteur type est
   un parent sur un téléphone : une bulle au survol n'y existe pas, et
   l'Accessibility Floor interdit déjà qu'une information dépende d'un
   survol. Le dépliant, lui, s'ouvre au doigt comme au clavier, fonctionne
   sans JavaScript, et annonce son état replié/déplié tout seul.

   Le résumé est une cible tactile à part entière, d'où sa hauteur imposée.
*/

.depliant {
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-leading);
  color: var(--color-on-surface-variant);
}

/* Dans un bandeau, le dépliant en épouse la teinte et le corps : il en fait
   partie, il n'est pas une note en marge. */
.banner .depliant {
  color: inherit;
  font-size: inherit;
  line-height: inherit;
}

/*
  Aucune déclaration `display` ici, et c'est la raison d'être de ce
  commentaire : un `<summary>` est un `list-item`, et lui donner `flex` —
  ce qu'une première version faisait pour centrer son texte — **supprime le
  triangle natif**. Le résumé se lisait alors comme un paragraphe en gras,
  et rien ne disait qu'il restait du texte dessous. La hauteur de cible
  s'obtient donc par la doublure, jamais par une mise en page qui coûterait
  le marqueur d'état.
*/
.depliant > summary {
  min-height: var(--touch-target-min);
  padding-block: var(--space-1);
  font-weight: var(--text-label-field-weight);
  color: inherit;
  cursor: pointer;
}

.depliant > :not(summary) {
  margin-top: var(--space-half);
}

/* ==========================================================================
   Résumé d'erreurs
   ==========================================================================
   En tête de formulaire, chaque entrée étant un lien d'ancre vers le champ
   fautif. Porter `role="alert"` et `tabindex="-1"` sur le conteneur, et lui
   donner le focus au rendu de la page en erreur.
*/

.error-summary {
  background-color: var(--color-error-container);
  color: var(--color-on-error-container);
  border-radius: var(--radius-lg);
  padding: var(--space-2) var(--space-3);
  min-width: 0;
}

.error-summary__title {
  font-size: var(--text-headline-sm-size);
  font-weight: var(--text-headline-sm-weight);
  line-height: var(--text-headline-sm-leading);
}

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

/* Chaque lien est une cible tactile à part entière, d'où la hauteur imposée. */
.error-summary__list a {
  display: inline-block;
  min-height: var(--touch-target-min);
  padding-block: var(--space-1);
  color: inherit;
  font-weight: var(--text-label-field-weight);
}

/*
  `:focus` et non `:focus-visible` : le focus est posé par script sur un
  conteneur non interactif, cas où plusieurs navigateurs n'activent pas
  `:focus-visible`. L'anneau ne s'afficherait jamais.
*/
.error-summary:focus {
  outline: var(--focus-ring-width) solid var(--color-error);
  outline-offset: var(--focus-ring-offset);
}

/* ==========================================================================
   Tableau repliable
   ==========================================================================

   Conçu pour le téléphone puis élargi : la forme par défaut est le bloc
   étiqueté, et le tableau se reconstitue au-delà du point de bascule.
   `.table--stacked` reste empilé à toute largeur — la galerie s'en sert pour
   montrer la forme repliée sur un grand écran.

   Balisage attendu :

     <table class="table" role="table">
       <thead role="rowgroup">
         <tr role="row"><th role="columnheader" scope="col">Joueur</th>…</tr>
       </thead>
       <tbody role="rowgroup">
         <tr role="row"><td role="cell" data-label="Joueur">…</td>…</tr>
       </tbody>
     </table>

   Les `role` explicites ne sont pas décoratifs : `display: block` sur les
   éléments d'un tableau efface ses rôles implicites, et un lecteur d'écran
   cesse d'annoncer lignes et colonnes. Les redonner restitue la structure.
   `data-label` porte l'étiquette affichée devant chaque valeur une fois
   replié : une cellule sans `data-label` s'affiche sans nom.
*/

.table-scroll {
  /* Au-delà du point de bascule, un tableau trop large défile dans son
     conteneur — jamais la page entière. */
  overflow-x: auto;
  min-width: 0;
}

/*
  Largeur naturelle du contenu, jamais moins que le conteneur : une table au
  contenu court remplit toujours la largeur disponible (min-width: 100%),
  mais une table dont une cellule porte un texte long (une adresse, par
  exemple) grandit au-delà plutôt que de se laisser comprimer mot par mot —
  c'est alors .table-scroll, jamais chaque colonne, qui absorbe le
  débordement. Un simple `width: 100%` forcerait la largeur totale de la
  table à celle du conteneur : l'algorithme de répartition automatique des
  colonnes comprimerait alors les colonnes les moins prioritaires jusqu'à
  faire passer chaque mot à la ligne.
*/
.table {
  width: max-content;
  min-width: 100%;
  border-collapse: collapse;
}

/*
  Mesure de lecture confortable par cellule — même principe que la colonne de
  page (720px, DESIGN.md : « au-delà, une ligne de texte devient pénible à
  suivre »), transposé au caractère plutôt qu'au pixel pour suivre la taille
  de police. Sans cette borne, une valeur libre et longue (une adresse, un
  horaire détaillé) grandit sans limite et élargit tout le tableau avec
  elle — la colonne « Action », toujours la plus utile, se retrouve alors
  loin derrière un défilement horizontal pour un gain de lisibilité nul :
  une adresse sur une seule ligne ne se lit pas mieux qu'en deux ou trois.
  32ch laisse la plupart des valeurs courtes (dates, statuts, nombres)
  parfaitement intactes ; seul un texte libre inhabituellement long s'enroule.
*/
/*
  Bornée au tableau reconstitué, jamais à sa forme repliée — corrigé le
  13 septembre 2026. Sans le `:not(...)` ni la media query, la même borne
  s'appliquait au bloc étiqueté du téléphone : sur un écran de 375 px, chaque
  ligne repliée s'arrêtait à 256 px et abandonnait le tiers droit de l'écran,
  bouton d'action compris. Une mesure de lecture n'a de sens qu'entre
  colonnes voisines.
*/
@media (min-width: 640px) {
  .table:not(.table--stacked) td,
  .table:not(.table--stacked) th {
    max-width: 32ch;
  }
}

.table caption {
  text-align: left;
  font-size: var(--text-label-caps-size);
  font-weight: var(--text-label-caps-weight);
  line-height: var(--text-label-caps-leading);
  letter-spacing: var(--text-label-caps-tracking);
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
  padding-bottom: var(--space-1);
}

.table th {
  text-align: left;
  font-size: var(--text-label-caps-size);
  font-weight: var(--text-label-caps-weight);
  line-height: var(--text-label-caps-leading);
  letter-spacing: var(--text-label-caps-tracking);
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}

.table th,
.table td {
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--color-outline-variant);
}

/* Forme repliée, par défaut : un bloc étiqueté par ligne. L'en-tête n'est pas
   retiré, seulement masqué : replié, chaque valeur porte son étiquette, mais
   un lecteur d'écran continue de lire les en-têtes de colonnes. */
.table thead {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.table tbody tr {
  display: block;
  padding-block: var(--space-1);
  border-bottom: 1px solid var(--color-outline-variant);
}

.table tbody tr:last-child {
  border-bottom: 0;
}

.table tbody td {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
  gap: var(--space-2);
  align-items: baseline;
  /* Sans quoi un badge, devenu élément de grille, s'étirerait sur toute la
     largeur de sa colonne et cesserait de ressembler à une pilule. */
  justify-items: start;
  padding-inline: 0;
  border-bottom: 0;
}

/*
  Chaque enfant d'une cellule repliée peut se réduire. Sans cette règle, la
  largeur minimale automatique d'un élément de grille vaut son contenu
  minimal : une adresse électronique de trente caractères, insécable pour ce
  calcul, débordait de sa piste de 201 px et donnait au conteneur une barre
  de défilement horizontale — sur la forme repliée, qui n'a pourtant rien à
  faire défiler. `overflow-wrap: break-word`, hérité du corps de page, ne
  suffit pas : il autorise la coupure au rendu, jamais au calcul du minimum,
  qui reste celui du mot entier — d'où `anywhere`, sa variante qui compte.
  Au rendu, les deux se comportent de la même façon : un mot n'est coupé que
  s'il ne tient pas autrement.
*/
.table tbody td > * {
  min-width: 0;
  overflow-wrap: anywhere;
}

/*
  Une cellule sans `data-label` n'a pas d'étiquette à montrer : elle occupe
  toute la largeur au lieu de laisser une colonne blanche devant sa valeur.
*/
.table tbody td:not([data-label]) {
  grid-template-columns: minmax(0, 1fr);
}

.table tbody td:not([data-label])::before {
  content: none;
}

/*
  Une cellule d'action repliée. `justify-items: start` ci-dessus fait se
  rétracter chaque élément de grille sur son contenu minimal : un bouton
  posé nu dans la piste de valeur s'y plie mot à mot, sur quatre lignes.
  Cette enveloppe occupe toute la piste et rend au bouton sa largeur — la
  même que celle de tous les boutons sous 640 px.

  Elle sert aussi à réunir l'état et son action en un seul élément de
  grille : deux enfants directs renverraient le second dans la colonne de
  l'étiquette, large de quelques dizaines de pixels.
*/
.table tbody td .cellule-action {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);
  width: 100%;
  min-width: 0;
}

.table tbody td > form {
  justify-self: stretch;
}

/*
  Un badge est insécable par nature ; dans la piste de valeur d'un tableau
  replié, un libellé long — « En attente — rang 12 » — élargirait la page à
  360 px. Il se plie ici, et nulle part ailleurs.
*/
.table tbody td .badge {
  white-space: normal;
  max-width: 100%;
}

.table tbody td::before {
  content: attr(data-label);
  font-size: var(--text-label-caps-size);
  font-weight: var(--text-label-caps-weight);
  line-height: var(--text-label-caps-leading);
  letter-spacing: var(--text-label-caps-tracking);
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}

/* ==========================================================================
   Tableau en rangées, y compris sur téléphone
   ==========================================================================
   Nouveau le 13 septembre 2026, pour la liste publique des inscrits. La
   forme repliée par défaut donne un bloc de cinq lignes étiquetées par
   joueur : sur une liste de quarante enfants, il faut faire défiler deux
   cents lignes pour vérifier un nom. Ce modificateur garde le tableau en
   rangées à toute largeur — une ligne par joueur, lisible d'un coup d'œil
   à sa lettre.

   Il ne convient qu'à un tableau de valeurs courtes et peu nombreuses. Le
   tableau de l'organisateur, à dix colonnes dont du texte libre, garde la
   forme repliée : ce n'est pas la même densité, ni le même besoin.

   La largeur reste naturelle plutôt que contrainte au conteneur — annule
   `.table--compacte` sur ce point : à 360 px, cinq colonnes ramenées de
   force dans l'écran plient chaque club sur trois lignes, et la « ligne
   unique par joueur » n'en est plus une. C'est `.table-scroll` qui absorbe
   le dépassement, comme partout ailleurs.

   Première colonne collante : elle porte le nom, c'est-à-dire la seule
   chose qui permette de savoir de qui parle la ligne qu'on fait défiler.
*/
/* `.table.` plutôt que `.table--rangee` seul : la règle doit l'emporter sur
   `.table--compacte { width: 100% }` si les deux se rencontrent un jour. */
.table.table--rangee {
  width: max-content;
  min-width: 100%;
}

/* Doublure resserrée par défaut, c'est-à-dire sur téléphone : cinq colonnes
   à 16 px de chaque côté ajoutaient 160 px de vide à faire défiler. Elle
   reprend sa valeur courante au-delà du point de bascule, plus bas. */
.table--rangee thead th,
.table--rangee tbody td {
  padding: var(--space-1);
}

/*
  Au-delà, la place ne manque plus : la table reprend la largeur du
  conteneur au lieu de sa largeur naturelle, et le défilement latéral
  disparaît. Mesuré : à largeur naturelle, cinq colonnes portaient la table
  à 824 px dans une colonne de 720, pour un seul nom de club inhabituellement
  long — faire défiler tout un tableau pour lui est un mauvais échange.
  C'est donc lui qui s'enroule, sur deux lignes, et lui seul.
*/
/*
  La colonne de lecture du site fait 640 px de contenu : elle est taillée
  pour de la prose et des formulaires, pas pour cinq colonnes de tableau.
  Mesuré : à largeur naturelle ce tableau en demande 824. Contraint à 640,
  il rend chaque nom de club sur deux ou trois lignes — et la « ligne unique
  par joueur » disparaît, pour tout le monde, à cause d'un seul nom long.

  Le tableau déborde donc sa colonne quand l'écran le permet — et lui seul,
  jamais le texte autour. Sous 1120 px il garde sa largeur naturelle et
  c'est .table-scroll qui défile, la première colonne restant collée.
*/
.table-scroll--large {
  min-width: 0;
}

@media (min-width: 640px) {
  .table--rangee thead th,
  .table--rangee tbody td {
    padding: var(--space-1) var(--space-2);
  }
}

@media (min-width: 1120px) {
  .table-scroll--large {
    /* 1040 = 640 de colonne + 200 de chaque côté. */
    width: 1040px;
    margin-inline: -200px;
  }
}

.table--rangee thead {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip-path: none;
  white-space: nowrap;
}

.table--rangee tbody tr {
  display: table-row;
  padding-block: 0;
  border-bottom: 0;
}

.table--rangee tbody td {
  display: table-cell;
  border-bottom: 1px solid var(--color-outline-variant);
}

.table--rangee tbody td::before {
  content: none;
}

.table--rangee tbody td .badge {
  white-space: nowrap;
}

.table--rangee thead th:first-child,
.table--rangee tbody td:first-child {
  position: sticky;
  left: 0;
  /* Opaque, sans quoi les colonnes défilantes se liraient au travers. */
  background-color: var(--color-background);
}

/* ==========================================================================
   Tableau compact
   ==========================================================================
   Nouveau le 13 septembre 2026. `width: max-content` ci-dessus donne à chaque
   colonne sa largeur naturelle sans repli — y compris à son en-tête. Sur la
   liste des inscrits de l'organisateur, dix colonnes portaient ainsi le
   tableau à 1918 px dans un conteneur de 1120 : les trois dernières, dont la
   seule colonne d'action, tombaient hors de l'écran, et « Identifiant »
   réservait 120 px pour afficher un nombre à un chiffre, parce que son
   en-tête, lui, ne se repliait pas.

   Ce modificateur rend la largeur au conteneur et autorise le repli. Il ne
   s'applique qu'aux tableaux dont les valeurs sont courtes et nombreuses —
   jamais à un tableau portant du texte libre long, que la répartition
   automatique comprimerait mot par mot : c'est le défaut que
   `width: max-content` avait été posé pour éviter, et il reste la règle par
   défaut.

   Les deux bornes ci-dessous empêchent qu'une seule colonne prenne toute la
   place rendue : un club ou une adresse électronique s'enroulent sur deux
   lignes plutôt que de repousser l'action hors de vue.
*/

.table--compacte {
  width: 100%;
}

@media (min-width: 640px) {
  .table--compacte:not(.table--stacked) td,
  .table--compacte:not(.table--stacked) th {
    max-width: 22ch;
  }

  .table--compacte:not(.table--stacked) td[data-label='Club'] {
    max-width: 17ch;
  }

  .table--compacte:not(.table--stacked) td[data-label='Courriel'] {
    max-width: 20ch;
  }
}

/*
  Deux bornes taillées sur la nature de la valeur, jamais une largeur uniforme.
  Un nom de club se replie très bien ; une adresse électronique se coupe très
  mal (« karima.belkacem@e / xample.net »), et mérite donc la plus large des
  deux, de quoi tenir sur une ligne dans la grande majorité des cas.

  (La coupure syllabique des en-têtes, essayée ici, a été retirée : quatre
  colonnes ne portent qu'un mot — « Identifiant », « Catégorie », « Téléphone »,
  « Licence » — et `hyphens: auto` n'a rien changé à leur largeur minimale,
  le moteur ne coupant pas les mots pour calculer un minimum de colonne.)

  Limite atteinte, mesurée : ce sont désormais les **en-têtes** qui fixent le
  plancher de six colonnes sur dix — « Identifiant » réserve 104 px pour un
  nombre à un chiffre, « Responsable légal » 120 px. Borner davantage la
  valeur n'y change plus rien. Descendre en dessous demanderait d'abréger ces
  libellés, ce qui les désaccorderait de l'étiquette que la même cellule
  affiche une fois repliée sous 640 px — un mauvais échange.
*/


/* Au-delà du point de bascule, le tableau redevient un tableau. */
@media (min-width: 640px) {
  .table:not(.table--stacked) thead {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
  }

  .table:not(.table--stacked) tbody tr {
    display: table-row;
    padding-block: 0;
    border-bottom: 0;
  }

  .table:not(.table--stacked) tbody td {
    display: table-cell;
    padding: var(--space-1) var(--space-2);
    border-bottom: 1px solid var(--color-outline-variant);
  }

  .table:not(.table--stacked) tbody td::before {
    content: none;
  }

  /* Redevenu tableau, la cellule s'élargit d'elle-même : le badge reprend sa
     forme de pilule d'un seul tenant. */
  .table:not(.table--stacked) tbody td .badge {
    white-space: nowrap;
  }

  /* Sauf en tableau compact, où « En attente — rang 1 » insécable réservait
     224 px à la colonne d'état, et le libellé du bouton 307 px à celle de
     l'action. Repliés, ils tiennent dans la moitié.

     La pilule y resserre aussi sa doublure : à 16 px de chaque côté, « EN
     ATTENTE » ne tenait pas sur une ligne et le badge se brisait en trois
     (« EN / ATTENTE / — RANG 1 »). À 8 px, il tient en deux lignes nettes. */
  .table--compacte:not(.table--stacked) tbody td .badge {
    white-space: normal;
    padding-inline: var(--space-1);
  }

  .table--compacte:not(.table--stacked) tbody td .button {
    white-space: normal;
    padding-inline: var(--space-2);
  }

  /*
    Doublure horizontale resserrée — le dernier levier, et le bon : dix
    colonnes à 16 px de chaque côté réservaient 320 px de vide, soit plus du
    quart du conteneur. À 8 px, le tableau entier tient enfin dans sa colonne
    et les dix en-têtes restent lisibles, les lignes étant déjà séparées par
    leur filet. La doublure verticale, elle, ne bouge pas : c'est elle qui
    rend une ligne longue suivable.
  */
  .table--compacte:not(.table--stacked) thead th,
  .table--compacte:not(.table--stacked) tbody td {
    padding-inline: var(--space-1);
  }

  /*
    L'en-tête se détache de la première ligne par un filet franc, là où les
    lignes entre elles gardent le filet clair : sans cela, sur un tableau
    long, l'œil perd la frontière entre les noms de colonnes et les données.
  */
  .table:not(.table--stacked) thead th {
    padding-block: var(--space-1);
    border-bottom: 1px solid var(--color-outline);
  }

  /*
    Survol de ligne : une aide au balayage, jamais une information — rien
    n'est dit ici qui ne soit déjà écrit dans les cellules. Uniquement sur le
    tableau reconstitué : replié en blocs, chaque ligne est déjà un bloc
    distinct et n'a rien à gagner d'un fond.
  */
  .table:not(.table--stacked) tbody tr {
    transition: background-color var(--motion-quick) var(--motion-ease);
  }

  .table:not(.table--stacked) tbody tr:hover {
    background-color: var(--color-surface-container);
  }

  /* Redevenue une cellule ordinaire, l'enveloppe d'action reprend une ligne :
     l'état, puis son bouton à côté, à la largeur de son texte. */
  .table:not(.table--stacked) tbody td .cellule-action {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    width: auto;
  }
}

/* ==========================================================================
   Fenêtre modale
   ==========================================================================
   Nouveau le 13 septembre 2026 — AD-22, Alpine.data('modale', …) dans
   js/alpine.js. Comportement (piège de focus, ARIA, fermeture, état
   d'échec) : voir js/alpine.js et EXPERIENCE.md, Interaction Primitives.

   [x-cloak] cache tout élément piloté par Alpine avant que le script
   n'ait démarré : sans cette règle, la modale apparaîtrait un instant à
   l'écran avant qu'Alpine ne pose son premier x-show. Sert aussi le champ
   de filtrage des résultats fédéraux (Component Patterns, « Filtrage des
   résultats ») : absent tant que JavaScript n'a pas démarré.

   Le fond de la trame reprend --color-on-surface avec `opacity`, jamais une
   fonction de couleur écrite en dur avec transparence : aucun nouveau jeton
   n'est introduit, comme l'exige DESIGN.md — une couleur = un seul jeton,
   jamais approximée en ligne.
*/

[x-cloak] {
  display: none !important;
}

.modale-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
}

.modale-overlay::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--color-on-surface);
  opacity: 0.5;
}

.modale {
  position: relative;
  width: 100%;
  max-width: 32rem;
  max-height: calc(100vh - var(--space-6));
  overflow-y: auto;
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-deep);
}
