:root {
  --bg: #fafaf9;
  --surface: #ffffff;
  --border: #e2e2e0;
  --text: #1f2320;
  --muted: #6b7066;
  --accent: #1a6b4a;
  --accent-weak: #e6f2ec;
  --warn: #b45309;
  /* Vert distinct de --accent (teinte plus "citron vert") pour "en cours" :
     garde une distinction visuelle claire avec le vert de --accent utilisé
     pour les vainqueurs/1ers, tout en étant vert plutôt que jaune.
     Assombri (#65a30d → #3f6212) : le badge "En cours" (`.badge.en_cours`,
     texte --live sur fond --live-weak) ne passait que 2,85:1, repéré lors
     d'un audit de contraste après un bug similaire sur le thème Ty
     Korrigans — maintenant ~6,5:1 sur --live-weak, ~7:1 sur --surface. */
  --live: #3f6212;
  --live-weak: #ecfccb;
  /* Couleur de texte des boutons/liens qui utilisent `--accent` en fond
     (bouton par défaut, `.tabs button.active`, `.btn-cta`) — blanc par
     défaut, à surcharger uniquement par les thèmes dont `--accent` est
     trop clair pour porter du texte blanc (voir plus bas, ex. Phosphor,
     Ty Korrigans). Centralise ce qui était avant une règle par thème du
     type `button:not(.secondary):not(...) { color: ... }` : fragile, elle
     s'appliquait par erreur à tout futur bouton avec son propre fond
     (repéré 3 fois de suite — `.badge-btn`, `.forfeit-btn`, puis
     `.stat-tile` — avant ce remplacement par variable, qui ne peut plus
     être piégée par un nouveau bouton). */
  --on-accent: #fff;
  /* Rouge dédié au bouton "Forfait" (voir .forfeit-btn) — distinct de
     --warn (plus ambré, déjà utilisé ailleurs pour un signal plus discret,
     la flèche de classement en baisse) : le forfait est une action, pas
     juste une indication, elle mérite sa propre couleur nette. */
  --danger: #dc2626;
  --radius: 8px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.4;
}

header.top {
  padding: 1.25rem 1rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* Petite marque d'appli au-dessus du h1 (nom d'événement/d'activité, ou
   propos de la page sur l'admin) — jamais le titre principal lui-même,
   pour ne pas rivaliser avec ce que l'utilisateur est venu voir. Hérite de
   la police du thème actif (contrairement au h1, jamais recoloré/reformaté
   par un thème arcade) : reste lisible et discrète quel que soit l'habillage. */
header.top .brand { color: var(--muted); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 0.3rem; }
header.top h1 { margin: 0; font-size: 1.4rem; }
header.top .sub { color: var(--muted); font-size: 0.9rem; margin-top: 0.15rem; }
header.top nav { margin-top: 0.6rem; display: flex; align-items: center; gap: 1rem; }
header.top nav a { color: var(--accent); text-decoration: none; font-size: 0.9rem; }
/* Seul appel à l'action de tout le nav public (les équipes s'inscrivent
   elles-mêmes) — sorti du lot des liens texte plutôt que traité à égalité
   avec "Espace organisateurs", qui n'intéresse pas le même public. */
.btn-cta {
  display: inline-block; background: var(--accent); font-weight: 700;
  font-size: 0.9rem; padding: 0.4rem 1rem; border-radius: 999px;
}
/* `header.top nav a` (couleur --accent, plus haut) est plus spécifique
   qu'une simple classe — sans ce doublon de sélecteur, son texte blanc se
   fait silencieusement écraser par la couleur du lien "plein" (texte vert
   sur fond vert, invisible). */
header.top nav a.btn-cta { color: var(--on-accent); }
.btn-cta:hover { opacity: 0.88; }
/* `.btn-cta` pose son propre `display: inline-block`, à la même
   spécificité que la règle navigateur `[hidden] { display: none; }` — sans
   ce sélecteur plus spécifique pour la contrer, poser `hidden` sur
   "Inscrire une équipe" (`toggleRegisterLink`, public.js, piloté par le
   réglage `registration_open`) n'aurait aucun effet visuel malgré
   l'attribut bien présent. Même piège déjà rencontré sur `.match-row` et
   `.forfeit-menu` (voir plus bas). */
.btn-cta[hidden] { display: none; }

/* Liens du nav repliables (Terrains/Espace organisateurs/Inscrire une
   équipe) — pensé pour un affichage TV pendant l'événement : une fois le
   nécessaire consulté, ces liens ne servent plus à rien sur un écran non
   piloté et ne font qu'occuper de la place. `.nav-links` regroupe les vrais
   liens pour pouvoir les masquer d'un bloc sans toucher au bouton
   lui-même (voir `setupNavToggle`, app.js — préférence retenue par
   navigateur via `localStorage`, comme le thème jour/nuit). */
header.top nav .nav-links { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.nav-toggle {
  background: transparent; border: none; color: var(--muted); cursor: pointer;
  padding: 0.3rem; margin-left: auto; border-radius: 999px; line-height: 1;
}
.nav-toggle:hover { color: var(--accent); }
.nav-toggle-icon {
  display: inline-block; transform: rotate(90deg); transition: transform 0.15s ease;
  font-size: 1rem;
}
header.top nav.nav-collapsed .nav-links { display: none; }
header.top nav.nav-collapsed .nav-toggle-icon { transform: rotate(-90deg); }
/* Bandeau plus compact une fois le nav replié : sans lien à afficher, plus
   besoin d'autant d'espace sous le titre pour ce qui n'est plus qu'un
   bouton chevron. `:has()` (déjà utilisé plus bas pour le thème Phosphor)
   plutôt qu'une classe posée par JS sur `header.top` en plus de `nav` — un
   seul point de vérité (`nav.nav-collapsed`) suffit. */
header.top:has(nav.nav-collapsed) { padding-bottom: 0.6rem; }
header.top:has(nav.nav-collapsed) nav { margin-top: 0.35rem; }

/* Logo de l'app en médaillon dans l'en-tête, à droite du titre — même
   traitement que le blason de Ty Korrigans (voir plus bas dans ce
   fichier), pour le thème par défaut cette fois : `:not([data-arcade])`
   plutôt qu'un `[data-arcade="..."]` précis, puisque c'est justement
   l'absence de thème arcade (voir `applyTheme()`, public.js, qui ne pose
   jamais `data-arcade` pour le thème par défaut). `max-width` sur le seul
   h1 (pas de padding sur l'en-tête entier) : laisse marque/sous-titre/nav
   inchangés, seul le titre a besoin de laisser la place, exactement comme
   chez Ty Korrigans. Deux variantes jour/nuit (`data-theme`) — mêmes
   images que `CUSTOM_CLUB_CRESTS.default` (admin.js), qui les réutilise
   pour l'affiche QR et les diplômes. */
body.public-page:not([data-arcade]) header.top,
body:not([data-arcade]) header.top {
  position: relative;
}
body.public-page:not([data-arcade]) header.top h1,
body:not([data-arcade]) header.top h1 {
  max-width: calc(100% - 132px);
}
body.public-page:not([data-arcade]) header.top::after,
body:not([data-arcade]) header.top::after {
  content: ""; position: absolute; top: 0.5rem; right: 1rem;
  width: 108px; height: 108px;
  background: url("/img/default-crest-light.png") no-repeat center / contain;
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.35));
}
body.public-page:not([data-arcade])[data-theme="dark"] header.top::after,
body:not([data-arcade])[data-theme="dark"] header.top::after {
  background-image: url("/img/default-crest-dark.png");
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.5));
}

/* 860px à l'origine : beaucoup trop étroit sur un écran d'organisateur
   (ordinateur, souvent large) — la plupart des blocs de contenu (tuiles de
   `.stat-tiles`, grille `.courts-grid` en `auto-fit`, lignes `.match-row`/
   `.card` en `justify-content: space-between`) s'adaptent déjà tout seuls à
   la largeur disponible, ce cap ne servait donc qu'à les brider. Élargi
   plutôt que retiré : les formulaires (`form.stacked`, 420px) et champs
   isolés gardent leur propre largeur resserrée pour rester lisibles, donc
   une valeur encore raisonnable (pas 100% du viewport) évite juste des
   lignes de texte interminables sur un très grand écran. Sans effet sous
   ce seuil (mobile, tablette) — c'est un maximum, pas une largeur fixe. */
main { max-width: 1320px; margin: 0 auto; padding: 1rem; }

section { margin-bottom: 1.75rem; }
section h2 { font-size: 1.1rem; margin: 0 0 0.6rem; }

/* Fait sortir une section de la largeur max de `main` (pensée pour du
   texte/tableaux, pas pour un diagramme aussi large que le bracket) —
   technique "breakout" classique : la section prend toute la largeur du
   viewport (avec un peu de marge) au lieu de rester bridée à la largeur de
   son parent centré. `.bracket` garde son `overflow-x: auto` par sécurité
   pour les très grands brackets qui dépasseraient quand même. */
.bracket-section-wide {
  max-width: none; width: 100vw;
  position: relative; left: 50%; right: 50%;
  margin-left: -50vw; margin-right: -50vw;
  padding-left: 3rem; padding-right: 1.5rem; box-sizing: border-box;
}

details { margin-bottom: 1.75rem; }
details summary {
  font-size: 1.1rem; font-weight: 600; cursor: pointer; color: var(--text);
}
details[open] summary { margin-bottom: 0.6rem; }

/* Ombre douce partagée par toutes les "cartes" de l'appli (équipes,
   activités, matchs, bracket, podium, bandeau live) — un peu de profondeur
   plutôt qu'un simple contour plat. Neutralisée en thème sombre (voir plus
   bas) où une ombre noire sur un fond déjà sombre ne fait que salir le
   rendu au lieu d'ajouter du relief. */
.card, .match-row, .bracket-match, .podium-step, .live-card {
  box-shadow: 0 1px 2px rgba(20, 20, 15, 0.05), 0 1px 5px rgba(20, 20, 15, 0.04);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  margin-bottom: 0.6rem;
}

.card a { color: inherit; text-decoration: none; display: block; }
.card h3 { margin: 0 0 0.6rem; font-size: 0.95rem; }

/* Champ label+contrôle empilé (onglet Réglages) : un `<label>` est en ligne
   par défaut, insuffisant pour l'empiler proprement au-dessus d'un
   `<select>` pleine largeur. Largeur plafonnée : un menu à quelques options
   ("Auto/Jour/Nuit") n'a pas besoin de courir sur toute la largeur d'une
   carte. */
.field { margin-bottom: 0.75rem; }
.field:last-child { margin-bottom: 0; }
.field label { display: block; margin-bottom: 0.3rem; }
.field select { max-width: 280px; }
.card .meta { color: var(--muted); font-size: 0.85rem; }

/* Utilitaire générique (pas seulement dans une .card) : sans ça, un
   élément portant à la fois "card" et "row" ne matchait jamais cette règle
   (sélecteur descendant .card .row, pas .card.row) et le bouton restait
   collé au contenu au lieu d'aller à droite. */
.row { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }

table { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: var(--radius); overflow: hidden; }
th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); font-size: 0.92rem; }
th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
tr:last-child td { border-bottom: none; }
td.num, th.num { text-align: center; }

.badge {
  display: inline-block;
  font-size: 0.75rem;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  background: var(--accent-weak);
  color: var(--accent);
}
/* Couleurs pilotées par variables plutôt que codées en dur (#f1f1ee ne
   suivait aucun thème) : sans ça, ce badge reste un carré gris clair figé
   même sur fond sombre — pas seulement les thèmes arcade (déjà corrigés
   plus bas par une règle dédiée, désormais redondante et retirée), mais
   aussi le mode nuit par défaut (admin en préférence système, écran public
   sans thème arcade) qui n'était jamais couvert par ce correctif-là
   (repéré à la revue de code). */
.badge.a_venir { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
.badge.en_cours { background: var(--live-weak); color: var(--live); }

/* Onglet Accueil (admin) : mini-cartes de compteurs, barre de progression
   par activité, checklist de prise en main. */
/* `align-items: center` : évite que les tuiles s'étirent par défaut
   (`stretch`) à la hauteur de la plus grande si l'une d'elles passe sur
   deux lignes (ex. libellé "Activité(s) — ..." plus long que les autres). */
.stat-tiles { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
.stat-tile { flex: 1 1 140px; }
.stat-tile .stat-value { font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
.stat-tile .stat-label { color: var(--muted); font-size: 0.85rem; margin-top: 0.2rem; }
/* Couleur de valeur pour une tuile-statut (ex. inscriptions ouvertes/
   fermées) plutôt qu'un nombre — même gabarit, juste la valeur teintée. */
.stat-value.is-live { color: var(--live); }
.stat-value.is-muted { color: var(--muted); }
/* Une tuile peut être un vrai <button> (cliquable, voir `statTile` dans
   admin.js) — neutralise les styles de bouton par défaut du navigateur
   (fond plein, texte blanc centré) pour qu'elle reste identique à la
   version <div> non cliquable, `.card` ne suffisant pas seul à tout
   reprendre (`color`/`text-align` ne sont pas repris par `.card`). */
button.stat-tile { color: var(--text); text-align: left; }

.progress-bar { background: var(--border); border-radius: 999px; height: 8px; overflow: hidden; margin-top: 0.35rem; }
.progress-bar-fill { background: var(--accent); height: 100%; }

.checklist-item {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.4rem 0; font-size: 0.92rem;
}
.checklist-mark { color: var(--muted); font-size: 1rem; width: 1.1rem; text-align: center; flex-shrink: 0; }
.checklist-item.done .checklist-mark { color: var(--accent); }
.checklist-item.done .checklist-label { color: var(--muted); text-decoration: line-through; }
.checklist-label { flex: 1; }

.badge-btn {
  display: inline-block; font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 999px;
  border: none; cursor: pointer; font-weight: 600; white-space: nowrap;
}
.badge-btn.a_venir { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
.badge-btn.en_cours { background: var(--live-weak); color: var(--live); }
.badge.terminee { background: var(--accent-weak); color: var(--accent); }
.badge-timer {
  margin-left: 0.35rem; font-variant-numeric: tabular-nums; font-weight: 700; opacity: 0.85;
}

.group-label {
  font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em;
  margin: 0.9rem 0 0.4rem;
}
.group-label:first-of-type { margin-top: 0.4rem; }

.matches-list { display: flex; flex-direction: column; gap: 0.4rem; }

/* Barre de recherche d'une page d'activité publique (activity.html) : entre
   #content-before et #content-after, jamais reconstruite (voir
   renderActivityContent, public.js) pour ne jamais perdre le focus en
   retapant dedans. Enfant normal de <main> (qui pose déjà largeur/centrage/
   padding horizontal) — juste de l'espacement vertical ici. */
.team-search-bar { margin: 1rem 0 1.5rem; }
.team-search-bar input { max-width: 320px; }
.match-row {
  display: flex; justify-content: space-between; align-items: center;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.55rem 0.75rem; font-size: 0.92rem; gap: 0.5rem; flex-wrap: wrap;
}
/* `display: flex` ci-dessus a la même spécificité que la règle navigateur
   `[hidden] { display: none; }` — sans ce sélecteur plus spécifique pour la
   contrer, poser/retirer `hidden` (recherche par équipe) n'aurait aucun
   effet visuel malgré l'attribut bien présent (même piège déjà rencontré
   sur `.forfeit-menu`). */
.match-row[hidden] { display: none; }
/* Prochain match réellement jouable sur un terrain libre — ni le terrain
   ni aucune des deux équipes déjà engagés ailleurs (voir
   `nextMatchByLocation`, admin.js) — repère pour démarrer plusieurs
   terrains à la fois sans avoir à vérifier soi-même ces deux contraintes
   pour chacun dans une liste triée par poule, pas par terrain. */
.match-row.next-match { border-color: var(--accent); border-width: 2px; }
.match-row .next-match-tag { color: var(--accent); font-weight: 700; }
.match-row .teams { flex: 1; min-width: 140px; }
/* Sépare les deux noms d'équipe d'une ligne de match — plus facile à
   distinguer qu'un simple "vs" au milieu du texte quand les deux noms sont
   longs (voir `teamsVs`, public.js, point d'entrée partagé par les trois
   écrans qui affichent ce genre de ligne). */
.vs-pill {
  display: inline-block; background: var(--accent-weak); color: var(--accent);
  font-size: 0.65rem; font-weight: 800; letter-spacing: 0.03em;
  padding: 0.1rem 0.4rem; border-radius: 5px; margin: 0 0.3rem; vertical-align: 1px;
}
/* Dans le bracket, les deux équipes sont déjà empilées (pas côte à côte) —
   la pastille se retrouve donc seule sur sa propre ligne, centrée, sans les
   marges horizontales pensées pour s'intercaler entre deux noms en ligne. */
.bracket-vs { text-align: center; line-height: 1; margin: 0.1rem 0; }
.bracket-vs .vs-pill { margin: 0; }
.match-row .score { font-weight: 600; white-space: nowrap; }
.match-row .tag { font-size: 0.75rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.match-row .score-side { display: flex; flex-direction: column; align-items: flex-end; gap: 0.15rem; }

/* Rafraîchissement silencieux (5s) sur l'écran public : sans indice
   visuel, une mise à jour de score passe inaperçue. Va jusqu'à
   `var(--surface)` plutôt que transparent, pour retomber exactement sur le
   fond normal de la ligne au lieu de laisser voir le fond de la page. */
@keyframes flash-update { 0% { background-color: var(--live-weak); } 100% { background-color: var(--surface); } }
.match-row.flash, .bracket-match.flash { animation: flash-update 2s ease-out; }

/* Bracket : `.bracket-tree` a une largeur/hauteur explicite posée en
   inline-style par public.js (calculée à partir du nombre d'équipes), qui
   contient les lignes SVG et les boîtes en `position: absolute` — voir
   `bracketRoundCenter` dans public.js pour la formule de centrage. */
.bracket { display: flex; align-items: flex-start; gap: 2.5rem; overflow-x: auto; padding-bottom: 0.5rem; }
.bracket-tree { position: relative; flex: none; }
.bracket-lines { position: absolute; top: 0; left: 0; overflow: visible; }
.bracket-line { stroke: var(--border); stroke-width: 2; }
.bracket-col-header {
  position: absolute; top: 0; font-size: 0.8rem; color: var(--muted);
  text-align: center; text-transform: uppercase; letter-spacing: 0.03em;
}
.bracket-third-place { flex: none; padding-top: 30px; }
.bracket-third-place h3 {
  font-size: 0.8rem; color: var(--muted); text-align: center; margin: 0 0 0.4rem;
  text-transform: uppercase; letter-spacing: 0.03em;
}

.bracket-match {
  position: relative;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.5rem 0.6rem; font-size: 0.85rem;
}
.bracket-match-abs { position: absolute; display: flex; flex-direction: column; justify-content: center; }
/* Match en cours : bordure + pastille pulsante, même couleur "live" que le
   bandeau "En ce moment" et le badge "En cours" — reconnaissable au même
   coup d'œil dans le bracket. */
.bracket-match.live { border-color: var(--live); }
.bracket-match.live .slot { padding-right: 0.8rem; } /* laisse la place à la pastille sans chevaucher un nom tronqué */
.bracket-match.live::before {
  content: ''; position: absolute; top: 0.4rem; right: 0.4rem;
  width: 8px; height: 8px; border-radius: 50%; background: var(--live);
  animation: pulse-live 1.8s ease-in-out infinite;
}
/* Troisième ligne (terrain/cible à gauche, chrono à droite quand le match
   est en cours) — la case a une hauteur fixe (calculée en JS pour les
   connecteurs du bracket, voir BRACKET_MATCH_H, augmentée pour lui faire de
   la place) plutôt que mesurée, donc cette ligne fait partie du flux normal
   comme les deux noms d'équipe au-dessus, pas posée en position absolue. */
.bracket-match-meta {
  display: flex; justify-content: space-between; align-items: center; gap: 0.4rem;
  margin-top: 0.2rem; font-size: 0.7rem; line-height: 1.2;
  color: var(--muted); font-variant-numeric: tabular-nums; overflow: hidden;
}
.bracket-match-location { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bracket-match-timer { white-space: nowrap; pointer-events: none; }
/* Case dont les deux équipes restent à déterminer : recule visuellement
   (pointillés, pas de fond) pour ne pas concurrencer les matchs réels ou
   déjà tranchés avec la même intensité visuelle. */
.bracket-match.tbd-both { background: transparent; border-style: dashed; box-shadow: none; }
.bracket-match .slot {
  padding: 0.1rem 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bracket-match .slot.winner { font-weight: 700; color: var(--accent); }
.bracket-match .slot.tbd { color: var(--muted); font-style: italic; }

/* Chemin du vainqueur du tournoi une fois la finale jouée (voir
   `championId`/`playedIn`, public.js — pas affiché tant que le tournoi
   n'est pas terminé). Même couleur que `.slot.winner`/`.rank-1` (déjà la
   couleur "vainqueur" de toute l'appli) plutôt qu'une teinte dédiée. */
.bracket-match.champion-path { border-color: var(--accent); border-width: 2px; }
.bracket-line.champion-path { stroke: var(--accent); stroke-width: 3; }

/* Bracket cliquable (admin, format élimination directe) : la case
   s'indique interactive au survol, et le panneau de saisie s'ancre en
   dessous d'elle (`.bracket-match` a déjà `position: relative`). */
.bracket-match.clickable { cursor: pointer; }
.bracket-match.clickable:hover { border-color: var(--accent); }
.bracket-popover {
  position: absolute; top: calc(100% + 0.5rem); left: 0; z-index: 20;
  width: 400px; max-width: 90vw;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); padding: 1.1rem;
  cursor: default; /* la case parente porte le curseur pointer, pas le panneau */
}
/* `scoreEntryRow` produit un `.match-row` pensé pour une ligne de tableau
   pleine largeur (groupes disposés horizontalement) — dans un panneau ça
   déborderait, donc on retire son propre cadre (déjà porté par
   `.bracket-popover`) et on empile ses groupes verticalement à la place,
   avec des champs plus grands qu'en ligne de tableau (plus confortable à
   viser dans un panneau isolé, notamment au doigt sur tablette). */
.bracket-popover .match-row {
  border: none; padding: 0; background: transparent; box-shadow: none;
  flex-direction: column; align-items: stretch; gap: 0.8rem;
  font-size: 1.05rem;
}
.bracket-popover .teams { font-weight: 600; }
.bracket-popover input, .bracket-popover select, .bracket-popover button { padding: 0.6rem 0.7rem; }
.bracket-popover .score-form input[type=number] {
  width: 4.5rem; font-size: 1.2rem; font-weight: 700; text-align: center;
}

form.stacked { display: flex; flex-direction: column; gap: 0.5rem; max-width: 420px; }
label { font-size: 0.85rem; color: var(--muted); }
input, select, button, textarea {
  font: inherit; padding: 0.5rem 0.6rem; border-radius: var(--radius); border: 1px solid var(--border);
}
input, select, textarea { width: 100%; background: var(--surface); color: var(--text); }
textarea { resize: vertical; }
input[type="checkbox"] { width: auto; }
.checkbox-label { display: flex; align-items: center; gap: 0.4rem; }
/* Cases à cocher agrandies sur l'écran Réglages seulement (diaporama,
   inscriptions ouvertes) — pas une taille par défaut partout, qui aurait
   aussi grossi les cases de sélection en lot des listes d'équipes sans que
   ça ait été demandé. `accent-color` reteint aussi la coche elle-même (bleu
   par défaut du navigateur sinon) pour rester dans la palette du thème
   actif. */
.settings-panel input[type="checkbox"] { width: 1.4rem; height: 1.4rem; accent-color: var(--accent); }
.settings-panel .checkbox-label { gap: 0.6rem; }
button {
  background: var(--accent); color: var(--on-accent); border: none; cursor: pointer; font-weight: 600;
}
button.secondary { background: var(--surface); color: var(--accent); border: 1px solid var(--accent); }
button:disabled { opacity: 0.5; cursor: not-allowed; }

.score-form { display: flex; align-items: center; gap: 0.4rem; }
.score-form input[type=number] { width: 3.2rem; text-align: center; }

/* Forfait (écran de saisie des scores, poules comme bracket — voir
   `scoreEntryRow`) : un seul petit bouton plutôt qu'un menu déroulant
   d'équipes en permanence affiché à côté d'un second bouton — le forfait
   est rare, il n'a pas besoin de sa place à demeure. Le choix de l'équipe
   n'apparaît qu'au clic, dans un petit menu ancré au bouton. */
.forfeit-wrap { position: relative; display: inline-block; }
.forfeit-btn {
  background: var(--danger); color: #fff; font-size: 0.75rem; font-weight: 700;
  padding: 0.3rem 0.65rem; border-radius: 999px; white-space: nowrap;
}
.forfeit-menu {
  position: absolute; top: calc(100% + 0.3rem); left: 0; z-index: 20;
  min-width: 11rem; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  padding: 0.3rem; display: flex; flex-direction: column; gap: 0.15rem;
}
/* `display: flex` ci-dessus a la même spécificité que la règle navigateur
   `[hidden] { display: none; }` — sans ce sélecteur plus spécifique pour la
   contrer, le menu restait visible en permanence malgré l'attribut `hidden`
   posé/retiré par admin.js. */
.forfeit-menu[hidden] { display: none; }
.forfeit-menu button {
  background: transparent; color: var(--text); border: none; font-weight: 500;
  text-align: left; padding: 0.4rem 0.5rem; border-radius: calc(var(--radius) - 2px);
}
.forfeit-menu button:hover, .forfeit-menu button:focus-visible { background: var(--danger); color: #fff; }

/* Équipe déclarée forfait (globalement, voir `PATCH /api/teams/:id/forfeit`) :
   surlignée partout où sa carte apparaît (onglet Équipes, inscriptions d'une
   activité) — un simple contour plus marqué plutôt qu'un fond teinté, pour
   rester lisible sans dépendre d'un mélange de couleurs (`color-mix`) propre
   à chaque thème. */
.card.forfeit { border-color: var(--danger); border-width: 2px; }
.forfeit-badge {
  display: inline-block; background: var(--danger); color: #fff; font-size: 0.7rem;
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
  padding: 0.1rem 0.45rem; border-radius: 999px; margin-left: 0.4rem;
}

/* Style "soulignement" — porté depuis tybuvette/app/backend/public/css/
   admin.css (.tab-btn), demandé pour une ressemblance visuelle entre les
   deux interfaces admin. Palette Tournamant inchangée (--accent/--muted/
   --text/--border, y compris les 6 thèmes arcade qui les redéfinissent) :
   seul le style des onglets est porté, pas l'identité de couleur de
   TyBuvette. --on-accent (contraste sur fond --accent) n'est plus
   nécessaire ici, --accent sert directement de couleur de texte/bordure. */
.tabs { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 1rem; border-bottom: 1px solid var(--border); }
.tabs button {
  background: transparent;
  color: var(--muted);
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 0.6rem 0.9rem;
  font-weight: 700;
}
.tabs button:hover { color: var(--text); }
.tabs button.active { background: transparent; color: var(--accent); border-bottom-color: var(--accent); }

.member-row { display: flex; gap: 0.4rem; }
.member-row input { flex: 1; }

.msg { font-size: 0.88rem; padding: 0.5rem 0.7rem; border-radius: var(--radius); margin-bottom: 0.6rem; }
.msg.error { background: #fdecea; color: #b3261e; }
.msg.ok { background: var(--accent-weak); color: var(--accent); }
@keyframes msg-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
@keyframes msg-out { to { opacity: 0; transform: translateY(-6px); } }
.msg-enter { animation: msg-in 0.25s ease-out; }
.msg-leave { animation: msg-out 0.35s ease-in forwards; }

.rank-1 { font-weight: 700; color: var(--accent); }

/* ---------- Mode jour/nuit — organisateur ET public ----------
   Un seul jeu de règles pour les deux interfaces, plutôt qu'un mécanisme
   par écran : `data-theme` (posé par `applyTheme()`, public.js — appelée
   aussi bien par admin.js au chargement de l'admin que par les pages
   publiques) reflète partout le même réglage `theme_override`. L'admin
   suivait auparavant sa propre préférence système (`prefers-color-scheme`)
   indépendamment de ce réglage — changement voulu : l'organisateur doit
   voir la même bascule jour/nuit que celle qu'il impose à l'écran public,
   pas une préférence locale déconnectée. Ce bloc-ci ne concerne que le
   thème par défaut (sans thème arcade) — Ty Korrigans a sa propre
   variante jour plus bas (`[data-arcade="tykorrigans"][data-theme="light"]`,
   sur le même principe admin+public) ; les autres thèmes arcade n'ont pas
   de variante jour du tout, voir le commentaire plus bas sur pourquoi le
   jour/nuit n'a alors plus d'effet une fois un thème arcade actif. */
body[data-theme="dark"] {
  --bg: #0d1512;
  --surface: #17231d;
  --border: #2a3d33;
  --text: #eef4f0;
  --muted: #8fa89b;
  --accent: #34d399;
  --accent-weak: #16352a;
  --warn: #fbbf24;
  --live: #a3e635;
  --live-weak: #2b3a0f;
  --danger: #f87171;
  --on-accent: #0d1512;
}

body[data-theme="dark"] .card,
body[data-theme="dark"] .match-row,
body[data-theme="dark"] .bracket-match,
body[data-theme="dark"] .podium-step,
body[data-theme="dark"] .live-card {
  box-shadow: none;
}
/* .msg.error a des couleurs codées en dur (pas de var --error-* existante)
   : sans ce correctif, la boîte resterait rose clair sur fond sombre. */
body[data-theme="dark"] .msg.error { background: #3a1a1a; color: #ff8a80; }

/* ---------- Pages publiques (écran) : typographie, largeur, podium, bandeau live ---------- */

body.public-page header.top h1 { font-size: 1.9rem; }
/* Plus large que l'admin (1320px) : ces écrans sont aussi pensés pour un
   affichage TV/projecteur pendant l'événement (voir le diaporama,
   `slideshow_enabled`) — un public plus large que celui d'un organisateur
   sur son ordinateur, donc plus de marge encore avant de plafonner. Sans
   effet sur mobile (le cas participant/téléphone), déjà bien géré en
   dessous de ce seuil. */
body.public-page main { max-width: 1500px; }

/* Tableau de bord de l'accueil (#leaderboard + colonne "en cours/à venir/
   activités/terminés" à droite) : empilé en une seule colonne par défaut
   (mobile — le classement garde la priorité, toujours en premier, mais on
   défile pour voir les matchs, comme avant), bascule en deux colonnes
   fixes à partir de 860px pour exploiter la largeur d'un écran TV/tablette
   plutôt que de laisser tout le classement seul au centre avec le reste en
   dessous. Seule mise en page à deux colonnes de l'écran public — d'où un
   point de rupture dédié plutôt qu'une grille `auto-fit` comme
   `.courts-grid` : les deux colonnes n'ont pas la même forme (classement
   fixe vs empilement de sections), pas un jeu de cartes identiques. */
.home-dashboard { display: flex; flex-direction: column; gap: 2rem; }
.home-dashboard .home-side { display: flex; flex-direction: column; gap: 2rem; min-width: 0; }
@media (min-width: 860px) {
  .home-dashboard { flex-direction: row; align-items: flex-start; }
  .home-dashboard .leaderboard-panel { flex: 0 0 480px; }
  .home-dashboard .home-side { flex: 1; }

  /* Beaucoup de poules (ou un classement général à beaucoup d'équipes)
     dépasse largement cette largeur fixe en hauteur — sans limite, le
     panneau pousserait toute la page, ce qui ne veut rien dire sur un
     écran TV non piloté (personne ne scroll). Hauteur en `vh` plutôt qu'un
     calcul pixel-parfait de la place restante sous l'en-tête (variable
     d'un thème à l'autre, voir `phosphor` qui grossit la police racine) :
     approximatif mais largement suffisant vu de loin, et `stepLeaderboardScroll`
     (public.js) fait défiler le surplus par paliers plutôt que de le
     couper. Seulement à partir de ce point de rupture : en mobile empilé,
     la page défile normalement comme le reste. */
  #leaderboard { max-height: 70vh; overflow: hidden; }
}

/* Bandeau d'annonce configurable (Réglages admin, "Message défilant") —
   vide par défaut (`announcement`, réglage serveur), entièrement caché
   côté client via `hidden` tant qu'il n'y a rien à montrer
   (`renderAnnouncement`, public.js). Couleur `--warn` plutôt que
   `--accent` (déjà partout ailleurs sur cette page — podium, boutons,
   badges) : une teinte distincte de l'accent de marque pour que l'annonce
   ressorte comme une info à part plutôt que de se fondre dans le reste.
   `color-mix` pour un fond juste teinté plutôt qu'un aplat `--warn` plein :
   un aplat aurait demandé de retrouver une couleur de texte lisible par-
   dessus pour chacun des thèmes (aucune n'est prévue, contrairement à
   `--accent`/`--on-accent`) — ici le texte reste `--warn` sur un fond
   proche de `--surface`, la même paire déjà utilisée sans souci ailleurs
   (`.rank-change.down`). */
.announcement-bar {
  overflow: hidden;
  background: color-mix(in srgb, var(--warn) 18%, var(--surface));
  border-top: 2px solid var(--warn);
  border-bottom: 2px solid var(--warn);
  padding: 0.55rem 0;
  margin-bottom: 1.5rem;
}
.announcement-track {
  display: inline-block;
  white-space: nowrap;
  color: var(--warn);
  font-weight: 800;
  font-size: 1.15rem;
  /* Repli sans mouvement (voir le bloc `prefers-reduced-motion` plus bas
     pour la version qui défile) : une seule ligne, tronquée avec "…" si
     trop longue plutôt que simplement coupée net par `.announcement-bar`. */
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
/* Défilement horizontal continu, seulement si l'utilisateur n'a pas
   demandé de réduire les animations (déjà la convention de cette page,
   voir le clignotement du thème Phosphor plus bas) — sans cette
   condition, un visiteur ayant activé cette préférence système se
   retrouverait avec un message qui ne finit jamais de défiler jusqu'à
   lui plutôt qu'un simple texte statique. `padding-left: 100%` place le
   texte juste hors du bord droit avant même la première frame de
   l'animation — technique standard du "marquee" CSS. */
@media (prefers-reduced-motion: no-preference) {
  .announcement-track {
    overflow: visible;
    text-overflow: clip;
    max-width: none;
    padding-left: 100%;
    animation-name: announcement-scroll;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
  }
}
@keyframes announcement-scroll {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-100%, 0, 0); }
}

/* Hiérarchie des titres : un seul titre "principal" par page (le
   classement général sur l'accueil), tout le reste en label de section
   discret — avant ça, "Classement général" et "Activités" avaient
   exactement le même poids visuel, sans rien pour guider l'œil sur un
   écran regardé de loin. */
body.public-page h2 {
  font-size: 1rem; margin: 0 0 0.6rem; color: var(--muted); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
}
body.public-page h2.section-title-primary {
  font-size: 1.9rem; color: var(--text); text-transform: none; letter-spacing: 0;
}

body.public-page table { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); }
body.public-page[data-theme="dark"] table { box-shadow: none; }
body.public-page td, body.public-page th { font-size: 1rem; padding: 0.65rem 0.75rem; }

/* Lisibilité à distance (écran public regardé de loin, pas un écran de
   travail) : score et vainqueur nettement plus marqués qu'en admin. */
body.public-page .match-row { padding: 0.75rem 1rem; font-size: 1.05rem; }
body.public-page .match-row .score { font-size: 1.35rem; font-weight: 800; }
body.public-page .rank-1 { font-weight: 800; }

.activity-card {
  border-left: 4px solid var(--sport-accent, var(--accent));
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
/* Rien n'indiquait que la carte menait à une page de détail (fond/texte
   identiques à une carte "simple lecture" ailleurs dans l'appli) — le
   chevron reste visible en permanence (utile au doigt, pas qu'à la souris),
   le survol ajoute un retour supplémentaire pour qui a une souris. */
.activity-card:hover { transform: translateY(-2px); box-shadow: 0 4px 14px rgba(20, 20, 15, 0.12); }
.activity-card-arrow { color: var(--muted); font-size: 1.3rem; line-height: 1; }

/* Bandeau des matchs en cours, toutes activités confondues. Grille qui
   passe à la ligne (pas de scroll horizontal) : c'est un écran non piloté
   par un humain, personne n'est là pour faire défiler — un match en cours
   ne doit jamais finir coupé hors champ. */
.live-banner { margin-bottom: 1.5rem; }
.live-banner-list { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.live-card {
  background: var(--surface); border: 1px solid var(--live); border-radius: var(--radius);
  padding: 0.6rem 0.9rem; flex: 1 1 220px; max-width: 320px;
}
.live-card .live-tag { color: var(--live); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.live-card .live-teams { font-weight: 600; margin: 0.2rem 0; }
.live-card .live-meta-row { display: flex; justify-content: space-between; align-items: flex-end; gap: 0.5rem; }
.live-card .live-meta { color: var(--muted); font-size: 0.8rem; }
/* Même couleur que le nom de l'activité (`.live-meta`), pas la couleur "en
   direct" du badge — un chrono n'a pas besoin d'attirer l'œil autant que le
   statut lui-même. Poussé à droite par `.live-meta-row` : coin bas-droit de
   la carte. */
.live-card .live-timer { color: var(--muted); font-size: 0.8rem; font-variant-numeric: tabular-nums; white-space: nowrap; }

@keyframes pulse-live { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.badge.en_cours, .badge-btn.en_cours, .live-card .live-tag { animation: pulse-live 1.8s ease-in-out infinite; }

/* ---------- Écran des terrains (/courts.html) : un terrain = une colonne ---------- */
.courts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
.court-col {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.9rem; display: flex; flex-direction: column; gap: 0.5rem;
}
.court-col-header { font-weight: 700; }
/* `.live-card` (empruntée pour le match en cours) a son propre fond et
   bordure colorée — pas la peine que `.court-col` en rajoute une deuxième
   par-dessus au même endroit, d'où `max-width` retiré via ce sélecteur
   plus spécifique plutôt qu'un `!important`. `flex: none` aussi : dans
   `.live-banner-list` (ligne qui wrap), `.live-card` a `flex: 1 1 220px`
   pour se répartir la largeur disponible — mais `.court-col` est un flex
   *colonne*, où ce même 220px se lit comme une hauteur minimale (l'axe
   principal d'un flex colonne), avec `flex-grow: 1` qui l'étire encore
   pour combler la hauteur en trop de sa ligne de grille (`.courts-grid`) —
   résultat, une tuile "En cours" bien plus haute que les trois lignes de
   texte qu'elle contient. Ici la carte doit garder sa hauteur naturelle,
   quelle que soit la hauteur de la colonne. */
.court-col .live-card { max-width: none; flex: none; }
.court-free { color: var(--muted); font-style: italic; padding: 0.6rem 0; }
.court-upcoming-title { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; margin-top: 0.3rem; }

/* Podium visuel (top 3 du classement général) */
.podium { display: flex; align-items: flex-end; justify-content: center; gap: 0.75rem; margin: 1rem 0 1.5rem; }
.podium-step {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1rem 0.75rem; text-align: center; width: 32%; max-width: 200px;
}
.podium-step.gold { order: 2; padding-top: 1.75rem; }
.podium-step.silver { order: 1; }
.podium-step.bronze { order: 3; }
.podium-rank {
  width: 2.2rem; height: 2.2rem; border-radius: 50%; margin: 0 auto 0.5rem;
  display: flex; align-items: center; justify-content: center; font-weight: 800;
}
.podium-step.gold .podium-rank { background: #d4af37; color: #3d2f00; }
.podium-step.silver .podium-rank { background: #b8bcc2; color: #2b2f33; }
.podium-step.bronze .podium-rank { background: #c98a4b; color: #35200f; }
.podium-name { font-weight: 700; margin: 0.2rem 0; }
.podium-points { color: var(--muted); font-size: 0.85rem; }

.rank-change { font-size: 0.8rem; margin-left: 0.35rem; }
.rank-change.up { color: var(--accent); }
.rank-change.down { color: var(--warn); }

/* Classement en direct d'un bracket élimination en cours (accueil public,
   single_activity_mode — voir renderEliminationLive, public.js). Équipes
   encore en lice d'abord, puis éliminées groupées par tour de sortie. */
.still-in-list { display: flex; flex-direction: column; gap: 0.4rem; margin: 0 0 0.9rem; }
.still-in-row {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.45rem 0.6rem; font-size: 0.9rem;
}
.still-in-row .status { font-size: 0.78rem; color: var(--muted); }
.out-round { margin-bottom: 0.7rem; }
.out-round .round-label { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin: 0 0 0.3rem; }
.out-round ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.35rem; }
.out-round li { font-size: 0.84rem; color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 0.2rem 0.5rem; }

/* Confettis (fin de tournoi) : posés en overlay plein écran, transparents
   aux clics, retirés du DOM par public.js une fois l'animation terminée. */
.confetti-layer { position: fixed; inset: 0; pointer-events: none; z-index: 500; overflow: hidden; }
.confetti-piece {
  position: absolute; top: -12px; width: 8px; height: 14px; opacity: 0.9;
  animation-name: confetti-fall; animation-timing-function: ease-in; animation-fill-mode: forwards;
}
@keyframes confetti-fall { to { transform: translateY(105vh) rotate(600deg); opacity: 0.4; } }

/* ==========================================================================
   Thèmes "arcade" de l'écran public — réglage `public_theme` (Réglages,
   admin), appliqué par public.js via `body.dataset.arcade`. Chacun
   redéfinit les mêmes variables que le mode sombre plus haut, donc la
   quasi-totalité des composants (cartes, badges, bracket, tableaux...) se
   réhabille automatiquement puisqu'ils sont déjà tous construits sur ces
   variables — seuls les quelques endroits en couleurs fixes (podium,
   ci-dessous) ont besoin d'un correctif dédié. Placé après le bloc
   `[data-theme="dark"]` : même spécificité (0,2,1), donc c'est l'ordre
   dans le fichier qui tranche quand les deux attributs sont présents en
   même temps (le jour/nuit continue de tourner en tâche de fond mais perd
   toujours face au thème arcade).
   ========================================================================== */
@keyframes arcade-blink { 50% { opacity: 0; } }

/* Chaque thème cible deux mondes : `body.public-page[data-arcade]` pour
   les quatre pages publiques (accueil, activité, terrains, inscription —
   toutes en `.public-page`), et `body[data-arcade]` tout court
   (spécificité volontairement égale) pour /admin.html, seule page sans
   cette classe. Le forçage jour/nuit (`data-theme`, voir plus haut) suit
   la même distinction quand un thème a une variante jour — admin compris,
   au même titre que les pages publiques. */

/* ---------- Phosphor : terminal ambre monochrome, écran de high-score ---------- */
/* `:has()` plutôt qu'une variable de plus dans le bloc ci-dessous : VT323
   a un x-height visuellement plus petit qu'une police normale à la même
   taille déclarée (caractéristique connue de cette police pixel) — un peu
   de texte fin illisible sinon sur tout le texte courant (tableaux,
   listes de matchs), pas seulement les titres. Sans risque pour la
   géométrie du bracket : ses dimensions sont posées en px direct par JS,
   jamais en rem, donc insensibles à la taille racine — seul le texte à
   l'intérieur grossit légèrement, marge déjà prévue par l'ellipse sur les
   noms d'équipe trop longs. */
:root:has(body[data-arcade="phosphor"]) { font-size: 107%; }

body.public-page[data-arcade="phosphor"],
body[data-arcade="phosphor"] {
  --bg: #05100a; --surface: #0a1f12; --border: #3a2c05;
  /* Le brun d'origine (#7a5200) tombait sous 3:1 de contraste avec --bg ET
     --surface (quasi illisible) — remonté pour rester nettement dans la
     même famille ambrée tout en repassant au-dessus de 4,5:1 sur les deux. */
  --text: #ffb000; --muted: #cc9933; --accent: #fff1c9; --accent-weak: #2a2005;
  --warn: #ff6b3d; --live: #33e1ff; --live-weak: #062830; --danger: #ff3b3b;
  --on-accent: #1a1200;
  font-family: "VT323", ui-monospace, monospace;
  text-transform: uppercase; letter-spacing: 0.03em;
  /* Pas de lueur ici (contrairement au h1 juste en dessous) : un flou sur
     du texte dense (tableaux, listes de matchs) le rend flou, pas juste
     stylé — la lueur reste réservée aux gros titres qui peuvent se le
     permettre. */
}
body.public-page[data-arcade="phosphor"] header.top h1,
body[data-arcade="phosphor"] header.top h1,
body[data-arcade="phosphor"] .qr-poster-title {
  font-family: "Press Start 2P", monospace; font-size: 1.35rem; letter-spacing: 0.02em; text-shadow: 0 0 8px rgba(255, 176, 0, 0.6);
}
/* Même lueur que le h1, mais sans passer en "Press Start 2P" : ce titre
   ("Classement général") est plus long qu'un nom d'événement — la police
   très large et carrée de la pastille h1 le ferait déborder ou passer à la
   ligne au lieu de simplement le mettre en valeur. */
body.public-page[data-arcade="phosphor"] h2.section-title-primary {
  text-shadow: 0 0 8px rgba(255, 176, 0, 0.6);
}
body.public-page[data-arcade="phosphor"] header.top h1::after,
body[data-arcade="phosphor"] header.top h1::after { content: "_"; margin-left: 0.15em; }
@media (prefers-reduced-motion: no-preference) {
  body.public-page[data-arcade="phosphor"] header.top h1::after,
  body[data-arcade="phosphor"] header.top h1::after { animation: arcade-blink 1s steps(1) infinite; }
}
/* Mascotte en médaillon dans l'en-tête, à droite du titre — même mécanisme
   que Ty Korrigans/le thème par défaut (`max-width` sur le seul h1,
   `header.top::after` positionné à droite). Lueur ambrée en guise
   d'ombre portée plutôt que le noir habituel des deux autres thèmes :
   cohérent avec la lueur déjà posée sur le h1 et le titre "Classement
   général" juste au-dessus. */
body.public-page[data-arcade="phosphor"] header.top,
body[data-arcade="phosphor"] header.top {
  position: relative;
}
body.public-page[data-arcade="phosphor"] header.top h1,
body[data-arcade="phosphor"] header.top h1 {
  max-width: calc(100% - 132px);
}
body.public-page[data-arcade="phosphor"] header.top::after,
body[data-arcade="phosphor"] header.top::after {
  content: ""; position: absolute; top: 0.5rem; right: 1rem;
  width: 108px; height: 108px;
  background: url("/img/phosphor-mascot.svg") no-repeat center / contain;
  filter: drop-shadow(0 0 10px rgba(255, 176, 0, 0.5));
}
body.public-page[data-arcade="phosphor"]::after,
body[data-arcade="phosphor"]::after {
  content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 300;
  /* Bandes assombries à 28% à l'origine : assez pour noircir un peu de
     texte à chaque triplet de lignes de pixels, cumulé au flou déjà en
     moins ci-dessus. Adoucies pour garder l'effet CRT visible sans
     rogner davantage sur le contraste. */
  background: repeating-linear-gradient(rgba(0, 0, 0, 0) 0px, rgba(0, 0, 0, 0) 2px, rgba(0, 0, 0, 0.16) 3px);
  mix-blend-mode: multiply;
}
body.public-page[data-arcade="phosphor"] .podium-step.gold .podium-rank { background: #fff1c9; color: #1a1200; }
body.public-page[data-arcade="phosphor"] .podium-step.silver .podium-rank { background: #ffb000; color: #1a1200; }
body.public-page[data-arcade="phosphor"] .podium-step.bronze .podium-rank { background: #7a5200; color: #1a1200; }
/* Texte des boutons/CTA sur fond `--accent` clair (Phosphor, Oché, Le
   Maître, Ty Korrigans) : géré par `--on-accent`, défini dans le bloc de
   variables de chaque thème plus haut/bas — voir sa définition par défaut
   dans `:root` pour l'historique (a remplacé une règle par thème du type
   `button:not(.secondary):not(...) { color: ... }`, qui s'est fait piéger
   trois fois par un futur bouton à fond propre avant d'être abandonnée). */

/* ---------- Oché : soirée fléchettes façon retransmission pro (PDC) ---------- */
body.public-page[data-arcade="oche"],
body[data-arcade="oche"] {
  --bg: #0b0e14; --surface: #131722; --border: #262c3a;
  --text: #eef1f5; --muted: #b7c0cc; --accent: #ffcc00; --accent-weak: #3a3210;
  /* `--live` servait à la fois de texte sur `--live-weak` ET sur
     `--surface` (étiquette "En cours" des `.live-card`, voir style.css plus
     bas) — le vert sombre d'origine (#0b6e4f) donnait ~2,9:1 sur ces deux
     fonds, illisible (min. lisible : 4,5:1). Vert plus vif, ~7,8:1 sur
     `--surface`. */
  --warn: #ff6b7a; --live: #22c55e; --live-weak: #0d2b20; --danger: #ff6b7a;
  --on-accent: #0b0e14;
  font-family: "Barlow Condensed", "Arial Narrow", sans-serif;
}
body.public-page[data-arcade="oche"] header.top,
body[data-arcade="oche"] header.top { border-bottom-width: 4px; border-bottom-color: var(--accent); }
body.public-page[data-arcade="oche"] header.top h1,
body[data-arcade="oche"] header.top h1,
body.public-page[data-arcade="oche"] h2.section-title-primary,
body.public-page[data-arcade="oche"] .score,
body.public-page[data-arcade="oche"] .podium-rank,
body[data-arcade="oche"] .qr-poster-title {
  font-family: "Anton", "Barlow Condensed", sans-serif; letter-spacing: 0.02em;
}
body.public-page[data-arcade="oche"] h2 { letter-spacing: 0.12em; }
body.public-page[data-arcade="oche"] .badge, body.public-page[data-arcade="oche"] .badge-btn,
body[data-arcade="oche"] .badge, body[data-arcade="oche"] .badge-btn {
  text-transform: uppercase; letter-spacing: 0.05em; border-radius: 3px;
}
body.public-page[data-arcade="oche"] .podium-step.gold .podium-rank { background: #ffcc00; color: #0b0e14; }
body.public-page[data-arcade="oche"] .podium-step.silver .podium-rank { background: #b7c0cc; color: #0b0e14; }
body.public-page[data-arcade="oche"] .podium-step.bronze .podium-rank { background: #ff6b7a; color: #0b0e14; }
/* Logo en médaillon dans l'en-tête, à droite du titre — même mécanisme
   que Ty Korrigans/le thème par défaut/Phosphor (`max-width` sur le seul
   h1, `header.top::after` positionné à droite). `position: relative` en
   plus de la règle `border-bottom` déjà posée sur `header.top` plus haut,
   pas à la place. */
body.public-page[data-arcade="oche"] header.top,
body[data-arcade="oche"] header.top {
  position: relative;
}
body.public-page[data-arcade="oche"] header.top h1,
body[data-arcade="oche"] header.top h1 {
  max-width: calc(100% - 132px);
}
body.public-page[data-arcade="oche"] header.top::after,
body[data-arcade="oche"] header.top::after {
  content: ""; position: absolute; top: 0.5rem; right: 1rem;
  width: 108px; height: 108px;
  background: url("/img/oche-badge.svg") no-repeat center / contain;
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.5));
}

/* ---------- Le Maître : palets bretons, granite/marin ---------- */
body.public-page[data-arcade="maitre"],
body[data-arcade="maitre"] {
  --bg: #1c262b; --surface: #232f35; --border: #3c4f57;
  --text: #eef2f0; --muted: #93a5ac; --accent: #d9a441; --accent-weak: #3a2f15;
  --warn: #e2593f; --live: #e2593f; --live-weak: #3a1912;
  /* `--warn`/`--live` (#e2593f) sont assez clairs pour porter du texte
     sombre (voir le bronze du podium juste plus bas) — trop clair pour du
     texte blanc lisible sur le bouton "Forfait" (.forfeit-btn), d'où un
     rouge plus soutenu dédié plutôt qu'une réutilisation. */
  --danger: #c23b28;
  --on-accent: #1c262b;
  font-family: "Public Sans", "Segoe UI", sans-serif;
}
body.public-page[data-arcade="maitre"]::before,
body[data-arcade="maitre"]::before {
  content: ""; position: fixed; top: 0; left: 0; right: 0; height: 10px; z-index: 300;
  background: repeating-linear-gradient(90deg, #0f171b 0 26px, #eef2f0 26px 52px);
}
body.public-page[data-arcade="maitre"] header.top,
body[data-arcade="maitre"] header.top { padding-top: calc(1.25rem + 10px); }
body.public-page[data-arcade="maitre"] header.top h1,
body[data-arcade="maitre"] header.top h1,
body.public-page[data-arcade="maitre"] h2.section-title-primary,
body[data-arcade="maitre"] .qr-poster-title {
  font-family: "Fjalla One", "Barlow Condensed", sans-serif; font-weight: 400; letter-spacing: 0.01em;
}
body.public-page[data-arcade="maitre"] .podium-step.gold .podium-rank { background: #d9a441; color: #1c262b; }
body.public-page[data-arcade="maitre"] .podium-step.silver .podium-rank { background: #3c4f57; color: #eef2f0; }
body.public-page[data-arcade="maitre"] .podium-step.bronze .podium-rank { background: #e2593f; color: #1c262b; }

/* ---------- Boulodrome : pétanque, plaque émaillée de terrasse de café.
   Volontairement clair (gravier au soleil) — rompt avec les quatre pistes
   sombres qui précèdent. ---------- */
body.public-page[data-arcade="boulodrome"],
body[data-arcade="boulodrome"] {
  --bg: #f3e9d2; --surface: #fffaf3; --border: #d9c9a0;
  --text: #1f3d2b; --muted: #7a6a4a; --accent: #b5121b; --accent-weak: #f8dede;
  --warn: #a85d1f; --live: #2f6b3f; --live-weak: #dfe9df;
  /* `--accent` est déjà un rouge profond ici (plaque émaillée) — un rouge
     un peu plus sombre pour le forfait garde les deux boutons
     distinguables au lieu de sembler être la même action. */
  --danger: #7a0f18;
  font-family: "PT Sans", Arial, sans-serif;
}
body.public-page[data-arcade="boulodrome"] header.top,
body[data-arcade="boulodrome"] header.top {
  border-top-width: 6px; border-top-style: solid; border-top-color: var(--accent);
  border-bottom-width: 6px; border-bottom-color: var(--accent);
}
body.public-page[data-arcade="boulodrome"] header.top h1,
body[data-arcade="boulodrome"] header.top h1,
body.public-page[data-arcade="boulodrome"] h2.section-title-primary,
body[data-arcade="boulodrome"] .qr-poster-title {
  font-family: "Bebas Neue", "PT Sans", sans-serif; letter-spacing: 0.04em;
}
body.public-page[data-arcade="boulodrome"] .podium-step.gold .podium-rank { background: #1f3d2b; color: #fff8ec; }
body.public-page[data-arcade="boulodrome"] .podium-step.silver .podium-rank { background: #7a6a4a; color: #fff8ec; }
body.public-page[data-arcade="boulodrome"] .podium-step.bronze .podium-rank { background: #b5121b; color: #fff8ec; }

/* ---------- Quilles du Nord : molkky/jeux en bois, minimalisme scandinave.
   Le plus épuré des cinq — un seul accent (le pin), pas de code couleur
   or/argent/bronze : juste des chiffres, comme des quilles numérotées. ---------- */
body.public-page[data-arcade="molkky"],
body[data-arcade="molkky"] {
  --bg: #f5f0e6; --surface: #ffffff; --border: #e2dccb;
  --text: #1c1c1a; --muted: #8c8672; --accent: #2f5233; --accent-weak: #e3ebe1;
  --warn: #9c5b3c; --live: #2f5233; --live-weak: #e3ebe1;
  /* Thème volontairement réduit à un seul accent (le pin, voir plus haut) —
     un rouge vif jurerait ; une terre cuite reste dans la même famille
     "bois/nature" tout en se distinguant clairement du pin. */
  --danger: #a13d2b;
  font-family: "Karla", "Segoe UI", sans-serif;
}
body.public-page[data-arcade="molkky"] header.top h1,
body[data-arcade="molkky"] header.top h1,
body.public-page[data-arcade="molkky"] h2.section-title-primary,
body[data-arcade="molkky"] .qr-poster-title {
  font-family: "Archivo", sans-serif; font-weight: 800; letter-spacing: 0.01em;
}
body.public-page[data-arcade="molkky"] .podium-step.gold .podium-rank,
body.public-page[data-arcade="molkky"] .podium-step.silver .podium-rank,
body.public-page[data-arcade="molkky"] .podium-step.bronze .podium-rank {
  background: #1c1c1a; color: #fff;
}

/* ---------- Ty Korrigans : club de fléchettes, thème "secret" fait sur
   mesure pour leur instance dédiée — volontairement absent de
   `ARCADE_OPTIONS` (admin.js), pas proposé dans le menu déroulant des
   réglages, activable seulement via un réglage direct
   (`public_theme: "tykorrigans"`). Palette tirée du blason du club
   (public/img/tykorrigans-crest.png) : vert "sous-bois" presque noir en
   fond, rouge "braise" (béret/cible du blason) réservé au statut "en
   direct" plutôt qu'au vert habituel des autres thèmes — les deux accents
   ne se marchent pas dessus. Accent principal en or plutôt qu'en vert
   "korrigan" d'origine (#4fbf93) : un vert clair sur ce fond vert sombre
   passait le seuil de contraste WCAG (~5,5:1) mais restait dur à lire en
   pratique, deux teintes trop proches (highlight des équipes gagnantes,
   badges, boutons...) — l'or, hors famille de couleur, se détache
   toujours du fond quel que soit l'éclairage. Évoque aussi la médaille du
   vainqueur, cohérent avec son usage (`.rank-1`). ---------- */
body.public-page[data-arcade="tykorrigans"],
body[data-arcade="tykorrigans"] {
  --bg: #081815; --surface: #123a30; --border: #1f5445;
  --text: #eef7f2; --muted: #9fc4b6; --accent: #d4af37; --accent-weak: #163f34;
  /* `--live` (vignette/badge "en cours", `.badge.en_cours`) même souci que
     l'ancien `--accent` : le rouge "braise" d'origine (#d43b35) sur son
     fond `--live-weak` (#3a1210, même famille rouge sombre) ne passait que
     ~3,5:1 — un rouge saturé plafonne de toute façon assez bas en
     luminance perçue (canal rouge peu pondéré), impossible à corriger sans
     l'éclaircir vers le corail. Remonté vers un rouge-corail plus clair :
     ~6,4:1 sur `--live-weak`, ~4,9:1 sur `--surface` (utilisé aussi hors
     badge, ex. `.live-tag`). `--live-weak`/`--danger` inchangés. */
  --warn: #e2a33d; --live: #ff7a5c; --live-weak: #3a1210; --danger: #e2483f;
  --on-accent: #08211a;
  font-family: "Mulish", -apple-system, "Segoe UI", sans-serif;
}
/* Variante "jour" — organisateur ET public (`data-theme`, posé par
   `applyTheme()`/`theme_override`, public.js, aussi bien pour l'admin que
   les pages publiques — même mécanisme jour/nuit que le thème par défaut
   plus haut dans ce fichier, longtemps resté sans effet visible pour un
   thème arcade puisqu'aucun n'en profitait). Plus spécifique que le bloc
   ci-dessus (3 sélecteurs simples contre 2), l'emporte sans dépendre de
   l'ordre des règles. Mêmes teintes de marque (or, corail) mais toutes
   réassombries pour rester lisibles sur fond clair plutôt que sur fond
   très sombre — inverser juste fond/texte du bloc ci-dessus aurait cassé
   le contraste de `--accent` (l'or clair, pensé pour se détacher d'un fond
   sombre, tombe à ~1,8:1 sur blanc) : chaque couleur est repensée pour son
   propre fond, même logique que `:root` vs le mode sombre système plus
   haut. Contrastes vérifiés ≥4,5:1 sur toutes les paires réellement
   utilisées (texte/fond, badge fond/texte, bouton blanc/fond). Pense à
   ajouter la même variante (les deux sélecteurs, avec et sans
   `.public-page`) pour un futur thème "club custom" qui en aurait besoin
   (voir CLAUDE.md). */
body.public-page[data-arcade="tykorrigans"][data-theme="light"],
body[data-arcade="tykorrigans"][data-theme="light"] {
  --bg: #f2f6ee; --surface: #ffffff; --border: #d8e0d1;
  --text: #10261c; --muted: #5c6f63;
  --accent: #7d621b; --accent-weak: #faf3de;
  --warn: #9a5f13; --live: #c23f2d; --live-weak: #fdedea; --danger: #b3261e;
  --on-accent: #fff;
}
body.public-page[data-arcade="tykorrigans"] header.top h1,
body[data-arcade="tykorrigans"] header.top h1,
body.public-page[data-arcade="tykorrigans"] h2.section-title-primary,
body[data-arcade="tykorrigans"] .qr-poster-title {
  font-family: "Baloo 2", "Comic Sans MS", sans-serif; font-weight: 700;
}
/* Laisse la place au blason agrandi (voir header.top::after plus bas) : sans
   ça un long nom d'événement passerait dessous sur un écran étroit. */
body.public-page[data-arcade="tykorrigans"] header.top h1,
body[data-arcade="tykorrigans"] header.top h1 {
  max-width: calc(100% - 132px);
}
/* Halo de lune discret derrière le titre — seul effet de signature du
   thème, pensé pour rester lisible plutôt que spectaculaire. */
body.public-page[data-arcade="tykorrigans"] header.top,
body[data-arcade="tykorrigans"] header.top {
  position: relative; overflow: hidden;
}
body.public-page[data-arcade="tykorrigans"] header.top::before,
body[data-arcade="tykorrigans"] header.top::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(420px 220px at 88% -20%, rgba(79, 191, 147, 0.18), transparent 70%);
}
/* Blason en médaillon dans l'en-tête, à côté du titre plutôt qu'en
   remplacement de la marque "Tournamant" — image plutôt que texte, seul
   thème dans ce cas puisque c'est le seul avec un vrai logo de club à
   faire vivre. */
body.public-page[data-arcade="tykorrigans"] header.top::after,
body[data-arcade="tykorrigans"] header.top::after {
  content: ""; position: absolute; top: 0.5rem; right: 1rem;
  width: 108px; height: 108px;
  background: url("/img/tykorrigans-crest.png") no-repeat center / contain;
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.5));
}
body.public-page[data-arcade="tykorrigans"] .podium-step.gold .podium-rank { background: #4fbf93; color: #08211a; }
body.public-page[data-arcade="tykorrigans"] .podium-step.silver .podium-rank { background: #7fa89c; color: #08211a; }
body.public-page[data-arcade="tykorrigans"] .podium-step.bronze .podium-rank { background: #d43b35; color: #2a0605; }

/* ---------- Correctifs communs aux thèmes arcade, utiles surtout côté
   admin (l'écran le plus chargé en badges/messages) : `.msg.error` a une
   couleur codée en dur plutôt que les variables --bg/etc, donc elle
   restait un carré rose clair figé au-dessus d'un fond soudain sombre au
   lieu de suivre le thème. (`.badge.a_venir`/`.badge-btn.a_venir` avaient
   le même souci — corrigé directement à la source plutôt que par une règle
   à part, voir plus haut.) ---------- */
body[data-arcade] .msg.error { background: #3a1a1a; color: #ff8a80; }

/* ---------- Affiche QR code (admin, onglet Réglages) ---------- */
/* `isolation: isolate` : `position: relative` seul ne crée pas de contexte
   d'empilement (il faudrait aussi un `z-index` explicite) — sans lui, le
   filigrane à `z-index: -1` ci-dessous ne reste pas juste "derrière le
   contenu, devant le fond de la carte" comme voulu : il s'échappe carrément
   derrière le fond opaque de `.card` (`background: var(--surface)`), qui
   appartient au même élément mais se peint dans un contexte d'empilement
   plus haut dans l'arbre faute de contexte propre ici — invisible en
   pratique. Bug resté muet jusqu'ici (opacité déjà très faible, jamais
   vraiment comparé à l'œil au rendu voulu). */
.qr-poster { position: relative; isolation: isolate; overflow: hidden; text-align: center; max-width: 340px; }
/* N'existent dans le DOM que pour un thème "sur mesure" avec un blason de
   club (voir CUSTOM_CLUB_CRESTS, admin.js) — pas besoin d'un sélecteur
   `[data-arcade]` ici. */
.qr-poster-crest { display: block; width: 160px; height: auto; margin: 0 auto 0.6rem; }
/* Filigrane derrière toute la carte, jamais derrière le QR lui-même : sa
   lisibilité vient du contraste noir-sur-blanc dans son propre pavé
   (`.qr-poster-imgwrap`, fond blanc opaque), un logo même discret dans son
   dos resterait un risque inutile pour le scan — voir `.qr-poster-img`.
   `z-index: -1` (pas juste "avant les autres dans le DOM") : passe
   automatiquement derrière tout contenu en flux normal de la carte, sans
   avoir à toucher chacun des autres éléments pour les repasser devant.
   Mêmes réglages que le filigrane des diplômes (`drawCrestWatermark`,
   admin.js : opacité 0.07, rotation ~-7°) pour un rendu identique.
   Centré sur toute la carte, il finissait quasi entièrement recouvert par
   le pavé blanc opaque du QR (`.qr-poster-imgwrap`, ~55% de la hauteur de
   la carte) — invisible en pratique, vérifié à l'écran (capture avant/
   après, y compris à pleine opacité) avant un premier correctif (calé en
   haut, sous le blason). Repositionné en bas à droite pour matcher la
   même ancre que sur les diplômes (`drawCrestWatermark`, ~72%/80%) — la
   moitié gauche déborde sous le pavé du QR (masquée, sans effet sur le
   scan puisqu'il reste toujours au-dessus), seule la partie qui dépasse
   en bas et à droite du pavé est réellement visible, comme prévu. */
.qr-poster-watermark {
  position: absolute; z-index: -1; top: 85%; left: 78%;
  width: 55%; max-width: 190px; height: auto;
  transform: translate(-50%, -50%) rotate(-6.9deg);
  opacity: 0.07; pointer-events: none;
}
.qr-poster-title { font-weight: 700; font-size: 1.3rem; color: var(--accent); }
.qr-poster-tagline { color: var(--muted); font-size: 0.85rem; margin-top: 0.3rem; }
/* Le QR reste toujours noir sur blanc dans son propre pavé, quel que soit
   le thème actif — c'est le contraste qui garantit qu'il reste scannable,
   pas une question de goût. Seul le cadre autour (bordure couleur --accent)
   porte l'identité du thème. */
.qr-poster-imgwrap {
  display: inline-block; background: #fff; padding: 0.9rem; border-radius: 10px;
  border: 3px solid var(--accent); margin-top: 1.1rem;
}
.qr-poster-img { width: 230px; height: 230px; display: block; }
.qr-poster-url { margin-top: 0.9rem; font-size: 0.78rem; color: var(--muted); word-break: break-all; }
/* Signature de marque, toujours en bas à droite de la carte quel que soit
   son contenu — même repère que sur les diplômes ("tournamant.bzh" en bas
   à droite, voir `drawDiploma`/`drawPodiumRecap`, admin.js), pour la même
   raison : cette affiche quitte l'appli une fois imprimée ou partagée. */
.qr-poster-brand {
  position: absolute; right: 0.7rem; bottom: 0.5rem;
  font-size: 0.68rem; color: var(--muted); pointer-events: none;
}

/* N'imprime que l'affiche (masque le reste de l'admin) — technique
   classique "print just this element" : tout est caché via `visibility`
   (pas `display`, sans quoi l'affiche — nichée dans <main>/#app/une
   section — disparaîtrait avec ses ancêtres masqués), puis l'affiche et
   son contenu sont explicitement rendus visibles. Elle reprend les
   couleurs du thème actif (fond, bordure, titre) plutôt qu'un noir sur
   blanc générique — les navigateurs n'impriment pas les couleurs de fond
   par défaut, il faut le demander explicitement via `print-color-adjust`. */
@media print {
  @page { size: A4; margin: 0; }
  body * { visibility: hidden; }
  /* Les bandeaux décoratifs de certains thèmes (rayure marinière de Le
     Maître, trame CRT de Phosphor) sont posés en ::before/::after sur
     `body` lui-même, donc pas atteints par `body *` ci-dessus. */
  body::before, body::after { visibility: hidden !important; }
  /* `visibility: hidden` masque le reste de la page mais ne le retire pas
     du flux — resté à sa hauteur normale (toute la page organisateur),
     ça dépassait la hauteur d'une page A4 et imprimait une deuxième page
     blanche derrière l'affiche. Aplati à hauteur 0 (et débordement coupé)
     pour ne laisser que l'affiche, qui n'en dépend plus une fois sortie du
     flux ci-dessous (`position: fixed`). */
  body { height: 0; overflow: hidden; }
  .qr-poster, .qr-poster * { visibility: visible; }
  .qr-poster {
    /* `fixed` plutôt que le `absolute` d'avant : ancrée directement à la
       page d'impression (100% = la feuille A4 posée par `@page` ci-dessus)
       plutôt qu'à un ancêtre — et donc bien en 210×297mm plutôt qu'un petit
       pavé de 380px perdu au milieu de la page. */
    position: fixed; inset: 0; width: 100%; height: 100%;
    box-sizing: border-box; margin: 0; max-width: none; padding: 20mm;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1.5rem;
    box-shadow: none; border: 3px solid var(--accent);
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  /* Le QR doit dominer la page — c'est lui qu'on scanne de loin, tout le
     reste (blason, titre, url) n'est là qu'en repère secondaire. Blason et
     titre réduits pour lui laisser la place plutôt que de rester à leur
     taille "carte d'aperçu" agrandie telle quelle sur une A4 entière. */
  .qr-poster-crest { width: 44mm; margin: 0; }
  .qr-poster-watermark { width: 140mm; max-width: none; }
  .qr-poster-title { font-size: 1.9rem; }
  .qr-poster-tagline { font-size: 1rem; }
  .qr-poster-imgwrap { padding: 12mm; }
  .qr-poster-img { width: 140mm; height: 140mm; }
  .qr-poster-url { font-size: 1.05rem; }
  .qr-poster-brand { right: 12mm; bottom: 10mm; font-size: 0.85rem; }
}

/* ---------- Diplômes (admin, onglet Diplômes d'une activité terminée) ----------
   Le canvas est dessiné en pixels fixes (1600x1131, voir admin.js) puis
   affiché à taille réduite ici (`width: 100%` sur une carte étroite) — le
   fichier téléchargé garde sa pleine résolution, seul l'aperçu est mis à
   l'échelle. */
.diploma-grid { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.75rem; }
.diploma-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.75rem; width: 280px; text-align: center;
}
.diploma-canvas { width: 100%; height: auto; border-radius: 4px; display: block; margin-bottom: 0.6rem; }
