/* =============================================================================
   VAATI — la seule source de vérité du style.
   =============================================================================

   ⚠ Le style vivait en QUATRE exemplaires : 1 530 lignes de CSS en ligne
   réparties sur quatre pages, sans aucune feuille externe, les douze variables
   de palette redéfinies dans chacune. `login.html` n'en déclarait que onze —
   une couleur y avait déjà divergé.

   C'est ce qui expliquait les chiffres de l'audit #48 : 81 couleurs en dur pour
   douze variables, 11 tailles de police, 11 styles de bouton dont sept uniques.
   La palette n'était pas ignorée par négligence : la maintenir supposait de la
   corriger quatre fois.

   Tout ce qui suit est un JETON. Aucune valeur ne doit être écrite en dur
   ailleurs.
   ============================================================================= */

/* --- La police, embarquée -------------------------------------------------
   Les quatre pages la chargeaient depuis Google Fonts. Un bot auto-hébergé qui
   sert ses propres pages n'a pas à dépendre d'un tiers pour son apparence — ni
   à lui faire connaître l'adresse de chacun de ses visiteurs. */
@font-face {
    font-family: 'Crimson Text';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/polices/crimson-latin.woff2') format('woff2');
}

@font-face {
    font-family: 'Cinzel';
    font-style: normal;
    font-weight: 400 600;
    font-display: swap;
    src: url('/polices/cinzel-latin.woff2') format('woff2');
}

:root {
    /* --- Palette : cinq couleurs, un rôle chacune -------------------------
       Les contrastes sont CALCULÉS, pas estimés — rapport WCAG sur --abysse. */
    --abysse:    #0E0618;   /* le fond, partout                              */
    --parchemin: #EDE7F2;   /* texte principal            — 16,4:1           */
    --brume:     #B8A8C8;   /* texte secondaire, libellés —  8,9:1           */
    --lueur:     #C084FC;   /* accents, liens, focus      —  7,5:1           */
    --oeil:      #F0503C;   /* LA signature, et les alertes — 5,6:1          */
    --murmure:   #8E7FA4;   /* texte volontairement discret —  5,4:1         */

    /* ⚠ `--murmure` remplace #3a2050 et #4a2070, mesurés à 1,42:1 et 1,65:1.
       Le seuil est 4,5:1. Ces deux teintes servaient à dire « discret » et
       disaient en réalité « invisible » : les messages d'état vide — « le bot
       n'a encore vu personne se connecter » — étaient illisibles sur les trois
       pages. Un texte qu'on ne peut pas lire ne se distingue pas d'un texte
       absent, et c'est précisément celui qui explique une page vide. */

    /* Nuances de fond, dérivées de l'abysse. */
    --abysse-clair: #150F1E;
    --abysse-haut:  #1E1530;

    /* ⚠ Ces deux-là ne passent PAS le seuil de lisibilité : 2,88:1 et 1,64:1.
       Elles restent — l'identité en dépend — mais JAMAIS pour du texte.
       Bordures et aplats seulement. C'est une règle, pas une suppression. */
    --sortilege: #7A3FA0;
    --bordure:   #4A2070;

    /* --- Typographie : trois rôles ----------------------------------------
       Titres en Cinzel, l'arcane de Vaati ; texte en Crimson Text, la serif du
       projet. Les deux sont EMBARQUÉES — l'esthétique est conservée, et elle ne
       dépend plus de Google. La pile système ne sert que de repli.

       Chiffres en tabulaire, pour que les colonnes de ports, de checks et de
       pourcentages s'alignent au lieu de se décaler d'un caractère à l'autre. */
    /* ⚠ Le repli emoji est EXPLICITE. Les noms de guildes et de salons Discord en
       contiennent couramment — « 🌀-archipelago ». Les deux polices embarquées ne
       couvrent que le latin : sans famille emoji nommée, le rendu dépend de la
       chaîne de repli du navigateur, qui varie d'un système à l'autre. */
    --emoji:   'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
    --titre:   'Cinzel', Georgia, var(--emoji), serif;
    --texte:   'Crimson Text', Georgia, var(--emoji), serif;
    --texte-repli: system-ui, -apple-system, 'Segoe UI', sans-serif;
    --chiffre: ui-monospace, 'Cascadia Mono', 'Consolas', monospace;

    /* Échelle fixe, SIX pas — là où onze valeurs se disputaient la place. */
    --t-xs: 11px;
    --t-s:  13px;
    --t-m:  15px;
    --t-l:  18px;
    --t-xl: 24px;
    --t-xxl: 32px;

    /* --- Espacement : base 4, six pas ------------------------------------- */
    --e-1: 4px;
    --e-2: 8px;
    --e-3: 12px;
    --e-4: 16px;
    --e-5: 24px;
    --e-6: 32px;

    /* --- Les paliers de progression ---------------------------------------
       ⚠ Contrastes CALCULÉS sur la piste de jauge (--abysse-clair), pas choisis
       à l'œil. Le seuil WCAG pour un objet graphique porteur de sens est 3:1.

       ⚠ Le dégradé qu'ils remplacent partait de --sortilege, à 2,88:1 — SOUS le
       seuil. La moitié gauche de chaque jauge était déjà trop pâle, et personne
       ne l'avait vu : le #49 avait relevé cette couleur pour du texte, jamais
       pour une jauge.

       ⚠ Ces teintes ne portent JAMAIS l'information seules — rouge et vert sont
       indistinguables pour environ 8 % des hommes. Le pourcentage et le compte
       sont toujours écrits à côté. */
    --palier-faible:  #F0503C;   /*   0–24 %  —  5,29:1  (c'est --oeil)        */
    --palier-basse:   #F5A623;   /*  25–49 %  —  9,26:1                        */
    --palier-moyenne: #E8C547;   /*  50–74 %  — 11,18:1                        */
    --palier-haute:   #4ADE80;   /*  75–99 %  — 10,77:1                        */
    --palier-pleine:  #FFD166;   /*    100 %  — 13,02:1                        */

    /* --- UN seul rayon. Les cinq précédents disparaissent. ----------------- */
    --rayon: 3px;

    /* --- Compatibilité ----------------------------------------------------
       ⚠ Ces alias portent les anciens noms, utilisés par les 1 530 lignes déjà
       en place. Ils permettent de retirer les quatre blocs `:root` dupliqués
       SANS réécrire l'existant : les pages continuent de fonctionner, et il n'y
       a plus qu'un endroit où changer une couleur.

       À résorber écran par écran, pas d'un coup. */
    --purple:       var(--sortilege);
    --purple-light: #B06AD4;
    --purple-dark:  #3A1A5A;
    --purple-glow:  var(--lueur);
    --dark:         var(--abysse);
    --dark-mid:     var(--abysse-clair);
    --dark-light:   var(--abysse-haut);
    --magenta:      #D946EF;
    --magenta-dark: #7A1A8A;
    --text-light:   var(--parchemin);
    --text-mid:     var(--lueur);
    --border:       var(--bordure);
}

/* --- Le focus clavier, visible ---------------------------------------------
   Il ne l'était nulle part : naviguer au clavier revenait à avancer à l'aveugle. */
:focus-visible {
    outline: 2px solid var(--lueur);
    outline-offset: 2px;
    border-radius: var(--rayon);
}

/* --- L'ŒIL DE VAATI : l'élément signature ----------------------------------

   Une seule audace, et elle n'est pas décorative.

   Ce bot est un PUR OBSERVATEUR — constat du #27 : ni LocationChecks, ni Say,
   ni Bounce. Il regarde une partie et rapporte, sans jamais y toucher. Vaati,
   dans ses formes finales, EST un œil géant qui observe.

   L'œil porte donc l'état de connexion : ouvert et lumineux quand le bot suit
   une room, mi-clos et éteint quand elle est perdue. Ce que quatre écrans
   peinent à dire, il le dit d'un regard.
   --------------------------------------------------------------------------- */
.oeil {
    --taille: 22px;
    position: relative;
    display: inline-block;
    width: var(--taille);
    height: var(--taille);
    border-radius: 50% 0;
    transform: rotate(45deg);
    background: radial-gradient(circle at 50% 50%,
                var(--oeil) 0 22%, var(--sortilege) 23% 46%, var(--abysse) 47%);
    box-shadow: 0 0 12px color-mix(in srgb, var(--oeil) 55%, transparent);
    transition: box-shadow .4s ease, opacity .4s ease;
    vertical-align: middle;
}

/* Connexion perdue : la paupière tombe, la lueur s'éteint. */
.oeil[data-etat="ferme"] {
    opacity: .35;
    box-shadow: none;
    background: radial-gradient(circle at 50% 50%,
                var(--brume) 0 22%, var(--bordure) 23% 46%, var(--abysse) 47%);
}

/* La pulsation ne dit rien qu'un état statique ne dise déjà : elle attire
   l'attention, sans plus. Elle est donc la première chose à taire. */
@media (prefers-reduced-motion: no-preference) {
    .oeil[data-etat="ouvert"] { animation: veille 4s ease-in-out infinite; }
}

@keyframes veille {
    0%, 100% { box-shadow: 0 0 12px color-mix(in srgb, var(--oeil) 55%, transparent); }
    50%      { box-shadow: 0 0 20px color-mix(in srgb, var(--oeil) 75%, transparent); }
}

/* --- Réduction des animations, partout -------------------------------------
   Le réglage système est respecté sans exception : ce qui bouge s'arrête. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

/* --- Les chiffres s'alignent -----------------------------------------------
   Ports, checks, pourcentages, dates : en colonne, ils doivent se comparer
   d'un coup d'œil, pas se décaler d'un caractère à l'autre. */
.chiffre, td.chiffre {
    font-family: var(--chiffre);
    font-variant-numeric: tabular-nums;
}

/* --- Les formulaires de configuration ---------------------------------------
   ⚠ Il n'y avait ni message d'erreur par champ, ni aide, ni valeur par défaut
   affichée : trente-cinq champs, et pour tout retour un mot — « Erreur » — dans
   une notification qui s'efface au bout de trois secondes.
   --------------------------------------------------------------------------- */

/* Les liens de l'en-tête d'`index.html`. Leur style vivait dans une chaîne de
   caractères, répété trois fois avec ses huit déclarations en ligne — parce que
   la barre était composée en `innerHTML`. Elle ne l'est plus. */
.lien-entete {
    display: inline-flex;
    align-items: center;
    padding: var(--e-1) var(--e-3);
    font-family: var(--titre);
    font-size: var(--t-xs);
    color: var(--brume);
    text-decoration: none;
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
}

.lien-entete:hover { color: var(--lueur); border-color: var(--sortilege); }

/* --- La première visite ------------------------------------------------------
   ⚠ `login.html` ne disait nulle part ce que fait le bot : un nom, une devise, un
   bouton. Qui arrive là n'a rien pour décider s'il veut continuer.
   --------------------------------------------------------------------------- */
.promesse {
    margin: var(--e-4) 0;
    font-size: var(--t-m);
    line-height: 1.5;
    color: var(--parchemin);
    text-align: left;
}

/* La preuve est DESSINÉE, pas photographiée : une capture serait un binaire à
   refaire à chaque changement d'interface, et elle vieillirait en silence. */
.apercu {
    margin: 0 0 var(--e-4);
    padding: var(--e-3);
    text-align: left;
    background: #313338;                 /* le fond d'un salon Discord */
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
}

.apercu-ligne { display: flex; gap: var(--e-3); align-items: flex-start; }

.apercu-avatar {
    flex: none;
    width: 32px; height: 32px;
    display: grid; place-items: center;
    border-radius: 50%;
    background: var(--abysse-haut);
    color: var(--oeil);
}

.apercu-nom { font-family: var(--titre); font-size: var(--t-s); color: var(--lueur); }

/* ⚠ La pastille « BOT » de Discord est minuscule dans l'original — je l'avais
   reproduite à 10 px, et le banc l'a refusée dans la seconde. Elle suit donc
   l'échelle du projet : discrète sur grand écran, lisible sur téléphone. La
   ressemblance ne justifie pas de recopier un défaut. */
.apercu-marque {
    margin-left: var(--e-1);
    padding: 0 var(--e-1);
    font-size: var(--t-xs);
    color: #fff;
    background: #5865f2;
    border-radius: 2px;
    vertical-align: middle;
}

/* La barre de couleur à gauche : la signature visuelle d'un embed Discord. */
.apercu-embed {
    margin-top: var(--e-2);
    padding: var(--e-2) var(--e-3);
    font-size: var(--t-s);
    color: #dbdee1;
    background: #2b2d31;
    border-left: 3px solid var(--lueur);
    border-radius: 0 var(--rayon) var(--rayon) 0;
}

.apercu-embed-2 { border-left-color: var(--oeil); }

.apercu figcaption {
    margin-top: var(--e-3);
    font-size: var(--t-s);
    font-style: italic;
    color: var(--brume);
    text-align: center;
}

/* --- La couleur d'une barre suit son palier ----------------------------------
   Une seule déclaration par palier, employée par la page publique ET par la page
   de suivi : les deux racontent la même partie, elles doivent la raconter de la
   même façon.
   --------------------------------------------------------------------------- */
[data-palier="faible"]  { background: var(--palier-faible)  !important; }
[data-palier="basse"]   { background: var(--palier-basse)   !important; }
[data-palier="moyenne"] { background: var(--palier-moyenne) !important; }
[data-palier="haute"]   { background: var(--palier-haute)   !important; }

/* Une partie finie mérite mieux qu'un aplat : le doré est la seule teinte de la
   gamme qui se remarque sans qu'on la cherche. */
[data-palier="pleine"] {
    background: linear-gradient(90deg, var(--palier-haute), var(--palier-pleine)) !important;
}

/* Le compte sous la barre — « 110 / 120 ». Un pourcentage seul ne dit pas si
   50 % vaut cinq lieux ou quatre cents. */
.compte-lieux {
    font-family: var(--chiffre);
    font-variant-numeric: tabular-nums;
    font-size: var(--t-s);
    color: var(--brume);
}

/* --- Les quatre états d'une zone ---------------------------------------------
   ⚠ Neuf zones échouaient en SILENCE : un `return` nu laissait le tableau sur
   ses données précédentes, ou sur « Chargement… », pour toujours. Rien ne
   distinguait « le serveur est tombé » de « ça arrive ».
   --------------------------------------------------------------------------- */

/* Le squelette occupe la MÊME hauteur qu'une ligne réelle : sans cela, la page
   se dépliait d'un coup à l'arrivée des données, et ce qu'on lisait en dessous
   sautait. */
.squelette {
    height: 1.25rem;
    margin: var(--e-2) 0;
    border-radius: var(--rayon);
    background: linear-gradient(90deg,
                var(--abysse-clair) 0%, var(--abysse-haut) 50%, var(--abysse-clair) 100%);
    background-size: 200% 100%;
}

@media (prefers-reduced-motion: no-preference) {
    .squelette { animation: attente 1.4s ease-in-out infinite; }
}

@keyframes attente {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* Le bouton de réessai vit DANS le message d'erreur : c'est là qu'on le
   cherche, pas dans un coin de la page. */
.etat-panne .btn {
    display: block;
    margin: var(--e-3) auto 0;
}

/* --- Le partage public d'une partie ------------------------------------------
   ⚠ Il vivait dans un bandeau a part, AU-DESSUS de l'en-tete. Le lien d'une
   partie est pourtant une de ses proprietes, au meme titre que son serveur et
   son login : le separer obligeait a le chercher ailleurs que la ou on lit tout
   le reste. Il est desormais une ligne du bloc d'informations.
   --------------------------------------------------------------------------- */
.ligne-partage {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--e-2);
    margin-top: var(--e-3);
    padding-top: var(--e-3);
    border-top: 1px solid var(--bordure);
}

.partage-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--e-2);
    flex: 1 1 20rem;
}

/* Le lien en clair, dans un champ. Un bouton seul ne permet ni de le relire, ni
   de le copier a la main quand le presse-papiers refuse — ce qui arrive des que
   la page n'est pas servie en HTTPS. */
.champ-lien {
    flex: 1 1 22rem;
    min-width: 0;
    padding: var(--e-1) var(--e-2);
    font-family: var(--chiffre);
    font-size: var(--t-s);
    color: var(--brume);
    background: var(--abysse);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
}

.champ-lien:focus { color: var(--parchemin); border-color: var(--lueur); }

/* Une mention volontairement en retrait, mais lisible : état vide, absence de
   donnée, précision secondaire. Remplaçait deux `style=` en ligne à 1,4:1. */
.mention-discrete {
    color: var(--murmure);
    font-style: italic;
    font-size: var(--t-s);
}

/* --- Le lien de retour ------------------------------------------------------
   ⚠ Il n'existait que dans le `<style>` de la page de suivi. Reutilise sur la
   page d'un serveur, il tombait au bleu par defaut du navigateur — mesure a
   2,11:1 pour un seuil de 4,5:1, et le banc l'a signale des la premiere mesure.

   Une classe definie dans UNE page n'est pas une classe : c'est un style local
   qui ressemble a une classe, et qui ment des qu'on l'emprunte. */
.back {
    display: inline-flex;
    align-items: center;
    color: var(--brume);
    text-decoration: none;
    font-family: var(--titre);
    font-size: var(--t-s);
    padding: var(--e-2) var(--e-3);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
    transition: border-color .15s ease, color .15s ease;
    min-height: 44px;
}

.back:hover,
.back:focus-visible {
    border-color: var(--lueur);
    color: var(--lueur);
}

/* --- La courbe d'évolution ---------------------------------------------------
   SVG écrit à la main, aucune bibliothèque — le #49 a embarqué les polices pour
   ne plus dépendre d'un tiers, charger un moteur de graphiques depuis un CDN
   reviendrait exactement dessus. */
.graphe {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
}

.graphe .grille {
    stroke: var(--bordure);
    stroke-width: 1;
}

/* ⚠ `--t-s`, pas une valeur en dur : ce jeton passe à 15 px sous 900 px, donc les
   graduations respectent le plancher de lisibilité comme le reste. Écrites à 11 px,
   elles étaient sous ce plancher — et le `viewBox` faisant exactement la largeur du
   conteneur, ces 11 px étaient bien 11 px à l'écran, pas une illusion d'échelle. */
.graphe .graduation {
    fill: var(--murmure);
    font-family: var(--chiffre);
    font-size: var(--t-s);
}

.graphe .courbe {
    fill: none;
    stroke-width: 1.75;
    stroke-linejoin: round;
    stroke-linecap: round;
}

/* Le total passe par-dessus, plus épais, dans la couleur des jauges pleines. */
.graphe .courbe-total {
    stroke: var(--parchemin);
    stroke-width: 3;
}

.legende {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: var(--e-2) var(--e-4);
    margin: var(--e-3) 0 0;
    padding: 0;
    font-size: var(--t-s);
    color: var(--brume);
}

.legende li {
    display: flex;
    align-items: center;
    gap: var(--e-2);
}

.legende b { color: var(--parchemin); font-variant-numeric: tabular-nums; }

.pastille {
    width: 12px;
    height: 12px;
    border-radius: 2px;
    flex: 0 0 auto;
}

.pastille-total { background: var(--parchemin); }

/* --- Les compteurs d'en-tête -------------------------------------------------
   ⚠ Ils ne vivaient que dans le `<style>` de la page d'un serveur. Réutilisés
   sur la page d'administration d'un serveur, ils y sont apparus SANS AUCUN
   style : trois libellés et trois nombres empilés en vrac. C'est exactement la
   faute de `.back` deux heures plus tôt — une classe définie dans UNE page n'est
   pas une classe, c'est un style local qui ment dès qu'on l'emprunte. */
.global-stats {
    display: flex;
    gap: var(--e-3);
    margin-bottom: var(--e-4);
    flex-wrap: wrap;
}

.global-stat {
    flex: 1;
    min-width: 160px;
    background: linear-gradient(160deg, var(--abysse-haut), var(--abysse-clair));
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
    padding: var(--e-4) var(--e-5);
    text-align: center;
}

.global-stat .label {
    font-family: var(--titre);
    font-size: var(--t-s);
    color: var(--brume);
    margin-bottom: var(--e-1);
}

.global-stat .value {
    font-family: var(--titre);
    font-size: var(--t-xl);
    color: var(--lueur);
}

/* --- La grille des serveurs Discord -----------------------------------------
   L'accueil ne montre plus les portails de tous les serveurs mêlés : il montre
   les serveurs, et on entre dans celui qu'on veut.

   ⚠ La grille reste affichée même avec un seul serveur — décision du
   propriétaire. Le chemin est alors le même pour tout le monde, quel qu'en soit
   le nombre. */
.grille-serveurs {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--e-4);
}

.tuile-serveur {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--e-3);
    padding: var(--e-5) var(--e-4);
    background: var(--abysse-clair);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
    color: var(--parchemin);
    text-decoration: none;
    text-align: center;
    transition: border-color .2s ease, transform .2s ease;
    /* Une tuile est une CIBLE : elle doit se toucher au doigt sans viser. */
    min-height: 160px;
    justify-content: center;
}

.tuile-serveur:hover,
.tuile-serveur:focus-visible {
    border-color: var(--lueur);
    transform: translateY(-2px);
}

/* L'icône du serveur, ou son initiale quand il n'en a pas. */
.tuile-icone {
    width: 64px;
    height: 64px;
    border-radius: 50%;
    object-fit: cover;
    background: var(--abysse-haut);
    border: 1px solid var(--bordure);
}

.tuile-initiale {
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--titre);
    font-size: var(--t-xl);
    color: var(--lueur);
}

.tuile-nom {
    font-family: var(--titre);
    font-size: var(--t-m);
    letter-spacing: .5px;
    /* Un nom de serveur Discord peut faire cent caractères sans un espace. */
    overflow-wrap: anywhere;
}

/* ⚠ La suspension se voit DÈS la grille, et pas seulement par la couleur : le
   mot « Suspendu » est écrit. Rouge et vert sont indistinguables pour environ
   8 % des hommes — une bordure teintée seule ne dirait rien à une personne sur
   douze, exactement comme les jauges du #61. */
.tuile-suspendue {
    border-color: var(--oeil);
    background: color-mix(in srgb, var(--oeil) 8%, var(--abysse-clair));
}

.tuile-etat {
    font-family: var(--titre);
    font-size: var(--t-s);
    color: var(--oeil);
    letter-spacing: .5px;
}

/* --- L'indicateur de fraîcheur ----------------------------------------------

   ⚠ Il n'existe QUE lorsqu'il a quelque chose à apprendre — le module ne le crée
   pas quand tout va bien. Un bandeau « en direct » permanent occuperait l'écran
   pour dire ce que l'absence de problème dit déjà, et on cesserait de le voir :
   donc on ne le verrait pas non plus quand il change.

   ⚠ En BAS À GAUCHE, à l'opposé de la pile de notifications : les deux peuvent
   s'afficher en même temps — une panne produit l'un et l'autre — et se
   chevaucher aurait rendu les deux illisibles. */
.fraicheur {
    position: fixed;
    left: var(--e-4);
    bottom: var(--e-4);
    z-index: 400;
    display: flex;
    align-items: center;
    gap: var(--e-3);
    padding: var(--e-2) var(--e-3);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
    background: var(--abysse-clair);
    box-shadow: 0 4px 16px rgba(0, 0, 0, .5);
    font-family: var(--titre);
    font-size: var(--t-xs);
    color: var(--parchemin);
    max-width: min(90vw, 30rem);
}

/* La gravité se lit à la bordure, pas seulement au texte — mais le texte reste
   explicite : « Hors ligne », « Reconnexion… », jamais une pastille seule. */
.fraicheur-reconnexion { border-left: 3px solid var(--brume); }
.fraicheur-perime { border-left: 3px solid var(--oeil); }
.fraicheur-hors-ligne { border-left: 3px solid var(--oeil); }

/* --- La pile de notifications -----------------------------------------------

   ⚠ Le conteneur de chaque page — `#toast`, ou `#notification` sur la page
   d'une partie — devient une PILE. Son ancienne apparence vivait dans `.toast`,
   recopié dans quatre feuilles, et dans `.notification`. Le module JavaScript
   remplace la classe par celle-ci : les anciennes règles ne s'appliquent donc
   plus, sans qu'on ait à les retirer partout, et la position devient la même
   sur toutes les pages.

   ⚠ Elle croît vers le HAUT, et les nouveaux messages s'ajoutent en bas. Insérer
   par le haut déplacerait sous le curseur les messages déjà lus. */
.pile-notes {
    position: fixed;
    right: var(--e-4);
    bottom: var(--e-4);
    z-index: 400;
    display: flex;
    flex-direction: column;
    gap: var(--e-2);
    /* Aucun fond ni bordure : c'est un conteneur, pas un message. */
    background: none;
    border: none;
    padding: 0;
    /* ⚠ La pile ne doit pas intercepter les clics de la page qu'elle survole ;
       chaque message, lui, les reprend. */
    pointer-events: none;
    max-width: min(90vw, 26rem);
}

.note {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--e-3);
    padding: var(--e-3) var(--e-4);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
    background: var(--abysse-clair);
    box-shadow: 0 4px 16px rgba(0, 0, 0, .5);
    font-size: var(--t-s);
    color: var(--parchemin);
    animation: note-entree .18s ease-out;
}

/* ⚠ La nature se lit à la BORDURE et au symbole, jamais à la seule couleur :
   rouge et vert sont indistinguables pour environ 8 % des hommes. */
.note-reussite { border-left: 3px solid var(--lueur); }
.note-echec { border-left: 3px solid var(--oeil); }

.note-reussite .note-texte::before { content: '✔ '; color: var(--lueur); }
.note-echec .note-texte::before { content: '⚠ '; color: var(--oeil); }

.note-texte { flex: 1; overflow-wrap: anywhere; }

.note-fermer {
    flex: none;
    background: none;
    border: none;
    color: var(--murmure);
    font-size: var(--t-l);
    line-height: 1;
    cursor: pointer;
    padding: 0 var(--e-1);
}

.note-fermer:hover { color: var(--parchemin); }

.note-fermer:focus-visible {
    outline: 2px solid var(--lueur);
    outline-offset: 2px;
}

@keyframes note-entree {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

/* ⚠ Un message qui glisse à chaque confirmation, sur une page où l'on en
   déclenche vingt d'affilée, donne la nausée à qui y est sensible. L'état reste
   parfaitement lisible sans animation. */
@media (prefers-reduced-motion: reduce) {
    .note { animation: none; }
}

/* --- Le réglage d'un fil ----------------------------------------------------- */

/* ⚠ Un avertissement qui n'est PAS une erreur : la liste d'objets n'est pas
   encore arrivée du serveur Archipelago, ce qui se résout tout seul. D'où
   l'ambre plutôt que le rouge — et le texte, qui dit quoi faire (attendre)
   plutôt que de constater. */
/* ⚠ Le TROISIÈME niveau du #66 : l'action requise. Il ne s'efface pas seul,
   parce qu'il décrit un état qui demande une intervention — une liste d'hôtes
   vide interdit toute connexion, une liste d'objets manquante empêche tout
   réglage. Un message temporaire aurait disparu avant qu'on ait compris.

   ⚠ Il unifie deux styles concurrents : cette classe, et un bloc rouge écrit en
   attributs `style=` dans `admin.html` et `admin-guilde.html`. Deux apparences
   pour la même intention, dont l'une échappait à toute feuille — donc au thème,
   au contraste mesuré du #61, et à toute correction ultérieure. */
.bandeau-avertissement {
    border: 1px solid var(--oeil);
    background: color-mix(in srgb, var(--oeil) 8%, var(--abysse-clair));
    border-radius: var(--rayon);
    padding: var(--e-3) var(--e-4);
    margin-bottom: var(--e-4);
    font-size: var(--t-s);
    font-family: var(--titre);
    color: var(--parchemin);
}

/* Le même bandeau, quand il s'agit d'un blocage et non d'une attente. */
.bandeau-avertissement.bandeau-grave {
    border-color: var(--oeil);
    background: color-mix(in srgb, var(--oeil) 14%, var(--abysse-clair));
}

/* ⚠ Deux tableaux côte à côte : un jeu déclare plusieurs centaines d'objets, et
   sur une seule colonne la page fait des mètres — le sélecteur de famille sort
   de l'écran dès qu'on descend.

   ⚠ Une seule colonne sous 900 px. À deux, chaque tableau aurait moins de la
   moitié d'un écran de téléphone pour quatre colonnes, dont un nom d'objet qui
   peut faire trente caractères : tout se serait replié en escalier. */
.deux-colonnes {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--e-4);
    align-items: start;
}

@media (max-width: 900px) {
    .deux-colonnes { grid-template-columns: 1fr; }
}

/* ⚠ Le titre d'une famille traverse le tableau. Il doit se distinguer d'une ligne
   d'objet au premier coup d'œil, sinon la liste redevient plate : d'où le fond,
   la casse et la couleur d'accent — trois signaux plutôt qu'un. */
.ligne-famille th {
    padding: 0;
    background: var(--abysse-haut);
    border-top: 1px solid var(--bordure);
}

/* ⚠ Le bouton occupe TOUTE la cellule. Une cible de la taille du texte seul
   obligerait à viser le titre au pixel près, sur une liste qui en compte
   plusieurs dizaines. */
.ligne-famille button {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--lueur);
    font-family: var(--titre);
    font-size: var(--t-s);
    letter-spacing: .5px;
    padding: var(--e-3);
    /* Un nom de famille peut être long — « A Town in Chaos » — et la colonne est
       étroite quand deux tableaux se partagent l'écran. */
    overflow-wrap: anywhere;
}

.ligne-famille button:hover { background: color-mix(in srgb, var(--lueur) 10%, transparent); }

.ligne-famille button:focus-visible {
    outline: 2px solid var(--lueur);
    outline-offset: -2px;
}

/* ⚠ La flèche double l'information de `aria-expanded`, qui ne se voit pas. Ouvert
   et fermé doivent se distinguer à l'œil autant qu'au lecteur d'écran. */
.famille-fleche {
    display: inline-block;
    width: 1em;
    color: var(--brume);
}

/* Le nombre d'objets de la famille : présent, mais il ne doit pas concurrencer
   le nom. */
.famille-compte {
    color: var(--murmure);
    font-family: var(--chiffre);
    font-size: var(--t-xs);
}

/* ⚠ Les lignes repliées sont MASQUÉES, pas retirées du HTML : la recherche du
   navigateur ne trouve que ce qui existe, et Ctrl+F est un usage courant sur une
   page de plusieurs centaines d'objets. */
.famille:not(.ouverte) tr:not(.ligne-famille) { display: none; }

/* --- L'interrupteur ----------------------------------------------------------

   ⚠ C'est une VRAIE case à cocher, redessinée — pas un <div> qui lui ressemble.
   Elle garde donc le clavier, l'espace pour basculer, le focus visible et son
   `aria-label`, tout ce qu'un faux interrupteur oblige à reconstruire à la main
   et qu'on reconstruit toujours à moitié.

   ⚠ L'état se lit à la POSITION du bouton, pas seulement à la couleur. Rouge et
   vert sont indistinguables pour environ 8 % des hommes, et cette page en aligne
   plusieurs centaines : une teinte seule aurait rendu la liste illisible pour
   une personne sur douze — le constat du #61, déjà appliqué aux jauges. */
.bascule {
    appearance: none;
    -webkit-appearance: none;
    /* Assez grand pour se viser au doigt : une case native fait 13 px. */
    width: 44px;
    height: 24px;
    border-radius: 12px;
    background: var(--abysse-haut);
    border: 1px solid var(--bordure);
    position: relative;
    cursor: pointer;
    flex: none;
    transition: background-color .15s ease, border-color .15s ease;
}

.bascule::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 2px;
    transform: translateY(-50%);
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--murmure);
    transition: left .15s ease, background-color .15s ease;
}

.bascule:checked {
    background: color-mix(in srgb, var(--lueur) 30%, var(--abysse-haut));
    border-color: var(--lueur);
}

.bascule:checked::after {
    left: 22px;
    background: var(--lueur);
}

.bascule:focus-visible {
    outline: 2px solid var(--lueur);
    outline-offset: 2px;
}

/* ⚠ Le mouvement se coupe pour qui l'a demandé au système. Un glissement par
   ligne sur une liste de trois cents objets n'est pas anodin : c'est le genre
   d'animation qui donne la nausée à qui y est sensible. L'état, lui, reste
   parfaitement lisible sans transition. */
@media (prefers-reduced-motion: reduce) {
    .bascule,
    .bascule::after { transition: none; }
}

/* Un plafond tient en cinq chiffres : un champ pleine largeur par ligne aurait
   poussé la colonne « déjà annoncés » hors de l'écran sur un téléphone. */
.saisie-courte {
    width: 6.5em;
    min-width: 0;
}

/* --- Les fils qu'on possède -------------------------------------------------
   ⚠ La même grille que les serveurs, mais des tuiles ALIGNÉES À GAUCHE et sans
   icône : elles portent trois lignes de texte de longueurs très inégales — un
   nom de serveur, un nom de salon, un nom de slot — que le centrage aurait
   rendues illisibles en escalier. */
.grille-slots {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--e-4);
}

.tuile-slot {
    display: flex;
    flex-direction: column;
    gap: var(--e-2);
    padding: var(--e-4);
    background: var(--abysse-clair);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
    color: var(--parchemin);
    text-decoration: none;
    transition: border-color .2s ease, transform .2s ease;
    min-height: 120px;
    justify-content: center;
}

.tuile-slot:hover,
.tuile-slot:focus-visible {
    border-color: var(--lueur);
    transform: translateY(-2px);
}

/* Le serveur est le contexte, pas le sujet : il s'efface devant le slot. */
.slot-guilde {
    font-family: var(--titre);
    font-size: var(--t-s);
    color: var(--murmure);
    letter-spacing: .5px;
    overflow-wrap: anywhere;
}

.slot-salon {
    font-size: var(--t-m);
    color: var(--parchemin);
    overflow-wrap: anywhere;
}

/* Le sujet de la tuile : c'est le fil qu'on possède. */
.slot-nom {
    font-family: var(--titre);
    font-size: var(--t-l);
    color: var(--lueur);
    letter-spacing: .5px;
    overflow-wrap: anywhere;
}

.slot-jeu {
    font-size: var(--t-s);
    color: var(--murmure);
    font-style: italic;
    overflow-wrap: anywhere;
}

/* ⚠ Une partie fermée reste AFFICHÉE, et le dit par un mot autant que par la
   teinte — même raison qu'un serveur suspendu : une bordure seule ne dirait
   rien à une personne sur douze. Elle reste consultable, d'où l'atténuation
   plutôt que la disparition. */
.tuile-slot.fermee {
    border-style: dashed;
    background: var(--abysse-haut);
}

.slot-ferme {
    font-family: var(--titre);
    font-size: var(--t-s);
    color: var(--murmure);
    letter-spacing: .5px;
}

/* --- Le bandeau d'un serveur suspendu ---------------------------------------
   ⚠ Un serveur bloqué par le propriétaire du bot ne l'apprenait NULLE PART. Le
   blocage ne s'applique que dans `DiscordService`, qui fait quitter le serveur,
   et dans le filtre d'autorisation, qui ne vise que les personnes. Son
   administrateur voyait donc un tableau de bord normal, des portails
   configurés, et un bot muet. */
.bandeau-suspendu {
    margin-bottom: var(--e-4);
    padding: var(--e-4);
    background: color-mix(in srgb, var(--oeil) 12%, var(--abysse-clair));
    border: 1px solid var(--oeil);
    border-radius: var(--rayon);
}

.bandeau-suspendu .titre {
    font-family: var(--titre);
    font-size: var(--t-l);
    color: var(--oeil);
    margin-bottom: var(--e-2);
}

.bandeau-suspendu .motif {
    color: var(--parchemin);
    margin-bottom: var(--e-2);
}

.bandeau-suspendu .depuis {
    font-size: var(--t-s);
    color: var(--murmure);
}

/* --- La notification ---------------------------------------------------------
   `server.html` n'en avait aucune : les actions de partage ont besoin de dire
   qu'elles ont abouti, ou pourquoi elles n'ont pas abouti. */
.notification {
    position: fixed;
    left: 50%;
    bottom: var(--e-5);
    transform: translate(-50%, 200%);
    padding: var(--e-3) var(--e-5);
    max-width: min(90vw, 34rem);
    color: var(--parchemin);
    background: var(--abysse-haut);
    border: 1px solid var(--lueur);
    border-radius: var(--rayon);
    transition: transform .2s ease;
    z-index: 400;
}

.notification.visible { transform: translate(-50%, 0); }
.notification.erreur { border-color: var(--oeil); }

@media (prefers-reduced-motion: reduce) {
    .notification { transition: none; }
}

/* L'avertissement d'un aperçu réservé au propriétaire : la page est là, mais
   personne d'autre ne la voit. */
.avertissement {
    color: var(--oeil);
    border-color: var(--oeil) !important;
}

/* --- Les états guidés --------------------------------------------------------
   ⚠ Un écran vide n'est pas une information. Les huit états vides du tableau de
   bord disaient tous « aucun X » et s'arrêtaient là — au moment précis où
   l'utilisateur a besoin qu'on lui dise quoi faire.
   --------------------------------------------------------------------------- */
.guide {
    max-width: 640px;
    margin: var(--e-6) auto;
    padding: var(--e-5);
    text-align: center;
    background: var(--abysse-clair);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
}

.guide-titre {
    font-family: var(--titre);
    font-size: var(--t-l);
    color: var(--lueur);
    letter-spacing: 1px;
}

.guide p { margin: var(--e-3) 0; color: var(--parchemin); line-height: 1.5; }

.guide-action { margin-top: var(--e-4); }

/* Les permissions demandées, une ligne chacune, avec sa raison. */
.permissions {
    margin: var(--e-4) 0 0;
    padding: 0;
    list-style: none;
    text-align: left;
    font-size: var(--t-s);
    color: var(--brume);
}

.permissions li {
    display: grid;
    grid-template-columns: 13rem 1fr;
    gap: var(--e-3);
    padding: var(--e-1) 0;
}

.permissions b { color: var(--parchemin); font-weight: 600; }

@media (max-width: 900px) {
    .permissions li { grid-template-columns: 1fr; gap: 0; }
}

/* Les fenêtres de configuration d'`admin.html` sont écrites en chaînes de
   caractères par le script, chaque champ portant son style EN LIGNE, répété.
   Deux classes suffisent, et elles vivent ici comme le reste. */
.etiquette {
    display: block;
    margin-bottom: var(--e-1);
    font-family: var(--titre);
    font-size: var(--t-xs);
    color: var(--lueur);
}

.saisie {
    width: 100%;
    padding: var(--e-2) var(--e-3);
    color: var(--parchemin);
    background: var(--abysse);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
}

/* L'aide sous un libellé. Elle dit ce que le réglage FAIT, pas ce qu'il est. */
.aide-champ {
    display: block;
    margin-top: var(--e-1);
    font-size: var(--t-s);
    font-style: italic;
    color: var(--brume);
}

/* La valeur par défaut, quand ne rien saisir a un sens. */
.defaut-champ {
    font-style: normal;
    color: var(--lueur);
}

/* ⚠ Le message reste SOUS le champ tant qu'on corrige. Une notification
   passagère disparaît pendant qu'on relit sa saisie — donc au moment précis où
   elle servirait. */
.erreur-champ {
    margin-top: var(--e-1);
    font-size: var(--t-s);
    color: var(--oeil);
}

.champ-fautif {
    border-color: var(--oeil) !important;
}

/* Un bouton d'enregistrement en cours : il dit ce qu'il fait, et refuse le
   second clic. Sans cela, tout formulaire pouvait être soumis deux fois. */
button[data-en-cours="1"], button:disabled {
    opacity: .6;
    cursor: progress;
}

/* ============================================================================
   JUSQU'AU TÉLÉPHONE
   ============================================================================

   Mesuré à 390 px sur les quatre écrans : 67 déclarations de police sous 14 px
   — dont huit à 10 px —, dix cibles tactiles entre 24 et 35 px là où il en faut
   44, et six tableaux totalisant trente-sept colonnes.

   ⚠ Le seuil est à 900 px, pas à 700. Mesuré à 768 px — un iPad en portrait,
   l'une des trois largeurs que le prompt nomme — les tableaux débordaient
   ENCORE de 143 px sur `index.html` et de 69 px sur `admin.html`. Un seuil posé
   au jugé aurait laissé toute une famille d'écrans dans l'état d'avant.

   ⚠ Ce bloc emploie `!important` et des sélecteurs d'attribut. Ce n'est pas de
   la paresse : les tailles fautives sont, pour la plupart, écrites en STYLE
   EN LIGNE dans le HTML généré, et un style en ligne bat toute règle de feuille.
   Les deux seules autres voies étaient de reprendre les soixante-sept
   déclarations une à une — donc de changer aussi l'apparence sur écran large,
   qui convient — ou de renoncer. Tout est confiné à cette requête média.
   ============================================================================ */
@media (max-width: 900px) {

    /* --- Les tableaux deviennent des cartes empilées ------------------------
       Le défilement horizontal était la solution en place, et le prompt a
       raison de la refuser : on ne compare pas deux lignes qu'on ne peut pas
       voir en même temps, et rien n'indique qu'il reste des colonnes à droite.

       ⚠ Choix assumé : TOUS les champs restent, aucun n'est replié derrière un
       dépliement. Ces tableaux comptent peu de lignes — une poignée de portails,
       une poignée de guildes. Replier aurait échangé un défaut de largeur contre
       un geste de plus sur des données déjà courtes, et le prompt demande de ne
       pas dégrader la fonctionnalité. */
    table, tbody, tr, td { display: block; width: 100%; }

    /* L'en-tête ne peut plus coiffer de colonnes : son texte part dans
       `data-colonne`, cellule par cellule (voir le script de chaque page). */
    thead {
        position: absolute;
        width: 1px; height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
    }

    tbody tr {
        background: var(--abysse-clair);
        border: 1px solid var(--bordure);
        border-radius: var(--rayon);
        padding: var(--e-2) var(--e-3);
        margin-bottom: var(--e-3);
    }

    tbody td {
        display: grid;
        grid-template-columns: 8rem 1fr;
        align-items: baseline;
        /* Sans cela, un badge « Oui » de trois lettres s'étire sur toute la
           largeur de la carte : en grille, un élément occupe sa case entière. */
        justify-items: start;
        gap: var(--e-3);
        border: none;
        padding: var(--e-2) 0;
        /* Une adresse `wss://hote:port` ne doit pas pousser la carte hors écran. */
        overflow-wrap: anywhere;
    }

    tbody td::before {
        content: attr(data-colonne);
        font-family: var(--titre);
        font-size: var(--t-xs);
        letter-spacing: .5px;
        color: var(--brume);
    }

    /* La ligne « aucun résultat » couvre toute la largeur : pas d'étiquette. */
    tbody td[colspan] { display: block; text-align: center; }
    tbody td[colspan]::before { content: none; }

    /* --- Les en-têtes se replient ------------------------------------------
       Mesuré : 161 px de débordement sur `admin.html`, 213 px sur
       `server.html`. Dans les deux cas une rangée souple qui ne se replie pas —
       titre, bouton et nom d'utilisateur poussés hors de l'écran, sans que rien
       n'indique qu'il y avait quelque chose à droite. */
    header, .header-inner, .header-left, .header-right, .nav, .actions {
        flex-wrap: wrap;
        gap: var(--e-2);
    }

    .header-inner { justify-content: flex-start; }

    /* Le titre de `server.html` porte l'adresse complète du serveur : elle doit
       se couper, pas pousser la page. */
    h1, #page-title { overflow-wrap: anywhere; }

    /* --- Le plancher de 14 px ----------------------------------------------
       ⚠ Le premier essai ne mordait pas, et la mesure l'a montré : les tailles
       fautives vivent dans le `<style>` de chaque page, chargé APRÈS
       `vaati.css`. À spécificité égale, la dernière règle l'emporte — d'où le
       `!important`, ici et pour les styles en ligne.

       Un plancher, pas une taille imposée : ce qui dépasse déjà 14 px ne bouge
       pas. */
    :root {
        --t-xs: 14px;
        --t-s:  15px;
    }

    [style*="font-size:10px"], [style*="font-size: 10px"],
    [style*="font-size:11px"], [style*="font-size: 11px"],
    [style*="font-size:12px"], [style*="font-size: 12px"],
    [style*="font-size:13px"], [style*="font-size: 13px"] { font-size: 14px !important; }

    .subtitle, .label, .badge, .badge-admin, .pill, .btn, .btn-sm, .back,
    .refresh-btn, .nav a, .header-right a, code, th, td, label, select, input,
    textarea, .hint-list, .hint-item, .conn-entry, .conn-time, .server-info,
    .avancement-detail, #username-display, .player-name, .stat-box .label,
    .file-label, .graphe-note,
    /* ⚠ `.player-jeu` remplace un `style="font-size:11px"` en ligne, que la règle
       ci-dessus rattrapait. Nommer la classe l'a sortie du filet : le banc l'a vu
       tout de suite, mais seulement une fois ses données factices corrigées — il
       mesurait jusque-là une liste de joueurs VIDE. */
    .player-jeu {
        font-size: 14px !important;
    }

    /* --- Les cibles tactiles ------------------------------------------------
       44 px de haut, et de quoi ne pas toucher la voisine par erreur. */
    button, .btn, .btn-sm, .back, .refresh-btn, select, input, textarea, a.btn {
        min-height: 44px;
    }

    /* Un lien de navigation reste un lien, mais il doit se toucher.
       ⚠ `display: inline-flex` est indispensable : `min-height` ne fait RIEN sur
       un élément en ligne. Les liens d'`index.html` sont écrits par le script
       avec un `padding` en ligne, et mesuraient 32 px — la règle de hauteur
       seule les laissait tels quels, sans erreur ni avertissement. */
    .nav a, .header-right a, #nav-links a, header .nav-links a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        padding: 0 var(--e-3);
    }

    .btn, .btn-sm { padding: var(--e-2) var(--e-3); }

    .actions, td .actions {
        display: flex;
        flex-wrap: wrap;
        gap: var(--e-2);
    }

    .actions .btn { flex: 1 1 auto; }

    /* Un lien seul dans une cellule est une cible aussi : on lui donne sa
       hauteur sans le transformer en bouton. */
    tbody td > a {
        display: inline-block;
        min-height: 44px;
        line-height: 44px;
    }

    /* --- Les formulaires et les fenêtres ------------------------------------
       Une grille à deux colonnes sur 390 px donne deux champs de 170 px : on
       repasse sur une colonne. Les grilles écrites EN LIGNE comprises. */
    .form-row, .form-grid { grid-template-columns: 1fr !important; }

    [style*="grid-template-columns:1fr 1fr"],
    [style*="grid-template-columns: 1fr 1fr"] { grid-template-columns: 1fr !important; }

    /* ⚠ `dvh` et non `vh` : sur téléphone, `100vh` ignore la barre d'adresse du
       navigateur, donc le bas d'une fenêtre modale se retrouve sous elle. */
    .modal, [id$="-overlay"] > div {
        width: 100% !important;
        max-width: 100% !important;
        max-height: 88dvh;
        overflow-y: auto;
        padding: var(--e-4) !important;
    }

    /* La fenêtre s'ancre en haut : centrée, le clavier virtuel la pousse et son
       titre sort de l'écran. */
    [id$="-overlay"] { align-items: flex-start !important; padding: var(--e-3); }

    /* Un champ que le clavier virtuel recouvrirait doit remonter avec de la
       marge, sinon il affleure tout juste le bord. */
    input, select, textarea { scroll-margin-block: var(--e-6); }

    /* --- Ce qui déborde ------------------------------------------------------ */
    .container, .header-inner { padding-left: var(--e-3); padding-right: var(--e-3); }

    .card, .modal, img, svg, pre, code { max-width: 100%; }

    /* `login.html` posait une carte de 360 px sans garde-fou : à 390 px elle
       tenait de justesse, et pas du tout sur un écran plus étroit. */
    .card { width: auto; }

    /* La zone sûre : sur iPhone, le bas de l'écran est mangé par la barre
       d'accueil. */
    body { padding-bottom: env(safe-area-inset-bottom); }
}

/* --- La page tracker --------------------------------------------------------
   ⚠ Tout y etait au meme niveau visuel : huit compteurs cote a cote, dont aucun
   ne repondait a « ou en est cette partie ? ». La progression globale passe
   devant, le detail suit. */
.avancement {
    max-width: 1400px;
    /* ⚠ La marge basse etait a 0 : descendu sous les compteurs, le bloc collait
       la carte suivante, bord contre bord, et les deux se lisaient comme un
       seul cadre. En haut il n'en faut pas plus, la grille de compteurs pose
       deja ses 16 px et les marges adjacentes fusionnent. */
    margin: var(--e-4) auto var(--e-5);
    padding: var(--e-4);
    background: var(--abysse-clair);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
}

.avancement-tete {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--e-3);
    margin-bottom: var(--e-3);
}

.avancement-titre {
    font-family: var(--titre);
    font-size: var(--t-l);
    color: var(--lueur);
    letter-spacing: 2px;
}

#avancement-chiffre {
    font-size: var(--t-m);
    color: var(--parchemin);
    font-family: var(--chiffre);
    font-variant-numeric: tabular-nums;
}

.jauge {
    height: 10px;
    background: var(--abysse);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon);
    overflow: hidden;
}

.jauge-barre {
    height: 100%;
    width: 0;
    background: linear-gradient(90deg, var(--sortilege), var(--lueur));
    transition: width .6s ease;
}

.avancement-detail {
    margin-top: var(--e-2);
    font-size: var(--t-s);
    color: var(--brume);
}

/* --- Les etats de la page ---------------------------------------------------
   ⚠ Un seul message couvrait « erreur de chargement » et « acces refuse ». Ce
   sont deux situations sans rapport : l'une se repare en attendant, l'autre en
   demandant un droit. */
.etat {
    padding: var(--e-4);
    border-radius: var(--rayon);
    font-size: var(--t-m);
    line-height: 1.5;
}

.etat a { color: var(--lueur); }

.etat-refus { background: var(--abysse-clair); border: 1px solid var(--bordure); color: var(--brume); }
.etat-vide  { background: var(--abysse-clair); border: 1px dashed var(--bordure); color: var(--brume); }
.etat-panne { background: color-mix(in srgb, var(--oeil) 10%, transparent);
              border: 1px solid color-mix(in srgb, var(--oeil) 45%, transparent);
              color: var(--parchemin); }

/* --- Le sélecteur de langue ------------------------------------------------
   ⚠ ICI et non dans le `<style>` de chaque page : il est posé par un module
   commun sur les neuf pages, donc son apparence doit venir d'un seul endroit.
   Recopiée neuf fois, elle aurait divergé — c'est déjà arrivé à `btn-edit-cfg`,
   employé sans jamais avoir été déclaré, qui s'affichait en blanc partout. */
.boite-langue {
    display: flex;
    align-items: center;
    gap: 6px;
    /* Il ne pousse rien : c'est un réglage, pas une destination. */
    flex: 0 0 auto;
}

.etiquette-langue {
    font-size: 12px;
    color: var(--brume);
    letter-spacing: .5px;
    /* ⚠ Insécable : « Language » sur deux lignes ferait grandir l'en-tête. */
    white-space: nowrap;
}

#choix-langue {
    /* ⚠ Les formulaires mettent tout `select` en `width:100%`, ce qui convient à
       des champs empilés. Dans une rangée d'en-tête, cette largeur chasserait les
       liens de navigation hors de l'écran — mesuré : 518 px de contenu dans 295. */
    width: auto;
    min-width: 0;
    padding: 4px 8px;
    font-size: 13px;
    background: var(--abysse-clair);
    border: 1px solid var(--bordure);
    border-radius: 2px;
    color: var(--parchemin);
}
