/* ============================================================
   Scoliom Scolarité — feuille de style application
   Identité : charte v2 « La Journée d'école » — le violet Majorelle en APLAT
   Jetons et polices : assets/css/tokens.css (source unique de vérité)
   Typographie : jetons --font-title / --font-ui / --font-num (tokens.css)
   © 2026 Scoliom — une marque d'ABC IT SARL AU
   ============================================================ */

/* ===== POLICES =====
   Les sept fontes de la charte v2 (Spectral, Archivo, Spline Sans Mono,
   IBM Plex Sans Arabic) sont déclarées dans assets/css/tokens.css, chargé
   AVANT cette feuille. Les @font-face qui vivaient ici chargeaient Inter et
   Plus Jakarta Sans — polices bannies — et les déclaraient sous
   `font-family: var(--font-title)`, ce qui n'est pas admis dans une règle
   @font-face : les six fichiers étaient téléchargés sans jamais servir. */

/* ===== RESET & VARIABLES ===== */
*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* Les jetons de l'identité vivent dans assets/css/tokens.css, chargé AVANT
   ce fichier. Ne PAS les redéfinir ici : cette feuille étant chargée après,
   toute redéfinition écraserait silencieusement la charte (et laisserait le
   produit avec deux identités selon les pages). */

/* ===== BASE ===== */
html { scroll-behavior: smooth; }

body {
    font-family: var(--font-ui);
    font-size: 15px;
    line-height: 1.55;
    color: var(--text);
    background: var(--off-white);
    -webkit-font-smoothing: antialiased;
}

h1, h2 {
    font-family: var(--font-display);
    font-weight: 600;
    line-height: 1.2;
    color: var(--ink);
    letter-spacing: -.01em;
}
/* Spectral n'est pas faite pour l'UI dense : au-delà de h2, on reste en Archivo. */
h3, h4, h5 {
    font-family: var(--font-body);
    font-weight: 600;
    line-height: 1.3;
    color: var(--ink);
}

h1 { font-size: 1.55rem; letter-spacing: normal; }
h2 { font-size: 1.3rem;  letter-spacing: normal; }
h3 { font-size: 1.1rem;  letter-spacing: normal; }
h4 { font-size: 1rem; }

a { color: var(--violet); text-decoration: none; }
a:hover { color: var(--violet-d); }

img, svg { max-width: 100%; vertical-align: middle; }

/* ===== ACCESSIBILITÉ ===== */
:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
    border-radius: 4px;
}

/* ============================================================
   SURFACES FLOTTANTES — l'ombre unique et l'echelle de z-index
   ------------------------------------------------------------
   UNE seule ombre dans le produit : `var(--shadow)`, la recette de la charte.
   Elle ne se pose que sur ce qui SURVOLE reellement le document : la modale,
   les toasts, la liste deroulante de la recherche globale, le sous-menu
   flottant de la barre compacte, la fenetre produit, et les deux panneaux
   lateraux (notifications, aide). Tout le reste est DELIMITE par un filet.
   La seule autre forme d'ombre admise est l'anneau de focus, ecrit une fois a
   la section des champs.

   ECHELLE DE Z-INDEX — toutes les valeurs de cette feuille, et rien d'autre :
     1 . 3   empilements LOCAUX a un composant (en-tete de groupe collant,
             tete de modale collante, colonnes figees de la grille de notes)
     50      barre superieure collante
     60      sous-menu flottant de la barre laterale compacte
     80      liste deroulante de la recherche globale
     120     panneaux lateraux — notifications ET aide, JUMEAUX : meme ombre,
             meme z-index, sans quoi l'un passe devant l'autre sans raison
     1000    voile de la modale
     2000    toasts : au-dessus de tout, y compris d'une modale ouverte, car
             ils annoncent le resultat de ce qu'on vient d'y faire
   ============================================================ */

/* ============================================================
   LAYOUT APPLICATION
   ============================================================ */
.app-layout {
    display: flex;
    min-height: 100vh;
}

/* ----- Sidebar ----- */
.sidebar {
    /* T1 — la largeur devient un JETON : le mode compact n est plus qu un
       changement de valeur, pas une seconde regle a tenir en parallele. */
    width: var(--nav-l, 250px);
    min-width: var(--nav-l, 250px);
    background: var(--nav);
    border-inline-end: 1px solid var(--nav-rule);
    color: var(--nav-tx-str);
    display: flex;
    flex-direction: column;
    padding: 1.1rem .9rem;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
}

/* LA MARQUE SE LIT COMME UN BLOC, PAS COMME TROIS ELEMENTS EMPILES.
   Auparavant le logo, « Scoliom » et « Scolarite » ne partageaient aucun
   axe : le mot etait en haut, le logo au milieu, le sous-titre retombait
   sous la base du logo. L oeil lisait trois choses au lieu d une.
   Le logo repose desormais sur une surface legerement eclaircie — sur
   fond encre, un trait blanc nu n a aucune assise — et « SCOLARITE »
   devient un descripteur en capitales espacees : sans cela il se lisait
   comme une entree de menu de plus.
   ⚠️ La pastille reste DISCRETE, pas Majorelle pleine : ce violet est
   celui du titre d ecran (« ou suis-je »). Deux pastilles pleines a
   l ecran en meme temps se disputeraient le regard. */
.sidebar-brand {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .15rem .35rem 1.3rem;
    font-family: var(--font-title);
    font-weight: 600;
    font-size: 1.3rem;
    color: var(--nav-tx-str);
    letter-spacing: -.01em;
}
.sidebar-brand > span { line-height: 1.02; }
.sidebar-brand:hover { color: var(--nav-tx-str); }
.sidebar-brand svg,
.sidebar-brand .logo-mark {
    width: 42px;
    height: 42px;
    flex-shrink: 0;
    padding: 6px;
    box-sizing: border-box;
    background: rgba(255, 255, 255, .10);
    border-radius: var(--radius-btn);
}
.sidebar-brand small {
    display: block;
    font-family: var(--font-ui);
    font-weight: 600;
    font-size: .62rem;
    opacity: .6;
    letter-spacing: .16em;
    text-transform: uppercase;
    margin-top: .2rem;
}

.sidebar-nav {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: .2rem;
    padding-top: .4rem;
}

.nav-item {
    display: flex;
    align-items: center;
    gap: .7rem;
    padding: .6rem .85rem;
    border-radius: var(--radius-btn);
    color: rgba(255,255,255,.88);
    font-size: .9rem;
    font-weight: 500;
    transition: background .18s ease, color .18s ease;
    white-space: nowrap;
}
.nav-item i {
    width: 20px;
    text-align: center;
    font-size: .95rem;
    opacity: .9;
}
.nav-item:hover {
    background: rgba(255,255,255,.12);
    color: var(--nav-tx-str);
}
/* §D1 — L'ITEM ACTIF EST UNE PILULE VIOLETTE PLEINE, ET SON ICÔNE EST BLANCHE.
   Il portait un voile blanc translucide. Composé sur l'encre de la barre, le
   pixel réellement peint n'était pas le violet d'identité que §D1 nomme, mais
   une encre éclaircie — la règle disait une chose, l'écran en montrait une
   autre. Un APLAT de `--violet` ne dépend d'aucune composition : ce qui est
   écrit est ce qui est peint. Les contrastes du libellé et de l'icône blancs
   sur cet aplat ont été mesurés au navigateur, sonde étalonnée : au-dessus du
   seuil AA. Le sous-item actif prend le même aplat, plus bas dans cette feuille
   (`.nav-enfant.active`) : deux aspects pour un même état se liraient comme
   deux états.
   ⚠️ Le liseré qui accompagnait l'entrée courante a été RETIRÉ : il était en
   `--violet`, donc invisible sur une pilule violette, et il décalait l'entrée
   active de sa propre épaisseur par rapport à ses voisines. */
.nav-item.active {
    background: var(--violet);
    color: var(--nav-tx-str);
    font-weight: 700;
}
.nav-item.active i { opacity: 1; }

.sidebar-foot {
    padding: 1rem .55rem .25rem;
    font-size: .7rem;
    line-height: 1.45;
    opacity: .7;
    color: var(--nav-tx-str);
}

/* ----- Zone principale ----- */
.main {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* ⚠️ GRILLE, PAS FLEX AVEC « space-between ».
   En flex, la zone de recherche etait un element souple place entre le
   titre et les indicateurs : sa position horizontale dependait donc de la
   LONGUEUR DU TITRE. « Accueil » et « Notes et bulletins » ne donnaient pas
   la meme, si bien que le champ se deplacait a chaque changement d onglet —
   l oeil devait le rechercher au lieu de le retrouver.
   Avec une colonne souple a gauche, la largeur du titre n influe plus : le
   champ garde la meme abscisse d un ecran a l autre.

   ⚠️ LES QUATRE PISTES ONT UN PLANCHER, ET C EST CE QUI MANQUAIT. La grille
   posait « minmax(0, 1fr) » sur le titre et « auto auto » sur les deux blocs
   de droite. Un « auto » ne cede rien : les deux prenaient tout ce qu ils
   voulaient, et le titre — seul a pouvoir tomber a zero — tombait a zero.
   MESURE DU 22/08/2026, SUR SEIZE LARGEURS : le libelle de l ecran etait
   invisible PARTOUT SAUF EN 1920 px, et la pastille d icone, qui ne sait pas
   retrecir, debordait de sa piste vide pour se poser SUR le champ de
   recherche — 27 px de chevauchement de 1500 a 901 px. Entre 1100 et 901 px
   la barre entiere debordait horizontalement, jusqu a 195 px : le bouton de
   deconnexion sortait de l ecran. Autrement dit, sur tous les portables du
   marche, cette barre n avait pas de titre et perdait ses commandes.
   Chaque piste declare donc desormais ou elle s arrete de retrecir, et les
   deux blocs de droite acceptent enfin de le faire (voir « min-width: 0 »
   sur « .topbar-meta » et « .topbar-user » plus bas, sans quoi un conteneur
   flex refuse de descendre sous son contenu). */
.topbar {
    background: var(--white);
    position: sticky;
    top: 0;
    z-index: 50;
    border-bottom: 1px solid var(--border);
    padding: 1rem 1.5rem;
    display: grid;
    /* DEUX RANGEES, ET C EST UN CONSTAT AVANT D ETRE UN CHOIX. Additionnees a
       leur largeur naturelle, les cinq choses que porte cette barre — titre,
       recherche, etablissement, annee, identite — demandent 1 671 px. Avec la
       barre laterale, il faut donc une fenetre de 1 921 px pour les ranger en
       une seule rangee. Sous ce seuil, quelque chose doit ceder ; l ancienne
       grille faisait ceder le TITRE jusqu a zero, puis debordait quand meme.
       La recherche prend donc sa propre rangee : c est le geste le plus
       frequent du secretariat, il merite un champ entier plutot qu un reste de
       192 px, et pose a gauche il ne se deplace plus jamais — ce que la grille
       cherchait deja a obtenir. Le prix est 32 px de hauteur, paye partout
       plutot qu une mise en page qui se reorganise a un seuil que personne ne
       verra venir. */
    grid-template-columns:
        /* Le plancher du titre SUIT la fenetre. Fixe a 15 rem il redevenait
           trop gourmand sous 1 024 px — la barre y debordait de nouveau de
           41 px, parce que les deux blocs de droite avaient alors atteint
           leur propre contenu minimal et ne pouvaient plus rien ceder. */
        minmax(clamp(7rem, 14vw, 15rem), 1fr)
        minmax(0, auto)     /* etablissement et annee */
        minmax(0, auto);    /* identite et actions */
    align-items: center;
    gap: .55rem 1rem;
}
/* Les places sont posees EXPLICITEMENT : le placement automatique ne revient
   jamais en arriere, et il poussait la recherche sur une troisieme rangee. */
.topbar-title { grid-column: 1; grid-row: 1; }
.topbar-meta  { grid-column: 2; grid-row: 1; }
.topbar-user  { grid-column: 3; grid-row: 1; }
/* Le champ prend la rangee entiere, mais pas la largeur entiere : sur un
   ecran large, une zone de saisie de 1 600 px n aide personne a viser.
   ⚠️ IL ETAIT CALE A GAUCHE, ET CA SE VOYAIT. Sur une fenetre de 1 600 px, la
   rangee 2 portait 640 px de champ suivis de PRES DE MILLE PIXELS DE VIDE,
   pendant que la rangee 1 avait son tiers gauche libre — le fondateur l a
   releve sur capture. Un champ pose a un bout d une rangee vide se lit comme
   un reste de mise en page, pas comme un choix.
   Centre, il devient l axe de la barre : c est la place que les moteurs de
   recherche ont apprise a tout le monde, et le vide se repartit au lieu de
   s accumuler d un cote. Le plafond monte a 46 rem — assez pour que le champ
   pese dans la rangee, pas assez pour qu on doive viser loin. */
.topbar .rg-zone { grid-column: 1 / -1; grid-row: 2; max-width: 46rem; margin-inline: auto; width: 100%; }

/* Le titre d ecran porte l ICONE de son module — la meme que la barre
   laterale. Un mot seul pose a gauche d une barre vide ne dit pas ou l on
   est : il faut le lire. L icone se reconnait avant d etre lue.
   L ellipse vit sur le LIBELLE, pas sur le conteneur : en flex, un
   text-overflow pose sur le parent ne tronque rien. */
.topbar-title {
    display: flex;
    align-items: center;
    gap: .6rem;
    min-width: 0;
}
.topbar-ico {
    width: 2.7rem; height: 2.7rem; flex: 0 0 2.7rem;
    display: grid; place-items: center;
    border-radius: var(--radius-btn);
    /* Pastille PLEINE, pas un voile : c est l ancre de la page, le seul
       element de la barre qui doive se voir avant d etre lu. Le voile pale
       la faisait disparaitre dans le blanc.
       Aucune ombre : la charte n en admet que sur ce qui SURVOLE le document
       (R2), et cette pastille est posee dans le flux de la barre. */
    background: var(--violet); color: var(--on-accent);
    font-size: 1.1rem;
}
.topbar-libelle {
    font-family: var(--font-title);
    font-weight: 700;
    /* La taille SUIT la fenetre au lieu de forcer l ellipse. A 1,7 rem fixe,
       « Notes & bulletins » demandait 208 px de libelle : sur un portable il
       ressortait « Not… », ce qui ne nomme plus rien. Le plancher de 1,15 rem
       reste au-dessus du corps du texte courant — un titre doit rester un
       titre, meme serre. */
    font-size: clamp(1.15rem, .95vw + .55rem, 1.7rem);
    color: var(--dark);
    letter-spacing: -.015em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.topbar-meta {
    display: flex;
    align-items: center;
    gap: .5rem;
    /* ⚠️ LE REPLI N EST PLUS PERMANENT, ET C EST UNE CAUSE UNIQUE POUR DEUX
       DEFAUTS. Etablissement et annee passaient a la ligne meme sur 1 600 px,
       ce qui donnait 70 px a la premiere rangee au lieu de 40 — pendant que le
       TIERS GAUCHE de cette rangee restait vide. Les deux viennent du meme
       endroit : la piste du titre est en `1fr` et absorbe tout le reste, meme
       quand le titre est VIDE (c est le cas sur l Accueil). Les deux blocs de
       droite se retrouvaient alors a leur contenu minimal, donc replies.
       Mesure : a 1 600 px il reste ~1 300 px utiles pour ~1 040 px de contenu.
       La place existe ; c est la repartition qui la donnait au vide.
       Le repli reste sous 900 px, ou il est necessaire. */
    flex-wrap: nowrap;
    min-width: 0;
}
@media (max-width: 900px) { .topbar-meta { flex-wrap: wrap; } }

.topbar-user {
    display: flex;
    align-items: center;
    gap: .6rem;
    font-size: .85rem;
    color: var(--text-muted);
    white-space: nowrap;
    /* Ce bloc est le plus large de la barre — 422 px mesures — et il etait
       le seul a ne jamais ceder un pixel. Il cede maintenant, et c est le
       NOM qui cede en premier : la cloche, l aide, la langue, le role et la
       deconnexion sont des COMMANDES, le nom est un rappel. Perdre la fin
       d un nom qu on connait coute infiniment moins que perdre un bouton. */
    min-width: 0;
}
/* Le nom tronque, l icone reste : « BENNANI Samira » devient « BENNANI… »
   avant que quoi que ce soit d autre ne disparaisse de la barre. */
#btn-profil {
    display: inline-flex; align-items: center; gap: .4rem;
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#btn-profil i { flex: 0 0 auto; }
.topbar-user strong {
    color: var(--dark);
    font-weight: 600;
}

.content {
    padding: clamp(1.25rem, 2.5vw, 2rem);
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

/* ----- Chips (établissement, année scolaire…) ----- */
.chip {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .3rem .75rem;
    border-radius: 8px;
    background: var(--violet-xl);
    color: var(--violet-d);
    font-size: .78rem;
    font-weight: 600;
    line-height: 1.3;
    white-space: nowrap;
}
.chip i { font-size: .75rem; opacity: .85; }

/* ============================================================
   CARTES
   ============================================================ */
/* Une carte est DELIMITEE par un filet ; elle ne flotte pas. L'ombre unique
   est reservee a ce qui survole reellement le document (R2).
   La carte consomme --surface, et non --white : le blanc est une COULEUR,
   la surface est un ROLE. Sous [data-theme="dark"], --white reste du blanc
   — c'est son nom — et la carte restait donc blanche pendant que son texte
   passait en encre claire. Les deux jetons valent la meme chose en theme
   clair : ce changement ne deplace aucun pixel aujourd'hui, il rend
   seulement la carte capable de suivre. Les graphiques lisent le meme
   jeton pour l'anneau qui separe les tranches de leurs donuts. */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.25rem 1.4rem;
}

.card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.card-title {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 1.05rem;
    color: var(--dark);
    display: flex;
    align-items: center;
    gap: .55rem;
    letter-spacing: normal;
}
.card-title i { color: var(--violet); font-size: .95rem; }

/* ============================================================
   KPI
   ============================================================ */
.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    /* Voir le motif du plancher a zero, plus bas, au bloc des cartes. */
    gap: 1rem;
}

.kpi {
    background: var(--white);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.1rem 1.25rem;
    display: flex;
    align-items: center;
    gap: .95rem;
    transition: border-color .15s ease;
}
/* Plus de carte qui levite : au survol, une carte change de FILET (R2). */
.kpi:hover,
.card:hover,
.table-wrap:hover { border-color: var(--violet); }

.kpi-icon {
    width: 44px;
    height: 44px;
    min-width: 44px;
    border-radius: var(--radius-btn);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
    background: var(--violet-xl);
    color: var(--violet);
}

.kpi-value {
    font-family: var(--font-mono);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    font-size: 1.55rem;
    line-height: 1.15;
    color: var(--ink);
    /* Un seul letter-spacing : la déclaration `normal` qui suivait annulait
       silencieusement le resserrement voulu pour les grands nombres. */
    letter-spacing: -.02em;
}

/* Un montant complet est deux fois plus long qu'un effectif : à la taille des
   autres KPI il passait à la ligne et cassait l'alignement de toute la rangée. */
#kpi-caisse .kpi-value,
.kpi-value:has(.montant) {
    font-size: 1.12rem;
    line-height: 1.35;
}

.kpi-label {
    font-size: .8rem;
    color: var(--text-muted);
    font-weight: 500;
}

/* Anatomie §D2 (direction « Console ») : le libellé se lit AVANT le chiffre,
   et le chiffre tient sur UNE ligne — valeur, unité, puce de tendance. Le
   retrait qui séparait autrefois le libellé du chiffre a suivi l'inversion.
   Cette classe est posée par `kpiHtml()`, la fabrique unique des cartes. */
.kpi-ligne {
    display: flex;
    align-items: baseline;
    gap: .35rem;
    margin-top: .1rem;
    /* §D2, mot pour mot : « ligne de valeur en white-space: nowrap ». La règle
       porte sur la LIGNE, pas seulement sur ses morceaux, et elle ne se réécrit
       pas : la première version de ce lot avait ouvert le repli ici, puis
       redéfini §D2 dans ce commentaire pour que le repli devienne conforme. La
       ligne se coupait alors aux largeurs les plus courantes, et la carte
       qui la portait cessait d'être alignée sur ses trois voisines.
       Ce qui a été réparé, c'est la CAUSE : la puce de tendance se tait quand
       la période précédente n'est plus comparable (voir `tendanceHtml`,
       assets/js/app.js), si bien que la ligne la plus longue de l'écran — un
       montant — n'a plus de puce à côté d'elle. */
    white-space: nowrap;
}
.kpi-value,
.kpi-tend { white-space: nowrap; }
/* Et avant tout, le chiffre cède du corps : §D2 veut que ce soit lui qui recule
   quand la place manque. */
.kpi-ligne:has(.kpi-tend) .kpi-value:has(.montant) { font-size: 1rem; }

/* Variantes de teinte — charte v2.
   La charte bannit les tuiles à icône pastel multicolores : l'ambre et le
   turquoise n'appartiennent à aucune palette du produit. Il ne reste donc que
   trois registres : l'accent d'identité par défaut, le vert pour une
   confirmation factuelle (validé, encaissé) et le rouge pour ce qui bloque
   réellement. « À traiter » n'est ni l'un ni l'autre : c'est du violet. */
.kpi-violet .kpi-icon,
.kpi-orange .kpi-icon { background: var(--veil);     color: var(--violet); }
.kpi-green  .kpi-icon { background: var(--green-bg); color: var(--green); }
.kpi-red    .kpi-icon { background: var(--error-bg); color: var(--error); }

/* ============================================================
   TOOLBAR (recherche + filtres + actions)
   ============================================================ */
.toolbar {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
}
.toolbar .spacer { flex: 1; }

/* ── LA BARRE DE SÉLECTION : SA HAUTEUR EST RÉSERVÉE ──
   Posée `hidden`, elle apparaissait au premier clic et poussait le tableau de
   51 px — mesuré : la première ligne passait de y=616 à y=667. Le saut se
   produit à l'instant précis où l'utilisateur enchaîne des clics, c'est-à-dire
   au pire moment possible : l'élève qu'il visait a bougé sous le curseur.
   La hauteur est donc celle d'une rangée de commandes, occupée ou non ; à vide,
   la barre dit à quoi servent les cases. */
#el-selbar { min-height: 36px; }

/* ⚠️ DANS UNE BARRE D OUTILS, UNE COMMANDE SE MESURE A SON CONTENU.
   La regle globale des formulaires, plus bas, pose « width: 100% » sur tout
   « input » et tout « select » — ce qui est JUSTE dans une colonne de
   formulaire, ou un champ doit remplir sa colonne, et FAUX dans une barre
   d outils, ou il doit se ranger a cote des autres. Rien ne distinguait les
   deux cas, si bien que chaque filtre prenait une rangee entiere pour lui
   seul. MESURE DU 22/08/2026 : l ecran « Listes & Recherches » empilait SEPT
   listes deroulantes de 1 000 px — 430 px de hauteur pour sept libelles de
   moins de vingt caracteres — avant meme d avoir montre une seule ligne de
   resultat.
   Le plafond compte autant que la mise en largeur automatique : sans lui,
   une liste de classes se calerait sur son option la plus longue et
   repousserait les commandes voisines hors de la barre.
   « > » et non un descendant quelconque : un formulaire pose DANS une barre
   d outils garde ses champs en pleine largeur, ce qui est bien ce qu il
   faut. */
.toolbar > select,
.toolbar > input:not(.search-input) {
    width: auto;
    max-width: 15rem;
}
/* Le GROUPE d actions de fin de barre : voir le motif au point d emploi,
   dans assets/js/app.js. « margin-left: auto » remplace l espaceur — le
   groupe se pousse lui-meme a droite, et comme il n a qu un seul element de
   grille, il se replie d un bloc au lieu de laisser un bouton derriere lui. */
.toolbar > .toolbar-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
    margin-left: auto;
}
/* Le groupe pose apres un espaceur ne se pousse pas deux fois. */
.toolbar > .spacer + .toolbar-actions { margin-left: 0; }

/* La recherche, elle, PREND LA PLACE QUI RESTE : c est le champ ou l on tape,
   pas un reglage qu on choisit. Elle garde un plancher lisible et un plafond
   qui l empeche de devenir une bande sur un grand ecran. */
.toolbar > .search-input {
    width: auto;
    /* ⚠️ CE QUI DECIDE DU REPLI D UNE BARRE, C EST SON `max-width` — PAS SA
       BASE, ET C EST CONTRE-INTUITIF. Mesure sur l ecran Eleves (1 366 px,
       1 052 px utiles) : brider la base a 8 rem ne change RIEN, la barre reste
       a 79 px ; brider aussi le `max-width` a 8 rem la fait tomber a 37 px d un
       coup. La taille maximale entre dans le calcul de la taille hypothetique,
       et c est elle que le navigateur compare a la place restante.
       ⚠️ ET ON NE LA BRIDE PAS POUR AUTANT. Pour que cette barre-la tienne sur
       une rangee il faudrait descendre a 11 rem — mesure : 176 + 152 (niveau)
       + 671 (six boutons) + 36 d ecarts = 1 035 pour 1 052. Ce serait payer une
       recherche etriquee sur TOUS les ecrans pour gagner 42 px sur UN seul,
       alors que cet ecran vient d en gagner 337 par ailleurs. Huit commandes ne
       tiennent pas sur 1 052 px : c est de l arithmetique, pas un defaut.
       La base passe tout de meme de 14 a 12 rem : elle ne corrige pas Eleves —
       et il ne faut pas ecrire qu elle le fait — mais elle laisse un peu plus
       de place aux barres qui, elles, sont a la limite. */
    flex: 1 1 12rem;
    max-width: 24rem;
}

/* Ce champ appartient a la FAMILLE DES CHAMPS (filet, rayon, police, couleur,
   transition : voir la section Formulaires). Il ne garde ici que sa souplesse
   dans la barre d'outils ; sa loupe et le rembourrage qui lui fait place sont
   poses la-bas, ou ils peuvent battre la regle globale. */
.search-input {
    flex: 1;
    min-width: 220px;
    max-width: 420px;
}
.search-input::placeholder { color: var(--text-muted); opacity: .75; }

/* ⚠️ « .filters » EST POSEE SUR LE CHAMP, PAS AUTOUR DE LUI — et sa
   definition disait l inverse. Elle etait ecrite comme un CONTENEUR :
   « display: flex » plus une regle « .filters select » pour ses enfants.
   RELEVE DU 22/08/2026 SUR TOUT LE DEPOT : elle sert vingt fois, et les
   vingt fois directement sur un « <select> ». Zero conteneur. La regle
   interne ne pouvait donc designer personne — un select n a pas de select
   dedans — et « display: flex » posait sur une commande de formulaire un
   mode d affichage de bloc : chaque filtre PRENAIT TOUTE LA LARGEUR. Sur le
   Recouvrement, quatre listes deroulantes de 1 000 px s empilaient donc en
   quatre rangees, chacune pour un libelle de quinze caracteres, pendant que
   le champ de recherche voisin tenait dans 240 px.
   La classe decrit desormais ce qu elle fait vraiment : un filtre compact
   qui se range dans la barre d outils et se replie quand la place manque.
   Le plafond compte autant que le plancher : sans lui, une liste de classes
   aux libelles longs s etirerait a la mesure de son option la plus large. */
.filters {
    flex: 0 1 auto;
    min-width: 9.5rem;
    max-width: 15rem;
}

/* ============================================================
   TABLEAUX
   ============================================================ */
.table-wrap {
    overflow-x: auto;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--white);
}

table.data {
    width: 100%;
    border-collapse: collapse;
    font-size: .88rem;
}

table.data th {
    /* `start` et non `left` : sans quoi l'en-tête reste à gauche en arabe. */
    text-align: start;
    font-family: var(--font-body);
    font-size: .72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--ink-soft);
    background: var(--veil);
    padding: .65rem .9rem;
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
}

/* Le rembourrage vertical vient de --td-y : le jeton de densité annoncé par
   tokens.css trouve ici son premier — et unique — consommateur. */
table.data td {
    padding: var(--td-y) .9rem;
    border-bottom: 1px solid var(--line);
    vertical-align: middle;
}

table.data tbody tr { transition: background .15s ease; }
table.data tbody tr:hover { background: var(--veil); }
table.data tbody tr:last-child td { border-bottom: none; }

/* Zébrage facultatif de la charte. */
table.data.zebre tbody tr:nth-child(even) { background: var(--zebre); }

.row-actions {
    display: flex;
    align-items: center;
    gap: .35rem;
    justify-content: flex-end;
    white-space: nowrap;
}

/* ============================================================
   BADGES
   ============================================================ */
.badge {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .22rem .7rem;
    border-radius: 8px;
    font-size: .72rem;
    font-weight: 700;
    line-height: 1.4;
    white-space: nowrap;
    background: var(--violet-xl);
    color: var(--violet-d);
}
/* Charte v2 : le vert est réservé aux confirmations factuelles (payé, présent,
   vérifié) et le rouge à ce qui est réellement fautif. L'avertissement n'est
   ni l'un ni l'autre : il est porté par l'accent d'identité. Les teintes
   ambre/ardoise employées ici venaient d'une palette générique. */
.badge-success { background: var(--green-bg); color: var(--green-tx); }
.badge-warning { background: var(--veil);     color: var(--violet-deep); }
.badge-danger  { background: var(--error-bg); color: var(--error-tx); }
.badge-info    { background: var(--veil);     color: var(--violet-deep); }
.badge-muted   { background: var(--veil);     color: var(--ink-soft); }

/* « En attente d'une action » — le statut qui réclame un geste.
   ⚠️ CETTE CLASSE N'EST ÉCRITE NULLE PART EN TOUTES LETTRES. Elle est
   CONSTRUITE : `badge(map, k)` d'assets/js/app.js compose `badge-${cls}`, et
   `cls` vaut « attente » pour trois statuts de dossier déclarés dans
   lib/dossiers_statuts.php (à réinscrire, pièces à compléter, sous réserve).
   Une recherche de « badge-attente » dans le dépôt ne rend donc RIEN, et la
   classe passe pour morte — elle ne l'est pas. Le liseré est ce qui la
   distingue de .badge-info, que la charte rend au même pixel (règle K7 : la
   couleur ne porte jamais seule l'information). */
.badge-attente {
    background: var(--veil);
    color: var(--violet-deep);
    border-inline-start: 3px solid var(--violet);
    padding-inline-start: .45rem;
    font-weight: 600;
}

/* ============================================================
   BOUTONS
   ============================================================ */
/* Archivo 600, rayon de contrôle. Aucune ombre, aucun dégradé, aucune
   translation : le survol change la COULEUR. Un bouton qui se soulève
   appartenait au vocabulaire « carte qui lévite » que la charte abandonne. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    padding: .58rem 1.15rem;
    border-radius: var(--radius-btn);
    border: none;
    font-family: var(--font-body);
    font-size: .88rem;
    font-weight: 600;
    line-height: 1.3;
    cursor: pointer;
    text-decoration: none;
    transition: background .15s ease, color .15s ease, border-color .15s ease;
    -webkit-user-select: none;
    user-select: none;
}
/* Ni ombre ni translation à annuler : plus rien n'en pose sur un bouton. */
.btn:disabled {
    opacity: .55;
    cursor: not-allowed;
}

.btn-primary {
    background: var(--violet);
    color: var(--on-accent);
}
.btn-primary:hover:not(:disabled) {
    background: var(--violet-deep);
    color: var(--on-accent);
}
.btn-primary:active:not(:disabled) { background: var(--violet-deep); }

/* Secondaire : fond blanc, filet lilas, encre. Au survol, filet ET texte
   passent au violet. */
.btn-outlined {
    background: var(--white);
    color: var(--ink);
    border: 1.5px solid var(--line);
}
.btn-outlined:hover:not(:disabled) {
    background: var(--white);
    color: var(--violet);
    border-color: var(--violet);
}

/* Un seul rouge dans le produit : la famille --error (R6). */
.btn-danger {
    background: var(--error);
    color: var(--on-accent);
}
.btn-danger:hover:not(:disabled) {
    background: var(--error-tx);
    color: var(--on-accent);
}

.btn-sm {
    padding: .38rem .8rem;
    font-size: .8rem;
}

.btn-icon {
    width: 34px;
    height: 34px;
    padding: 0;
    border-radius: var(--radius-btn);
    background: var(--violet-xl);
    color: var(--violet-d);
    font-size: .85rem;
}
.btn-icon:hover:not(:disabled) {
    background: var(--violet-card);
    color: var(--violet-d);
}
/* Un bouton d'icone qui porte AUSSI btn-primary n'est pas une action mise en
   avant : c'est un ETAT. La grille de notes s'en sert pour dire « une
   appreciation existe deja ici » -- classe posee par assets/js/app.js sur les
   boutons d'appreciation et de decision de passage. Avant la consolidation,
   .btn-primary etait declare deux fois et sa seconde declaration passait apres
   .btn-icon ; en fusionnant les deux generations, .btn-icon est devenu le
   dernier a parler et les deux etats se sont mis a rendre pareil. Il faut donc
   nommer la combinaison, faute de quoi la cascade rend le signal muet. */
.btn-icon.btn-primary {
    background: var(--violet);
    color: var(--on-accent);
}
.btn-icon.btn-primary:hover:not(:disabled) {
    background: var(--violet-deep);
    color: var(--on-accent);
}

.btn-icon.btn-danger {
    background: var(--error-bg);
    color: var(--error-tx);
}
/* Le survol appuie la teinte du fond sans quitter la famille : un mélange,
   là où vivait une nuance de palette étrangère. */
.btn-icon.btn-danger:hover:not(:disabled) {
    background: color-mix(in srgb, var(--error) 15%, var(--error-bg));
    color: var(--error-tx);
}

/* ============================================================
   FORMULAIRES
   ============================================================ */
.form-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: .9rem 1rem;
}
/* minmax(0, 1fr) et NON 1fr seul, pour la meme raison que .kpi-grid plus bas :
   une piste de grille a par defaut un plancher qui vaut le contenu le plus
   large qu'elle porte. Une carte dont une ligne refuse de se replier imposait
   donc sa largeur a la piste, la piste voisine se faisait ecraser, et la PAGE
   se mettait a defiler lateralement. Le plancher a zero rend la piste
   maitresse de sa largeur : c'est au contenu de s'adapter. */
.form-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.form-grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
.form-grid .full { grid-column: 1 / -1; }

/* ═══════════ LE MENU « PLUS » ═══════════
   Le motif est dans app.js, a menuPlusHtml(). Ici, trois choses seulement :
   il flotte AU-DESSUS de son voisinage, il s ouvre vers le haut quand on le
   lui dit, et sa cible tactile tient le plancher de WCAG 2.2. */
.menu-plus { position: relative; display: inline-flex; }
/* 24 px de cote au minimum (WCAG 2.2, 2.5.8). Le bouton du produit fait plus,
   mais on le declare : une reduction future se heurtera a ce plancher. */
.menu-plus-btn { min-width: 2.25rem; min-height: 2.25rem; padding-inline: .6rem; }

.menu-plus-liste {
    position: absolute;
    right: 0;
    top: calc(100% + .35rem);
    z-index: 30;
    min-width: 14rem;
    display: flex;
    flex-direction: column;
    padding: .3rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    /* La seule ombre que la charte admet : celle de ce qui SURVOLE vraiment le
       document (R2). Un menu flottant en fait partie ; une commande posee dans
       le flux, non. */
    box-shadow: var(--shadow);
}
/* Ouvert vers le haut : pose par le script apres avoir MESURE la place. */
.menu-plus-liste--haut { top: auto; bottom: calc(100% + .35rem); }

.menu-plus-item {
    display: flex;
    align-items: center;
    gap: .55rem;
    min-height: 2.25rem;
    padding: .45rem .6rem;
    border: 0;
    border-radius: var(--radius-btn);
    background: transparent;
    color: var(--ink);
    font: 500 .88rem/1.2 var(--font-body);
    text-align: left;
    cursor: pointer;
}
.menu-plus-item:hover { background: var(--veil); }
.menu-plus-item:focus-visible { outline: 2px solid var(--violet); outline-offset: -2px; }
/* Ce qui DETRUIT porte le rouge — et seulement dans ce menu, ou l entree est
   lue avant d etre cliquee. Sur une barre, ce meme rouge sautait aux yeux
   avant qu on ait cherche quoi que ce soit. */
.menu-plus-item--danger { color: var(--error-tx); }
.menu-plus-item--danger:hover { background: var(--error-bg); }

/* L en-tete de la section statistique de l ecran Eleves : son titre et le
   bouton qui la replie, sur UNE ligne. Le bouton occupait auparavant une bande
   entiere de 31 px pour lui seul, avant le bloc qu il commande. */
.el-stats-tete {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin: 1rem 0 .5rem;
}
.el-stats-tete .tuiles-titre { margin: 0; }

/* La carte qui porte le filtre COURANT se distingue par son filet, jamais par
   un aplat : l aplat violet reste reserve a l action primaire (R9), et une
   rangee de quatre cartes dont une serait pleine ferait croire a un bouton. */
.kpi.el-kpi--actif,
.kpi.kpi--actif { border-color: var(--violet); }

/* Une grille de CARTES — a distinguer de « .form-grid », qui aligne des
   CHAMPS. Une grille de champs a un nombre de colonnes voulu, fixe : les
   libelles doivent se repondre d une ligne a l autre. Une grille de cartes,
   elle, ne sait pas combien de cartes on lui donnera, et deux colonnes
   imposees laissent un trou des qu il n y en a qu une. */
.cartes-grille {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
    gap: 1rem;
}
.cartes-grille > .card { max-width: 34rem; }

.form-group {
    display: flex;
    flex-direction: column;
    gap: .35rem;
}

.form-group label,
label {
    font-size: .8rem;
    font-weight: 600;
    color: var(--dark);
}
.form-group label .req,
label .req { color: var(--error); }
.form-group .hint {
    font-size: .72rem;
    color: var(--text-muted);
}

/* Filet lilas fin (et non un filet violet epais) : la charte reserve le violet
   plein aux actions.
   `input:not([type])` : un <input> sans attribut type n'etait atteint par aucun
   selecteur de cette liste et se rendait au style brut du navigateur — coins
   vifs, filet gris. C'est le cas de 136 champs emis par assets/js/app.js, dont
   la grille de saisie des notes et le sujet d'un envoi. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="search"],
input[type="tel"],
input[type="file"],
input:not([type]),
select,
textarea {
    width: 100%;
    padding: .55rem .85rem;
    border: 1.5px solid var(--line);
    border-radius: var(--radius-btn);
    font-family: inherit;
    font-size: .88rem;
    color: var(--text);
    background: var(--white);
    transition: border-color .18s ease, box-shadow .18s ease;
}
textarea { resize: vertical; min-height: 90px; }

/* ⚠️ `input.search-input` ET NON `.search-input`. Ces champs n'ont pas
   d'attribut type : depuis que la liste ci-dessus comprend `input:not([type])`,
   elle les atteint a (0,1,1), et une classe seule, a (0,1,0), ne la battrait
   pas — le raccourci `background:` de la regle globale effacerait la loupe et
   son rembourrage de gauche. La regle est donc posee ICI, apres la liste, et
   avec la specificite qu'il faut.
   ⚠️ La loupe est peinte dans le FOND, par une URI de donnees : une image de
   fond ne peut pas lire un jeton CSS. Sa teinte est celle de --ink-soft en
   plein jour, recopiee — elle ne suivra donc pas le theme sombre. Le porter en
   `mask-image` demanderait un pseudo-element, qu'un <input> n'a pas : c'est un
   changement de balisage, il n'appartient pas a ce lot. */
input.search-input {
    padding: .55rem .9rem .55rem 2.4rem;
    background: var(--white) no-repeat .85rem center;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' width='14' height='14'%3E%3Cpath fill='%235A5478' d='M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z'/%3E%3C/svg%3E");
}

/* ⚠️ K7 — `outline: none` ici ANNULAIT la règle `:focus-visible` globale
   (plus haut dans ce fichier) par simple spécificité : `input:focus` (0,1,1)
   l'emporte sur `:focus-visible` (0,1,0). Le focus ne se voyait donc plus
   qu'à un changement de COULEUR de bordure — c'est-à-dire pas du tout pour
   qui ne distingue pas ce violet du gris, et mal au clavier.
   On garde le retrait de l'outline natif (il doublerait le halo) mais on rend
   un anneau explicite, et seulement au focus CLAVIER : `:focus-visible` ne se
   déclenche pas au clic à la souris, donc l'écran ne se met pas à clignoter
   pour qui n'en a pas besoin. */
input:focus,
select:focus,
textarea:focus,
.search-input:focus,
.rg-zone .rg-champ:focus {
    outline: none;
    border-color: var(--violet);
    box-shadow: none;
}
/* L'ANNEAU DE FOCUS UNIQUE (R2). Il s'ecrit ICI et nulle part ailleurs : trois
   traitements concurrents cohabitaient — cet anneau, un second anneau plus pale
   sur la recherche globale, et un contour plein de 3 px pose sur TOUT champ en
   focus, souris comprise, qui defaisait la decision K7 rappelee ci-dessus. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.search-input:focus-visible,
.rg-zone .rg-champ:focus-visible {
    border-color: var(--violet);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 40%, transparent);
}

input:disabled,
select:disabled,
textarea:disabled,
input[readonly] {
    background: var(--off-white);
    color: var(--text-muted);
    cursor: not-allowed;
}

input[type="checkbox"],
input[type="radio"] {
    width: 16px;
    height: 16px;
    accent-color: var(--violet);
    cursor: pointer;
}

::placeholder { color: var(--text-muted); opacity: .7; }

/* ============================================================
   MODALES
   ============================================================ */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(26,16,64,.55);
    display: none;
    z-index: 1000;
    padding: 1rem;
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
}
.modal-overlay.open {
    display: flex;
    align-items: center;
    justify-content: center;
}

.modal {
    background: var(--white);
    border-radius: var(--radius);
    width: min(640px, 94vw);
    max-height: 92vh;
    overflow: auto;
    box-shadow: var(--shadow);
    animation: modal-pop .22s ease;
}
.modal.modal-lg { width: min(940px, 94vw); }

/* ── LA FICHE ÉLÈVE A SA PROPRE LARGEUR ──
   Elle portait `.modal-lg`, déclarée une fois et posée sur la plupart des
   grandes modales du produit : l'élargir là aurait déplacé tous ces écrans pour
   régler le problème d'un seul. Ses neuf onglets et leurs comptes ne tiennent
   pas sur une rangée à 940 px ; ils tiennent à 1 120. Sous cette largeur de
   fenêtre, la barre reprend deux rangées — c'est-à-dire le comportement qu'elle
   avait partout, pas une régression. */
.modal.modal-fiche { width: min(1120px, 94vw); }

@keyframes modal-pop {
    from { opacity: 0; transform: translateY(14px) scale(.98); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}

.modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.15rem 1.4rem;
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    background: var(--white);
    z-index: 2;
    /* Les quatre angles de la modale sont d'accord : la tete et le pied
       reprennent le rayon de la boite au lieu d'en inventer un. */
    border-radius: var(--radius) var(--radius) 0 0;
}
.modal-head h3 {
    font-size: 1.05rem;
    display: flex;
    align-items: center;
    gap: .55rem;
}
.modal-head h3 i { color: var(--violet); }

.modal-close {
    width: 32px;
    height: 32px;
    border: none;
    border-radius: var(--radius-btn);
    background: var(--violet-xl);
    color: var(--violet-d);
    font-size: .95rem;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background .15s ease;
}
.modal-close:hover { background: var(--violet-card); }

.modal-body { padding: 1.25rem 1.4rem; }

.modal-foot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: .6rem;
    padding: 1rem 1.4rem;
    border-top: 1px solid var(--border);
    position: sticky;
    bottom: 0;
    background: var(--white);
    border-radius: 0 0 var(--radius) var(--radius);
}

/* ============================================================
   ALERTES
   ============================================================ */
.alert {
    display: flex;
    align-items: flex-start;
    gap: .65rem;
    /* Rayon de CONTROLE et non de surface : une alerte vit DANS une carte,
       un rayon de surface l'y mettrait en concurrence avec son contenant. */
    padding: .85rem 1.1rem;
    border-radius: var(--radius-btn);
    font-size: .88rem;
    line-height: 1.5;
    border: 1px solid transparent;
}
.alert i { margin-top: .15rem; }

.alert-success {
    background: var(--green-bg);
    color: var(--green-tx);
    border-color: color-mix(in srgb, var(--green) 25%, transparent);
}
.alert-error {
    background: var(--error-bg);
    color: var(--error-tx);
    border-color: color-mix(in srgb, var(--error) 25%, transparent);
}
.alert-info {
    background: var(--violet-xl);
    color: var(--violet-d);
    border-color: var(--border);
}
/* « Attention sans gravité » : à distinguer d'une erreur (rouge) et d'une simple
   information (violet). Plusieurs écrans l'employaient déjà sans qu'elle existe :
   l'encadré s'affichait alors sans couleur, donc sans signal. */
.alert-warning {
    background: var(--veil);
    color: var(--violet-deep);
    border-color: var(--line);
}
/* Alias : « danger » et « error » désignent la même chose côté interface. */
.alert-danger {
    background: var(--error-bg);
    color: var(--error-tx);
    border-color: color-mix(in srgb, var(--error) 25%, transparent);
}

/* ============================================================
   ÉTAT VIDE
   ============================================================ */
.empty-state {
    text-align: center;
    padding: 2.5rem 1.5rem;
    color: var(--text-muted);
}
.empty-state i {
    font-size: 2.2rem;
    color: var(--violet-l);
    opacity: .6;
    margin-bottom: .75rem;
    display: block;
}
/* h3 et non h4 : assets/js/app.js émet un <h3> dans chacun de ses états
   vides — le titre n'était donc jamais stylé. */
/* h3 ET h4 : le back-office emet des h3, mais les deux seuls etats vides en h4
   du produit sont ceux du portail des familles et de la preinscription. Ne
   nommer que h3 leur retirait leur mise en forme. */
.empty-state h3,
.empty-state h4 {
    color: var(--dark);
    margin-bottom: .3rem;
    font-weight: 700;
}
.empty-state p { font-size: .85rem; }

/* ============================================================
   TOASTS — les cartes « fragment » de la charte
   Sur le site, ces cartes flottent au-dessus des captures et racontent un
   moment de la journée. Dans le produit, ce sont les mêmes : carte blanche,
   filet lilas, rayon de surface, ombre unique — c'est bien un objet qui
   SURVOLE le document.
   ============================================================ */
.toast-wrap {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 2000;
    display: flex;
    flex-direction: column;
    gap: .6rem;
    max-width: min(380px, calc(100vw - 2rem));
}

.toast {
    background: var(--white);
    border: 1px solid var(--border);
    /* Propriete LOGIQUE : un `border-left` resterait a gauche en arabe. */
    border-inline-start: 3px solid var(--violet);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: .7rem .9rem;
    font-size: .86rem;
    line-height: 1.45;
    color: var(--ink);
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    animation: toast-in .24s cubic-bezier(.16,.84,.44,1);
}
.toast.success { border-inline-start-color: var(--green); }
.toast.success i { color: var(--green); }
.toast.error { border-inline-start-color: var(--error); }
.toast.error i { color: var(--error); }
/* La pastille d'icône d'une confirmation est verte, comme sur le site. */
.toast.success > i:first-child {
    color: var(--green);
    background: var(--green-bg);
    border-radius: 50%;
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    flex: 0 0 auto;
}

@keyframes toast-in {
    from { opacity: 0; transform: translateX(24px); }
    to   { opacity: 1; transform: translateX(0); }
}

/* ============================================================
   GRAPHIQUES (Chart.js) — conteneur à HAUTEUR FIXE obligatoire :
   en mode responsive sans ratio, un canvas sans hauteur bornée
   grandit indéfiniment et étire la page verticalement.
   ============================================================ */
.chart-box {
    position: relative;
    height: 240px;
    width: 100%;
}
.chart-box canvas {
    position: absolute;
    inset: 0;
    max-height: 100%;
}
/* Listes/tableaux longs du tableau de bord : hauteur bornée + défilement interne */
.table-scroll {
    max-height: 320px;
    overflow-y: auto;
}

/* ============================================================
   BARRES DE PROGRESSION
   ============================================================ */
.progress {
    width: 100%;
    height: 8px;
    border-radius: var(--radius-btn);
    background: var(--violet-xl);
    overflow: hidden;
}
.progress-fill {
    height: 100%;
    border-radius: var(--radius-btn);
    background: var(--violet);
    transition: width .35s ease;
}
.progress-fill.warn { background: var(--violet-deep); }
.progress-fill.full { background: var(--error); }

/* ============================================================
   ONGLETS PILULES
   ============================================================ */
.tabs-inline {
    display: flex;
    align-items: center;
    gap: .4rem;
    flex-wrap: wrap;
}

.tab-pill {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    padding: .42rem .95rem;
    border-radius: var(--radius-btn);
    border: 1.5px solid var(--violet-card);
    background: var(--white);
    color: var(--text-muted);
    font-family: inherit;
    font-size: .82rem;
    font-weight: 600;
    cursor: pointer;
    transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.tab-pill:hover {
    border-color: var(--violet);
    color: var(--violet-d);
}
.tab-pill.active {
    background: var(--violet);
    border-color: transparent;
    color: var(--on-accent);
}

/* ============================================================
   PIÈCES JUSTIFICATIVES (lignes de documents)
   ============================================================ */
.piece-row {
    display: flex;
    align-items: center;
    gap: .8rem;
    padding: .7rem .9rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-btn);
    background: var(--white);
    transition: background .15s ease, border-color .15s ease;
}
.piece-row:hover {
    background: var(--violet-xxl);
    border-color: var(--violet-card);
}
.piece-row + .piece-row { margin-top: .5rem; }
.piece-row .piece-name {
    flex: 1;
    min-width: 0;
    font-size: .88rem;
    font-weight: 600;
    color: var(--dark);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.piece-row .piece-meta {
    font-size: .75rem;
    color: var(--text-muted);
    white-space: nowrap;
}
/* Le balisage emet `<span class="piece-icon"><i>` : le selecteur qui visait
   `i.piece-icon` ne rencontrait donc jamais rien, et l'icone tombait sur
   l'encre et la chasse du texte voisin. Le selecteur porte desormais sur la
   classe, la ou elle est reellement posee. */
.piece-row .piece-icon {
    font-size: 1.1rem;
    color: var(--violet);
    width: 24px;
    text-align: center;
}

/* ============================================================
   PAGE DE CONNEXION
   ============================================================ */
.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    background: var(--bg);
}

.login-card {
    width: 420px;
    max-width: 100%;
    background: var(--white);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 2.2rem 2rem;
}
.login-card .form-group { margin-bottom: .9rem; }

.login-card .logo,
.login-page .logo {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .65rem;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 1.4rem;
    color: var(--dark);
    letter-spacing: normal;
    margin-bottom: .4rem;
}
.logo-mark {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
}
.login-card .login-sub {
    text-align: center;
    font-size: .85rem;
    color: var(--text-muted);
    margin-bottom: 1.5rem;
}
.login-card .btn { width: 100%; }
.login-card .login-foot {
    margin-top: 1.4rem;
    text-align: center;
    font-size: .72rem;
    color: var(--text-muted);
    line-height: 1.5;
}

/* ============================================================
   UTILITAIRES LÉGERS
   ============================================================ */
.text-muted { color: var(--text-muted); }
.text-center { text-align: center; }
.text-right { text-align: right; }
.mt-1 { margin-top: .5rem; }
.mt-2 { margin-top: 1rem; }
.mb-1 { margin-bottom: .5rem; }
.mb-2 { margin-bottom: 1rem; }
.flex { display: flex; align-items: center; gap: .6rem; }
.flex-wrap { flex-wrap: wrap; }
.grow { flex: 1; }
.nowrap { white-space: nowrap; }

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width: 1100px) {
    .form-grid.cols-3 { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 900px) {
    .app-layout { flex-direction: column; }

    .sidebar {
        width: 100%;
        min-width: 0;
        height: auto;
        position: static;
        padding: .75rem .9rem;
    }
    .sidebar-brand { padding-bottom: .6rem; }
    .sidebar-nav {
        flex-direction: row;
        overflow-x: auto;
        gap: .35rem;
        padding: .2rem 0 .4rem;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: thin;
    }
    .nav-item {
        flex: 0 0 auto;
        padding: .5rem .8rem;
        font-size: .82rem;
    }
    .sidebar-foot { display: none; }

    .topbar {
        flex-wrap: wrap;
        padding: .7rem 1rem;
        gap: .5rem;
    }
    .topbar-title { white-space: normal; }

    .form-grid.cols-2,
    .form-grid.cols-3 { grid-template-columns: 1fr; }

    .search-input { max-width: none; }

    .kpi-value { font-size: 1.6rem; }

    .modal,
    .modal.modal-lg { width: 96vw; }

    .toast-wrap { left: 1rem; right: 1rem; max-width: none; }
}

@media (max-width: 520px) {
    .content { padding: 1rem .8rem; }
    .card { padding: 1rem; }
    .kpi-grid { grid-template-columns: 1fr 1fr; }
    /* `stretch` et non `flex-start` : en colonne, le corps est un enfant dont
       la taille TRANSVERSE est la largeur. Posé à `flex-start`, il épousait son
       texte — et la courbe, qui fait toute la largeur de son parent, épousait
       le texte avec lui. Deux cartes voisines rendaient alors deux courbes de
       deux largeurs. La tuile d'icône garde la sienne : elle est fixée. */
    .kpi { flex-direction: column; align-items: stretch; gap: .5rem; }
    .row-actions { justify-content: flex-start; }
}

/* ============================================================
   IMPRESSION
   ============================================================ */
@media print {
    body { background: #fff; font-size: 12px; }
    .sidebar,
    .topbar,
    .toolbar,
    .toast-wrap,
    .modal-overlay,
    .btn,
    .row-actions,
    .tabs-inline {
        display: none !important;
    }
    .app-layout { display: block; }
    .main { display: block; }
    .content { padding: 0; }
    .card,
    .table-wrap {
        box-shadow: none;
        border: 1px solid #ddd;
        break-inside: avoid;
    }
    table.data th { background: #f4f4f6; color: #333; }
    a { color: inherit; text-decoration: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   CHARTE v2 « LA JOURNÉE D'ÉCOLE » — les composants qui lui sont PROPRES
   Ce bloc primait autrefois sur une base pré-charte, par sa seule position en
   fin de feuille : `.btn`, les tableaux, les cartes, les toasts, les champs et
   la carte de connexion y étaient définis une SECONDE fois. Ces doublons ont
   rejoint leur composant, plus haut — une définition par composant, celle de
   la charte. Ne subsiste ici que ce qui n'existe qu'ici.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── Chiffres tabulaires ───────────────────────────────────────────────────
   Un montant, une heure, un numéro : mono tabulaire, calé à droite, et isolé
   du sens d'écriture — sinon « 1 400,00 DH » se lit à l'envers en arabe. */
.montant, .heure, .num-recu, .matricule, .code-massar,
table.data td.num, table.data th.num {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'tnum' 1;
    direction: ltr;
    unicode-bidi: isolate;
    white-space: nowrap;
}
table.data td.num, table.data th.num,
table.data td .montant, table.data td .heure { text-align: right; }

/* États d'un règlement : le vert dit « c'est encaissé », le rouge « il manque
   quelque chose ». Aucun autre usage de ces deux couleurs. */
.montant.est-paye   { color: var(--green-tx); font-weight: 600; }
.montant.est-impaye { color: var(--error-tx); font-weight: 600; }

/* ── La « fenêtre produit » ───────────────────────────────────────────────
   Le composant signature du site vitrine. Réutilisé pour tout aperçu, état
   vide ou page d'aide, afin que la plateforme et le site parlent la même
   langue. Elle SURVOLE le document : elle porte donc l'ombre unique. */
.fenetre {
    background: var(--white);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
}
.fenetre-barre {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .55rem .8rem;
    background: var(--veil);
    border-bottom: 1px solid var(--line);
}
.fenetre-pastilles { display: flex; gap: .3rem; }
.fenetre-pastilles i,
.fenetre-pastille {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--pastille);
    display: inline-block;
}
/* La première pastille porte l'identité. */
.fenetre-pastilles i:first-child,
.fenetre-pastille:first-child { background: var(--violet); }
.fenetre-url {
    flex: 1;
    text-align: center;
    font-family: var(--font-mono);
    font-size: .76rem;
    color: var(--ink-soft);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    direction: ltr;
}

/* ── Le mot-symbole des pages de connexion ───────────────────────────────── */
.login-page .logo span,
.login-card .logo span { font-family: var(--font-display); font-weight: 600; }

/* Un champ de recherche posé dans une CARTE (et non dans une barre d'outils
   flex) ne recevait jamais son `flex: 1` : il restait à sa largeur minimale de
   220 px et tronquait son propre libellé d'aide. */
.card > .search-input,
.card > .form-group > .search-input { width: 100%; max-width: none; }

/* ── BOUTON « ŒIL » SUR LES CHAMPS DE MOT DE PASSE ───────────────────────
   Charte v2 : pas d'ombre, rayon de bouton, survol par la couleur, et le
   contour de focus commun a tout le produit — la regle globale :focus-visible
   en tete de feuille le pose deja, une seconde copie ici ne faisait que
   dupliquer le meme dessin. Le bouton se superpose au champ sans en reduire la
   largeur ;
   le padding de fin est logique (padding-inline-end) pour que le retournement
   RTL place l'icone du bon cote. */
.champ-mdp { position: relative; display: block; }
.champ-mdp > input { width: 100%; padding-inline-end: 2.6rem; }
.oeil-mdp {
    position: absolute;
    inset-block-start: 50%;
    inset-inline-end: .3rem;
    transform: translateY(-50%);
    width: 2rem;
    height: 2rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    border-radius: var(--radius-btn);
    color: var(--ink-soft);
    cursor: pointer;
    font-size: .95rem;
    line-height: 1;
    transition: color .15s ease, background .15s ease;
}
.oeil-mdp:hover { color: var(--violet); background: var(--veil); }
/* Etat « mot de passe visible » : l'accent d'identite le signale. */
.oeil-mdp[aria-pressed="true"] { color: var(--violet); }

/* ── Recouvrement — choix du canal & aperçu de relance ─────────────────────────
   Un canal refusé (consentement) ou sans coordonnée reste visible mais grisé et
   expliqué (title) : on ne masque pas l'option, on dit pourquoi elle est fermée.
   Rien de rouge ici — « en retard » vit à l'accent d'identité. */
.rec-canaux { display: flex; gap: .5rem; flex-wrap: wrap; }
.rec-canal.is-off,
.rec-canal[disabled] {
    opacity: .5;
    cursor: not-allowed;
    border-style: dashed;
}
.rec-canal[disabled]:hover { border-color: var(--violet-card); color: var(--text-muted); }

.rec-preview {
    border: 1px solid var(--line);
    border-radius: var(--radius-btn);
    background: var(--veil);
    padding: .8rem 1rem;
    color: var(--ink);
    font-size: .9rem;
    line-height: 1.55;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 260px;
    overflow: auto;
}
.rec-preview:empty::before { content: 'L’aperçu du message s’affichera ici.'; color: var(--ink-faint); }
.rec-preview-sujet { font-weight: 700; color: var(--violet-deep); margin-bottom: .5rem; }

/* Bandeau de synthèse d'une famille (montant, palier, retard) en tête de modale. */
.rec-somme {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    padding: .6rem .85rem;
    margin-bottom: 1rem;
    background: var(--veil);
    border-radius: var(--radius-btn);
    font-size: .9rem;
    color: var(--ink);
}

/* ============================================================
   CSP style-src — classes issues d'attributs style= (ne rien mêler ici)
   ------------------------------------------------------------
   Section ajoutée pour le retrait de 'unsafe-inline' de style-src : elle
   reprend les déclarations qui vivaient dans des attributs style= des pages
   chargeant cette feuille (dashboard.php, login.php) et du bandeau de
   démonstration injecté dans le tableau de bord (lib/demo_provision.php).
   ⚠️ La mission CSP s'était interdit de remplacer une couleur littérale par un
   jeton, et renvoyait cette tokenisation « à la charte, pas à la CSP ». C'est
   fait ici : les couleurs de cette section sont désormais prises dans les
   jetons, ce qui les rend sensibles au thème comme le reste du produit.
   ============================================================ */

/* Monogramme de la barre latérale (dashboard.php) : encre claire sur le fond
   --nav. La couleur est portée par le CONTEXTE .sidebar-brand, et non par
   .logo-mark seule : la même marque est posée sur des surfaces claires
   ailleurs (carte de connexion), où elle doit rester bicolore. */
.sidebar-brand .logo-mark { color: var(--bg); }

/* Liens de service sous les formulaires de connexion (login.php). */
.login-lien { text-align: center; margin-top: 1rem; }
.login-lien--petit { font-size: .9rem; }

/* Bandeau de démonstration (lib/demo_provision.php, injecté par dashboard.php).
   Deux classes pour les deux fonds possibles (normal / urgence). Le compte à
   rebours continue de piloter la couleur en CSSOM (b.style.background) : une
   déclaration en ligne posée par script prime sur la classe, la bascule reste
   donc pleinement fonctionnelle par-dessus ces règles. */
.demo-bar {
    display: flex;
    align-items: center;
    gap: 1rem;
    justify-content: center;
    flex-wrap: wrap;
    padding: .6rem 1rem;
    color: var(--on-accent);
    font-size: .9rem;
    background: var(--violet-deep);
}
.demo-bar--urgent { background: var(--error); }
.demo-bar-form { margin: 0; }
.demo-bar-btn {
    background: var(--white);
    color: var(--violet-deep);
    border: none;
    border-radius: 8px;
    padding: .4rem .9rem;
    font-weight: 700;
    font-size: .85rem;
    cursor: pointer;
    white-space: nowrap;
}
.demo-bar-max { opacity: .85; font-size: .82rem; }

/* ============================================================
   CSP style-src — classes issues des attributs style= des gabarits JS
   ------------------------------------------------------------
   Deuxième volet du retrait de 'unsafe-inline' : les gabarits injectés par
   assets/js/app.js (innerHTML) portent aujourd'hui 163 attributs style=. Un
   attribut style= posé par innerHTML est refusé au parsage dès que style-src
   contient un nonce — le nonce ne couvre JAMAIS les attributs. Cette section
   fournit la classe équivalente de CHAQUE valeur rencontrée, pour que le
   passage du JS aux classes n'ait plus qu'à substituer.

   RÈGLES SUIVIES ICI, sans exception :

   1. PARITÉ DE STRUCTURE. Chaque déclaration reprend la valeur d'origine, à
      ceci près que les COULEURS ont depuis rejoint les jetons : la mission CSP
      s'était interdit de les substituer et renvoyait ce travail « à la charte,
      pas à la CSP » — c'est le lot de consolidation qui l'a fait. Conséquence
      voulue : ces classes suivent maintenant le thème, ce que les attributs
      style= d'origine ne faisaient pas.

   2. SECTION EN FIN DE FICHIER. À égalité de spécificité, la dernière règle
      l'emporte : placée ici, une classe bat .card, .chip, .flex, .toolbar,
      .btn-primary ou .row-actions sans avoir besoin de !important.

   3. !important RÉSERVÉ À display. Un attribut style= l'emportait sur tout ;
      une classe, non. Là où une règle EXISTANTE plus spécifique reprendrait la
      main (en-têtes de tableau, cellules, survol de ligne, survol de bouton),
      la classe est doublée d'un sélecteur companion de spécificité suffisante
      — signalé au cas par cas ci-dessous. Aucune propriété pilotée ensuite en
      CSSOM (width des barres de progression, borderColor des champs de notes)
      ne porte de !important : une écriture el.style.prop perdrait contre lui.

   4. NOMMAGE. .u-<propriété>-<valeur> pour les utilitaires mécaniques (unité
      écrite en toutes lettres : .u-maxw-10rem vs .u-maxw-140px, le point
      décimal devient un tiret : 5.5rem -> 5-5rem) ; nom métier pour les
      valeurs composites attachées à un écran précis.

   Aucun de ces noms n'existait dans le dépôt (vérifié sur les 104 fichiers
   css/js/php/html hors vendor) : tant qu'aucune classe n'est posée dans les
   gabarits, cette section ne modifie rien.
   ============================================================ */

/* ----- Masquage : attribut hidden ----------------------------------------
   Dix éléments naissaient avec style="display:none" puis étaient révélés par
   el.style.display = ''. Ces deux moitiés migrent ENSEMBLE vers l'attribut
   hidden + el.hidden = booléen (stratégie imposée aux missions JS).
   Le !important est indispensable : la majorité de ces éléments sont des
   .form-group, et .form-group{display:flex} l'emporterait sinon sur la valeur
   display:none du navigateur — c'est très exactement la raison pour laquelle
   les auteurs avaient dû écrire le display en ligne. Même règle que la
   coquille de la PWA famille (famille/app.css).
   Vérifié : les seuls éléments du back-office portant déjà hidden sont trois
   <input type="file"> volontairement invisibles — comportement inchangé. */
.u-hidden,
[hidden] { display: none !important; }

/* ----- Curseur, texte, couleurs ----------------------------------------- */
.u-pointer          { cursor: pointer; }                    /* ×26 */
.u-tx-violet        { color: var(--violet); }               /* ×6  */
.u-tx-vert          { color: var(--green); }                /* ×2  */
.u-tx-vert-vif      { color: var(--green); }                /* ×2  */
.u-tx-rouge         { color: var(--error); }                /* ×3  */
/* « Brique » : montants dus et alertes de retard du recouvrement. Le jeton
   existe et pointe sur la famille --error : ces deux classes la suivent
   désormais au lieu de figer une valeur claire. */
.u-tx-brique        { color: var(--brique); }               /* ×2  */
.u-tx-brique-fort   { color: var(--brique); font-weight: 700; }   /* ×1  */
.u-tx-violet-fonce  { color: var(--violet-deep); }          /* ×2  */
.u-fw-600           { font-weight: 600; }
.u-break-all        { word-break: break-all; }              /* ×2  */
.u-pre-wrap         { white-space: pre-wrap; }

/* Alignements. Companion obligatoire : table.data th impose text-align:start
   et table.data td.num impose right — deux sélecteurs à (0,1,2) que la classe
   seule, à (0,1,0), ne battrait pas. */
.u-ta-c { text-align: center; }                             /* ×5 */
.u-ta-r { text-align: right; }                              /* ×2 */
table.data th.u-ta-c,
table.data td.u-ta-c { text-align: center; }
table.data th.u-ta-r,
table.data td.u-ta-r { text-align: right; }

/* ----- Tailles ----------------------------------------------------------- */
.u-maxw-5-5rem { max-width: 5.5rem; }                       /* ×2 */
.u-maxw-6rem   { max-width: 6rem; }
.u-maxw-10rem  { max-width: 10rem; }                        /* ×6 */
.u-maxw-140px  { max-width: 140px; }
.u-maxw-150px  { max-width: 150px; }                        /* ×2 */
.u-maxw-220px  { max-width: 220px; }
.u-maxw-240px  { max-width: 240px; }
.u-maxw-260px  { max-width: 260px; }
.u-maxw-640px  { max-width: 640px; }
.u-maxw-760px  { max-width: 760px; }                        /* ×4 */
.u-maxw-860px  { max-width: 860px; }                        /* ×3 */
.u-minw-140px  { min-width: 140px; }
.u-w-2-5rem    { width: 2.5rem; }                           /* ×5 */
.u-w-34px      { width: 34px; }
.u-h-260px     { height: 260px; }

/* ----- Flux : écarts, alignements, grille -------------------------------- */
.u-gap-035     { gap: .35rem; }
.u-gap-04      { gap: .4rem; }                              /* ×2 */
.u-gap-05      { gap: .5rem; }                              /* ×3 */
.u-wrap-gap-05 { flex-wrap: wrap; gap: .5rem; }             /* ×3 */
/* Posée sur un .form-group : flex-direction:column reste hérité de
   .form-group, exactement comme avec l'attribut d'origine. */
.u-flex-gap-15 { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.u-jc-start    { justify-content: flex-start; }
.u-jc-center   { justify-content: center; }
.u-as-end      { align-self: end; }
.u-grid-full   { grid-column: 1/-1; }

/* ----- Marges ------------------------------------------------------------ */
.u-mb-05  { margin-bottom: .5rem; }
.u-ml-05  { margin-left: .5rem; }
.u-mr-06  { margin-right: .6rem; }
.u-mt-03  { margin-top: .3rem; }
.u-mt-055 { margin-top: .55rem; }                           /* ×2 */
.u-my-02  { margin: .2rem 0; }
.u-page-sub { margin: .2rem 0 1rem; }   /* sous-titre de page, ×4 */

/* ----- Corps de texte ---------------------------------------------------- */
.u-fs-08    { font-size: .8rem; }                           /* ×2 */
.u-fs-085em { font-size: .85em; }        /* em et non rem : valeur d'origine */
.u-fs-088   { font-size: .88rem; }
.u-fs-13    { font-size: 1.3rem; }                          /* ×3 */
.u-note-09-mt-08  { font-size: .9rem; margin-top: .8rem; }
.u-note-09-mt-06  { font-size: .9rem; margin-top: .6rem; }
.u-note-085-mt-05 { font-size: .85rem; margin: .5rem 0 0; }

/* ----- Opacités et état « annulé » --------------------------------------- */
.u-op-5   { opacity: .5; }
.u-op-55  { opacity: .55; }                                 /* ×3 */
.u-op-6   { opacity: .6; }
.u-annule { opacity: .55; text-decoration: line-through; }

/* ----- Blocs et cartes --------------------------------------------------- */
.u-scroll-180  { max-height: 180px; overflow: auto; }       /* ×2 */
.u-carte-etape { border-left: 4px solid var(--violet); }  /* carte « Premiers pas » */
.u-carte-caisse { background: var(--bg); }      /* carte du dossier de caisse */
.u-liste-etapes { margin: .3rem 0 .3rem 1.2rem; line-height: 2; }
.u-graph-vide   { padding: 1rem .25rem; }    /* graphique sans donnée */
.u-logo-apercu  { height: 44px; border-radius: 8px; } /* vignette du logo */

/* Cellule « aucun résultat » d'un tableau. Companion obligatoire : la règle
   « table.data td » impose son propre rembourrage, à (0,1,2). */
.u-vide-1 { text-align: center; padding: 1rem; }            /* ×2 */
.u-vide-2 { text-align: center; padding: 2rem; }
table.data td.u-vide-1 { text-align: center; padding: 1rem; }
table.data td.u-vide-2 { text-align: center; padding: 2rem; }

/* ----- Pastilles de bilan (caisse, recouvrement) ------------------------- */
.u-chip-succes { background: var(--green-bg); color: var(--green-tx); }
.u-chip-erreur { background: var(--error-bg); color: var(--error-tx); }

/* ----- Codes d'accès affichés une seule fois ----------------------------- */
.u-code-lot     { font-weight: 800; letter-spacing: .05em; }
.u-code-portail { font-size: 1.6rem; font-weight: 800; letter-spacing: .08em; }

/* ----- Arbre des classes (module Élèves) --------------------------------- */
.u-arbre-cycle {
    font-weight: 700;
    font-size: .72rem;
    text-transform: uppercase;
    letter-spacing: .05em;
}
.u-arbre-classes { margin: .15rem 0 .35rem .5rem; }

/* ----- Grille d'appel : bouton « retard » sélectionné --------------------
   Le bouton porte alors .btn-primary, dont l'état de survol impose
   background:var(--violet-deep) à (0,3,0) (règle « .btn-primary:hover »).
   L'attribut d'origine tenait au survol ; le companion à (0,4,0) le fait tenir
   aussi. */
/* LA PASTILLE DE « DEJA FAIT » — une confirmation, pas un aplat d action.
   ⚠️ Elle s appelait « .etat-fait », du nom des Absences ou elle est
   nee. QUATRE ecrans s en servent aujourd hui : le « present » de l Appel,
   l appreciation de la saisie des notes, celle du recapitulatif de classe et
   la decision du recapitulatif annuel. Un nom de module sur une regle que
   quatre modules partagent finit par empecher le cinquieme de s en servir —
   on croit toucher aux Absences. Elle porte donc ce qu elle FAIT.
   Le motif complet est dans app.js, a la fabrique des trois boutons d appel.
   Le filet reprend la couleur du texte : sans lui, une pastille pale sur une
   ligne claire ne se distinguerait pas assez d un bouton simplement cerne. */
.etat-fait,
.btn.etat-fait {
    background: var(--green-bg);
    color: var(--green-tx);
    border-color: var(--green-tx);
}
.btn.etat-fait:hover:not(:disabled) {
    background: var(--green-tx);
    color: var(--green-bg);
}
.abs-btn-retard { background: var(--violet-deep); }
.btn.abs-btn-retard,
.btn.abs-btn-retard:hover:not(:disabled) { background: var(--violet-deep); }

/* ----- Échéancier : lignes annulée / en retard ---------------------------
   Remplace l'attribut unique construit dans assets/js/app.js (trStyle), dont
   les deux seules valeurs possibles sont reprises ici.
   Le fond « retard » se posait par-dessus le survol de ligne
   (table.data tbody tr:hover, spécificité (0,2,3)) : le
   companion de même spécificité, placé plus bas, conserve ce comportement.
   Réserve : si un jour la classe .zebre était ajoutée à ce tableau, son
   nth-child (0,3,3) reprendrait la main — elle n'y est pas aujourd'hui. */
.ech-annulee { opacity: .6; }
.ech-retard  { background: var(--error-bg); }
table.data tbody tr.ech-retard,
table.data tbody tr.ech-retard:hover { background: var(--error-bg); }

/* ----- Calendrier : la ligne qu'on est venu voir -------------------------
   Posée sur les CELLULES, pas sur la ligne : la table est en
   border-collapse, où le fond et l'ombre d'un <tr> ne se
   peignent pas de façon fiable. Un fond de <td> passe toujours par-dessus
   celui du <tr>, donc le survol ne l'efface pas — inutile de courir après
   la spécificité de table.data tbody tr:hover.
   Cadre en --violet et non en littéral : le jeton est réécrit en thème
   sombre (tokens.css), un violet en dur y deviendrait illisible. */
table.data tbody tr.cal-cible > td {
    background: var(--veil);
    border-top: 2px solid var(--violet);
    border-bottom: 2px solid var(--violet);
}

/* ----- Séparateur de mois du calendrier ----------------------------------
   ⚠️ IL SE LISAIT COMME UN ÉVÉNEMENT. C'était un <tr><td class="text-muted">
   ordinaire : même fond, même hauteur, même famille que les lignes de
   données. « Septembre 2026 » passait pour un événement ainsi intitulé — le
   fondateur l'a signalé, et c'est exactement ce qu'on lit.

   Trois choses le distinguent maintenant, et aucune n'est décorative :
   le FOND (--veil, la surface calme de la charte, jamais employée pour une
   ligne de données) ; la CAPITALE espacée, réservée aux étiquettes et jamais
   à un contenu saisi ; et la RÈGLE ÉPAISSE au-dessus, qui ferme visuellement
   le groupe précédent plutôt que d'ouvrir une ligne de plus.

   Le premier séparateur n'a pas de règle : il n'y a rien à fermer au-dessus
   de lui, et une barre y ressemblerait à un bord de tableau raté. */
table.data tbody tr.cal-mois > th {
    background: var(--veil);
    color: var(--ink-soft);
    font-family: var(--font-body);
    font-size: .74rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    text-align: start;
    padding: var(--sp-2) var(--sp-3);
    border-top: 2px solid var(--line);
}
table.data tbody tr.cal-mois:first-child > th { border-top: 0; }

/* ----- Barres de progression --------------------------------------------
   Six barres naissaient avec une largeur en ligne : deux à zéro (simples
   états initiaux) et quatre à une largeur calculée. La classe ne porte que le
   constant
   — la largeur nulle de départ — et la valeur réelle est posée en CSSOM,
   el.style.width = pct + '%', ce qui reste autorisé sans 'unsafe-inline'.
   Sans cette règle, un .progress-fill sans largeur s'afficherait PLEIN le
   temps d'une image : c'est un div de niveau bloc dans un conteneur à
   width:100%. PAS de !important : il bloquerait l'écriture CSSOM qui suit.
   Les missions JS doivent poser la largeur dans la MÊME tâche synchrone que
   l'affectation d'innerHTML : le style n'ayant pas encore été résolu, la
   transition width .35s de .progress-fill ne se déclenche pas et le rendu est
   celui d'avant, sans animation d'apparition. */
.progress-init { width: 0%; }

/* ═══════════════ Accueil — indicateurs + widgets des modules ═══════════════
   Écran d'entrée : les quatre indicateurs du jour d'abord — c'est ce qui
   réclame une action — puis les modules en widgets, chacun reconnaissable
   à son icône ET à sa teinte. Les teintes restent des NUANCES sourdes (jamais saturées) pour
   rester compatibles avec l'identité violette. */

/* ── Indicateurs de l'accueil ──
   Ils sont SOUS le bandeau, sur fond clair : enfermes dans l'aplat violet ils
   devenaient decoratifs et surtout indifferenciables les uns des autres, alors
   que leur role est justement de hierarchiser ce qui reclame une action. */
.acc-kpi { margin-top: 1.1rem; }
/* ⚠️ LA RANGÉE EST ALIGNÉE, LIGNE PAR LIGNE. Les quatre valeurs ne sont pas
   composées au même corps — un montant est plus long qu'un effectif et cède
   du sien — et la puce de tendance rehausse la ligne qui la porte. Chaque
   carte prenait donc une hauteur de ligne différente, et comme la carte
   centre son corps, libellés, chiffres et courbes de la rangée se décalaient
   les uns des autres de quelques pixels. Un plancher commun les remet sur la
   même ligne, quelles que soient les données du jour.
   ⚠️ Réservé à l'accueil : ailleurs, une carte d'indicateur n'a pas de rangée
   de quatre à tenir, et lui imposer ce plancher la grandirait sans raison. */
.acc-kpi .kpi-ligne { min-height: 2rem; }

/* `.kpi-contexte`, `.kpi--action` et `.kpi--alerte` ont été RETIRÉES : aucun
   gabarit du dépôt ne les émet (recherche sur php/js/css, famille comprise,
   vendor exclu). `.kpi--action` portait de surcroît une TROISIÈME copie de la
   paire de teintes du domaine pilotage — c'est une classe morte, elle se
   supprime, elle ne se tokenise pas. Les états d'un indicateur passent par la
   famille sémantique (.kpi-violet / .kpi-green / .kpi-red), qui, elle, sert. */

/* ── Widgets des modules ── */
/* Accueil COMPACT : les onze tuiles + les indicateurs depassaient le bas d un
   ecran de portable (1366x768), obligeant a defiler des l ouverture — or
   l accueil est la carte du produit, il doit se lire d un coup d oeil. */
.tuiles-titre {
    margin: 1.1rem 0 .7rem;
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-muted);
}
/* `.tuiles-grid` a été RETIRÉE : plus aucun gabarit ne l'émettait depuis que
   les domaines sont devenus des cartes. L'arbitrage « 4+4+3 » qu'elle portait
   supposait une grille pleine largeur par section — la mise en page ci-dessous
   n'en a plus, et laisser une règle morte décrivant une disposition abandonnée
   trompe le prochain lecteur plus sûrement qu'elle ne l'aide. */

/* ═══ « Tous les modules » — des CARTES DE DOMAINE, pas des demi-rangées ═══
   Mesuré : 18 modules en 7 groupes, dont 6 de deux ou trois entrées. Une grille
   pleine largeur par groupe laissait deux colonnes vides à chaque section et
   faisait dépasser la page de deux écrans. Les domaines se rangent maintenant
   côte à côte : c'est la largeur qui travaille.
   `align-items: start` : une carte de deux modules ne s'étire pas à la hauteur
   de sa voisine de cinq — sans lui, la grille rendrait le blanc qu'on vient
   d'enlever. */
.mod-domaines {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
    gap: .9rem;
    align-items: start;
}
.mod-domaine {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .85rem .8rem .7rem;
}
.mod-domaine-t {
    margin: 0 0 .55rem;
    padding-inline-start: .15rem;
    font-size: .74rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.mod-domaine-t i { margin-inline-end: .3rem; opacity: .8; }
.mod-liste { display: flex; flex-direction: column; gap: .15rem; }

/* La tuile devient une LIGNE : l'icône garde sa pastille et sa couleur de
   domaine (elle la lit dans la table des teintes, plus bas), mais le texte
   passe à côté au lieu de dessous — deux fois moins haut, et on lit le nom sur
   la même ligne de regard que l'icône. */
.tuile {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    text-align: start;
    gap: .6rem;
    padding: .45rem .5rem;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-btn);
    text-decoration: none;
    color: inherit;
    transition: background-color .15s ease, border-color .15s ease;
}
.tuile:hover,
.tuile:focus-visible {
    background: var(--veil);
    border-color: var(--border);
}
.tuile:focus-visible { outline: 2px solid var(--violet); outline-offset: 1px; }
.tuile-ico {
    width: 34px;
    height: 34px;
    min-width: 34px;
    border-radius: 50%;              /* pastille ronde — écho du point du logo */
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: .95rem;
    background: var(--tuile-bg, var(--veil));
    color: var(--tuile-fg, var(--violet));
}
/* Sur une LIGNE, un agrandissement de la pastille au survol fait sauter le
   texte voisin. La réponse au geste est donc portée par la LIGNE — voile lilas
   et filet, ci-dessus : la charte ne pose d'ombre que sur ce qui survole
   réellement le document (R2), et une pastille de 34 px n'en est pas. */
.tuile-txt { display: flex; flex-direction: column; gap: .05rem; min-width: 0; }
.tuile-nom { font-weight: 600; font-size: .89rem; color: var(--ink); line-height: 1.25; }
/* La description tient sur deux lignes au plus : au-delà, une entrée bavarde
   étirerait sa carte de domaine et ramènerait la hauteur qu'on vient de
   gagner. Le nom, lui, n'est jamais tronqué. */
.tuile-sous {
    font-size: .75rem; color: var(--text-muted); line-height: 1.35;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
}

/* ═══════ LA TABLE DES TEINTES DE DOMAINE — UNE SEULE, QUATRE LECTEURS ═══════
   Palette « vives sur pastel » (P2, retenue par le fondateur) : fond pastel
   clair, encre franchement saturée. Une famille de couleur par domaine métier.
   ⚠️ Ces paires vivaient ici en clair, avec leurs variantes sombres : elles
   sont devenues les jetons --dom-* de tokens.css. La feuille ne fait plus que
   NOMMER le domaine de chaque onglet.
   ⚠️ R7 — USAGE STRICT : repérer un MODULE. Jamais un état, jamais un statut,
   jamais une alerte : la sémantique reste au violet, au vert et au rouge (R6).
   ⚠️ QUATRE LECTEURS, UNE SEULE TABLE. La tuile du catalogue, l'icône de la
   barre latérale (§D1), la carte d'indicateur de l'accueil et la rangée
   « à traiter » (§D4) lisent les MÊMES variables. Une seconde table aurait
   divergé au premier module ajouté, et personne ne l'aurait vu.
   ⚠️ `--dom-encre` est la variante POSÉE SUR L'ENCRE de la barre latérale, qui
   est en encre pleine dans les deux thèmes : les encres claires ci-dessus y
   seraient illisibles. Contrastes mesurés sur le fond de barre #221C44, du
   moins bon au meilleur : 6,60 · 6,90 · 7,04 · 7,25 · 7,48 · 7,74 · 8,12 · 8,41.
   ⚠️ `data-tab` sur une rangée ou une carte n'est PAS un déclencheur : la
   navigation passe par `data-act` et par l'`href`. L'attribut ne sert qu'à
   cette table. */
/* ⚠️ LE PRÉFIXE `--tuile-*` EST HISTORIQUE, ET IL RESTE. Ces deux variables
   ne servent plus la seule tuile : elles portent la teinte du DOMAINE pour ses
   quatre lecteurs. Les renommer n'aurait rien appris à personne et aurait
   cassé `tools/_test_modules_page.js`, qui les lit par leur nom pour prouver
   qu'aucune teinte de tuile n'est écrite en dur — une suite qui garde cette
   preuve vaut mieux qu'un nom plus joli. `--dom-encre`, elle, est neuve : la
   barre latérale n'avait pas d'équivalent. */
.tuile[data-tab="dashboard"], :is(.nav-item, .kpi, .rang)[data-tab="dashboard"],
.tuile[data-tab="accueil"],   :is(.nav-item, .kpi, .rang)[data-tab="accueil"],
.tuile[data-tab="personnel"], :is(.nav-item, .kpi, .rang)[data-tab="personnel"] {
    --tuile-bg: var(--dom-pilotage-bg); --tuile-fg: var(--dom-pilotage-fg);
    --dom-encre: var(--dom-pilotage-sur-encre); }
.tuile[data-tab="eleves"],    :is(.nav-item, .kpi, .rang)[data-tab="eleves"] {
    --tuile-bg: var(--dom-scolarite-bg); --tuile-fg: var(--dom-scolarite-fg);
    --dom-encre: var(--dom-scolarite-sur-encre); }
.tuile[data-tab="inscriptions"],   :is(.nav-item, .kpi, .rang)[data-tab="inscriptions"],
.tuile[data-tab="reinscriptions"], :is(.nav-item, .kpi, .rang)[data-tab="reinscriptions"],
.tuile[data-tab="caisse"],         :is(.nav-item, .kpi, .rang)[data-tab="caisse"],
.tuile[data-tab="recouvrement"],   :is(.nav-item, .kpi, .rang)[data-tab="recouvrement"],
.tuile[data-tab="services"],       :is(.nav-item, .kpi, .rang)[data-tab="services"],
.nav-groupe[data-groupe="admissions"] > .nav-parent,
.nav-groupe[data-groupe="finances"] > .nav-parent {
    --tuile-bg: var(--dom-admissions-argent-bg); --tuile-fg: var(--dom-admissions-argent-fg);
    --dom-encre: var(--dom-admissions-argent-sur-encre); }
.tuile[data-tab="communication"], :is(.nav-item, .kpi, .rang)[data-tab="communication"],
.tuile[data-tab="circulaires"],   :is(.nav-item, .kpi, .rang)[data-tab="circulaires"],
.nav-groupe[data-groupe="communication"] > .nav-parent {
    --tuile-bg: var(--dom-communication-bg); --tuile-fg: var(--dom-communication-fg);
    --dom-encre: var(--dom-communication-sur-encre); }
.tuile[data-tab="notes"],     :is(.nav-item, .kpi, .rang)[data-tab="notes"],
.tuile[data-tab="bulletins"], :is(.nav-item, .kpi, .rang)[data-tab="bulletins"],
.tuile[data-tab="massar"],    :is(.nav-item, .kpi, .rang)[data-tab="massar"],
.nav-groupe[data-groupe="notes"] > .nav-parent {
    --tuile-bg: var(--dom-resultats-bg); --tuile-fg: var(--dom-resultats-fg);
    --dom-encre: var(--dom-resultats-sur-encre); }
.tuile[data-tab="absences"],   :is(.nav-item, .kpi, .rang)[data-tab="absences"],
.tuile[data-tab="calendrier"], :is(.nav-item, .kpi, .rang)[data-tab="calendrier"],
.tuile[data-tab="transport"],  :is(.nav-item, .kpi, .rang)[data-tab="transport"],
.nav-groupe[data-groupe="vie"] > .nav-parent {
    --tuile-bg: var(--dom-vie-scolaire-bg); --tuile-fg: var(--dom-vie-scolaire-fg);
    --dom-encre: var(--dom-vie-scolaire-sur-encre); }
.tuile[data-tab="documents"], :is(.nav-item, .kpi, .rang)[data-tab="documents"] {
    --tuile-bg: var(--dom-documents-bg); --tuile-fg: var(--dom-documents-fg);
    --dom-encre: var(--dom-documents-sur-encre); }
.tuile[data-tab="listes"],       :is(.nav-item, .kpi, .rang)[data-tab="listes"],
.tuile[data-tab="parametres"],   :is(.nav-item, .kpi, .rang)[data-tab="parametres"],
.tuile[data-tab="utilisateurs"], :is(.nav-item, .kpi, .rang)[data-tab="utilisateurs"],
.tuile[data-tab="sauvegarde"],   :is(.nav-item, .kpi, .rang)[data-tab="sauvegarde"],
.tuile[data-tab="demos"],        :is(.nav-item, .kpi, .rang)[data-tab="demos"],
.tuile[data-tab="modules"],      :is(.nav-item, .kpi, .rang)[data-tab="modules"],
.nav-groupe[data-groupe="admin"] > .nav-parent {
    --tuile-bg: var(--dom-outils-bg); --tuile-fg: var(--dom-outils-fg);
    --dom-encre: var(--dom-outils-sur-encre); }

/* §D1 — LA COULEUR DE DOMAINE DESCEND DANS LA NAVIGATION. Le libellé reste en
   encre claire (le contraste du texte n'est pas négociable) ; c'est l'ICÔNE qui
   porte la teinte. Le chevron d'un groupe est exclu : il dit l'état du repli,
   pas le domaine.
   L'entrée ACTIVE reprend son icône en blanc, sur la pilule violette pleine
   posée en tête de feuille. */
.nav-item[data-tab] > i:first-child,
.nav-groupe > .nav-parent > i:first-child { color: var(--dom-encre, inherit); opacity: 1; }
.nav-item.active > i:first-child { color: var(--nav-tx-str); }

/* La carte d'indicateur qui NOMME son domaine prend sa teinte — MAIS PAS SI
   ELLE NOMME AUSSI UN TON SÉMANTIQUE. Le `data-tab` d'une carte sert deux
   choses à la fois : dire son domaine, et conduire à son onglet. Sans ce
   `:not`, une carte qui navigue perdait EN SILENCE le ton qu'elle déclarait —
   mesuré sur le Tableau de bord, où la carte « Encaissé ce mois » demande
   `kpi-green` (R6 : une confirmation factuelle) et recevait la teinte de son
   domaine à la place. Le violet, lui, reste le DÉFAUT de la fabrique — le ton
   d'une carte qui n'en nomme aucun — et c'est pourquoi lui seul cède la place
   au domaine. R7 le dit dans ces termes : les teintes de domaine servent au
   repérage de module, « jamais pour un état, un statut ou une alerte — la
   sémantique reste R6 ». */
.kpi[data-tab]:not(.kpi-green, .kpi-red, .kpi-orange) .kpi-icon {
    background: var(--tuile-bg); color: var(--tuile-fg);
}
/* La courbe se peint dans la teinte de la tuile qui la surmonte : même
   sélecteur, donc jamais l'une sans l'autre. */
.kpi .sparkline { color: var(--violet); }
.kpi[data-tab]:not(.kpi-green, .kpi-red, .kpi-orange) .sparkline { color: var(--tuile-fg); }

@media (max-width: 640px) {
    /* Une seule colonne de domaines : sur un téléphone, deux cartes côte à côte
       rendraient les lignes trop étroites pour leur nom. */
    .mod-domaines { grid-template-columns: 1fr; }
    .tuile { padding: .55rem .5rem; }
    .tuile-ico { width: 38px; height: 38px; min-width: 38px; font-size: 1.05rem; }
}

/* ── Barre de pilules de NIVEAU MODULE (entrées de menu fusionnées) ──
   Un écran fusionné peut porter DEUX niveaux de pilules : le module
   (Notes ↔ Bulletins importés) et, en dessous, les vues internes du module
   (Saisie · Récap · Bulletins). Deux barres identiques se confondaient :
   le niveau module prend l'aspect d'un sélecteur segmenté (fond voile,
   pilule active en relief blanc), le niveau vue garde les pilules bordées. */
.tabs-inline--modules {
    display: inline-flex;
    /* Le conteneur parent peut être un flex en colonne (align-items: stretch) :
       sans ces deux règles, le bandeau s'étirerait sur toute la largeur. */
    width: fit-content;
    align-self: flex-start;
    gap: .2rem;
    padding: .25rem;
    margin-bottom: .9rem;
    background: var(--veil);
    border-radius: var(--radius-btn);
}
.tabs-inline--modules .tab-pill {
    border-color: transparent;
    background: transparent;
    color: var(--violet-deep);
}
.tabs-inline--modules .tab-pill:hover {
    border-color: transparent;
    background: rgba(255, 255, 255, .55);
}
.tabs-inline--modules .tab-pill.active {
    background: var(--white);
    border-color: transparent;
    color: var(--violet);
}

/* ═══════════════ Barre latérale — groupes PLIABLES ═══════════════
   Un module à sous-onglets est un groupe : l'entête (bouton) déplie ses
   enfants, qui portent la navigation. Le groupe de l'onglet courant s'ouvre
   seul ; la barre reste courte quel que soit le nombre de modules. */
.nav-groupe { display: flex; flex-direction: column; }

.nav-parent {
    width: 100%;
    border: 0;
    background: transparent;
    font-family: inherit;
    text-align: start;
    cursor: pointer;
}
.nav-parent > span { flex: 1; }

.nav-chevron {
    font-size: .68rem;
    opacity: .6;
    transition: transform .2s ease;
}
.nav-groupe.is-open .nav-chevron { transform: rotate(180deg); }

/* Groupe replié mais contenant l'onglet courant : l'entête reste marquée. */
.nav-groupe.is-active > .nav-parent { color: var(--nav-tx-str); background: rgba(255,255,255,.10); }

/* Repli/dépli : bascule NETTE. L'animation par `grid-template-rows: 0fr`
   laissait un vide sous les groupes repliés — les enfants d'une grille ont
   `min-height: auto` (ils refusent de s'écraser) et `0fr` ne cadre que la
   PREMIÈRE ligne, or il y a un enfant par sous-onglet. */
.nav-sous {
    display: none;
    flex-direction: column;
    gap: .1rem;
    padding-inline-start: .55rem;
    margin: .15rem 0 .25rem .95rem;
    border-inline-start: 1px solid rgba(255,255,255,.16);
}
.nav-groupe.is-open .nav-sous { display: flex; }
.nav-enfant {
    font-size: .85rem;
    padding: .45rem .7rem;
    color: rgba(255,255,255,.78);
}
.nav-enfant i { font-size: .8rem; width: 16px; }
.nav-enfant.active {
    background: var(--violet);
    color: var(--nav-tx-str);
    font-weight: 700;
}
/* `display: none` retire aussi les enfants repliés du parcours de tabulation. */

/* ── Indicateurs : empêcher tout débordement horizontal ──
   Un enfant de grille/flex a `min-width: auto` : il REFUSE de descendre sous
   la largeur de son contenu. Un montant long en chasse fixe élargissait donc sa
   carte et poussait la page — défilement horizontal mesuré sur les largeurs de
   portable. `min-width: 0` rend la compression possible ; les paliers ci-dessous
   évitent d'y arriver.
   ⚠️ Le `overflow-wrap: anywhere` qui accompagnait ce réglage a été RETIRÉ :
   il coupait le montant en deux, ce que l'anatomie §D2 interdit.
   ⚠️ ET LE CORPS PREND TOUTE LA LARGEUR QUI RESTE (`flex: 1`). Sans cela il
   épousait son texte, et la courbe — qui fait 100 % de son parent — épousait le
   texte avec lui : quatre courbes de quatre largeurs dans une rangée de quatre
   cartes identiques, avec un vide à droite de chacune. C'était le défaut D5
   (« les vides dans les cartes ») reconduit dans les cartes mêmes qui devaient
   le fermer. §D2 demande une sparkline PLEINE LARGEUR ; elle l'est maintenant.
   Le sélecteur vise `.kpi-corps` (posé par `kpiHtml()`) plutôt que `.kpi > div`,
   qui attrapait aussi la tuile d'icône. */
/* ⚠️ LES CHIFFRES D UNE RANGEE S ALIGNENT, quoi qu il arrive au-dessus.
   La carte centrait son contenu verticalement ; comme les cartes d une
   rangee ont la meme hauteur mais des sous-libelles de longueurs
   differentes — un qui tient sur une ligne, un autre qui se replie sur deux
   — les valeurs se posaient a des hauteurs differentes. MESURE sur le
   Tableau de bord : 28 px d ecart entre le premier chiffre et le troisieme.
   Or une rangee d indicateurs existe POUR ETRE COMPAREE : des chiffres
   decales obligent l oeil a chercher chacun d eux. Le libelle occupe le
   haut, la ligne de valeur est chassee en bas, et les bases coincident. */
.kpi { align-items: stretch; }
.kpi-corps { min-width: 0; flex: 1; display: flex; flex-direction: column; }
.kpi-corps .kpi-ligne { margin-top: auto; }
/* L icone reste en haut : etiree sur toute la hauteur elle deviendrait une
   bande, et la tuile teintee de la direction « Console » n est pas une bande. */
.kpi-icon { align-self: flex-start; }

/* Deux par ligne des le telephone : empiles un par un, les quatre
   indicateurs repoussaient les modules hors de l'ecran. */
/* minmax(0, 1fr) et NON 1fr seul. Une piste de grille a par defaut un plancher
   qui vaut son contenu ; comme la ligne de valeur est en `nowrap`, un montant
   long faisait grandir la piste au-dela de sa part, la grille debordait son
   conteneur et la PAGE se mettait a defiler lateralement. Le plancher a zero
   rend la piste maitresse de sa largeur : c'est la valeur qui s'adapte. */
.kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* ⚠️ « auto-fit », ET NON « repeat(4) ». La grille imposait QUATRE colonnes
   au-dessus de 1 250 px. L Accueil en porte quatre, tout allait bien ; le
   Tableau de bord en porte CINQ, et la cinquieme tombait seule sur une
   deuxieme rangee, avec 800 px de vide a sa droite. Une grille ne doit pas
   savoir combien de cartes on lui donnera. Le plancher a 12 rem laisse cinq
   cartes tenir des 1 366 px ; « min(…, 100%) » garde le motif du plancher a
   zero decrit juste au-dessus, sans lequel un montant long fait defiler la
   page. Et « auto-fit » replie les pistes vides : quatre cartes occupent
   toujours toute la largeur, elles ne se serrent pas a gauche. */
@media (min-width: 1250px) { .kpi-grid { grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr)); } }
/* ⚠️ LE PLAFOND EST SUR LA CARTE, PAS SUR LA PISTE, ET L ESSAI INVERSE A
   ETE FAIT. « auto-fit » replie les pistes vides : une carte seule occupait
   donc la largeur entiere — 1 050 px pour un chiffre a deux caracteres, ce
   qui arrive des que le Transport n a plus qu une anomalie a montrer. Borner
   la PISTE a « 28rem » au lieu de « 1fr » a casse tout autre chose : le
   nombre de repetitions de « auto-fit » se calcule sur une taille de piste
   definie, et un pourcentage dans le plancher la rend indefinie — le Tableau
   de bord est retombe a DEUX colonnes, ses cinq cartes sur trois rangees.
   La piste garde donc « 1fr », et c est la carte qui refuse de s etirer :
   sur une piste large, elle reste a sa taille et se range a gauche. */
.kpi-grid > .kpi { max-width: 28rem; }

@media (max-width: 700px) {
    .kpi-grid { gap: .6rem; }
    .kpi { padding: .8rem .85rem; gap: .6rem; }
    .kpi-icon { width: 34px; height: 34px; min-width: 34px; font-size: .9rem; }
    .kpi-value { font-size: 1.1rem; }
    .kpi-label { font-size: .72rem; line-height: 1.3; }
}
/* Entre 1250 et 1500 px, quatre montants côte à côte restent serrés : le
   chiffre cède un peu de corps plutôt que la mise en page. */
@media (min-width: 1250px) and (max-width: 1500px) {
    .kpi-value { font-size: 1.3rem; }
    /* Le montant de la rangée d'accueil cède un cran de plus : quatre cartes
       s'y partagent la largeur, et sa ligne — insécable par §D2 — sortait de
       son corps sur les portables de cette bande. La règle voisine ne le
       réduisait que s'il portait une puce de tendance ; il n'en porte plus.
       Mesuré aux quatre largeurs de la bande : plus aucun dépassement. */
    .acc-kpi .kpi-value:has(.montant) { font-size: 1rem; }
}

/* ── Barre de navigation en petite largeur ──
   Sous 900 px, .sidebar-nav devient une rangée horizontale défilante. Deux
   écueils : les groupes, restés en colonne, cassaient la rangée et
   étiraient toute la bande à l'ouverture ; et douze entrées libellées
   rendaient la barre interminable.
   Réponse : une barre d'ICONES. Seul l'onglet actif porte son libellé — la
   barre tient alors dans une largeur de téléphone. Les groupes n'y déplient
   plus : leur entête conduit directement au premier sous-onglet (app.js). */
@media (max-width: 900px) {
    .sidebar-nav { align-items: center; }

    /* La règle qui masque « Réduire le menu » sous 900 px ne peut PAS vivre
       ici : `.nav-bascule` est déclaré 365 lignes plus bas avec
       `display: flex`, à spécificité égale, et l emporterait. Elle est donc
       posée APRÈS cette déclaration — voir le bloc « §29 bis » plus loin. */

    /* font-size: 0 efface le nœud texte sans toucher au balisage ; l'icône
       et le libellé de l'onglet actif reprennent leur corps. */
    .nav-item {
        flex: 0 0 auto;
        gap: 0;
        padding: .55rem .7rem;
        font-size: 0;
    }
    .nav-item i { font-size: 1rem; }
    .nav-item.active,
    .nav-groupe.is-active > .nav-parent {
        gap: .45rem;
        padding: .55rem .85rem;
        font-size: .82rem;
    }
    .nav-item.active i,
    .nav-groupe.is-active > .nav-parent i { font-size: .95rem; }

    .nav-groupe { flex: 0 0 auto; }
    .nav-chevron { display: none; }          /* rien ne se déplie ici */
    .nav-sous    { display: none !important; }
}

/* ── Petite largeur : contraindre la barre et alléger le bandeau ──
   1. `.sidebar-nav` porte `overflow-x: auto`, mais dans une barre en colonne
      sa LARGEUR restait `auto` : elle s'étendait à son contenu (632 px) et
      poussait la page — d'où un défilement horizontal de tout l'écran au
      lieu d'un défilement interne à la barre.
   2. La salutation prenait une place que le contenu métier mérite mieux. */
@media (max-width: 900px) {
    .sidebar-nav { width: 100%; min-width: 0; max-width: 100%; }
}
@media (max-width: 700px) {
    .acc-kpi        { margin-top: .7rem; }
    .tuiles-titre   { margin: 1.1rem 0 .6rem; }
}

/* Barre du haut en petite largeur : la zone compte (langue + nom + rôle +
   déconnexion) mesurait 385 px dans 358 px utiles et poussait la page de
   11 px. Elle se replie désormais au lieu de déborder. */
@media (max-width: 900px) {
    .topbar-meta,
    .topbar-user {
        min-width: 0;
        flex-wrap: wrap;
        row-gap: .35rem;
    }
    .topbar-user { justify-content: flex-end; }
}

/* ── Tri par en-tête et pagination des listes (tâche 8) ──
   L'en-tête triable se signale au survol : sans indice visuel, personne ne
   devine qu'une colonne se clique. La flèche du tri courant est ajoutée au
   libellé côté JavaScript. */
.th-triable { user-select: none; }
.th-triable:hover { color: var(--violet); }
.pagination-barre {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; flex-wrap: wrap; padding: 10px 4px 2px;
    font-size: .85rem; color: var(--text-muted);
}
.pagination-boutons { display: flex; align-items: center; gap: 4px; }
.pagination-page { padding: 0 8px; font-variant-numeric: tabular-nums; }
.pagination-taille { display: flex; align-items: center; gap: 6px; }
.pagination-taille select { padding: 3px 6px; }

/* Ligne de filtres par colonne : une seconde ligne d'en-tête, discrète, qui ne
   doit pas concurrencer visuellement les libellés au-dessus. */
.ligne-filtres th { padding: 4px 6px 8px; background: inherit; }
.ligne-filtres .filtre-col {
    width: 100%; box-sizing: border-box; padding: 4px 7px;
    font-size: .82rem; font-weight: 400; border: 1px solid var(--border);
    border-radius: 8px; background: var(--white); color: var(--ink);
}
.ligne-filtres .filtre-col:focus { outline: 2px solid var(--violet); outline-offset: -1px; }
/* Colonne de dates : les deux bornes empilées, sinon la colonne double de
   largeur et pousse le tableau hors de l'écran. */
.ligne-filtres .filtre-intervalle { display: flex; flex-direction: column; gap: 3px; }
.ligne-filtres .filtre-intervalle .filtre-col { padding: 3px 5px; font-size: .76rem; }
/* Une colonne filtrée doit se voir : sinon on cherche pourquoi la liste est
   courte sans repérer le filtre resté en place. La classe est posée par le
   script — un sélecteur CSS ne sait pas lire la valeur courante d'un <select>. */
.ligne-filtres .filtre-col.filtre-actif {
    border-color: var(--violet); background: var(--veil);
}

/* ═══════════════ RECHERCHE GLOBALE ═══════════════
   Zone centrale de la barre supérieure. Elle occupe la place disponible entre
   le titre et les indicateurs : c'est le geste le plus fréquent, il ne doit pas
   se chercher. */
.rg-zone { position: relative; min-width: 0; }
.rg-loupe {
    position: absolute; left: .7rem; top: 50%; transform: translateY(-50%);
    font-size: .86rem; color: var(--text-muted); pointer-events: none;
    transition: color .15s ease;
}
/* Un champ blanc sur une barre blanche ne se distinguait que par un filet
   d un pixel : rien n indiquait qu on pouvait y ecrire. Il repose
   desormais sur un fond legerement teinte, et redevient blanc quand il
   prend le focus — la saisie s eclaire au lieu de rester plate. */
/* ⚠️ SELECTEUR RENFORCE — SANS LUI, RIEN DE CE QUI SUIT NE S APPLIQUE.
   La regle globale des champs cible input[type="search"], dont la
   specificite (0,1,1) BAT celle de .rg-champ (0,1,0). Tout le
   style de la zone de recherche etait donc inerte depuis sa creation : le
   champ heritait d un padding de .85rem alors que la loupe est posee a
   .7rem — l icone se superposait au texte saisi, et c est ce que voyait
   l utilisateur. Deux classes suffisent a retablir l ordre voulu.
   ⚠️ Ne jamais retomber sur « .rg-champ » seul. */
.rg-zone .rg-champ {
    width: 100%; box-sizing: border-box; padding: .52rem 1.1rem .52rem 2.2rem;
    font-size: .9rem; border: 1px solid var(--border); border-radius: var(--radius-btn);
    background: var(--surface-2); color: var(--text);
    transition: background .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.rg-zone .rg-champ::placeholder { color: var(--text-muted); opacity: 1; }
.rg-zone .rg-champ:hover { border-color: var(--border-str); }
/* Le fond redevient blanc au focus — la saisie s'eclaire. L'anneau, lui, est
   celui de tout le produit : il est ecrit une seule fois, a la section des
   champs. */
.rg-zone .rg-champ:focus { background: var(--white); }
.rg-zone:focus-within .rg-loupe { color: var(--violet); }
/* Le badge « / » a ete retire de la barre : il annoncait un raccourci de
   culture developpeur que le secretariat lisait comme un caractere parasite.
   Le raccourci lui-meme demeure, et s annonce desormais dans l infobulle du
   champ. Les regles de mise en forme du badge partent avec lui : du style
   sans element est du style qui se perime en silence. */

/* Liste deroulante : elle SURVOLE le document, elle porte donc l'ombre unique
   et le rayon de surface. */
.rg-resultats {
    position: absolute; top: calc(100% + .35rem); left: 0; right: 0; z-index: 80;
    max-height: min(70vh, 520px); overflow-y: auto;
    background: var(--white); border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow);
}
/* L'en-tête de groupe reste visible pendant qu'on parcourt ses lignes : on doit
   toujours savoir DANS QUEL MODULE on est en train de regarder. */
.rg-groupe {
    position: sticky; top: 0; z-index: 1;
    display: flex; align-items: center; gap: .45rem;
    padding: .42rem .75rem; background: var(--surface);
    font: 700 .72rem/1 var(--font-body);
    letter-spacing: .04em; text-transform: uppercase; color: var(--encre-doux);
    border-bottom: 1px solid var(--border);
}
.rg-groupe .rg-compte { margin-left: auto; font-weight: 600; letter-spacing: 0; }
/* ARBITRAGE RENDU ICI : le filet d'une ligne, et le fond de la ligne designee,
   ne peuvent pas etre la MEME couleur.
   `--bord-doux` (separateur) et `--violet-pale` (ligne survolee) retombaient
   tous deux sur le voile lilas : le filet bas de la ligne designee disparaissait
   dans son propre fond, et on ne voyait plus ou finissait le resultat qu'on
   s'appretait a ouvrir. La charte tranche — toutes les bordures sont au trait
   lilas : le separateur prend donc `--border`, comme les trois autres filets de
   cette liste deroulante, et le fond de la ligne designee garde le voile. Les
   deux fantomes perdent ici leur dernier consommateur. */
.rg-item {
    display: flex; align-items: baseline; gap: .5rem;
    padding: .46rem .75rem; cursor: pointer; border-bottom: 1px solid var(--border);
}
.rg-item:last-child { border-bottom: 0; }
/* Le survol se voit, mais il n ARME PAS la touche Entree : passer la souris
   n est pas un choix. Seules les fleches et le clic designent un resultat. */
.rg-item:hover,
.rg-item.rg-actif { background: var(--veil); }
.rg-titre { font-weight: 600; color: var(--ink); }
.rg-detail { font-size: .8rem; color: var(--encre-doux); margin-left: auto; text-align: right; }
.rg-vide, .rg-pied {
    padding: .8rem .75rem; text-align: center;
    font-size: .84rem; color: var(--encre-doux);
}
.rg-vide { display: flex; align-items: center; justify-content: center; gap: .5rem; }
.rg-pied { border-top: 1px solid var(--border); background: var(--surface); }

/* Sur écran étroit la barre supérieure se resserre : la recherche passe sous le
   titre plutôt que d'écraser les indicateurs. */
@media (max-width: 900px) {
    /* LA DISPOSITION NE CHANGE PLUS ICI — elle est deja a deux rangees plus
       haut, a toutes les largeurs. Cette requete ne fait que RESSERRER : des
       marges plus courtes, un titre plus petit. Elle posait autrefois sa
       propre grille avec « minmax(0, 1fr) auto auto », qui reproduisait a
       l identique le defaut de la grille principale : le titre y tombait a
       zero lui aussi, si bien que sous 900 px l ecran n avait pas davantage
       de nom qu au-dessus. */
    /* En petite largeur le titre redescend : a 1,7 rem il mangerait la
       rangee entiere et repousserait les indicateurs hors de vue. */
    /* Le plancher du titre redescend AVEC la fenetre : a 15 rem il debordait
       sur telephone, ou la rangee entiere ne fait pas 15 rem. */
    .topbar { padding: .7rem 1rem;
              grid-template-columns: minmax(clamp(5rem, 20vw, 11rem), 1fr) minmax(0, auto) minmax(0, auto); }
    .topbar-ico { width: 2.1rem; height: 2.1rem; flex-basis: 2.1rem; font-size: .9rem; }
    .topbar-libelle { font-size: 1.25rem; }
    /* Le champ retrouve toute la largeur : sous 900 px, 40 rem valent deja
       plus que la rangee, et le plafond ne servirait qu a laisser un blanc. */
    .topbar .rg-zone { max-width: none; margin-inline: 0; }
}

/* ── TELEPHONE : LA BARRE CESSE D ETRE UNE GRILLE ───────────────────────────
   ⚠️ TROIS COLONNES DANS 390 PX, C ETAIT LE DEFAUT. Sous 900 px la barre
   gardait `1fr auto auto`. Sur un telephone, la piste de titre prenait
   clamp(5rem, 20vw, 11rem) — environ 78 px — et les 310 px restants devaient
   loger l etablissement, l annee, la langue, le nom, le role et la
   deconnexion. Constate sur capture : « Groupe Scolaire A… » et
   « 2026-2027 (c… » tronques a mi-mot, « Superadmin Scoliom » COUPE par le
   bord droit, et les six elements repartis sur quatre rangees en escalier,
   alignees a droite — la barre mangeait 280 px avant le moindre contenu.

   Sous 640 px, chaque bloc prend donc sa propre rangee, sur UNE colonne, et
   tout s aligne A GAUCHE. On ne gagne pas de hauteur — on gagne une barre
   qui se lit, et plus rien ne sort du cadre. */
@media (max-width: 640px) {
    .topbar {
        grid-template-columns: minmax(0, 1fr);
        row-gap: .5rem;
    }
    .topbar-title { grid-column: 1; grid-row: 1; }
    .topbar-meta  { grid-column: 1; grid-row: 2; }
    .topbar-user  { grid-column: 1; grid-row: 3; }
    .topbar .rg-zone { grid-column: 1; grid-row: 4; }

    /* Alignes a gauche : le ferrage a droite produisait un escalier, chaque
       rangee commencant a un endroit different. */
    .topbar-meta,
    .topbar-user { justify-content: flex-start; column-gap: .45rem; }

    /* Les selecteurs prennent la largeur qu il leur faut plutot que de
       tronquer leur libelle au milieu d un mot. */
    .topbar-meta > * { flex: 1 1 auto; min-width: 0; max-width: 100%; }
    .topbar-meta select.chip { width: 100%; }

    /* ⚠️ LE NOM SORTAIT DU CADRE, et la premiere tentative n a rien fait :
       `.topbar-user strong` ne visait AUCUN element. Le nom est un noeud de
       TEXTE NU dans `<a id="btn-profil">` (dashboard.php:303), sans balise
       propre — il n y a donc pas de selecteur qui l atteigne.
       `font-size: 0` efface un noeud de texte sans toucher au balisage, et
       l icone reprend son corps : c est exactement ce que fait deja la barre
       laterale sous 900 px. On reste dans la technique de la maison.
       Le nom reste dans le DOM — les lecteurs d ecran l annoncent toujours ;
       le role et la porte de sortie suffisent a l oeil sur un telephone. */
    #btn-profil { font-size: 0; gap: 0; }
    #btn-profil i { font-size: 1.05rem; }
}

/* ── Pied de la liste deroulante : le pont vers la page complete ────────── */
.rg-tout {
    display: flex; align-items: center; justify-content: center; gap: .45rem;
    width: 100%; padding: .6rem .75rem; border: 0; border-top: 1px solid var(--border);
    background: var(--surface-2); color: var(--violet);
    font: 600 .82rem/1 var(--font-body); cursor: pointer;
}
.rg-tout:hover { background: var(--surface-3); }

/* ── PAGE DE RESULTATS ──────────────────────────────────────────────────
   Les modules deviennent des onglets portant leur compte reel. On lit
   d abord COMBIEN, ensuite on choisit ou regarder. */
.rgp-barre { margin-bottom: 1rem; }

.rgp-onglets { flex-wrap: wrap; }
.rgp-onglets .tab-pill { display: inline-flex; align-items: center; gap: .4rem; }
/* Le compte est la raison d etre de l onglet : il se lit sans effort, et il
   reste lisible sur la pastille active. */
.rgp-compte {
    display: inline-block; min-width: 1.5rem; padding: .05rem .35rem;
    border-radius: 8px; background: var(--surface-2);
    color: var(--text-muted); font-size: .72rem; font-weight: 700;
    font-variant-numeric: tabular-nums; text-align: center;
}
.tab-pill.active .rgp-compte {
    background: color-mix(in srgb, var(--on-accent) 25%, transparent);
    color: var(--on-accent);
}

.rgp-liste { padding: 0; overflow: hidden; }
.rgp-ligne {
    display: flex; align-items: center; gap: .75rem; width: 100%;
    padding: .7rem 1rem; border: 0; border-bottom: 1px solid var(--border);
    background: transparent; text-align: left; cursor: pointer;
    font: inherit; color: var(--text);
}
.rgp-ligne:last-child { border-bottom: 0; }
.rgp-ligne:hover, .rgp-ligne:focus-visible { background: var(--surface-3); }
.rgp-ligne-titre { font-weight: 600; }
.rgp-ligne-detail { color: var(--text-muted); font-size: .85rem; }
/* La fleche est poussee a droite et ne prend la couleur d accent qu au survol :
   elle indique « ceci s ouvre » sans crier. */
.rgp-ligne-fleche { margin-left: auto; color: var(--text-faint); font-size: .78rem; }
.rgp-ligne:hover .rgp-ligne-fleche { color: var(--violet); }

.rgp-pager {
    display: flex; align-items: center; justify-content: center; gap: .75rem;
    margin-top: 1rem;
}
.rgp-pager-etat { color: var(--text-muted); font-size: .85rem; font-variant-numeric: tabular-nums; }

@media (max-width: 700px) {
    .rgp-ligne { flex-wrap: wrap; gap: .25rem .75rem; }
    .rgp-ligne-fleche { display: none; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   T1 — BARRE LATERALE COMPACTE
   Deux modes sur ordinateur : deployee (icone + libelle + sous-menus) et
   compacte (icones seules, ~70 px). L espace rendu va aux tableaux, aux
   graphiques et aux ecrans de saisie — ce sont eux qui manquent de largeur.

   Ce qui est DELIBERE :
   • le repli est un CHOIX, jamais un effet du survol : une barre qui
     s ouvre toute seule quand le pointeur passe deplace le contenu sous le
     curseur et fait cliquer a cote ;
   • les sous-menus s ouvrent AU CLIC, en panneau flottant, et ne poussent
     pas la page ;
   • le libelle reste accessible par l infobulle, au survol ET au focus
     clavier : une icone seule n a jamais suffi a comprendre une destination ;
   • sous 900 px la barre redevient la rangee horizontale existante — le mode
     compact ne s applique qu a l ordinateur, ou l espace horizontal est la
     ressource rare.
   ═══════════════════════════════════════════════════════════════════════════ */

body.nav-compact { --nav-l: 70px; }

@media (min-width: 901px) {
    body.nav-compact .sidebar { padding-inline: .45rem; overflow: visible; }

    /* Tout ce qui est du TEXTE disparait ; les icones restent. */
    body.nav-compact .sidebar-nav .nav-item > span:not(.nav-chevron),
    body.nav-compact .nav-parent > span,
    body.nav-compact .nav-chevron,
    body.nav-compact .sidebar-brand span,
    body.nav-compact .sidebar-foot { display: none; }

    /* Le libelle d une entree simple est un noeud de texte : on le masque en
       annulant la taille de police, l icone gardant la sienne. */
    body.nav-compact .sidebar-nav > .nav-item,
    body.nav-compact .nav-parent,
    body.nav-compact .nav-enfant { font-size: 0; justify-content: center; padding-inline: 0; }
    body.nav-compact .sidebar-nav .nav-item i { font-size: 1rem; width: auto; margin: 0; }

    body.nav-compact .sidebar-brand { justify-content: center; padding-inline: 0; }

    /* SOUS-MENU FLOTTANT : hors flux, il ne pousse rien. */
    body.nav-compact .nav-groupe { position: relative; }
    body.nav-compact .nav-sous {
        position: absolute;
        inset-inline-start: calc(100% + .35rem);
        top: 0;
        min-width: 200px;
        padding: .35rem;
        background: var(--nav);
        border: 1px solid var(--nav-rule);
        border-radius: var(--radius);
        box-shadow: var(--shadow);
        z-index: 60;
    }
    body.nav-compact .nav-groupe:not(.is-open) .nav-sous { display: none; }
    body.nav-compact .nav-groupe.is-open .nav-sous { display: flex; flex-direction: column; }
    /* Dans le panneau, les libelles REVIENNENT : c est tout son interet. */
    body.nav-compact .nav-sous .nav-enfant {
        font-size: .84rem; justify-content: flex-start; padding: .45rem .6rem;
    }
    body.nav-compact .nav-sous .nav-enfant i { font-size: .8rem; width: 16px; margin-inline-end: .5rem; }
}

/* Le bouton de bascule, en tete de barre. */
.nav-bascule {
    display: flex; align-items: center; justify-content: center; gap: .4rem;
    width: 100%; margin-bottom: .5rem; padding: .35rem;
    background: transparent; border: 0; border-radius: var(--radius-btn); cursor: pointer;
    color: rgba(255,255,255,.65); font: inherit; font-size: .78rem;
}
.nav-bascule:hover { background: rgba(255,255,255,.08); color: var(--nav-tx-str); }
.nav-bascule:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.nav-bascule i { transition: transform .18s ease; }
body.nav-compact .nav-bascule i { transform: rotate(180deg); }
body.nav-compact .nav-bascule span { display: none; }

/* Le pied de barre et le catalogue se separent du reste par un filet. */
.nav-catalogue { margin-top: .5rem; border-top: 1px solid var(--nav-rule); padding-top: .7rem; }

/* Une transition legere, et RIEN quand le systeme demande la sobriete. */
@media (prefers-reduced-motion: no-preference) {
    .sidebar { transition: width .18s ease, min-width .18s ease; }
}
@media (prefers-reduced-motion: reduce) {
    .sidebar, .nav-bascule i { transition: none; }
}

/* §29 bis — Sous 900 px, la barre est deja une rangee horizontale : le mode
   compact n a pas lieu d etre et ne doit rien reduire.

   ⚠️ « RÉDUIRE LE MENU » S AFFICHAIT ET NE FAISAIT RIEN. Signalé deux fois par
   le fondateur. Deux causes, l une derrière l autre :

   1. LA RÈGLE EXISTANTE NE COUVRAIT QUE LA MOITIÉ DU CAS.
      `body.nav-compact .nav-bascule` ne masque le bouton qu UNE FOIS DÉJÀ
      COMPACT — donc jamais au premier chargement, qui est précisément le
      moment où l on clique. Le bouton bascule `body.nav-compact`, qui ne fait
      que poser `--nav-l: 70px` et masquer les libellés ; or sous 900 px la
      barre latérale est déjà en `width: 100%` (elle est passée à
      l horizontale) et ses libellés sont déjà effacés par `font-size: 0`.
      Le clic ne changeait RIEN.

   2. MA PREMIÈRE CORRECTION ÉTAIT AU MAUVAIS ENDROIT, et c est la leçon.
      Je l avais posée dans le premier `@media (max-width: 900px)` du fichier,
      ligne ~2500. Mais `.nav-bascule { display: flex }` est déclaré ligne
      ~2867, soit 365 lignes PLUS BAS, à spécificité identique : à égalité,
      la dernière règle l emporte. La correction était déployée sur les trois
      environnements et n avait aucun effet — mesuré, pas supposé.
      👉 Dans une feuille de 4 000 lignes, une règle de même spécificité doit
      se placer par rapport à sa DÉCLARATION DE BASE, pas par rapport au
      premier bloc de media query qui semble convenir.

   Un bouton qui ne fait rien n est pas neutre : on le presse, il ne se passe
   rien, et on en conclut que le produit est cassé. */
@media (max-width: 900px) {
    body.nav-compact { --nav-l: auto; }
    .nav-bascule { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   K8 — ACCUEIL « À FAIRE AUJOURD'HUI »
   Une tâche se lit en trois temps, et l'ordre compte : le NOMBRE d'abord —
   c'est lui que l'œil cherche —, la phrase qui dit de quoi il s'agit, puis
   l'action NOMMÉE. Avant, la carte entière était cliquable et rien ne le
   disait : on pouvait la regarder dix fois sans savoir qu'elle menait quelque
   part.
   ⚠️ L'alerte n'est PAS portée par la seule couleur (règle K7) : le nombre
   change de graisse et la flèche reste, quel que soit le ton.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════ §D4 — LA RANGÉE « À TRAITER » ═══════
   Elle remplace les cartes-tâches, qui prenaient chacune la largeur d'une carte
   d'indicateur pour porter une phrase de six mots. Une rangée : un filet
   vertical de 3 px à la teinte du domaine (lue dans la table des teintes, via
   `data-tab`), le compte en mono, la phrase, le lien d'action à droite. */
.rangs { display: flex; flex-direction: column; }
.rang {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: .9rem;
    padding: .7rem .9rem;
    border-inline-start: 3px solid var(--tuile-fg, var(--violet));
    border-block-end: 1px solid var(--line);
    color: inherit;
    text-decoration: none;
    transition: background-color .15s ease;
}
.rang:last-child { border-block-end: 0; }
.rang:hover { background: var(--veil); }
.rang:focus-visible { outline: 2px solid var(--violet); outline-offset: -2px; }

.rang-nb {
    font-size: 1.35rem;
    font-weight: 700;
    line-height: 1.1;
    min-width: 2.4rem;
    text-align: end;
    color: var(--ink);
}
.rang-txt { font-size: .89rem; line-height: 1.3; min-width: 0; }
.rang-lien {
    font-size: .8rem; font-weight: 600; color: var(--violet); white-space: nowrap;
}
.rang-lien i { font-size: .7rem; }

/* ⚠️ LA SEULE ENTORSE ADMISE À R8, et c'est précisément parce que cette ligne
   EST l'exception : impayés, promesses dépassées, élèves sans classe. R8 veut
   que le défaut se taise et que l'exception parle — la peindre applique la
   règle, elle ne l'enfreint pas. Le ton se voit AUSSI hors couleur : le fond
   change en même temps que le filet. */
.rang--defaut {
    background: var(--error-bg);
    border-inline-start-color: var(--error);
}
.rang--defaut:hover { background: var(--error-bg); border-inline-start-width: 5px; }
.rang--defaut .rang-nb,
.rang--defaut .rang-lien { color: var(--error-tx); }

/* A7 — l'écran vide est un RÉSULTAT : il se présente, il ne se subit pas. */
.tache-vide { display: flex; align-items: center; gap: 1rem; padding: 1.2rem; }
.tache-vide > i { font-size: 1.6rem; color: var(--violet); opacity: .55; }
.tache-vide p { margin: .15rem 0 0; font-size: .86rem; }

/* ═══════════ « LA JOURNÉE D'ÉCOLE » — les bandes de l'Accueil ═══════════
   Tout passe par des JETONS : aucune couleur écrite en dur, donc le thème
   sombre suit sans une seule règle en double. Aucun `style=` non plus — la CSP
   n'en accepte aucun (verif_styles_inline veille). */

/* Bande 1 — L'EN-TÊTE DE CONSOLE. Elle ne peut jamais être vide : elle date la
   journée, nomme l'année et porte l'acte du matin. Le texte à gauche, l'action
   à droite ; sous 760 px la colonne se replie et le bouton passe dessous, à
   pleine largeur — un bouton de 40 px à côté d'un titre de deux lignes ne se
   vise pas au pouce. */
/* ⚠️ GRILLE ET NON FLEX, POUR QUE LA PASTILLE ET L ACTE PUISSENT SE PARTAGER
   UNE RANGEE. En flex, `.acc-pastilles` vivait DANS `.acc-tete-dit` et l acte
   en etait le frere : le bouton prenait donc une rangee entiere pour lui seul.
   Mesure a 390 px : bande de 169 px, dont ~56 px pour cette seule rangee, et
   un bouton etale sur 364 px pour trois mots.
   Les pastilles sont devenues soeurs (app.js, les DEUX bandes) et la grille
   les range : sous l identite sur ecran large, A COTE de l acte sur telephone.
   La colonne `auto` se replie a zero quand il n y a pas d acte — c est le cas
   du Tableau de bord, qui partage ces classes sans porter de bouton. */
.acc-tete {
    display: grid;
    /* ⚠️ UNE SEULE RANGÉE SUR ÉCRAN LARGE. La bande empilait identité, date
       PUIS pastille sur trois lignes, avec un vide de plusieurs centaines de
       pixels à leur droite — mesuré 121 px de haut là où le contenu tient en
       une ligne et demie. La pastille rejoint l'identité, l'acte reste à
       l'extrémité : la bande retombe à ~85 px et le premier chiffre de
       l'écran remonte d'autant.
       `auto 1fr auto` et non trois `auto` : la colonne du milieu absorbe le
       vide, sans quoi il se rouvrirait entre la pastille et l'acte. */
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "dit past acte";
    align-items: center;
    gap: .7rem 1rem;
    margin: 0 0 1.4rem;
}
.acc-tete-dit  { grid-area: dit; min-width: 0; }
.acc-pastilles { grid-area: past; }
/* R9 — l'unique action primaire de l'écran. */
.acc-acte { grid-area: acte; align-self: start; }
.acc-salut {
    margin: 0; font-family: var(--font-title);
    font-size: 1.5rem; font-weight: 700; line-height: 1.2; color: var(--text);
}
.acc-date { margin: .2rem 0 0; font-size: .92rem; color: var(--text-muted); }
.acc-annee { font-weight: 600; color: var(--violet); }

.acc-pastilles { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
.acc-pastille {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .32rem .7rem;
    background: var(--violet-xxl);
    border: 1px solid var(--rule); border-radius: 8px;
    font-size: .84rem; color: var(--text); text-decoration: none;
}
a.acc-pastille:hover { border-color: var(--violet); }
a.acc-pastille:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.acc-pastille i { color: var(--violet); font-size: .8rem; }

/* ⚠️ LA BANDE « L'ÉCOLE EN BREF » A DISPARU avec ses cartes à moitié vides
   (D5 de l'analyse). Ses chiffres ont rejoint les cartes d'indicateur (les
   élèves, avec leur courbe), l'en-tête (la caisse et Massar, plus les absences
   du jour — celles-là seulement si elles ne sont pas à zéro, D10 ; la caisse,
   elle, se dit même fermée, parce qu'un tiroir fermé est un fait de la journée)
   et le tableau des rendez-vous (les événements). Les règles `.acc-brief*` sont
   parties avec elles.
   ⚠️ `.acc-briefs` RESTE : la frise d'activité d'une fiche élève s'en sert
   (`actTimelineHtml`, assets/js/app.js). C'est le pluriel qui vit, pas le
   singulier. */
.acc-briefs {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: .7rem; margin-bottom: 1.6rem;
}

/* ═══════ §D2 — LA PUCE DE TENDANCE ═══════
   Elle vit DANS la ligne de valeur, qui ne se coupe jamais : elle reste donc
   courte. Sa couleur ne dit pas le sens de la flèche, elle dit si le mouvement
   est bon POUR CET INDICATEUR (R6) — une hausse d'absences n'est pas une
   confirmation factuelle. Neutre quand il n'y a rien à féliciter ni à
   signaler. */
.kpi-tend {
    display: inline-flex; align-items: center; gap: .2rem;
    padding: .1rem .35rem;
    border-radius: 8px;
    background: var(--veil);
    color: var(--ink-soft);
    font-size: .72rem; font-weight: 600;
}
.kpi-tend i { font-size: .65rem; }
.kpi-tend .num { font-size: .72rem; }
.kpi-tend--bon     { background: var(--green-bg); color: var(--green-tx); }
.kpi-tend--mauvais { background: var(--error-bg); color: var(--error-tx); }

/* ═══════ C2 — LA SPARKLINE ═══════
   Repère interne de 100 x 28 étiré à la largeur disponible : c'est la HAUTEUR
   qui est fixée ici, la largeur suit la carte. Le tracé se colore par
   `currentColor`, posé juste au-dessus depuis la table des teintes. */
.sparkline {
    display: block;
    width: 100%;
    height: 30px;
    margin-top: .45rem;
}

/* ═══════ §D3 — LA GRILLE À DEUX COLONNES DE L'ACCUEIL ═══════
   « à traiter aujourd'hui » à gauche, le graphique multi-séries à droite. La
   colonne des tâches est la plus large : c'est elle qu'on lit, l'autre qu'on
   regarde. Sous 1100 px, les deux passent l'une sous l'autre. */
.acc-duo {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    /* ⚠️ `start` et non l'étirement par défaut : à hauteurs égales, la carte des
       tâches — la plus courte des deux — finissait par une bande blanche de la
       hauteur de l'écart. C'est le défaut D5 reconduit, à l'endroit même où on
       venait de le fermer. Chaque carte prend désormais sa hauteur. */
    align-items: start;
    gap: 1rem;
    margin-bottom: 1rem;
}
/* Une carte de la console : les rangées et le tableau touchent ses bords, seule
   la tête garde le retrait. Sans cela, un filet de rangée s'arrêterait à vingt
   pixels du bord et le tableau flotterait dans une gouttière (D4 de l'analyse). */
.acc-carte { padding: 1.25rem 0 0; display: flex; flex-direction: column; }
.acc-carte > .card-head { padding: 0 1.4rem; }
.acc-carte > .rangs,
.acc-carte > .table-wrap { margin-block-end: 0; }
.acc-carte > .empty-state { padding-inline: 1.4rem; }
.card-sous { font-size: .8rem; color: var(--text-muted); }
/* Le graphique garde un fond de carte, donc un retrait à lui. */
.acc-graph { padding: 0 1.4rem 1.25rem; height: 240px; }

/* Bande 5 — une ligne, pas une mosaïque : la page « Tous les modules » existe. */
/* `.acc-explorer` a été RETIRÉE avec la ligne « votre espace compte N modules » :
   la bande 5 montre désormais les cartes de domaine elles-mêmes (`.mod-domaines`,
   partagées avec la page « Tous les modules »). Une règle qui ne s'applique plus
   à rien décrit une interface qui n'existe pas. */

@media (prefers-reduced-motion: reduce) {
    .acc-pastille, .rang { transition: none; }
}

/* Sous cette largeur, la grille à deux colonnes se déplie : la colonne du
   graphique y devient plus étroite que la colonne des tâches qu'elle accompagne,
   et six mois d'étiquettes s'y serrent. Le point de rupture est un choix de
   composition, pas le seuil d'un défaut mesuré. */
@media (max-width: 1100px) {
    .acc-duo { grid-template-columns: 1fr; }
}

@media (max-width: 760px) {
    /* ⚠️ L ACTE NE PREND PLUS TOUTE UNE RANGEE. Il s etalait sur 364 px pour
       trois mots et poussait le contenu de 56 px vers le bas — mesure a
       390 px. Il partage desormais la rangee des pastilles.

       ET L IDENTITE REPREND LA PREMIERE RANGEE ENTIERE : en gardant la grille
       large, « Bonjour <prenom> » n aurait dispose que d environ 230 px des
       390, l acte occupant sa colonne des la premiere ligne. Le titre y
       passerait a la ligne pour rien. */
    .acc-tete {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas:
            "dit  dit"
            "past acte";
        align-items: center;
    }
    .acc-acte { justify-content: center; white-space: nowrap; }
}

@media (max-width: 640px) {
    .acc-salut { font-size: 1.25rem; }
    .acc-briefs { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
    /* La rangée se plie en deux lignes : le lien d'action passe sous la phrase
       plutôt que de la comprimer à six caractères. */
    .rang { grid-template-columns: auto 1fr; row-gap: .3rem; }
    .rang-lien { grid-column: 2; }
    .acc-graph { height: 200px; padding-inline: 1rem; }
    .acc-carte > .card-head { padding-inline: 1rem; }
}

/* `.accueil-jour` a été retirée : plus aucun émetteur dans le produit (vérifié
   par recherche sur l'arbre entier). Ce que la liste rendait est devenu les
   pastilles de l'en-tête. */

/* Actions rapides : des VERBES, pas des noms de modules. */
.actions-rapides { display: flex; flex-wrap: wrap; gap: .5rem; }
.action-rapide {
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .5rem .85rem;
    background: var(--white); border: 1px solid var(--rule); border-radius: var(--radius-btn);
    font-size: .86rem; font-weight: 600; color: inherit; text-decoration: none;
}
.action-rapide:hover { border-color: var(--violet); color: var(--violet); }
.action-rapide:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.action-rapide i { color: var(--violet); font-size: .82rem; }

/* D2 — le périmètre d'un chiffre, dit sous lui plutôt que deviné. */
.kpi-perimetre { display: block; opacity: .8; font-size: .74rem; }

/* « .tache » n'existe plus (voir les rangées §D4) ; sa règle de mouvement
   réduit est partie avec elle. `.tache-vide`, elle, reste : c'est l'encart de
   PANNE de l'accueil, et il n'anime rien. */

/* ═══════════════════════════════════════════════════════════════════════════
   T6 — CLOCHE ET PANNEAU DES NOTIFICATIONS
   ⚠️ Le badge ne repose PAS sur la seule couleur (règle K7) : il porte un
   NOMBRE, et la cloche change d'état visuel. Un point coloré muet ne dirait
   ni combien ni quoi.
   ═══════════════════════════════════════════════════════════════════════════ */

.topbar-cloche {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.1rem; height: 2.1rem;
    background: transparent; border: 0; border-radius: var(--radius-btn);
    color: inherit; cursor: pointer; font-size: 1rem;
}
.topbar-cloche:hover { background: rgba(0,0,0,.06); }
.topbar-cloche:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.topbar-cloche.a-du-neuf i { color: var(--violet); }

.cloche-badge {
    position: absolute; top: -2px; inset-inline-end: -2px;
    min-width: 1.05rem; height: 1.05rem; padding: 0 .22rem;
    background: var(--error); color: var(--on-accent);
    border-radius: 8px;
    font-size: .66rem; font-weight: 700; line-height: 1.05rem;
    text-align: center; font-variant-numeric: tabular-nums;
}

.notif-panneau {
    position: fixed; inset-block: 0; inset-inline-end: 0;
    width: min(400px, 100vw);
    display: flex; flex-direction: column;
    /* ⚠️ OPACITÉ EXPLICITE. Une première version écrivait `var(--card)` — un
       jeton inventé, qui n'existe nulle part : la déclaration était ignorée, le
       panneau n'avait aucun fond et laissait voir la page au travers. Les
       jetons de surface du produit sont `--white` et `--border`/`--rule`.
       Le repli littéral qui gardait ce risque a été retiré : c'est maintenant
       tools/verif_jetons_css.php qui refuse un `var(--x, repli)` dont le jeton
       ne serait pas défini — un garde-fou qui échoue tôt vaut mieux qu'un
       garde-fou qui masque. */
    background: var(--white);
    /* Filet APPUYE, comme le panneau d'aide : voir le motif la-bas. */
    border-inline-start: 1px solid var(--line-strong);
    box-shadow: var(--shadow);
    z-index: 120;
}
.notif-panneau[hidden] { display: none; }

.notif-tete {
    display: flex; align-items: center; gap: .5rem;
    padding: .9rem 1rem; border-bottom: 1px solid var(--rule);
}
.notif-tete strong { flex: 1; font-size: .95rem; }
.notif-fermer {
    background: transparent; border: 0; cursor: pointer;
    color: var(--text-muted); font-size: 1rem; padding: .2rem .35rem;
}
.notif-fermer:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }

.notif-onglets {
    display: flex; flex-wrap: wrap; gap: .3rem;
    padding: .55rem 1rem; border-bottom: 1px solid var(--rule);
}
.notif-onglet {
    padding: .22rem .6rem;
    background: transparent; border: 1px solid var(--rule); border-radius: var(--radius-btn);
    font-size: .74rem; cursor: pointer; color: inherit;
}
.notif-onglet.actif { background: var(--violet); border-color: var(--violet); color: var(--on-accent); }
.notif-onglet:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }

.notif-liste { flex: 1; overflow-y: auto; }

.notif-ligne {
    display: flex; gap: .6rem; align-items: flex-start;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--rule);
    color: inherit; text-decoration: none;
}
a.notif-ligne:hover { background: rgba(0,0,0,.035); }
.notif-ligne:focus-visible { outline: 2px solid var(--violet); outline-offset: -2px; }

/* La pastille marque le NON-LU. Elle double le gras du titre : deux signaux
   pour une information, jamais la couleur seule. */
.notif-point { width: .5rem; height: .5rem; margin-top: .38rem; border-radius: 50%; background: transparent; flex: 0 0 auto; }
.notif-ligne.non-lue .notif-point { background: var(--violet); }
.notif-ligne.non-lue strong { font-weight: 700; }
.notif-ligne:not(.non-lue) strong { font-weight: 500; opacity: .85; }

.notif-corps { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.notif-corps strong { font-size: .88rem; line-height: 1.3; }
.notif-detail { font-size: .8rem; color: var(--text-muted); }
.notif-quand { font-size: .72rem; color: var(--text-muted); }

@media (max-width: 560px) {
    .notif-panneau { width: 100vw; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   PAGE « TOUS LES MODULES »
   Sa mise en page vit plus haut, avec `.mod-domaines` : chaque domaine est une
   CARTE, et les cartes se rangent côte à côte.
   ⚠️ DEUX défauts ont été payés ici, et tous deux ont été vus par le fondateur,
   pas par un lint :
   1. une classe « tuiles » qui n'existait nulle part — la grille ne
      s'appliquait jamais et les cartes s'empilaient en colonne. Le lint des
      jetons ne pouvait pas le voir : une classe absente n'est pas un jeton
      absent ;
   2. sept titres pleine largeur surmontant chacun une demi-rangée : la page
      demandait deux écrans de défilement pour dix-huit liens.
   ═══════════════════════════════════════════════════════════════════════════ */

/* `.tuiles-titre` sert encore à l'ACCUEIL (bandes « à faire », « en bref »,
   « actions rapides ») : elle reste. C'est son couplage à `.tuiles-grid` qui
   est parti avec elle. */
.tuiles-titre i { margin-inline-end: .35rem; opacity: .75; }

/* Sous-titre de page : une phrase, pas un paragraphe. */
.u-page-sub { margin: 0 0 1.1rem; font-size: .88rem; }

/* Sur cette page, la description compte autant que le nom : c'est elle qui
   apprend ce qu'on n'a jamais ouvert. On lui laisse deux lignes pleines
   plutôt que de la tronquer à une. */
#content .tuile-sous { line-height: 1.35; }

/* ── Cycle de vie du dossier (fiche) ──
   Trois moments, pas onze statuts : « Soumis » et « Pièces à compléter » sont
   le même moment vu sous deux angles. Onze pastilles pour trois moments
   brouilleraient le message au lieu de l'éclairer. */
.cycle-vie { display: flex; align-items: center; gap: .35rem; margin: .1rem 0 .9rem; flex-wrap: wrap; }
.cycle-etape {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .22rem .6rem;
    border: 1px solid var(--border); border-radius: 8px;
    font-size: .76rem; color: var(--text-muted);
    background: var(--white);
}
/* L'étape franchie et l'étape courante ne se distinguent pas que par la
   couleur : la première porte une coche, la seconde est pleine et en gras. */
.cycle-etape.atteinte { border-color: var(--violet); color: var(--violet-deep); }
.cycle-etape.courante { background: var(--violet); border-color: var(--violet); color: var(--on-accent); font-weight: 700; }
.cycle-fleche { color: var(--text-muted); font-size: .7rem; }
/* Hors parcours (liste d'attente, refus, sortie) : on ne montre PAS une
   progression. Présenter un refus comme une étape franchie mentirait sur ce
   qui s'est passé. */
.cycle-hors { font-size: .78rem; color: var(--text-muted); }

/* ── 4B — corps d'un message dans l'aperçu résolu ──
   Le texte saisi porte ses propres retours à la ligne : sans `pre-wrap`, un
   message écrit en paragraphes s'affiche en un seul bloc, et l'aperçu ne montre
   pas ce qui partira. */
.apercu-corps {
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--ink);
    margin: .35rem 0 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PHASE 5 — PHOTO DE L'ÉLÈVE DANS LA FICHE
   Le back-office n'avait AUCUN motif de vignette : `img, svg { max-width:100% }`
   et rien d'autre — pas d'`object-fit`, pas de ratio, pas de conteneur. Une
   photo d'identité y serait arrivée à sa taille propre, déformant la grille
   d'identité selon qu'elle est carrée, en portrait ou en paysage.
   ═══════════════════════════════════════════════════════════════════════════ */

/* La photo à gauche, les champs d'identité à droite ; sur mobile, l'un sous
   l'autre. `align-items: start` empêche la vignette de s'étirer à la hauteur
   des champs. */
.fiche-identite {
    display: flex;
    gap: 1rem;
    align-items: start;
    flex-wrap: wrap;
}
/* ⚠️ `flex-basis: 20rem` devenait une HAUTEUR imposee une fois la direction
   passee en `column` sur telephone : flex-basis suit l axe principal. Il
   creusait ~200 px de vide entre l identite et « Responsables ». On borne la
   base a `auto` dans la colonne, ci-dessous. */
.fiche-identite-champs { flex: 1 1 20rem; min-width: 0; }

.fiche-photo { display: flex; flex-direction: column; gap: .4rem; align-items: center; }
.fiche-photo-actions { display: flex; flex-direction: column; gap: .3rem; width: 100%; }

/* Ratio d'identité 3/4 : c'est le format d'une photo scolaire, et le fixer
   évite que la mise en page saute d'un élève à l'autre. `object-fit: cover`
   recadre au centre plutôt que de déformer un visage — un portrait étiré est
   pire que pas de photo. */
.eleve-photo {
    width: 7.5rem;
    aspect-ratio: 3 / 4;
    object-fit: cover;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--veil);
    display: block;
}

/* À défaut de photo : la pastille d'initiales. Elle existait UNIQUEMENT en PDF
   (pastille GD du trombinoscope) ; en HTML l'absence d'image ne produisait rien
   — un trou dans la page, et rien qui laisse deviner qu'une photo peut être
   posée.
   ⚠️ ELLE N'OCCUPE PLUS LA PLACE DE L'IMAGE. Elle la réservait « pour que la
   grille ne bouge pas quand on en ajoute une » : c'était payer le format
   d'identité entier sur CHAQUE fiche — aucun élève de la base n'a de photo —
   pour éviter un décalage qui n'a lieu qu'une fois, au téléversement, sur une
   fiche qui se redessine de toute façon.
   Elle a maintenant la taille d'une pastille d'identité et se range dans leur
   rangée. Quand une photo EXISTE, c'est `.eleve-photo` qui sert, et le format
   d'identité revient : ce n'est alors plus un décor, c'est une donnée. */
.eleve-photo-vide {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 8px;
    border: 1px solid var(--border);
    font-size: .9rem;
    font-weight: 700;
    letter-spacing: .05em;
    color: var(--violet-deep);
    background: var(--veil);
    user-select: none;
}

/* Sans photo, le bloc cesse d'être une colonne : la pastille et son geste
   discret se rangent sur la ligne des faits d'identité. */
.fiche-photo--sans {
    flex-direction: row;
    align-items: center;
    gap: .4rem;
}

/* ── UN TITRE DE SECTION QUI PORTE SON GESTE ──
   « Rattacher un responsable » et « Souscrire à un service » étaient deux
   boutons pleine largeur posés sous leur bloc : du décor pour des gestes rares.
   Le bouton monte sur le titre. La ligne garde une hauteur suffisante pour que
   la cible reste au-dessus du plancher WCAG 2.2, sans que le titre change de
   taille. */
.fiche-titre {
    display: flex;
    align-items: center;
    gap: .5rem;
    min-height: 1.75rem;
    flex-wrap: wrap;
}

@media (max-width: 600px) {
    /* Sur un téléphone, la photo passe au-dessus et se réduit : la fiche sert
       d'abord à lire des informations, pas à regarder un portrait. */
    .fiche-identite { flex-direction: column; }
    .fiche-photo { align-items: flex-start; }
    .fiche-identite-champs { flex: 1 1 auto; }
    .eleve-photo { width: 5.5rem; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PHASE 6 — SAISIE DES NOTES : CE QUI N'EST PAS ENCORE ENREGISTRÉ
   Rien ne distinguait une note déjà enregistrée d'une note qui ne l'était pas.
   L'enseignant fermait l'onglet en croyant son travail acquis — et une heure de
   saisie disparaissait sans un mot.
   ⚠️ La couleur ne porte pas seule l'information (règle K7) : la cellule touchée
   prend AUSSI un fond et une bordure marquée, et un compteur textuel s'affiche
   à côté du bouton d'enregistrement.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ⚠️ SANS !important, ET C'EST LE CORRECTIF. Le `!important` qui vivait ici
   battait l'ecriture en ligne `inp.style.borderColor` d'assets/js/app.js : une
   note a la fois MODIFIEE et INVALIDE restait bordee de violet, le signal rouge
   ne s'affichait jamais. Le companion `input.note-modifiee` porte la regle a
   (0,2,1) — assez pour battre la regle globale des champs et `input:focus` —
   et laisse la declaration en ligne reprendre la main, comme elle le doit. */
.note-modifiee {
    background: var(--veil);
    font-weight: 600;
}
input.note-modifiee { border-color: var(--violet); }

/* Le compteur « N note(s) non enregistrée(s) ». Il n'est visible que s'il y a
   quelque chose à perdre : un avertissement permanent ne se lit plus. */
.chip-alerte {
    background: var(--veil);
    color: var(--violet-deep);
    border-inline-start: 3px solid var(--violet);
    padding-inline-start: .45rem;
    font-weight: 600;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PHASE 6 — SAISIE DES NOTES : LA COLONNE ÉLÈVE RESTE VISIBLE
   Une grille à six évaluations déborde de l'écran. En faisant défiler vers la
   droite pour atteindre la dernière colonne, l'enseignant perdait de vue le nom
   de l'élève — il saisissait une note en comptant les lignes de tête. C'est
   exactement ainsi qu'on attribue une note au voisin.
   ⚠️ `position: sticky` exige un fond OPAQUE : sans lui, les colonnes défilantes
   se lisent en transparence sous le nom.
   ═══════════════════════════════════════════════════════════════════════════ */
#notes-sa-table th:nth-child(1),
#notes-sa-table td:nth-child(1),
#notes-sa-table th:nth-child(2),
#notes-sa-table td:nth-child(2) {
    position: sticky;
    background: var(--white);
    z-index: 2;
}
#notes-sa-table th:nth-child(1),
#notes-sa-table td:nth-child(1) { inset-inline-start: 0; }
/* La deuxième colonne se cale APRÈS la première : la largeur du numéro d'ordre
   est fixée (u-w-2-5rem) pour que ce décalage soit exact. */
#notes-sa-table th:nth-child(2),
#notes-sa-table td:nth-child(2) { inset-inline-start: 2.5rem; }
/* L'en-tête doit passer au-dessus des cellules figées, pas dessous. */
#notes-sa-table thead th:nth-child(1),
#notes-sa-table thead th:nth-child(2) { z-index: 3; }
/* Un liseré marque la frontière entre le figé et le défilant — sans lui, on ne
   comprend pas pourquoi une partie du tableau ne bouge pas. */
#notes-sa-table th:nth-child(2),
#notes-sa-table td:nth-child(2) { border-inline-end: 2px solid var(--border); }

/* ----- Grille d'appel : cibles tactiles ----------------------------------
   L'appel se fait sur une tablette, debout dans un couloir. Les trois boutons
   mesuraient 28,8 px de haut (31,8 px en état non sélectionné, à cause du
   filet) — soit 65 % de la cible de 44 px, séparés de 5,6 px.

   ⚠️ CLASSE DÉDIÉE, ET C'EST DÉLIBÉRÉ. `.btn-sm` sert 245 fois dans le produit
   et `.btn-icon` 78 fois : les agrandir déplacerait tous les écrans pour régler
   un problème d'un seul.

   Le filet TRANSPARENT est posé sur les deux états. Sans lui, seul l'état non
   sélectionné portait une bordure : le bord gauche du bouton avançait de
   3,000 px exactement au tap « absent », et un second tap au même endroit tombait
   dans le vide. Mesuré sur Chrome, 820×1180, dpr=2. */
.btn-tap { min-height: 44px; min-width: 44px; border: 1.5px solid transparent; }
.btn-outlined.btn-tap { border-color: var(--line); }

/* La colonne « Retard (min) » troque un tiret (23,3 px) contre un champ de
   96 px dès qu'on choisit « retard ». Le tableau étant en largeur automatique,
   TOUTES les colonnes se recalculent : la colonne d'appel glissait de 4,8 px
   vers la gauche à chaque passage en retard, et les trois boutons avec elle —
   et la rangée gagnait 6,8 px, poussant vers le bas toutes les lignes suivantes.
   On réserve donc d'avance l'empreinte du champ : 6rem (sa largeur maximale)
   plus les 2 × 0,9rem de remplissage d'une cellule. La colonne ne bouge plus. */
#abs-ap-table td:nth-child(4) { min-width: calc(6rem + 1.8rem); }

/* ════════════════════════════════════════════════════════════════════
   K7 — CIBLES TACTILES : on agrandit la ZONE, jamais le dessin.
   ════════════════════════════════════════════════════════════════════
   `.btn-icon` fait 34 × 34 px. Le porter à 44 px changerait l'aspect de tout
   le produit — c'est une refonte du design system, et T5 l'interdit. La zone
   cliquable, elle, n'a pas à se voir : un pseudo-élément centré la porte à
   44 px sans déplacer un seul pixel visible.
   ⚠️ Sous `pointer: coarse` UNIQUEMENT — au doigt. À la souris, une cible de
   34 px se vise sans peine, et des zones invisibles de 44 px qui se
   chevauchent dans une barre d'actions feraient cliquer sur le MAUVAIS
   bouton : le remède serait pire que le mal.
   ⚠️ `position: relative` est nécessaire au pseudo-élément ; il est posé DANS
   la media query pour ne rien changer au rendu sur poste fixe. */
@media (pointer: coarse) {
    .btn-icon,
    .tab-pill,
    .modal-close {
        position: relative;
    }
    .btn-icon::after,
    .tab-pill::after,
    .modal-close::after {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        width: 44px;
        height: 44px;
        transform: translate(-50%, -50%);
    }
}

/* ── LA CASE D'UNE LISTE : ON AGRANDIT LA ZONE, JAMAIS LE DESSIN ──
   Même doctrine que K7 ci-dessus, appliquée là où on l'avait oubliée. La case
   des listes fait 16 × 16 px dans une cellule de 45 × 49 px, et la RATER n'est
   pas neutre : le clic sur la cellule OUVRE LA FICHE de l'élève. Le coût d'une
   erreur de visée n'est donc pas « rien ne se passe », c'est « une fenêtre
   s'ouvre, il faut la fermer, et on a perdu sa place ».
   Le remède est un <label> qui remplit la cellule plutôt qu'un pseudo-élément :
   il vaut à la souris comme au doigt, il n'ajoute aucune zone invisible qui
   pourrait chevaucher une voisine, et il donne au passage à la case le NOM
   accessible qu'elle n'avait pas. Le dessin de la case, lui, ne bouge pas. */
.cell-check {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    min-height: 24px;
    margin: -.35rem -.5rem;
    padding: .35rem .5rem;
    cursor: pointer;
}

/* ═══════════════════ LE CENTRE D'AIDE ═══════════════════
   Volet lateral : meme gabarit que celui de la cloche, pour que les deux se
   comportent pareil. Les proprietes sont LOGIQUES (inline-start/end) et non
   physiques : l'espace famille est en arabe, et le back-office le sera
   peut-etre — une marge « left » se retrouverait du mauvais cote.
   ⚠️ JUMEAUX POUR DE BON. Les deux panneaux portaient jusqu'ici deux ombres
   differentes et deux z-index differents : ils prennent desormais l'ombre
   unique et la meme valeur d'empilement (voir l'echelle en tete de feuille). */
.aide-panneau {
    position: fixed;
    inset-block: 0;
    inset-inline-end: 0;
    width: min(30rem, 100vw);
    background: var(--white);
    /* Filet APPUYE, et non le filet ordinaire : la recette d'ombre unique de la
       charte ne porte que vers le bas, elle ne detache donc pas un panneau
       ancre au bord droit. C'est le trait qui doit faire la separation. */
    border-inline-start: 1px solid var(--line-strong);
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
    z-index: 120;
}
.aide-tete {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .9rem 1rem;
    border-bottom: 1px solid var(--border);
}
.aide-tete strong { flex: 1; font-size: 1rem; }
.aide-recherche { padding: .75rem 1rem; border-bottom: 1px solid var(--border); }
.aide-recherche .search-input { width: 100%; }
.aide-corps { flex: 1; overflow-y: auto; padding: 1rem; }
.aide-attente { text-align: center; padding: 2.5rem 0; color: var(--text-muted); }
.aide-vide { color: var(--text-muted); margin: .3rem 0 1rem; }

/* La liste des articles : une cible large, pas une ligne de texte. */
.aide-liste { display: flex; flex-direction: column; gap: .35rem; margin-bottom: 1rem; }
.aide-item {
    display: block;
    padding: .55rem .7rem;
    border-radius: var(--radius-btn);
    text-decoration: none;
    color: inherit;
    border: 1px solid transparent;
}
.aide-item:hover { background: var(--surface-3); border-color: var(--border); }
.aide-item:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.aide-item-t { display: block; font-weight: 600; }
.aide-item-r { display: block; font-size: .85rem; color: var(--text-muted); margin-top: .12rem; }

.aide-cat { border: 1px solid var(--border); border-radius: var(--radius-btn); margin-bottom: .5rem; }
.aide-cat summary {
    padding: .55rem .7rem;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: .5rem;
    font-weight: 600;
}
.aide-cat summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.aide-cat-n { margin-inline-start: auto; color: var(--text-muted); font-weight: 400; font-size: .85rem; }
.aide-cat .aide-liste { padding: 0 .5rem .5rem; margin-bottom: 0; }

/* L'article. Une aide se lit en diagonale : de l'air entre les blocs. */
.aide-article h3 { margin: 0 0 .4rem; font-size: 1.15rem; }
.aide-resume { color: var(--text-muted); margin: 0 0 1rem; }
.aide-h { margin: 1.3rem 0 .5rem; font-size: .95rem; }
.aide-h:first-child { margin-top: 0; }
.aide-p { margin: 0 0 .75rem; line-height: 1.62; }
.aide-puces, .aide-etapes { margin: 0 0 .9rem; padding-inline-start: 1.35rem; line-height: 1.62; }
.aide-puces li, .aide-etapes li { margin-bottom: .45rem; }
.aide-etapes li::marker { font-weight: 700; color: var(--violet); }
.aide-avert {
    margin: 0 0 .9rem;
    padding: .6rem .75rem;
    background: var(--violet-xl);
    border-inline-start: 3px solid var(--violet);
    border-radius: var(--radius-btn);
    line-height: 1.55;
}
.aide-retour { margin-bottom: .9rem; }

/* ⚠️ LA CAPTURE N'EST JAMAIS ANNOTEE DANS L'IMAGE. Les encadres sont poses
   par-dessus, en pourcentages : une seule serie d'images sert les deux
   langues, et refaire une capture ne demande aucune retouche. Les positions
   arrivent par le CSSOM (aide.js) — `style=` est interdit par la CSP. */
.aide-fig { margin: 0 0 1rem; }
.aide-fig-cadre { position: relative; display: inline-block; max-width: 100%; }
.aide-fig-cadre img {
    display: block;
    max-width: 100%;
    height: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-btn);
}
/* Le halo clair qui detache l'encadre de la capture est un CONTOUR, non une
   ombre : la charte n'admet d'ombre que sur ce qui survole le document (R2),
   et cet encadre est colle a l'image. Meme dessin, une forme d'ombre en
   moins. */
.aide-zone {
    position: absolute;
    border: 2px solid var(--brique);
    border-radius: 8px;
    outline: 2px solid color-mix(in srgb, var(--white) 55%, transparent);
    display: flex;
    align-items: flex-start;
    justify-content: flex-start;
}
/* Le numero de l'etape, colle au coin de la zone. */
.aide-zone::before {
    content: attr(data-n);
    position: absolute;
    inset-block-start: -11px;
    inset-inline-start: -11px;
    min-width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--brique);
    color: var(--on-accent);
    font-size: .72rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}
.aide-fig figcaption { color: var(--text-muted); font-size: .85rem; margin-top: .35rem; }

@media (max-width: 640px) {
    .aide-panneau { width: 100vw; }
}

/* Un libelle pour les lecteurs d'ecran, invisible a l'oeil. `display: none`
   ne conviendrait pas : il retirerait aussi l'element de l'arbre
   d'accessibilite, donc du lecteur d'ecran — c'est-a-dire du seul public a
   qui il s'adresse. */
.u-visuallyhidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* La page plein ecran du centre d'aide : sommaire a gauche, article a droite.
   Sous 900 px la colonne passe au-dessus — un sommaire de 18 rem a cote d'un
   article de 12 rem ne se lit ni d'un cote ni de l'autre. */
.aide-page {
    display: grid;
    grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
    gap: 1.5rem;
    align-items: start;
}
.aide-page-nav {
    position: sticky;
    top: 1rem;
    max-height: calc(100vh - 6rem);
    overflow-y: auto;
    background: var(--white);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .25rem;
}
.aide-page-nav .aide-recherche { border-bottom: 1px solid var(--border); }
.aide-page-nav #aide-page-som { padding: .5rem; }
.aide-page-corps {
    background: var(--white);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.5rem;
    min-height: 20rem;
}
/* La lecture longue a besoin d'une mesure : au-dela de ~75 caracteres par
   ligne, l'oeil perd le debut de la ligne suivante. */
.aide-page-corps .aide-article { max-width: 46rem; }

@media (max-width: 900px) {
    .aide-page { grid-template-columns: 1fr; }
    .aide-page-nav { position: static; max-height: none; }
}

/* ── 10. ÉCRAN DE CONNEXION À DEUX PANNEAUX (login.php) ──────────────────
   Espace de noms .cnx-* et NON .login-* : portail.php partage cette feuille
   et s'appuie sur .login-page et .login-card, ainsi que sur la cascade
   .login-card .login-sub. Redéfinir ces classes casserait la connexion des
   familles. Des classes étrangères sont surclassées ici — .logo-mark,
   .lang-switch, .alert, .form-group, .btn, .btn-primary, .login-foot et
   .hint — mais toutes en DESCENDANCE d'une classe .cnx-*, qui n'existe nulle
   part ailleurs dans le dépôt. Aucune ne peut atteindre une autre page.

   Les couleurs du panneau sont LITTÉRALES : --violet et --veil s'inversent
   sous [data-theme="dark"], et ce panneau est un champ de marque, pas une
   surface thématisée. C'est déjà la pratique de la maison : dans login.php,
   le SVG du logo et le bouton du courriel de réinitialisation écrivent le
   violet en clair. Repères donnés par leur TEXTE et non par un numéro de
   ligne : login.php est remanié souvent, et une coordonnée périmée envoie le
   lecteur ailleurs.

   ── LA RÈGLE DE MISE EN PAGE, en une phrase ──
   Un aplat ne se remplit pas, il se BORNE. Les deux panneaux portent un bloc
   dense, ancré en haut sur une MÊME ligne, et le mou se partage à ÉGALITÉ
   au-dessus et au-dessous de la composition — jamais entre deux blocs de
   contenu. Il n'existe donc aucun espace flexible interne : ni
   justify-content: space-between, ni gap sur les panneaux.

   Trois rangées de grille règlent le partage — un plancher souple en haut,
   le contenu au milieu, un plancher souple en bas — et les deux panneaux
   tiennent celle du MILIEU, qui se dimensionne sur le PLUS HAUT des deux
   contenus. Un centrage panneau par panneau y renoncerait : les deux
   contenus n'ont pas la même hauteur, et leurs hauts s'écarteraient.

   Conséquence à connaître : le violet ne peut plus venir de .cnx-recit seul,
   qui n'occupe que la rangée du milieu. Il est peint par .cnx::before, un
   élément de grille posé sur la colonne du récit et sur toutes les rangées.
   .cnx-recit garde la même couleur : deux surfaces bord à bord, aucune
   couture possible. */

.cnx {
    min-height: 100vh;
    min-height: 100dvh;
    display: grid;
    /* La colonne du formulaire est la plus large des deux : la tâche de la
       page est le formulaire, le récit l'accompagne. minmax(0, …) et non des
       parts seules : la colonne du récit n'a donc AUCUN plancher propre, et
       une frise trop longue passe à la ligne au lieu d'élargir la colonne.
       C'est la requête de conteneur de la frise, plus bas, qui décide de sa
       disposition — pas la largeur de la colonne qui s'adapte à la frise. */
    grid-template-columns: minmax(0, 46fr) minmax(0, 54fr);
    /* Le contenu dans la rangée du milieu : c'est ce qui partage le mou à
       égalité en haut et en bas SANS renoncer au haut commun des deux
       panneaux. */
    grid-template-rows: minmax(var(--cnx-haut), 1fr) auto minmax(var(--cnx-bas), 1fr);
    background: var(--bg);

    /* Le côté du DESSIN, pas de la pastille : la pastille l'entoure. Le
       verrou de marque recule par paliers quand la place manque, mais jamais
       sous le plancher écrit dans le palier le plus étroit — en dessous, la
       ligne du cahier cesse d'être une ligne et le dessin devient une tache. */
    --cnx-logo: 96px;
    --cnx-gouttiere: clamp(2rem, 3.8vw, 4.5rem);
    /* PLANCHERS de respiration, et non plus des rembourrages : au-dessus
       d'eux, les deux rangées souples se partagent le mou. C'est pourquoi le
       haut n'est plus proportionnel à la fenêtre — il n'a plus à l'être, et
       un haut fixe trop généreux DÉGRADE le résultat là où la composition
       est haute. */
    --cnx-haut: clamp(2.5rem, 6vh, 4rem);
    --cnx-bas: clamp(2.5rem, 5.6vh, 3.5rem);
    /* Filet décoratif, volontairement sous le seuil exigé d'un texte : il ne
       porte aucune information — elle est dans le libellé et dans l'icône,
       qui sont au-dessus du seuil. Sur cet aplat, aucun APLAT de séparation
       ne se détache assez pour tenir ce rôle ; un voile de blanc, si. */
    --cnx-filet: rgba(255, 255, 255, .30);
    /* L'aplat de marque, écrit UNE fois : le champ (.cnx::before) et le
       panneau (.cnx-recit) le partagent, ce qui évite d'écrire deux fois le
       même littéral pour la même surface. Littéral et non var(--violet) :
       tokens.css bascule --violet en thème sombre, et ce panneau est un champ
       de MARQUE, pas une surface thématisée. */
    --cnx-aplat: #6050DC;
}
/* Le champ de marque, peint par un pseudo-élément : la colonne du récit sur
   TOUTES les rangées, pendant que .cnx-recit ne tient que celle du milieu. Un
   pseudo-élément et non un <div> : login.php ne gagne pas un nœud. Il est le
   premier élément de grille, donc il peint SOUS le récit. */
.cnx::before {
    content: '';
    grid-column: 1;
    grid-row: 1 / -1;
    background: var(--cnx-aplat);
}
/* Placement EXPLICITE : le formulaire est premier dans le DOM (c'est la tâche
   de la page), le récit s'affiche pourtant en premier à l'écran. La grille
   suit le sens d'écriture : en dir="rtl" la première colonne est la plus à
   droite, les deux panneaux s'échangent sans une seule règle de plus. */
.cnx-recit { grid-column: 1; grid-row: 2; }
.cnx-form  { grid-column: 2; grid-row: 2; }

/* ── Panneau du récit : violet en APLAT, ni dégradé ni ombre portée ── */
.cnx-recit {
    background: var(--cnx-aplat);
    color: #fff;
    display: flex;
    flex-direction: column;
    /* NI justify-content NI gap : le bloc part du haut de SA RANGÉE, et
       c'est la grille — non un rembourrage — qui pose le mou de part et
       d'autre. Aucun padding-block ici : il s'ajouterait aux rangées et
       décalerait ce panneau par rapport à l'autre. */
    padding-inline: var(--cnx-gouttiere);
}

/* ── Le verrou de marque : pastille + mot-symbole à deux étages ── */
.cnx-logo {
    display: flex;
    align-items: center;
    gap: 20px;
    margin-block-end: 48px;
}
/* Le logo retrouve le fond blanc pour lequel il a été dessiné : ses quatre
   éléments restent lisibles, dont la ligne du cahier, qui est la partie la
   plus fragile du dessin. Posée à même le violet, une variante monochrome
   perdrait précisément cette ligne. */
.cnx-logo-chip {
    display: inline-flex;
    border-radius: var(--radius);
    background: #fff;
    /* Rembourrage OPTIQUE et non géométrique : l'encre du dessin n'est pas
       centrée dans son cadre, elle penche à droite. Le rembourrage compense
       ce biais, et il le compense proportionnellement à la taille du dessin,
       pour que la correction suive les paliers.
       PROPRIÉTÉS PHYSIQUES, délibérément, seule exception de ce bloc : ce
       décentrement appartient au DESSIN, pas au sens de lecture — il ne doit
       pas se retourner le jour où dir="rtl" arrivera. */
    padding: 16px calc(16px + var(--cnx-logo) / 16) 16px calc(16px - var(--cnx-logo) / 16);
}
/* .logo-mark impose sa propre taille et portail.php la consomme : on ne la
   touche pas, on la surclasse en descendance d'une classe qui n'existe
   qu'ici. */
.cnx-logo-chip .logo-mark {
    display: block;
    width: var(--cnx-logo);
    height: var(--cnx-logo);
}
/* Le mot-symbole se lit comme un BLOC, doctrine de la classe .sidebar-brand
   du menu latéral, où le nom et sa précision s'empilent. */
.cnx-logo-mot { display: block; }
.cnx-logo-nom {
    display: block;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 32px;
    line-height: 1.1;
    letter-spacing: -.01em;
    color: #fff;
}
.cnx-logo-suite {
    display: block;
    margin-block-start: 4px;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 12px;
    line-height: 1.4;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: #EEEBFB;
}

/* Le plafond qui bridait tout le récit d'une seule valeur disparaît : il
   descend sur chaque enfant, à la valeur que la mesure impose. */
.cnx-recit-corps {
    max-width: none;
    /* Conteneur de requête : le seuil de la frise se lit sur la largeur
       RÉELLE de la colonne, et non sur celle de la fenêtre. Le jour où le
       partage des deux colonnes bouge, le seuil suit seul. */
    container-type: inline-size;
    container-name: cnx-recit;
}
/* .eyebrow pose la couleur violette de la charte — invisible sur un fond
   violet. Le surtitre reprend donc le lilas du voile. */
.cnx-surtitre {
    margin: 0 0 12px;
    font-size: 12px;
    line-height: 1.6;
    color: #EEEBFB;
}
.cnx-titre {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: clamp(1.75rem, 3.2vw, 2.5rem);
    line-height: 1.14;
    letter-spacing: -.01em;
    color: #fff;
    margin: 0 0 24px;
    /* Une largeur en em, et non en pixels : les retours à la ligne réels ont
       été relevés en français ET en anglais, à plusieurs corps, et la fenêtre
       où les deux langues donnent le même découpage sans ligne orpheline est
       la même en unités relatives à tous les corps. La règle se maintient
       donc seule quand le clamp() fait varier le corps. */
    max-width: 13em;
}
.cnx-chapo {
    margin: 0 0 32px;
    font-size: 16px;
    line-height: 1.6;
    color: #EEEBFB;
    /* Corridor de lisibilité relevé dans les deux langues : cette valeur en
       tombe au centre. */
    max-width: 31em;
}

/* ── Les quatre piliers : une FRISE, pas une liste ──
   Filet séparateur et libellé, plutôt qu'une grille de cartes à icônes, qui
   ferait argumentaire là où on veut une preuve de périmètre. Le poids ne
   vient pas d'un effet : en deux rangées la frise remplit la colonne au pixel
   (sur une seule rangée elle se cale à gauche et laisse la fin de piste vide),
   le libellé passe au blanc quand le reste du texte secondaire garde le lilas —
   la hiérarchie plate est rompue — et l'icône cesse d'être exactement à la
   taille du texte. */
.cnx-piliers {
    list-style: none;
    margin: 0 0 32px;
    padding: 0;
    /* GRILLE et non flex-wrap. Avec flex, le quatrième pilier tombait SEUL
       sur une seconde rangée dès que la colonne se resserrait. Un nombre de
       colonnes EXPLICITE ne peut produire qu'une rangée pleine ou deux
       rangées égales : l'orpheline devient impossible, dans toute langue,
       présente ou future. */
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px 24px;
}
/* Une seule rangée quand la colonne tient les DEUX langues. Le seuil est pris
   sur l'ANGLAIS, la plus longue des deux, et non sur le français : une frise
   qui change de forme quand on change de langue serait un défaut de plus.
   Conséquence assumée — dans la bande où le français tenait sur une rangée et
   pas l'anglais, il se replie désormais comme lui.
   minmax(0, max-content) et non max-content seul : si une langue future
   dépassait, les pistes se resserrent et le libellé passe à la ligne, au
   lieu de déborder la colonne. */
@container cnx-recit (min-width: 700px) {
    .cnx-piliers { grid-template-columns: repeat(4, minmax(0, max-content)); }
}
.cnx-piliers li {
    display: flex;
    align-items: center;
    gap: 12px;
    padding-block: 8px;
    /* Le filet est porté par CHAQUE item, le premier compris : aucune règle
       :nth-child, et le passage à deux rangées reste propre. */
    border-inline-start: 1px solid var(--cnx-filet);
    padding-inline-start: 20px;
    font-size: 16px;
    font-weight: 600;
    line-height: 1.5;
    color: #fff;
}
/* Pas de pastille derrière l'icône : sur ce fond, aucun aplat de séparation
   ne se détache sans faire tomber le blanc du libellé sous le seuil. L'icône
   prend donc le lilas du voile. */
.cnx-piliers i {
    width: 22px;
    text-align: center;
    flex-shrink: 0;
    font-size: 18px;
    color: #EEEBFB;
}

.cnx-preuve {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin: 0;
    padding-block-start: 24px;
    border-block-start: 1px solid var(--cnx-filet);
    font-size: var(--fs-base);
    line-height: 1.55;
    color: #EEEBFB;
    /* Pleine colonne : cette ligne n'a pas de raison d'être plus étroite que
       la frise qu'elle referme. Selon la largeur et la langue elle tient d'un
       seul tenant ou se coupe ; text-wrap: balance ne supprime pas la coupure,
       il en équilibre les deux morceaux et supprime l'orpheline. */
    max-width: none;
    text-wrap: balance;
}

/* ── Panneau du formulaire ── */
.cnx-form {
    display: flex;
    flex-direction: column;
    align-items: center;
    /* NI justify-content: center : c'est lui qui produisait le vide au-dessus
       ET au-dessous de la carte, et qui empêchait tout repère partagé avec la
       colonne de gauche. Le centrage vertical est désormais l'affaire des
       trois rangées de .cnx, qui l'appliquent aux DEUX panneaux à la fois :
       la carte et la pastille du logo partagent la même rangée, donc la même
       ligne de départ. Aucun padding-block, pour la même raison qu'à gauche. */
    padding-inline: var(--cnx-gouttiere);
    background: var(--bg);
}
/* Recette de la classe .fenetre, le composant signature du site vitrine, et
   déjà celle que portail.php applique à la connexion des familles via
   .login-card. Honnêteté du dessin : le remplissage ne porte presque rien et
   la bordure porte peu — c'est --shadow qui dessine l'arête, par un halo
   violet sur fond lilas. Sur cette palette aucune carte blanche ne se détache
   par son remplissage ; c'est pourquoi la maison a écrit CETTE recette, et on
   n'en invente pas une autre. La carte s'élargit, mais le rembourrage la
   suit : la largeur de saisie ne bouge pratiquement pas. */
.cnx-form-boite {
    width: 100%;
    max-width: 500px;
    padding: 40px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
/* Le sélecteur de langue MONTE sur la ligne du h1 : au lieu de flotter dans
   une boîte sans fond, il s'accroche à l'arête droite du rembourrage de la
   carte et à l'axe médian du titre. align-items: center aligne par le CENTRE
   et NON par la ligne de base.
   Ce que ce déplacement change : l'ordre de LECTURE — le h1 précède désormais
   le sélecteur dans le DOM, alors qu'il le suivait.
   Ce qu'il ne change PAS : l'ordre de TABULATION. Le sélecteur reste le
   PREMIER élément focusable, et il doit le rester : le repousser dans le DOM
   le décrocherait de sa place à l'écran, ce qui est un défaut
   d'accessibilité et non une correction. */
.cnx-form-tete {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
}
/* ⚠️ LA NEUTRALISATION LOCALE DU SÉLECTEUR DE LANGUE A ÉTÉ RETIRÉE, et son
   retrait vaut d'être expliqué. Cinq règles vivaient ici pour corriger, SUR LA
   SEULE PAGE DE CONNEXION, les écarts à la charte d'un fragment partagé par
   SIX pages : sa pilule de 999 px, son ombre posée sur une commande, ses
   couleurs en dur. Le fragment servait aussi le tableau de bord, le portail
   des familles, la préinscription, la demande de démonstration et le paiement
   en ligne — qui, eux, gardaient le défaut intact. Un correctif qui ne suit
   pas sa cause corrige une page et en laisse cinq.
   Le fragment lit désormais assets/css/lang-switch.css, écrite à la charte et
   VUE par le contrôle. Il n'y a plus rien à neutraliser : ce qui reste ici est
   ce qui appartient VRAIMENT à cette page — le fond transparent, parce que le
   sélecteur s'y pose sur un aplat violet et non sur du blanc. */
.cnx-form-tete .lang-switch {
    background: transparent;
    border-color: var(--line);
}

.cnx-h1 {
    margin: 0;
    font-size: 1.75rem;
    line-height: 1.2;
}
.cnx-soustitre {
    margin: 8px 0 24px;
    font-size: var(--fs-md);
    line-height: 1.5;
    color: var(--ink-soft);
}

/* .alert ne porte AUCUNE marge propre : sans cette règle, une alerte colle au
   premier libellé du formulaire. Posée ici, et NON sur .alert, que portail.php
   consomme aussi. Sa place ne bouge pas : entre le sous-titre et le
   formulaire, c'est-à-dire là où l'utilisateur agit. */
.cnx-form-boite .alert { margin-bottom: 16px; }
.cnx-form-boite .form-group { margin-bottom: .9rem; }
.cnx-form .btn { width: 100%; }
/* Le bouton principal s'épaissit par une règle DESCENDANTE : .btn est partagé
   par toute la plateforme et n'est pas touché. */
.cnx-form .btn-primary { padding-block: 12px; }

/* Le copyright a quitté la carte : il ferme le PANNEAU, pas l'objet.
   Il n'existe aucune règle .login-foot nue dans cette feuille — seulement
   .login-card .login-foot, qui appartient à portail.php — donc sans cette
   règle-ci le pied perdrait tout son style. Sa largeur maximale est celle de
   la carte : les deux se calent sur la même arête. */
.cnx-form .login-foot {
    width: 100%;
    max-width: 500px;
    margin-block-start: 24px;
    text-align: center;
    font-size: var(--fs-sm);
    line-height: 1.5;
    color: var(--ink-soft);
}

/* Case « se souvenir » et lien d'oubli sur une même ligne : space-between
   suit le sens d'écriture, la ligne se retourne seule en RTL. */
.cnx-options {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: .6rem 1rem;
    margin: .25rem 0 1.15rem;
    font-size: .87rem;
}
/* La feuille vise ailleurs le sélecteur NU `label`, en gras et à l'encre
   sombre : la classe l'emporte en spécificité et ramène ce libellé au
   registre secondaire. */
.cnx-memoriser {
    display: inline-flex;
    align-items: flex-start;
    gap: .5rem;
    font-size: .87rem;
    font-weight: 500;
    color: var(--ink-soft);
    cursor: pointer;
}
.cnx-memoriser input { margin-top: .18rem; flex-shrink: 0; }
/* `.hint` n'est stylé QUE par `.form-group .hint`. Ce conseil-ci vit dans un
   <label>, hors form-group : sans taille ni graisse propres, il hériterait du
   libellé et le conseil pèserait autant que l'option qu'il commente. On
   reprend le corps de la règle du formulaire ; la couleur reste héritée. */
.cnx-memoriser .hint {
    display: block;
    margin-top: .12rem;
    font-size: .72rem;
    font-weight: 400;
}

.cnx-pied {
    margin: 24px 0 0;
    padding-block-start: 20px;
    border-block-start: 1px solid var(--line);
    text-align: center;
    font-size: .88rem;
    color: var(--ink-soft);
}
.cnx-pied a { font-weight: 600; }

/* ── Premier palier à deux panneaux : la colonne intérieure se resserre, le
   verrou de marque et le titre reculent d'un cran. La frise passe à deux
   rangées, elle ne disparaît pas. ── */
@media (min-width: 1078px) and (max-width: 1199px) {
    .cnx { --cnx-logo: 80px; }
    .cnx-titre { font-size: clamp(1.625rem, 2.9vw, 2.25rem); }
    .cnx-logo { margin-block-end: 40px; }
}

/* ── Sous le seuil : le récit se replie en BANDEAU, il ne disparaît pas.
   LE SEUIL EST CELUI DE LA CARTE, pas un chiffre rond : au-dessus de lui la
   carte tient sa largeur maximale, en dessous elle serait comprimée et de la
   largeur de saisie serait perdue. Il se déduit de la part de la colonne du
   formulaire et de sa gouttière — les deux sont écrites plus haut — et il a
   été vérifié au pixel dans les deux sens.

   grid-template-rows: auto 1fr est la correction du bandeau : sans lui les
   rangées s'étiraient, et le bandeau devenait un aplat à moitié vide.
   C'est aussi ce qui annule les trois rangées du haut de ce bloc : empilé, le
   récit n'a plus de panneau voisin avec qui partager une rangée.

   Le repli est INVERSÉ par rapport à la version précédente : ce sont le
   chapô, le surtitre et la ligne de preuve qui se retirent d'abord. Le
   discours cède, la preuve reste — tant qu'elle tient sur une rangée.

   RESSERREMENT, mesuré : empilée, la composition avait la même hauteur quelle
   que soit la fenêtre, et débordait de tout ce qui manquait. Le resserrement
   de ce palier — les marges ci-dessous comprises — la fait tenir sur les
   tailles de tablette et d'ordinateur, et réduit fortement le débordement sur
   les petits téléphones sans le supprimer : les modes qui portent une alerte
   défilent encore en dessous d'environ 375 px de large sur 700 de haut. Rien
   n'est gagné sur le corps du texte ni sur la hauteur des champs. ── */
@media (max-width: 1077px) {
    .cnx {
        grid-template-columns: 1fr;
        grid-template-rows: auto 1fr;
        --cnx-logo: 56px;
        --cnx-haut: 16px;
        --cnx-bas: 16px;
        --cnx-gouttiere: clamp(1.25rem, 5vw, 2rem);
    }
    /* Empilé, .cnx-recit tient la rangée entière et porte lui-même le
       violet ; laissé actif, le champ de marque couvrirait toute la page. */
    .cnx::before { display: none; }
    /* Hors des trois rangées, --cnx-haut et --cnx-bas redeviennent ce qu'ils
       étaient : le rembourrage du bandeau. */
    .cnx-recit { grid-column: 1; grid-row: 1; padding-block: var(--cnx-haut) var(--cnx-bas); }
    .cnx-form  { grid-column: 1; grid-row: 2; padding-block: 16px 24px; }
    .cnx-surtitre, .cnx-chapo, .cnx-preuve { display: none; }
    .cnx-logo { margin-block-end: 12px; gap: 16px; }
    .cnx-logo-nom { font-size: 24px; }
    .cnx-titre {
        font-size: clamp(20px, 4.4vw, 26px);
        margin: 0 0 12px;
        max-width: none;
    }
    /* La frise ne survit que tant qu'elle tient sur UNE rangée : la même
       requête de conteneur qui la met en ligne la fait exister. Repliée en
       deux rangées dans un bandeau, elle prend la hauteur que le formulaire
       réclame — et c'est le formulaire, la tâche de la page. */
    .cnx-piliers { display: none; margin: 0; gap: 10px 20px; }
    @container cnx-recit (min-width: 700px) { .cnx-piliers { display: grid; } }
    .cnx-piliers li { padding-block: 2px; padding-inline-start: 16px; font-size: 14px; gap: 10px; }
    .cnx-piliers i { font-size: 16px; width: 18px; }
    /* Le mou de la rangée du formulaire se partage au-dessus et au-dessous de
       la carte, comme il se partage entre les deux panneaux quand ils
       cohabitent. DEUX MARGES AUTOMATIQUES et non justify-content: center :
       quand la place manque, une marge automatique retombe à zéro, alors
       qu'un centrage ferait sortir le haut de la carte hors de la zone que
       l'utilisateur peut atteindre en défilant. */
    .cnx-form-boite { padding: 24px; margin-block-start: auto; }
    .cnx-form-boite .alert { margin-bottom: 10px; }
    .cnx-form-boite .form-group { margin-bottom: .75rem; }
    .cnx-soustitre { margin: 8px 0 16px; }
    .cnx-options { margin: .2rem 0 1rem; }
    .cnx-pied { margin-block-start: 14px; padding-block-start: 14px; }
    .cnx-form .login-foot { margin-block-start: 14px; margin-block-end: auto; }
}
/* ── Palier étroit : le titre du bandeau se retire, faute de largeur — c'est
   le comportement d'avant ce remaniement, conservé tel quel. La frise s'est
   déjà retirée plus haut : à cette largeur, sa requête de conteneur ne peut
   plus être satisfaite. ── */
@media (max-width: 560px) {
    .cnx {
        --cnx-logo: 48px;
        --cnx-haut: 12px;
        --cnx-bas: 12px;
    }
    .cnx-titre { display: none; }
    .cnx-logo { margin-block-end: 10px; gap: 14px; }
    .cnx-logo-nom { font-size: 22px; }
    .cnx-logo-suite { font-size: 11px; }     /* plancher de tokens.css */
    /* Le titre de la carte descend d'un cran pour rendre sa ligne au
       sélecteur de langue. Cela suffit pour les titres courts (connexion,
       session expirée, compte désactivé). ⚠️ Cela NE suffit PAS pour les deux
       titres longs — mot de passe oublié et nouveau mot de passe — où le
       sélecteur décroche encore sous le titre sur un téléphone : défaut connu,
       à reprendre avec le fragment lang_switch. */
    .cnx-h1 { font-size: 1.5rem; }
    .cnx-form { padding-block: 16px 20px; }
    .cnx-form-boite { padding: 20px; }
    .cnx-form-boite .form-group { margin-bottom: .7rem; }
    .cnx-soustitre { margin: 6px 0 12px; }
    .cnx-options { margin: .2rem 0 .8rem; }
    .cnx-pied { margin-block-start: 10px; padding-block-start: 10px; }
    .cnx-form .login-foot { margin-block-start: 10px; }
}

/* ── ÉCRANS COURTS ─────────────────────────────────────────────────────────
   CE BLOC EST PLACÉ APRÈS LES PALIERS DE LARGEUR, ET C'EST LÀ LE CORRECTIF.
   Écrit avant eux, il était intégralement NEUTRALISÉ dans la mise en page
   empilée : à spécificité égale, seule la position dans le fichier tranche.
   Le palier écrit pour les écrans courts ne protégeait rien exactement là où
   il aurait fallu.
   Il est désormais scindé en trois : ce qui vaut à toute largeur, ce qui ne
   vaut qu'en deux panneaux, ce qui ne vaut qu'empilé. ── */
@media (max-height: 760px) {
    .cnx { --cnx-haut: 16px; --cnx-bas: 16px; }
    /* Les deux modes à alerte sont les plus hauts de la page : l'un ajoute
       une ligne, l'autre en ajoute plusieurs. Le sursis vient d'ici — la
       place de l'alerte, elle, ne bouge pas. */
    .cnx-form-boite .alert { margin-bottom: 10px; }
}
/* Court ET large : les deux panneaux se partagent la largeur, la page a de la
   place, et seul le rythme VERTICAL du récit se resserre. La carte n'est pas
   touchée : à cette largeur la composition tient déjà, et la comprimer ne
   ferait qu'agrandir le blanc sous elle. */
@media (min-width: 1078px) and (max-height: 760px) {
    .cnx { --cnx-logo: 72px; }
    .cnx-titre { font-size: clamp(1.5rem, 2.4vw, 2rem); margin-block-end: 20px; }
    .cnx-logo { margin-block-end: 32px; }
    .cnx-logo-nom { font-size: 27px; }
}
/* Court ET empilé : le bandeau et le formulaire s'AJOUTENT au lieu de se
   partager la largeur — c'est le seul cas où la page manque vraiment de
   hauteur, et le seul où la carte se resserre. Le plafond de ce palier n'est
   pas un chiffre rond : c'est la hauteur relevée de la composition empilée
   dans son mode le plus haut, celui qui porte l'alerte la plus longue. Le
   verrou de marque descend à son plancher, jamais en dessous. */
@media (max-width: 1077px) and (max-height: 790px) {
    .cnx { --cnx-logo: 48px; --cnx-haut: 12px; --cnx-bas: 12px; }
    .cnx-form { padding-block: 16px 20px; }
    .cnx-form-boite { padding: 20px; }
    .cnx-form-boite .form-group { margin-bottom: .7rem; }
    .cnx-soustitre { margin: 6px 0 14px; }
    .cnx-options { margin: .2rem 0 .75rem; }
    .cnx-logo { margin-block-end: 10px; }
    /* Le titre du bandeau cède à son tour : au palier étroit il se retirait
       faute de largeur, ici il se retire faute de hauteur. Le verrou de
       marque reste — c'est lui qui dit où l'on est. */
    .cnx-titre { display: none; }
    .cnx-piliers { gap: 8px 16px; }
    .cnx-piliers li { padding-block: 2px; }
    .cnx-pied { margin-block-start: 10px; padding-block-start: 10px; }
    .cnx-form .login-foot { margin-block-start: 10px; }
}
