/* =========================================================================
   SWISS LINGUA - DESIGN SYSTEM v1
   Feuille de style partagée. Tous les cockpits l'importent :
       <link rel="stylesheet" href="design-system.css">
   Source unique de vérité visuelle (voir ../tokens.md).
   Premium · suisse · avant-gardiste. Un seul design system, cohérence totale.
   ========================================================================= */

/* -------------------------------------------------------------------------
   Polices
   - Inter Variable (UI, corps, champs), servie DEPUIS NOS SERVEURS.
   - Bembo (serif éditorial) en local. ⚠️ Regular SEULEMENT : le navigateur
     synthétise gras/italique en proto. Prod = Bembo complète (Monotype) OU
     substitut open (ET Book / EB Garamond). Voir ../tokens.md.

   Inter arrivait jusqu'ici par
   `@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700")`.
   Trois raisons de l'avoir retiré, dans l'ordre d'importance :

   1. CHAQUE VISITEUR ANNONÇAIT SON ADRESSE À UN TIERS. Le principe directeur
      n°6 du projet est la conformité nLPD native, et la vitrine avait déjà
      tranché dans l'autre sens (voir le commentaire de `apps/vitrine/src/
      styles/global.css`) : les cockpits étaient seuls à exposer leurs
      utilisateurs, et ce sont eux qui portent les données de l'école.
   2. QUATRE FICHIERS STATIQUES NE PORTENT AUCUN AXE. `font-optical-sizing:
      auto` était déclaré plus bas et ne faisait rien : un fichier statique n'a
      pas d'axe `opsz`. Idem pour toute graisse intermédiaire : 450, la seule
      qui tienne un corps de 13 px sans paraître maigre, était inatteignable.
   3. UN `@import` DISTANT BLOQUE LE RENDU le temps d'une résolution DNS, d'une
      poignée de main TLS et de deux allers-retours, sur un domaine que nous ne
      maîtrisons pas.

   Le fichier est le sous-ensemble latin d'Inter Variable 4.1 (axes `opsz`
   14→32 et `wght` 100→900), 71 Ko, distribué sous SIL Open Font License 1.1
   (texte dans `INTER-LICENSE.txt`). Latin seul, et non latin-ext : le
   français, l'allemand et l'espagnol y tiennent en entier, et le chinois
   n'était de toute façon couvert par aucune des variantes. Les langues hors
   couverture retombent sur la pile système, ce qui est le comportement voulu.
   ------------------------------------------------------------------------- */
@font-face {
  font-family: "Inter Variable";
  src: url("/fonts/inter-variable-latin.woff2") format("woff2");
  font-weight: 100 900; /* plage variable, PAS une valeur */
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Bembo";
  /* Servie depuis public/fonts de CHAQUE cockpit. Le chemin precedent
     (../fonts/bembo-src/) ne pointait sur rien : aucune app n'a de dossier
     fonts a la racine, la requete tombait en 404 et tous les titres, tous les
     chiffres passaient au serif generique du systeme. Verifie au navigateur :
     document.fonts.check('16px Bembo') renvoyait false dans les 4 cockpits. */
  src: url("/fonts/bembo-regular.woff2") format("woff2"), url("/fonts/bembo-regular.woff")
    format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* =========================================================================
   TOKENS (variables CSS) - repris de ../tokens.md
   ========================================================================= */
:root {
  /* ── Couleurs de marque ─────────────────────────────────────────────── */
  --ink: #14140f; /* Texte principal                            */
  --navy: #2a317d; /* Primaire : titres, boutons, liens, nav     */
  --navy-700: #222a6e; /* Marine au survol                           */
  --coral: #e84647; /* Accent fort : CTA, emphase, badge live     */
  --coral-700: #c73132; /* Corail au survol                           */
  --coral-800: #ab2829; /* Corail au survol d'un bouton plein         */
  --salmon: #f6bcb3; /* Accent doux, tints                         */
  --salmon-soft: #fceae5; /* Tint corail très léger                     */
  --ivory: #fff7ee; /* Surface chaude principale (fond de page)   */
  --greige: #f0ede6; /* Surface secondaire                         */
  --surface: #ffffff; /* Cartes                                     */
  --on-dark: #ffffff; /* Texte/éléments sur fond foncé (navy, corail)*/
  /* Muet assombri pour viser AA texte normal sur les DEUX surfaces :
     ≈ 4.7:1 sur blanc, ≈ 4.4:1 sur ivoire (vs 4.2/3.9 avant). WCAG 2.2 AA. */
  --muted: #5c5c56; /* Texte secondaire                           */
  /* Encre adoucie : entre l'encre pleine et le muet, pour un texte qui reste
     de la lecture (statut de presence, quantieme) sans peser comme un titre.
     Employee trois fois SANS etre definie : la declaration etait invalide et
     la couleur ne rendait rien, en silence. C'est la garde des tokens de la
     tranche des etats qui l'a attrape. 10,6:1 sur blanc, 10,0:1 sur ivoire. */
  --ink-soft: #3f3f38;
  --line: #e7e2d8; /* Bordures, séparateurs                      */
  --line-strong: #d8d2c5; /* Bordures appuyées                          */
  /* Bordure des CHAMPS de saisie : sur une carte blanche, elle est le seul
     indice qui délimite le contrôle, donc WCAG 2.2 AA (1.4.11) exige 3:1.
     #8f8878 mesure 3.52:1 sur blanc, 3.32:1 sur ivoire, 3.01:1 sur greige.
     Les bordures décoratives (cartes, séparateurs) gardent la teinte douce. */
  --field-border: #8f8878; /* Bordure de champ, contraste AA             */

  /* ── Couleurs de LIEU du calendrier ─────────────────────────────── */
  /* Six teintes separees en TEINTE (bleu, rouge, vert, ocre, violet, sarcelle),
     attribuees par le RANG du lieu dans le referentiel, jamais par sa cle :
     « fribourg » et « estavayer » sont des cles Swiss Lingua, et la plateforme
     est white-label. Six et non quatre : les quatre lieux d'aujourd'hui plus la
     marge, sans ouvrir un choix de couleur libre que rien ne mesurerait.

     Le lisere d'une entree de calendrier porte de l'information (le lieu), donc
     WCAG 2.2 critere 1.4.11 exige 3:1. Chacune tient au moins 4.59:1 sur greige
     ET sur warning-soft, les deux fonds qu'une entree peut prendre : mesure par
     verify-contraste-socle.mjs, dans les deux regimes.

     La couleur ne dit JAMAIS le lieu a elle seule : il est aussi ecrit dans
     l'entree et dans son nom accessible. */
  --calendar-loc-1: #2a317d; /* marine,   9.79:1 sur greige                */
  --calendar-loc-2: #c73132; /* corail,   4.59:1                          */
  --calendar-loc-3: #27684c; /* vert,     5.66:1                          */
  --calendar-loc-4: #8a5a12; /* ocre,     5.06:1                          */
  --calendar-loc-5: #6b3fa0; /* violet,   6.32:1                          */
  --calendar-loc-6: #0f6b6b; /* sarcelle, 5.38:1                          */

  /* ── États ──────────────────────────────────────────────────────────── */
  /* Teintes de TEXTE assombries pour viser ≥ 4.5:1 sur leur fond -soft
     (petit texte 12px des badges). Voir revue contraste WCAG 2.2 AA. */
  --success: #27684c;
  --success-soft: #e3f1ea; /* 5.5:1 sur soft */
  --warning: #8a5a12;
  --warning-soft: #fbefd9; /* 5.6:1 sur soft */
  /* Le danger doit se distinguer de la MARQUE, et il ne le faisait pas :
     `--danger` valait `#c73132`, c'est-à-dire exactement `--coral-700`, et
     `--danger-soft` valait `#fceae5`, exactement `--salmon-soft`. Le bouton
     « Créer un client » et le bouton « Supprimer » étaient donc de la même
     couleur, sur les deux tons. Tant que c'est vrai, personne ne peut
     apprendre ce que le rouge veut dire dans cette interface.
     #b3282a mesure 6,45:1 sur blanc, 6,23:1 sur le blanc froid de l'outil,
     6,08:1 sur l'ivoire et 5,51:1 sur son propre fond doux. */
  --danger: #b3282a;
  --danger-soft: #fbe9e9;
  --info: #2a317d;
  --info-soft: #e8e9f3;

  /* Un bouton désactivé était rendu en `--salmon`, c'est-à-dire l'accent de
     marque affaibli : il se lisait comme un bouton actif d'une autre couleur.
     Le désactivé n'est pas une couleur de marque, c'est une absence. */
  --disabled-bg: #f2f2f0;
  --disabled-fg: #767670;

  /* ── Typographie ────────────────────────────────────────────────────── */
  /* Decision cliente du 17.08 : Bembo juge ILLISIBLE, les titres passent en
     Inter. Le token garde son nom et le @font-face reste charge : revenir a
     la serif redevient une ligne si la direction artistique change. */
  --font-serif: "Inter Variable", Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-sans: "Inter Variable", Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", monospace;

  --text-display: 3.5rem;
  --text-h1: 2.5rem;
  --text-h2: 1.875rem;
  --text-h3: 1.375rem;
  --text-h4: 1.125rem;
  --text-lead: 1.25rem;
  --text-body: 1rem;
  --text-sm: 0.875rem;
  --text-xs: 0.75rem;
  --text-2xs: 0.6875rem; /* 11px : libellés ultra-denses (scale CECRL, tabbar) */
  --text-stat: 1.75rem; /* KPI compact (cartes résumé), entre h3 et kpi */
  /* Valeur d'indicateur. Descendue de 2.25rem a 1.875rem apres retour de
     Patrick : a 36px, un chiffre a cinq caracteres ecrasait sa tuile et le
     tableau de bord ressemblait a une vitrine de chiffres. La graisse de la
     serif suffit a porter la hierarchie. */
  --text-kpi: 1.875rem;

  /* Graisse du corps. 450 n'existe QUE depuis le passage à la fonte variable :
     à 13 px sur fond clair, un 400 pur paraît maigre, un 500 paraît appuyé. */
  --weight-body: 400;
  --weight-strong: 600;

  --tracking-tight: -0.02em;
  --tracking-normal: 0;
  --tracking-wide: 0.08em;

  /* ── Espace (4 / 8 pt) ──────────────────────────────────────────────── */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;
  --space-10: 8rem;

  /* ── Rayons ─────────────────────────────────────────────────────────── */
  --radius-sm: 0.375rem;
  --radius-md: 0.625rem;
  --radius-lg: 1rem;
  --radius-xl: 1.5rem;
  --radius-pill: 999px;

  /* ── Ombres (chaudes, teintées encre, à étalement négatif) ──────────── */
  /* L'étalement négatif vient de la vitrine, dont l'audit a relevé qu'elle
     avait les meilleures ombres du produit : le flou déborde moins que la
     boîte, donc l'ombre se lit comme une élévation et non comme un halo. */
  --shadow-xs: 0 1px 2px -1px rgba(20, 20, 15, 0.06);
  --shadow-sm: 0 4px 12px -4px rgba(20, 20, 15, 0.08);
  --shadow-md: 0 8px 24px -8px rgba(20, 20, 15, 0.1);
  --shadow-lg: 0 18px 44px -18px rgba(20, 20, 15, 0.14);
  --shadow-focus: 0 0 0 3px rgba(42, 49, 125, 0.35);

  /* ── Mouvement ──────────────────────────────────────────────────────── */
  --dur-fast: 120ms;
  --dur-base: 240ms;
  --dur-slow: 480ms;
  --dur-counter: 1200ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* ── Layout ─────────────────────────────────────────────────────────── */
  --container: 1200px;
  --sidebar-w: 264px;
  --topbar-h: 64px;
  --tabbar-h: 56px;
}

/* =========================================================================
   RÉGIME DE DENSITÉ
   =========================================================================
   Les six surfaces partagent cette feuille, et elles ont été dessinées avec
   la MÊME densité alors qu'elles ne se consultent pas au même rythme. Le
   secrétariat passe la journée dans le bureau et y voyait SIX lignes de
   clients dans un écran de 1440 x 900 ; un élève ouvre son espace deux
   minutes par semaine. Les deux avaient le même corps de 16 px et la même
   marge intérieure de 32 px.

   D'où deux régimes, un seul jeu de noms. Décision du 13.08 (retour de
   Patrick) : LES CINQ COCKPITS posent le régime OUTIL, via
   `data-densite="outil"` sur <html> de leur entry-server. L'écart d'échelle
   entre le bureau et les espaces rendait le produit incohérent d'un onglet à
   l'autre ; l'uniformité l'emporte, avec un corps monté à 14 px pour rester
   confortable en lecture. Le régime de LECTURE reste la valeur par défaut de
   la feuille : il ne sert plus qu'aux pages hors cockpit (l'écran de
   connexion, servi avant que l'attribut soit posé) et il est l'échelle de
   REPLI si un document oublie l'attribut : un oubli rend l'ancienne échelle,
   jamais un écran cassé.

   Le contrat d'empreinte des cinq copies est donc préservé : une seule
   feuille, deux régimes, chaque app choisit le sien. Une valeur par app dans
   la feuille aurait au contraire cassé la garde `verify-design-system.mjs`.

   Sept tailles, pas plus, et un interligne par taille. L'audit du 11.08 en a
   mesuré de 7 à 10 par écran au bureau, avec 14 à 18 interlignes distincts et
   deux valeurs hors échelle nées de cascades en `em` (16,8 px et 26,2388 px).
   ========================================================================= */
:root[data-densite="outil"] {
  --text-display: 1.75rem; /* 28px, réservé aux rares écrans de titre     */
  --text-h1: 1.25rem; /* 20px, titre de page                        */
  --text-h2: 1.25rem; /* 20px, même rang : un écran a UN titre      */
  --text-h3: 1rem; /* 16px, titre de section                     */
  --text-h4: 1rem; /* 16px                                       */
  --text-lead: 1rem; /* 16px                                       */
  /* Le texte monte d'un pixel sur toute la gamme (retour de Patrick du 13.08 :
     le corps de 13 restait un cran trop petit pour la journée). La hiérarchie
     ne bouge pas, chaque rang garde son écart ; la densité des tableaux non
     plus, la ligne de 44 px étant une hauteur IMPOSÉE, pas dérivée du corps. */
  --text-body: 0.875rem; /* 14px, le corps de l'outil                  */
  --text-sm: 0.8125rem; /* 13px, information secondaire               */
  --text-xs: 0.75rem; /* 12px, micro-libellé                        */
  --text-2xs: 0.75rem; /* 12px                                       */
  --text-stat: 1.125rem; /* 18px, valeur chiffrée                      */
  --text-kpi: 1.125rem; /* 18px, même rang que stat                   */

  /* 450 tient un corps de 14 px là où 400 paraît maigre. Impossible avant la
     fonte variable : les quatre fichiers statiques n'offraient que 400, 500,
     600 et 700. */
  --weight-body: 450;

  --lh-tight: 1.25; /* titres et valeurs                          */
  --lh-body: 1.4; /* 14px -> 19,6px                             */

  /* ── Surfaces de travail : le régime outil passe au plat ──────────────
     Décision de direction du 12.08. Le crème `--ivory` reste la couleur de la
     MARQUE : la vitrine et les espaces de lecture le gardent. Les surfaces où
     l'on travaille huit heures par jour passent à un blanc chaud très doux
     (#faf9f5), filets nets, sans ombre. Décision du 13.08 : le crème plein
     (#fff7ee) était trop fort pour huit heures de travail, le blanc froid
     (#fbfbfa) trop dur ; le fond de l'outil s'approche du blanc en gardant la
     chaleur de la marque. Le crème plein reste à la vitrine et aux espaces de
     lecture, qui composent blanc, crème et marine.

     Ce n'est pas un goût, c'est une conséquence de ce que ces écrans font. Une
     ombre porte une hiérarchie de profondeur ; un tableau de gestion n'en a
     pas besoin, il a besoin de lignes qui se distinguent et de statuts qui se
     lisent d'un coup d'oeil. Les cartes empilées avec ombre et rayon de 16 px
     ajoutaient du relief là où il n'y a que de la donnée à aligner. */
  --page-bg: #faf9f5;
  --surface: #ffffff;
  --surface-alt: #f6f5f1;
  --line: #e6e6e2;
  --line-strong: #d5d5cf;
  --radius-sm: 0.25rem; /* 4px  */
  --radius-md: 0.375rem; /* 6px  */
  --radius-lg: 0.5rem; /* 8px, au lieu de 16 */
  --radius-xl: 0.75rem; /* 12px, au lieu de 24 */
  --shadow-xs: none;
  --shadow-sm: none;
  --shadow-md: 0 4px 12px -6px rgba(20, 20, 15, 0.12);
  --shadow-lg: 0 12px 32px -14px rgba(20, 20, 15, 0.16);
}

/* =========================================================================
   RESET LÉGER
   ========================================================================= */
*,
*::before,
*::after {
  box-sizing: border-box;
}
* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;

  /* Devient enfin effectif : un fichier statique n'a pas d'axe `opsz`, la
     déclaration ne faisait rien avant le passage à la fonte variable. */
  font-optical-sizing: auto;

  /* Trois variantes de caractère d'Inter, choisies pour la LISIBILITÉ d'un
     outil de gestion, pas pour l'allure :
     - cv05 donne une queue au « l » minuscule. Sans elle, « l », « I » et
       « 1 » partagent la même hampe nue : dans « Illégal », dans un montant
       comme « 1'869.00 » et dans un numéro de facture, on devine au lieu de
       lire.
     - cv08 désambiguïse d'autres formes proches de la même façon.
     - ss03 corrige le dessin des apostrophes et des guillemets, ce qui compte
       en français où l'apostrophe est partout, y compris dans le séparateur
       de milliers suisse (1'869). */
  font-feature-settings: "cv05" 1, "cv08" 1, "ss03" 1;
}

body {
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--weight-body);
  line-height: 1.6;
  color: var(--ink);
  background: var(--page-bg, var(--ivory)); /* crème en lecture, blanc chaud doux en outil */
  /* `-webkit-font-smoothing: antialiased` amincit les traits : utile sur les
     surfaces sombres (la barre latérale marine, voir plus bas), néfaste sur un
     fond clair où il fatigue à la lecture prolongée. Il vivait ici, donc il
     s'appliquait à tout le texte de l'application. */
  -moz-osx-font-smoothing: auto;
  text-rendering: optimizeLegibility;
}

/* Le lissage aminci reste là où il sert : texte clair sur fond foncé. */
.sidebar,
.topbar--dark,
[data-surface="dark"] {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Chiffres à chasse fixe partout où ils s'empilent ou se comparent. Mesuré
   avant : « 111000 » occupait 124,52 px en proportionnel contre 155,63 px en
   tabulaire, soit 25 % d'écart, si bien que les 240 montants d'une colonne de
   `/factures` ne tombaient pas les uns sous les autres. La fonte embarquait
   déjà `tnum` ; `tbody td` était en `font-variant-numeric: normal`. */
td,
th,
time,
.amount,
.kpi__value,
[data-numeric] {
  font-variant-numeric: tabular-nums;
}

/* Un montant ne se coupe jamais entre le nombre et sa devise. */
.amount,
.kpi__value {
  white-space: nowrap;
}

/* La serif de marque a trois emplois, et pas un de plus : le titre de page, les
   valeurs chiffrées, les titres éditoriaux de la vitrine. Elle était mesurée à
   2 % des noeuds de texte de `/clients` (6 contre 257 pour Inter) : trop peu
   pour faire système, assez pour faire accident. Sur une colonne de montants,
   au contraire, elle devient une aide à la lecture : une valeur en serif dans
   un tableau en sans-serif se repère sans avoir à lire. */
.amount {
  font-family: var(--font-serif);
  /* Compensation de hauteur, mesurée et non choisie : à corps égal, la hauteur
     des chiffres de Bembo vaut 64 unités contre 74 pour Inter (relevé au canvas
     sur les deux fontes servies, `actualBoundingBoxAscent` du glyphe « 0 » à
     100 px). Sans compensation, un montant paraît d'un cran plus petit que le
     libellé de sa propre ligne, ce qui est l'inverse de l'effet voulu : dans un
     tableau de gestion, le montant est l'information qu'on cherche. 74/64 vaut
     1,156 ; on retient 1,15. */
  font-size: 1.15em;
}

/* Le corps de l'outil respire moins que celui d'un texte suivi : 1.6 sur 13 px
   donne 20,8 px de pas, soit près de deux fois la hauteur des capitales pour
   une ligne qui ne dépasse jamais sa cellule. Le régime de lecture, lui, garde
   son 1.6. */
:root[data-densite="outil"] body {
  line-height: var(--lh-body);
}

img,
svg,
video,
canvas {
  display: block;
  max-width: 100%;
}
button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}
ul,
ol {
  list-style: none;
  padding: 0;
}
a {
  color: var(--navy);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
a:hover {
  color: var(--navy-700);
}
a:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
  border-radius: var(--radius-sm);
}

/* =========================================================================
   TYPOGRAPHIE DE BASE
   Titres en Bembo (chaleur éditoriale), corps en Inter.
   ========================================================================= */
h1,
h2,
h3,
h4 {
  font-family: var(--font-serif);
  color: var(--navy);
  font-weight: 400; /* Bembo Regular ; le gras est synthétisé */
  line-height: 1.15;
  letter-spacing: var(--tracking-tight);
}
h1 {
  font-size: var(--text-h1);
  line-height: 1.1;
}
h2 {
  font-size: var(--text-h2);
}
h3 {
  font-size: var(--text-h3);
  line-height: 1.25;
}
h4 {
  font-size: var(--text-h4);
  line-height: 1.3;
}

p {
  max-width: 68ch;
}

small,
.text-xs {
  font-size: var(--text-xs);
}
.text-sm {
  font-size: var(--text-sm);
}
.lead {
  font-size: var(--text-lead);
  color: var(--ink);
  line-height: 1.5;
}
.muted {
  color: var(--muted);
}

/* Titre d'affichage vitrine : Inter Bold majuscules (style hero marque) */
.display {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: var(--text-display);
  line-height: 1.05;
  letter-spacing: var(--tracking-tight);
  text-transform: uppercase;
  color: var(--navy);
}

/* Sur-titre / label de section.
   Corail pur en petit texte échoue l'AA (3.67:1 sur ivoire) : on prend le
   marine (10.8:1). Le corail reste pour les gros titres, icônes et fonds
   pleins (brief). Variante .eyebrow--coral disponible si fond plein. */
/* Micro-titre en capitales espacees. Deux emplois, un seul rendu :
   - en <p>, c'est un SUR-TITRE, pose au-dessus du titre reel ;
   - en <h3>/<h4>, c'est le TITRE d'une sous-section, quand le dessin ne veut
     pas d'un titre de carte plein.
   La balise porte le role, la classe porte l'apparence : ne jamais choisir la
   balise pour son look. */
.eyebrow {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--navy);
  /* Un sur-titre ANNONCE ce qui suit : il lui faut de l'air, sinon il se lit
     comme la première ligne du contenu. Il n'en avait aucun (mesuré à 0 pixel
     le 12.08 sur « Planifier une séance »), parce que le reset du socle retire
     les marges des titres et que rien ne les rendait ici. */
  margin-block-end: var(--space-3);
}
/* Dernier de son conteneur, il n'annonce rien : pas de marge à laisser. */
.eyebrow:last-child {
  margin-block-end: 0;
}

/* Variante corail réservée aux fonds pleins sombres (jamais sur clair). */
.eyebrow--coral {
  color: var(--coral);
}

/* Sur-titre sur fond foncé : saumon clair (≈ 7:1 sur navy), lisible et chaud. */
.eyebrow--on-dark {
  color: var(--salmon);
}

/* Mot d'emphase au fil du texte : Bembo corail, DROIT (pas d'italique).
   L'accent se fait par la couleur, pas par l'italique.
   ⚠️ Sur fond clair uniquement. Sur fond foncé, utiliser .emphasis--on-dark. */
.emphasis {
  font-family: var(--font-serif);
  color: var(--coral);
}
/* Variante italique : réservée aux NOMS et VALEURS DYNAMIQUES (langue d'un
   apprenant, nom de personne, ville…). Ne pas utiliser pour de l'emphase
   purement stylistique. NB : Bembo Regular seul → italique synthétisé en proto. */
.emphasis--dynamic {
  font-style: italic;
}
/* Emphase sur fond foncé : corail pur sur navy ≈ 2:1 (illisible). On passe
   au saumon clair (≈ 7:1 sur navy), cohérent avec .eyebrow--on-dark. */
.emphasis--on-dark {
  color: var(--salmon);
}

/* Utilitaire texte corail foncé (AA sur fond clair, ≈ 5.3:1 sur blanc) */
.text-coral-700 {
  color: var(--coral-700);
}

/* Utilitaire texte vert (AA sur fond clair, ≈ 6.4:1 sur blanc). Pendant de
   .text-coral-700 pour les variations positives : la couleur ACCOMPAGNE le
   signe, elle ne le remplace jamais. */
.text-success {
  color: var(--success);
}

/* Séparateur horizontal doux (remplace les <hr> inline) */
.divider {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--space-5) 0;
}

/* =========================================================================
   CARTE
   ========================================================================= */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: var(--shadow-sm);
  /* Les ecrans posent leurs cartes dans une grille de page. Un grid item vaut
     min-content par defaut : un tableau large poussait alors sa carte au-dela
     de la coquille (mesure : carte a 1124 px dans un contenu a 644). Avec
     min-width 0, la carte suit sa colonne et le tableau defile dans son
     ScrollX, ce pour quoi il est fait. */
  min-width: 0;
  transition: box-shadow var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.card--hover:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}
.card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
}
.card__title {
  font-size: var(--text-h3);
  margin: 0;
}
/* SOUS-TITRE DE SECTION. Un écran qui empile huit sections leur donnait huit
   titres de 22 pixels, exactement le rang du titre de l'écran : rien ne
   hiérarchisait plus rien, et l'oeil n'avait aucun point d'entrée (audit du
   11.08). Le sous-titre descend d'un cran, franchement, pour que le titre de
   l'écran reste le seul à porter ce rang. */
.card__title--sub {
  font-size: var(--text-h4);
}

/* =========================================================================
   SECTION REPLIÉE (<details>) - LES TÂCHES QUI NE SONT PAS CELLE DU MOMENT
   =========================================================================
   Ce patron ne remplace ni une page ni une modale, et n'ouvre pas de volet en
   place (règle du 06.08) : ce qu'il replie, ce sont des sections SECONDAIRES
   d'un écran dont une seule est la tâche du moment. La saisie, elle, reste où
   elle est, en modale ou sur sa page.

   Le triangle natif est conservé : c'est l'affordance la mieux comprise et la
   seule qui marche sans JavaScript, au clavier comme au doigt. Le titre reste
   un titre (<h3> dans le <summary>, ce que la spécification HTML autorise
   explicitement), pour que la navigation par titres d'un lecteur d'écran
   continue de traverser l'écran. */
.fold {
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.fold__summary {
  /* 44 pixels : on déplie une section au doigt, debout, comme le reste. */
  min-height: 44px;
  display: list-item;
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  color: var(--ink);
}
.fold__summary:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
  border-radius: var(--radius-md);
}
/* En ligne, et non en bloc : un titre en bloc renverrait le triangle natif sur
   sa propre ligne, et la cible du dépliage cesserait de couvrir le titre. */
.fold__title {
  display: inline;
  font-size: var(--text-h4);
  margin: 0;
}
.fold__state {
  font-size: var(--text-sm);
  color: var(--ink-soft);
}
.fold__body {
  padding: 0 var(--space-4) var(--space-4);
}

/* =========================================================================
   KPI - label + grand chiffre Bembo
   ========================================================================= */
.kpi {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  /* Contexte de dimensionnement pour le chiffre (voir .kpi__value). */
  container-type: inline-size;
}
.kpi__label {
  font-family: var(--font-sans);
  /* Remonté de --text-xs : en capitales et en gris, 12px était le maillon
     faible de la tuile, le libellé se lisait moins bien que le chiffre. */
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
  text-wrap: balance;
  /* Un libelle plus long que sa colonne passait par-dessus le bord de la
     carte : releve du 11.08, « Moyenne hebdomadaire de lecons » depassait de 52
     pixels a 375, « Questionnaires remplis » de 63. Rien ne le coupait ni ne le
     renvoyait a la ligne, et le texte se lisait sur le fond de la page. */
  min-width: 0;
  overflow-wrap: break-word;
}
.kpi__value {
  font-family: var(--font-serif);
  /* Le chiffre se cale sur la largeur DE SA TUILE (cqi), pas sur la fenêtre :
     dans une grid--4 une valeur longue se réduit au lieu de casser sur trois
     lignes. Le plafond reste --text-kpi, le plancher garde la valeur lisible.
     Repli --text-stat pour les navigateurs sans requêtes de conteneur. */
  font-size: var(--text-stat);
  font-size: clamp(1.625rem, 12cqi, var(--text-kpi));
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  /* Plus de break-word : un nombre ne doit jamais se couper au milieu. Les
     séparateurs produits par shared/format.ts sont insécables, donc un montant
     reste d'un bloc ; seule une VALEUR TEXTUELLE (« Aucun score ») peut encore
     passer à la ligne, entre deux mots. */
  overflow-wrap: normal;
}
.kpi__value--navy {
  color: var(--navy);
}
.kpi__unit {
  font-size: 0.5em;
  color: var(--muted);
  margin-left: var(--space-1);
}
.kpi__delta {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}
.kpi__delta--up {
  color: var(--success);
}
.kpi__delta--down {
  color: var(--danger);
}

/* Badge de fraîcheur neutre (« Synchronisé », « Mis à jour il y a 16 s ») */
.badge-fresh {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--muted);
}
.badge-fresh::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--success);
}

/* =========================================================================
   BADGE DE STATUT
   ========================================================================= */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  white-space: nowrap;
  background: var(--greige);
  color: var(--ink);
  /* `white-space: nowrap` garde la pastille d'un seul tenant, ce qui est voulu :
     « En attente » coupé sur deux lignes serait illisible. Mais un libellé long
     poussait alors la page hors de l'écran au format téléphone, mesuré au banc
     le 10.08 sur la séance du professeur (« Vague de feedback · Début » à 391
     pixels pour 375 d'écran). Une pastille ne doit pas pouvoir élargir le
     document : sous le seuil, elle se laisse rogner par des points de suspension
     plutôt que de tout emporter. Le texte complet reste dans le document, donc
     lisible par un lecteur d'écran. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.badge--success {
  background: var(--success-soft);
  color: var(--success);
}
.badge--warning {
  background: var(--warning-soft);
  color: var(--warning);
}
.badge--danger {
  background: var(--danger-soft);
  color: var(--danger);
}
.badge--info {
  background: var(--info-soft);
  color: var(--info);
}
.badge--navy {
  background: var(--navy);
  color: var(--on-dark);
}
/* Corail plein : fond coral-700 (#C73132) + texte blanc gras → ~5.0:1 (AA).
   Le coral pur #E84647 en fond de badge échoue (3.89:1 avec blanc, 12px). */
.badge--coral {
  background: var(--coral-700);
  color: var(--on-dark);
  font-weight: 700;
}

/* Statuts en couleur PLEINE dans le régime outil.
   Sur fond doux, « Payée » et « Émise » sont deux pastilles pâles que rien ne
   sépare à distance de lecture, alors qu'elles disent l'inverse l'une de
   l'autre : l'une close l'affaire, l'autre annonce qu'il reste à encaisser.
   L'audit du 11.08 l'a relevé sur `/factures`, où 89 lignes sur 240 sont au
   statut « Émise » et doivent se repérer à l'oeil.
   Contrastes du texte blanc, vérifiés : succès 6,62:1, avertissement 5,91:1,
   danger 6,45:1, information 11,45:1. Le rayon passe de la pilule au coin
   court : une pastille pleine et arrondie fait bouton, et invite au clic. */
:root[data-densite="outil"] .badge--success {
  background: var(--success);
  color: var(--on-dark);
  border-radius: var(--radius-sm);
}
:root[data-densite="outil"] .badge--warning {
  background: var(--warning);
  color: var(--on-dark);
  border-radius: var(--radius-sm);
}
:root[data-densite="outil"] .badge--danger {
  background: var(--danger);
  color: var(--on-dark);
  border-radius: var(--radius-sm);
}
:root[data-densite="outil"] .badge--info {
  background: var(--info);
  color: var(--on-dark);
  border-radius: var(--radius-sm);
}
:root[data-densite="outil"] .badge--navy,
:root[data-densite="outil"] .badge--coral {
  border-radius: var(--radius-sm);
}

/* =========================================================================
   BOUTONS
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  white-space: nowrap; /* un bouton ne casse jamais son libellé */
  transition:
    background var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
  user-select: none;
}
.btn:active {
  transform: translateY(1px);
}
.btn:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
/* Un bouton désactivé cesse d'être une version pâle de lui-même. `opacity: .5`
   sur un bouton marine plein donne un bleu délavé qui se lit comme un bouton
   actif d'une autre couleur, et sur un bouton corail, comme un rose de marque.
   Un désactivé se reconnaît à ce qu'il n'a plus de couleur du tout. */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--disabled-bg);
  color: var(--disabled-fg);
  border-color: var(--line);
  box-shadow: none;
  cursor: not-allowed;
  pointer-events: none;
}

/* Primaire marine plein */
.btn-primary {
  background: var(--navy);
  color: var(--on-dark);
  border-color: var(--navy);
}
.btn-primary:hover {
  background: var(--navy-700);
  border-color: var(--navy-700);
}

/* Accent corail plein. Le corail de marque (--coral) porte du blanc a 3.89:1,
   sous le seuil AA de 4.5:1 pour ce corps de texte : le REMPLISSAGE du bouton
   descend a --coral-700 (5.37:1) et le survol a --coral-800 (6.85:1). Le corail
   vif reste partout ailleurs, ou il ne porte pas de texte (barre d'accent de la
   nav, pastilles, filets). */
.btn-coral {
  background: var(--coral-700);
  color: var(--on-dark);
  border-color: var(--coral-700);
  font-weight: 700;
}
.btn-coral:hover {
  background: var(--coral-800);
  border-color: var(--coral-800);
}

/* Bouton de DESTRUCTION. Il manquait, et c'est le pendant de la séparation
   entre `--coral-700` et `--danger` faite plus haut : tant qu'aucune classe ne
   portait le danger, une suppression ne pouvait que réemployer le corail de
   marque. `--danger` sur blanc mesure 6,45:1 avec du texte blanc.
   `.btn-coral` reste ce qu'il est aujourd'hui dans le code, l'action
   PRINCIPALE d'un formulaire (« Enregistrer », « Inscrire ») : aucun de ses
   emplois actuels n'est une suppression, vérifié sur les cinq apps. Le jour où
   l'on rendra l'action principale marine partout, ce sera une tranche à part,
   avec la revue des écrans, pas un remplacement à l'aveugle. */
.btn-danger {
  background: var(--danger);
  color: var(--on-dark);
  border-color: var(--danger);
  font-weight: 600;
}
.btn-danger:hover {
  background: #922022;
  border-color: #922022;
}

/* Un lien habillé en bouton plein garde son texte clair au survol et au focus :
   la règle de lien globale (a:hover, spécificité 0,1,1) gagnerait sinon sur
   .btn-primary/.btn-coral (0,1,0) et posait du navy sur fond navy, texte
   illisible (retour de Patrick du 04.08, vu sur tous les cockpits). */
a.btn-primary:hover,
a.btn-primary:focus-visible,
a.btn-coral:hover,
a.btn-coral:focus-visible {
  color: var(--on-dark);
}

/* Fantôme (contour) */
.btn-ghost {
  background: transparent;
  color: var(--navy);
  border-color: var(--line-strong);
}
.btn-ghost:hover {
  background: var(--greige);
  border-color: var(--navy);
}

/* Discret (lien-bouton) */
.btn-quiet {
  background: transparent;
  color: var(--navy);
  padding-inline: var(--space-3);
}
.btn-quiet:hover {
  background: var(--greige);
}

.btn--sm {
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-xs);
}
.btn--lg {
  padding: var(--space-4) var(--space-7);
  font-size: var(--text-body);
}
.btn--block {
  width: 100%;
}

/* Corail plein en PETITE taille (texte blanc < 14px gras) : on bascule le fond
   sur coral-700 (#C73132). Blanc sur coral-700 ≈ 5.0:1 → AA même en 12px, alors
   que le coral pur (#E84647) échouerait (≈4.0:1). S'applique à .btn-coral.btn--sm. */
.btn-coral.btn--sm {
  background: var(--coral-700);
  border-color: var(--coral-700);
}
.btn-coral.btn--sm:hover {
  background: var(--coral-800);
  border-color: var(--coral-700);
  filter: brightness(0.94);
}

/* =========================================================================
   PROGRESSION (barre animable)
   ========================================================================= */
.progress {
  width: 100%;
  height: 8px;
  background: var(--greige);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.progress__bar {
  height: 100%;
  /* Largeur pilotée par --value, posé en style inline par le composant.
     Le repli à 0 couvre le cas « pas encore de valeur ». */
  width: var(--value, 0);
  background: var(--navy);
  border-radius: var(--radius-pill);
  transition: width var(--dur-counter) var(--ease-out);
}
.progress__bar--coral {
  background: var(--coral);
}
.progress__bar--salmon {
  background: var(--salmon);
}
.progress__bar--success {
  background: var(--success);
}
.progress__bar--warning {
  background: var(--warning);
}
.progress__bar--critical {
  background: var(--coral-700);
}

/* =========================================================================
   PILL (étiquette douce, ex : langue, niveau CECRL)
   ========================================================================= */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--greige);
  color: var(--ink);
  border: 1px solid var(--line);
}
.pill--navy {
  background: var(--info-soft);
  color: var(--navy);
  border-color: transparent;
}
.pill--salmon {
  background: var(--salmon-soft);
  color: var(--coral-700);
  border-color: transparent;
}

/* =========================================================================
   TABLE (serveur, élégante, lignes aérées)
   ========================================================================= */
.table-wrap {
  width: 100%;
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  /* BLOC DE CONTENEUR DU CONTENU DÉFILANT, et pas une coquetterie d'empilement.
     Un `overflow` ne rogne QUE les descendants dont le bloc de conteneur est à
     l'intérieur de lui. Sans `position` ici, un enfant en `position: absolute`
     s'ancrait au premier ancêtre positionné, donc HORS du conteneur : il
     échappait au rognage et se posait à sa position de flux, très loin à
     droite dans un tableau large. Mesuré au banc le 07.08 sur la liste des
     cours de l'entreprise : un `.sr-only` d'en-tête de colonne (1 pixel de
     large, invisible) se retrouvait à 866 pixels et poussait le document à
     867 pour 375 d'écran. Le tableau, lui, défilait correctement : le rapport
     de débordement ne montrait rien, puisqu'il ignore à juste titre ce qui vit
     dans un conteneur à défilement. Ancrer ici referme cette porte pour tout
     absolu présent ou futur du tableau. */
  position: relative;
}
.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.table thead th {
  text-align: left;
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  font-size: var(--text-xs);
  color: var(--muted);
  background: var(--greige);
  padding: var(--space-3) var(--space-5);
  white-space: nowrap;
}
.table tbody td {
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--line);
  color: var(--ink);
  vertical-align: middle;
}
.table tbody tr {
  transition: background var(--dur-fast) var(--ease-out);
}
.table tbody tr:hover {
  background: var(--ivory);
}
/* UN LIEN DE CELLULE EST UNE CIBLE, PAS DU TEXTE COURANT.
   Laissé en `inline`, un lien dont l'intitulé s'enroule se fragmente en une
   boîte par ligne. Sa boîte englobante devient l'UNION de ces fragments : un
   escalier, avec des trous entre les lignes et des zones qui n'appartiennent
   pas au lien. Amener cette union en vue fait défiler deux conteneurs à la
   fois (le tableau à l'horizontale, le document à la verticale) et le résultat
   n'est pas stable d'une tentative à l'autre. Le point visé est alors calculé
   dans une position de défilement et évalué dans une autre : le geste tombe à
   côté, sur la cellule, sur la carte ou sur la barre de recherche, sans que
   rien ne le signale.
   Mesuré au banc le 06.08 sur la liste des cours du professeur, au format
   téléphone : trois fragments, aucune ouverture possible au doigt. En
   `inline-block`, le lien n'a plus qu'une boîte, son intitulé continuant de
   s'enrouler à l'intérieur. La colonne étroite est le cas courant du
   téléphone : la règle vaut donc pour tous les tableaux des cockpits. */
.table tbody td a {
  display: inline-block;
}
.table--num td,
.table--num th {
  font-variant-numeric: tabular-nums;
}
/* En-tête de colonne triable */
.table th[aria-sort] {
  cursor: pointer;
}
.table th[aria-sort]:hover {
  color: var(--navy);
}

/* =========================================================================
   DÉFILEMENT HORIZONTAL (tableaux larges) - flèches + fondus de bord
   Les flèches n'apparaissent que s'il reste du contenu à faire défiler
   (classes .scrollx--l / .scrollx--r posées par le composant ScrollX).
   ========================================================================= */
.scrollx {
  position: relative;
}
.scrollx__viewport {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  /* Même raison que `.table-wrap` ci-dessus, et le piège est ici plus sournois :
     `.scrollx` EST positionné (pour ses flèches), si bien qu'un absolu du
     contenu s'y ancrait sans erreur apparente. Or le rognage appartient au
     viewport, pas à `.scrollx` : l'absolu échappait quand même. Les flèches
     restent ancrées à `.scrollx`, dont elles sont les enfants directs. */
  position: relative;
}
.scrollx__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--navy);
  cursor: pointer;
  box-shadow: var(--shadow-md);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}
.scrollx__arrow:hover {
  background: var(--greige);
  border-color: var(--navy);
}
.scrollx__arrow:active {
  transform: translateY(-50%) scale(0.92);
}
.scrollx__arrow:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
.scrollx__arrow svg {
  width: 18px;
  height: 18px;
}
.scrollx__arrow--left {
  left: var(--space-3);
}
.scrollx__arrow--right {
  right: var(--space-3);
}
.scrollx--l .scrollx__arrow--left {
  opacity: 1;
  pointer-events: auto;
}
.scrollx--r .scrollx__arrow--right {
  opacity: 1;
  pointer-events: auto;
}
/* Fondus de bord : signalent visuellement le contenu masqué à gauche/droite. */
.scrollx::before,
.scrollx::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 56px;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.scrollx::before {
  left: 0;
  background: linear-gradient(90deg, var(--surface) 25%, transparent);
}
.scrollx::after {
  right: 0;
  background: linear-gradient(270deg, var(--surface) 25%, transparent);
}
.scrollx--l::before {
  opacity: 1;
}
.scrollx--r::after {
  opacity: 1;
}
/* AU DOIGT, PAS DE FLÈCHES.
   La flèche est un disque de 36 pixels posé EN SURIMPRESSION du contenu, à
   mi-hauteur du conteneur et sur ses deux bords. Dès que le tableau défile
   d'un pixel, elle passe en pointer-events: auto et devient la cible la plus
   haute (z-index: 3) : ce qui se trouve dessous n'est plus activable. Sur un
   tableau étroit, dessous, c'est la première colonne, celle qui porte le lien
   d'ouverture de la ligne. Mesuré au banc le 06.08 sur la liste des cours du
   professeur : le lien occupait x 73..129, la flèche x 61..97, et le geste
   tombait sur le chevron.
   Au doigt, le défilement d'un tableau se fait en balayant, jamais en visant
   un disque de 36 pixels. La flèche n'y apporte rien et coûte une colonne :
   elle est retirée, les fondus de bord continuant seuls à signaler le contenu
   masqué. Le seuil de largeur accompagne le pointeur grossier pour couvrir la
   fenêtre de bureau rétrécie, où le tableau déborde toujours et où les
   colonnes sont tout aussi serrées. */
@media (pointer: coarse), (max-width: 768px) {
  .scrollx__arrow {
    display: none;
  }
}

/* =========================================================================
   FEUILLE DE PRÉSENCE - CARTES SOUS 600 PIXELS
   =========================================================================
   Le pointage est le geste le plus fréquent de l'école : plusieurs fois par
   jour, par chaque professeur, debout, en salle, sur téléphone. Il se faisait
   dans un tableau de trois colonnes qui mesurait 595 pixels dans un conteneur
   de 288 (contre-analyse du 10.08). La colonne des statuts était donc hors
   écran ; les flèches de défilement sont masquées à cette largeur, et c'est
   une décision assumée (au doigt, un tableau se balaie, il ne se vise pas) ;
   et après avoir fait défiler jusqu'aux boutons, le nom de l'élève avait
   disparu, si bien qu'on pointait de mémoire.

   Faire défiler MIEUX n'aurait rien réglé : le problème n'est pas la manière
   de défiler, c'est qu'il faille défiler pour poser un statut. Sous 600
   pixels, la feuille cesse donc d'être un tableau et devient une liste de
   cartes, une par élève, le nom en titre et les quatre statuts dessous.

   Les deux structures coexistent dans le document et une seule est peinte :
   `display: none` la retire aussi de l'arbre d'accessibilité, donc rien n'est
   annoncé deux fois. Ce choix évite une requête média en JavaScript, qui
   dépendrait de l'hydratation et déciderait trop tard.

   Le seuil est à 600 et non 768 : entre les deux, le tableau tient encore, et
   une tablette en portrait n'a pas besoin de cartes. */
.attendance-cards {
  display: none;
}
@media (max-width: 600px) {
  .attendance-sheet__table {
    display: none;
  }
  .attendance-cards {
    display: grid;
    /* Deux pixels entre deux élèves, et non douze : la carte porte déjà son
       filet, qui suffit à les séparer, et chaque pixel se paie autant de fois
       qu'il y a d'élèves. Mesuré : c'est ce qui fait tenir une classe de cinq
       dans le premier écran (audit du 11.08, resserré le 14.08 pour l'iPhone
       SE, où les quatre pixels d'origine coûtaient huit des cinquante qui
       manquaient). */
    gap: 2px;
  }
  /* La feuille de séance respire moins que les autres cartes, et seulement à la
     verticale : sa marge intérieure est la dernière chose qui sépare le titre
     de la première carte d'élève, et elle se paie sur le premier écran, celui
     où se fait le pointage. Les côtés gardent la marge commune. */
  .attendance-sheet {
    padding-block: var(--space-2);
  }
}
.attendance-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  /* Marge intérieure DISSYMÉTRIQUE, et mesurée : quatre pixels en haut et en
     bas, douze sur les côtés. Chaque pixel vertical se paie cinq fois sur une
     classe de cinq, et c'est à ce prix-là qu'elle tient sur le premier écran
     (audit du 11.08). L'air qui compte est celui qui sépare le nom des
     pastilles, pas celui qui double le filet de la carte : il est mis là
     (gap: var(--space-2)) et pris ici. */
  padding: var(--space-1) var(--space-3);
  background: var(--surface);
  display: grid;
  /* Deux pixels entre le nom et ses pastilles : l'air qui sépare les deux
     lignes d'une même carte se paie cinq fois sur une classe de cinq, et il ne
     sépare rien que la graisse du nom ne distingue déjà. Les pastilles, elles,
     gardent leurs 44 pixels : c'est la cible du doigt, jamais une marge. */
  gap: 2px;
}
/* Le nom et le statut posé partagent une SEULE ligne. Empilés, ils coûtaient
   deux lignes par carte, soit une carte entière sur une classe de cinq, pour
   deux informations qui se lisent d'un coup d'oeil ensemble : qui, et où il en
   est. Le statut reste écrit EN MOTS, jamais porté par la seule pastille
   enfoncée : c'est la pièce que lit le contrôle qualité EduQua. */
.attendance-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  /* Interligne BORNÉ : l'interligne de lecture du corps (1,7) donnait 26 pixels
     de haut à une ligne de 14, cinq fois sur une classe de cinq. Une ligne de
     liste n'a pas de suite à lire sous elle, elle n'a pas besoin de cet air. */
  line-height: 1.25;
}
.attendance-card__name {
  font-weight: 600;
  color: var(--ink);
  /* La taille du corps secondaire, et non celle du corps de texte : à 16 pixels
     la ligne du nom en mesurait 26 de haut, cinq fois sur une classe de cinq.
     Le nom reste le plus lourd de sa carte par sa GRAISSE, qui suffit à le
     distinguer de son statut posé juste à côté. */
  font-size: var(--text-sm);
  /* Un nom long ne doit ni déborder ni pousser la carte hors de l'écran. */
  overflow-wrap: anywhere;
  min-width: 0;
}
/* Le nom ouvre la progression de l'élève : c'est le bouton qui remplace la
   quatrième ligne de la carte (une rangée de 44 pixels par élève, pour une
   LECTURE courte). Il se dépouille de la silhouette de bouton, il n'en garde
   que la cible. */
button.attendance-card__name {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
button.attendance-card__name:hover {
  text-decoration: underline;
}
button.attendance-card__name:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
  border-radius: var(--radius-sm);
}
.attendance-card__status {
  font-size: var(--text-sm);
  color: var(--ink-soft);
  white-space: nowrap;
}
/* QUATRE COLONNES pour les quatre statuts, et non deux.
   Cette règle en portait deux jusqu'au 11.08, au motif qu'à 375 pixels quatre
   boutons en ligne tomberaient sous la largeur lisible. C'était juste pour la
   largeur, et faux pour l'écran : deux rangées de 44 pixels par élève portaient
   la feuille de séance à 4 651 pixels de haut, six écrans de téléphone pour
   cinq élèves, et le pointage ne tenait plus dans le premier. Or c'est le geste
   le plus fréquent de l'école, fait debout, entre deux cours.

   Ce qui rachète la largeur perdue : le libellé se replie sur deux lignes au
   lieu de se faire couper. Un statut TRONQUÉ serait dangereux, et pas seulement
   inélégant : en espagnol, « Ausencia justificada » abrégé en « Ausencia... »
   se confond avec « Ausente », qui dit l'inverse au dossier EduQua. Un libellé
   replié reste entier, dans les six langues, y compris les composés allemands
   (« Entschuldigt »), que la césure automatique coupe proprement puisque
   `<html lang>` porte la langue réelle (shared/document.ts). */
.attendance-card__actions {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-1);
}
.attendance-card__actions .pill {
  /* 44 pixels : la cible d'un geste fait debout, souvent d'une seule main. Le
     minimum WCAG 2.2 AA est de 24, insuffisant ici. */
  min-height: 44px;
  justify-content: center;
  cursor: pointer;
  font-size: var(--text-xs);
  /* Le libellé se replie et se centre : la pastille garde sa hauteur de cible,
     le mot garde ses lettres. */
  padding: var(--space-1);
  text-align: center;
  line-height: 1.15;
  white-space: normal;
  overflow-wrap: break-word;
  hyphens: auto;
  min-width: 0;
}

/* =========================================================================
   BARRE DE POINTAGE - L'ACTION SOUS LE POUCE
   =========================================================================
   La feuille de séance porte huit blocs. Son action principale vivait au fil
   du contenu, donc à plusieurs écrans du pouce, et l'état de l'enregistrement
   ne s'annonçait que par une notification fugace : un pointage refusé par le
   serveur ressemblait exactement à un pointage accepté (contre-analyse du
   11.08).

   `sticky` et non `fixed`, et c'est le coeur du réglage : une barre fixe
   masquerait la fin du contenu, qu'il faudrait compenser par une réserve en
   bas de page, en plus de celle que la barre d'onglets réclame déjà
   (.app-col > .app-main). Collée, la barre remonte tant que sa place naturelle
   est sous la fenêtre, et la retrouve en fin de défilement : elle est toujours
   atteignable et ne recouvre jamais rien.

   Elle se cale AU-DESSUS de la barre d'onglets, encoche comprise, et passe
   dessous dans l'ordre d'empilement (z-index 30 contre 40) : la navigation
   reste la dernière couche. */
.marking-bar {
  position: sticky;
  bottom: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.marking-bar__state {
  display: grid;
  gap: 2px;
  min-width: 0;
}
.marking-bar__count {
  font-weight: 600;
  color: var(--ink);
}
.marking-bar__note {
  font-size: var(--text-sm);
  color: var(--ink-soft);
}
/* L'action principale de l'écran : 44 pixels de haut, jamais moins, et elle ne
   se comprime pas quand le décompte s'allonge. */
.marking-bar__action {
  min-height: 44px;
  flex: none;
}
@media (max-width: 768px) {
  .marking-bar {
    /* --tabbar-h est un MINIMUM (min-height), et la barre basse mesure 59
       pixels au rendu, pas 56 : sans les quatre pixels ajoutés, le filet du bas
       de cette barre passait sous celui du haut de la barre d'onglets. */
    bottom: calc(var(--tabbar-h) + var(--space-1) + env(safe-area-inset-bottom, 0px));
  }
}

/* =========================================================================
   NAVIGATION - SIDEBAR
   ========================================================================= */
/* Sidebar MARINE (couleur primaire de marque) : tranche avec le panneau ivoire,
   rendu bancaire-grade. Collante (reste en vue quand le contenu défile). */
.sidebar {
  width: var(--sidebar-w);
  height: 100vh;
  position: sticky;
  top: 0;
  align-self: start;
  overflow-y: auto;
  overflow-x: hidden;
  background: var(--navy);
  border-right: 1px solid rgba(255, 255, 255, 0.06);
  padding: var(--space-5) var(--space-3);
  display: flex;
  flex-direction: column;
  gap: 2px;
  transition: width var(--dur-base) var(--ease-out);
}
/* Filet lumineux discret en haut (profondeur premium). */
.sidebar::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.12), transparent);
}

/* Tête : pastille de marque + libellé + bouton replier. */
.sidebar__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
  padding: 0 var(--space-2);
}
.sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  padding: 0;
  font-family: var(--font-serif);
  font-size: var(--text-h4);
  color: var(--on-dark);
}
/* Monogramme de marque. Le fond est le corail FONCÉ, pas le corail vif : en
   blanc sur --coral, les deux lettres mesuraient 3.89:1, sous le seuil AA de
   4.5:1 du texte normal (audit du 08.08). Sur --coral-700 elles mesurent
   5.37:1. Le halo suit la même teinte, sans quoi il jurerait avec le carré.
   Mesure vérifiée par scripts/verify-contraste-socle.mjs. */
.sidebar__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: var(--radius-md);
  background: var(--coral-700);
  color: var(--on-dark);
  font-family: var(--font-serif);
  font-size: 1.05rem;
  letter-spacing: 0.01em;
  box-shadow: 0 4px 12px rgba(199, 49, 50, 0.35);
}
.sidebar__brand-text {
  color: var(--on-dark);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidebar__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: rgba(255, 255, 255, 0.6);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.sidebar__toggle:hover {
  background: rgba(255, 255, 255, 0.1);
  color: var(--on-dark);
}
.sidebar__toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(246, 188, 179, 0.5);
}
.sidebar__toggle svg {
  width: 18px;
  height: 18px;
}

/* Libellé de groupe. Le blanc était posé à 42 % d'opacité : composé sur le
   marine, cela donnait 3.34:1, sous le seuil AA de 4.5:1 (audit du 08.08). À
   58 % il mesure 4.95:1 et reste en retrait des entrées de navigation, qui
   sont en blanc plein. Mesure vérifiée par scripts/verify-contraste-socle.mjs. */
.sidebar__group-label {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.58);
  padding: var(--space-4) var(--space-3) var(--space-2);
}

/* Item de nav (peut être un <button> : reset propre). */
.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  width: 100%;
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  color: rgba(255, 255, 255, 0.72);
  font-size: var(--text-sm);
  font-weight: 500;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.nav-item:hover {
  background: rgba(255, 255, 255, 0.07);
  color: var(--on-dark);
}
.nav-item:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(246, 188, 179, 0.5);
}
.nav-item--active {
  background: rgba(255, 255, 255, 0.1);
  color: var(--on-dark);
  font-weight: 600;
}
.nav-item--active:hover {
  background: rgba(255, 255, 255, 0.13);
}
/* Barre d'accent corail à gauche de l'item actif (signature de marque). */
.nav-item--active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 22px;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--coral);
}
.nav-item__icon {
  width: 20px;
  height: 20px;
  flex: none;
  color: rgba(255, 255, 255, 0.55);
  transition: color var(--dur-fast) var(--ease-out);
}
.nav-item:hover .nav-item__icon {
  color: var(--on-dark);
}
.nav-item--active .nav-item__icon {
  color: var(--salmon);
}
.nav-item__label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* État RÉTRACTÉ : icônes seules. La pastille de marque reste (identité). */
.sidebar--collapsed {
  padding-inline: var(--space-2);
}
.sidebar--collapsed .sidebar__head {
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  padding: 0;
}
.sidebar--collapsed .sidebar__brand-text,
.sidebar--collapsed .sidebar__group-label,
.sidebar--collapsed .nav-item__label {
  display: none;
}
.sidebar--collapsed .nav-item {
  justify-content: center;
  padding-inline: 0;
}
.sidebar--collapsed .nav-item--active::before {
  display: none;
}

/* La colonne de contenu suit la largeur de la sidebar en douceur. */
.app-shell {
  transition: grid-template-columns var(--dur-base) var(--ease-out);
}

/* =========================================================================
   NAVIGATION - TOPBAR
   ========================================================================= */
.topbar {
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding: 0 var(--space-7);
  background: color-mix(in srgb, var(--ivory) 88%, transparent);
  backdrop-filter: saturate(140%) blur(8px);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 20;
}
.topbar__title {
  font-size: var(--text-h4);
  margin: 0;
}
.topbar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* Ce bloc absorbe la compression a la place du titre : ses enfants savent se
     reduire (libelles remplaces par des icones, nom tronque), le titre non. */
  min-width: 0;
}

/* Bouton burger (caché en desktop, affiché sous --bp-nav par les cockpits) */
.topbar__burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  /* `width` n'est qu'une base de calcul dans une boîte flexible : sous
     pression, le rétrécissement par défaut la ramène à la largeur de son
     contenu. C'est ce qui réduisait ce bouton à 14 pixels de large pour 40 de
     haut dès que la barre débordait (mesuré au banc), sous le minimum de 24 du
     critère 2.5.8 de WCAG 2.2. Or c'est le SEUL accès à la navigation du
     bureau au format téléphone : trop étroit, il se rate au doigt et le
     cockpit devient impraticable. */
  flex: none;
  border-radius: var(--radius-md);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--navy);
  cursor: pointer;
}
.topbar__burger:hover {
  background: var(--greige);
  border-color: var(--navy);
}
.topbar__burger:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

/* =========================================================================
   NAVIGATION MOBILE - DRAWER (partagé, piloté par les cockpits sous 1100px)
   Réutilise les .nav-item. Ouvert via [data-nav-open] sur le drawer.
   ========================================================================= */
.nav-drawer {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none; /* activé en .is-open */
  /* `allow-discrete` retient l'affichage le temps de la sortie. Sans lui, le
     retrait de .is-open remet `display: none` au premier calcul de style : les
     enfants n'ont plus de boîte, donc plus rien à transitionner, et le volet
     disparaît d'un coup après être arrivé en glissant. Mesuré le 12.08 : à la
     fermeture, zéro animation en cours sur le panneau. */
  transition: display var(--dur-base) allow-discrete;
}
.nav-drawer.is-open {
  display: block;
}
.nav-drawer__scrim {
  position: absolute;
  inset: 0;
  background: rgba(20, 20, 15, 0.45);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.nav-drawer.is-open .nav-drawer__scrim {
  opacity: 1;
}
.nav-drawer__panel {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: min(84vw, var(--sidebar-w));
  background: var(--surface);
  border-right: 1px solid var(--line);
  padding: var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  overflow-y: auto;
  transform: translateX(-100%);
  transition: transform var(--dur-base) var(--ease-out);
  box-shadow: var(--shadow-lg);
}
.nav-drawer.is-open .nav-drawer__panel {
  transform: translateX(0);
}
/* L'ÉTAT DE DÉPART, sans lequel rien de tout cela ne joue. Un élément qui
   vient d'apparaître n'a pas d'état précédent : le navigateur calcule
   directement sa position finale et la transition déclarée juste au-dessus ne
   s'exécute jamais. C'était le cas depuis toujours, mesuré au navigateur le
   12.08 sur la feuille réelle : `transform` valait déjà l'identité au premier
   calcul, aucune animation en cours. La transition était écrite, lisible en
   revue, et ne produisait rien.

   @starting-style fournit cet état de départ. Le volet part donc du bord
   gauche, celui d'où il vient, et y retourne quand on le ferme : le retour en
   arrière se comprend sans le lire. Un moteur qui ignore cette règle retombe
   sur le comportement d'avant (apparition sèche), jamais sur un volet
   invisible. */
@starting-style {
  .nav-drawer.is-open .nav-drawer__panel {
    transform: translateX(-100%);
  }
  .nav-drawer.is-open .nav-drawer__scrim {
    opacity: 0;
  }
}
.nav-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-3);
}
.nav-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--navy);
  cursor: pointer;
}
.nav-drawer__close:hover {
  background: var(--greige);
}
.nav-drawer__close:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
/* Le mouvement réduit est traité une fois pour toutes par le filet universel
   (`transition: none !important`, section ACCESSIBILITÉ) : le volet apparaît
   et disparaît alors sans glisser, ce qui est le comportement attendu. Le
   bloc qui annulait ici ces deux transitions à la main a donc disparu. */

/* =========================================================================
   AVATAR
   ========================================================================= */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-pill);
  background: var(--navy);
  color: var(--on-dark);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  flex: none;
  overflow: hidden;
}
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.avatar--sm {
  width: 28px;
  height: 28px;
  font-size: var(--text-xs);
}
.avatar--lg {
  width: 56px;
  height: 56px;
  font-size: var(--text-h4);
}
.avatar--coral {
  background: var(--coral);
}

/* =========================================================================
   BOUTON-ICÔNE (cible tactile 44px, ex : cloche de notifications)
   ========================================================================= */
.icon-btn {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast)
    var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-fast)
    var(--ease-out);
}
.icon-btn svg {
  width: 22px;
  height: 22px;
}
.icon-btn:hover {
  border-color: var(--navy);
  color: var(--navy);
}
.icon-btn:active {
  transform: scale(0.94);
}
.icon-btn:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

/* =========================================================================
   CHIP DE FILTRE
   ========================================================================= */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 500;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast)
    var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.chip:hover {
  border-color: var(--navy);
}
.chip:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
.chip--active {
  background: var(--navy);
  border-color: var(--navy);
  color: var(--on-dark);
}
.chip__close {
  font-size: 1em;
  line-height: 1;
  opacity: 0.7;
}
.chip__close:hover {
  opacity: 1;
}

/* =========================================================================
   CHAMPS DE SAISIE (toujours Inter, jamais de serif)
   ========================================================================= */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* AIR ENTRE DEUX BLOCS DE CHAMPS
   Un champ et son libellé respirent (le `gap` ci-dessus), et deux champs d'une
   MÊME grille aussi (le `gap` de `.grid`). Mais deux blocs FRÈRES n'avaient
   rien : ni une grille suivie d'une autre, ni une grille suivie d'un champ
   pleine largeur. Mesuré le 12.08 sur « Planifier une séance » du planning :
   0 pixel entre le bas d'un champ et le libellé du bloc suivant, aux quatre
   enchaînements du formulaire. Le libellé « Date » touchait le champ « Cours »,
   et rien ne disait où finissait une ligne et où commençait la suivante.

   La valeur est celle du `gap` des grilles, et c'est le point : entre deux
   lignes de champs, l'écart doit être le MÊME qu'elles soient dans la même
   grille ou dans deux grilles successives. Un rythme, pas deux. */
.field + .field,
.field + .grid,
.grid + .field,
.grid + .grid {
  margin-top: var(--space-4);
}
/* DANS une grille, les frères sont CÔTE À CÔTE, et leur écart est le `gap` :
   la marge ci-dessus y décalerait la deuxième colonne vers le bas. Vu à la
   première mesure, « Professeur » descendu de 16 pixels sous « Cours », et les
   deux heures sous leur date. L'écartement des colonnes appartient à la
   grille ; celui des blocs empilés, à la règle du dessus. */
.grid > .field + .field,
.grid > .field + .grid,
.grid > .grid + .field,
.grid > .grid + .grid {
  margin-top: 0;
}
.label {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
}
.input,
.select,
.textarea {
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: 1.4;
  color: var(--ink);
  background-color: var(--surface);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  width: 100%;
  appearance: none; /* tue le rendu natif (flèche système, ombrages OS) */
  -webkit-appearance: none;
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast)
    var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}
.input::placeholder,
.textarea::placeholder {
  color: var(--muted);
}

/* Survol : la bordure se renforce discrètement (retour d'interactivité). */
.input:hover:not(:disabled):not(:focus),
.select:hover:not(:disabled):not(:focus),
.textarea:hover:not(:disabled):not(:focus) {
  border-color: var(--muted);
}

/* Focus : bordure marine + halo DOUX (pas le halo dur des boutons). */
.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--navy);
  box-shadow: 0 0 0 3px rgba(42, 49, 125, 0.14);
}
.input:disabled,
.select:disabled,
.textarea:disabled {
  background-color: var(--greige);
  color: var(--muted);
  cursor: not-allowed;
}
.textarea {
  min-height: 120px;
  resize: vertical;
}

/* -------------------------------------------------------------------------
   ÉTAT D'UN CHAMP : obligation, erreur, aide

   L'état est porté par les attributs que le champ a DÉJÀ besoin de porter
   pour être accessible (`aria-invalid`, `required`), jamais par une classe
   qu'on ajoute en plus. Une classe de style se pose sans l'attribut, et le
   champ devient rouge pour l'oeil sans l'être pour un lecteur d'écran ; ici,
   l'un ne peut pas exister sans l'autre.
   ------------------------------------------------------------------------- */
.input[aria-invalid="true"],
.select[aria-invalid="true"],
.textarea[aria-invalid="true"] {
  border-color: var(--danger);
}
/* Le champ en erreur garde sa bordure rouge au focus : le halo marine du
   focus normal effacerait la seule marque visuelle de l'erreur au moment
   précis où l'on revient corriger. */
.input[aria-invalid="true"]:focus,
.select[aria-invalid="true"]:focus,
.textarea[aria-invalid="true"]:focus {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px rgba(179, 40, 42, 0.16);
}

/* Marque d'obligation, posée en CSS et jamais dans le libellé.

   Écrite en `<span>` dans le <label>, elle entrerait dans le TEXTE du libellé :
   « Prénom » deviendrait « Prénom* », et toute recherche par libellé exact
   échouerait. Mesuré le 12.08, le banc E2E ne trouvait plus le champ
   (`getByLabel('Prénom', { exact: true })`), et `aria-hidden` sur le span ne
   suffisait pas à l'en retirer.

   L'obligation reste annoncée aux technologies d'assistance par `aria-required`
   sur le champ, qui est le bon porteur. La seconde déclaration donne au
   pseudo-élément une alternative VIDE là où elle est supportée, pour qu'un
   astérisque ne soit pas lu après chaque libellé ; les navigateurs qui
   l'ignorent gardent la première, donc la marque reste visible partout. */
.field__label--required::after {
  content: "*";
  content: "*" / "";
  color: var(--danger);
  margin-inline-start: 2px;
  font-weight: 600;
}

/* Message d'erreur d'un champ. Il ne disparaît pas tout seul, contrairement à
   une notification : on met le temps qu'on met à corriger une saisie. */
.field__error {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--danger);
  margin: 0;
}

/* Aide : ce qu'il faut savoir AVANT de remplir, ou ce qui manque encore pour
   qu'un bouton s'active. */
.field__hint {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--muted);
  margin: 0;
}
/* Sous un bouton, l'aide a besoin de sa propre respiration : elle suit un
   élément qui n'est pas dans le flux d'un `.field`. `display: block` n'est pas
   décoratif : le porteur est un <output>, dont l'affichage natif est en ligne,
   et la marge verticale d'un élément en ligne ne pousse rien. */
.field__hint--block {
  display: block;
  margin-top: var(--space-2);
}

/* Menu déroulant : chevron maison (marine muet), plus la flèche système. Le
   fond reste blanc ; on ne pose QUE l'image de chevron par-dessus. */
.select {
  cursor: pointer;
  padding-right: calc(var(--space-4) + 1.25rem);
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%235C5C56'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='m6%209%206%206%206-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 1rem;
}
/* Chevron marine au focus/survol (petit soin d'état). */
.select:hover:not(:disabled),
.select:focus {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%232A317D'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='m6%209%206%206%206-6'/%3E%3C/svg%3E");
}

/* Contrôles compacts pour barres d'action inline (formulaires d'ajout d'un
   profil, d'un enfant, d'une inscription…). Largeur auto : ils s'alignent aux
   boutons dans une .row au lieu de s'étirer à 100 %. */
.input--sm,
.select--sm {
  width: auto;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
}
.select--sm {
  padding-right: calc(var(--space-3) + 1.15rem);
  background-position: right var(--space-2) center;
  background-size: 0.9rem;
}

/* Barre de formulaire inline : champs + action alignés, aérés, retour ligne
   propre sur mobile. Remplace les style= flex répétés dans les cockpits. */
.form-inline {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
/* Chaque contrôle garde sa taille naturelle et passe à la ligne en bloc,
   plutôt que d'être écrasé (bouton dont le libellé se coupe sur 2 lignes).
   `max-width` est le garde-fou de ce refus de rétrécir : sans lui, un enfant
   plus large que la barre ne peut ni se contracter ni passer à la ligne, et il
   déborde du document entier. Deux écrans du bureau le montraient à 375 pixels
   (mesuré au banc le 07.08) : la phrase d'aide de la liste des cours poussait
   la page à 549, et le champ « nom du dossier » de la GED à 385. Borner à la
   largeur de la barre suffit : le texte s'enroule, le champ se rétrécit, et un
   bouton, toujours plus étroit qu'elle, ne bouge pas. */
.form-inline > * {
  flex: 0 0 auto;
  max-width: 100%;
}
/* La borne vaut aussi pour le CONTROLE dans son champ : un select garde la
   largeur de sa plus longue option (« Nom Prenom · periode · montant »), et
   place dans un .field il echappait au max-width du form-inline, qui borne
   l'enfant direct : /staff/pilotage debordait de 122 px sur un telephone
   (garde des defauts de design, 16.08). */
.field > .input,
.field > .select,
.field > textarea {
  max-width: 100%;
}
/* Un fieldset REFUSE de retrecir sous son contenu : min-inline-size vaut
   min-content par defaut, calcule comme si son contenu ne repliait jamais
   (particularite HTML, pas un effet de nos regles). Les chips de periode du
   pilotage imposaient 468 px a un ecran de 375 et figeaient le viewport
   emule a 497. Aucun ecran ne veut ce comportement. */
fieldset {
  min-inline-size: 0;
}
/* Champs et boutons d'une même barre à la MÊME hauteur (alignement propre). */
.form-inline > .btn,
.form-inline > .input,
.form-inline > .select {
  min-height: 2.375rem;
}

/* =========================================================================
   ALERTE (info / succès / avertissement / danger)
   ========================================================================= */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  font-size: var(--text-sm);
  color: var(--ink);
}
.alert__icon {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 1px;
}
.alert__title {
  font-weight: 600;
  margin-bottom: var(--space-1);
}
.alert--info {
  background: var(--info-soft);
  border-color: color-mix(in srgb, var(--navy) 20%, transparent);
}
.alert--info .alert__icon {
  color: var(--navy);
}
.alert--success {
  background: var(--success-soft);
  border-color: color-mix(in srgb, var(--success) 25%, transparent);
}
.alert--success .alert__icon {
  color: var(--success);
}
.alert--warning {
  background: var(--warning-soft);
  border-color: color-mix(in srgb, var(--warning) 25%, transparent);
}
.alert--warning .alert__icon {
  color: var(--warning);
}
.alert--danger {
  background: var(--danger-soft);
  border-color: color-mix(in srgb, var(--danger) 25%, transparent);
}
.alert--danger .alert__icon {
  color: var(--danger);
}

/* -------------------------------------------------------------------------
   ALERTE EN LIGNE (.notice)

   Ce que `.alert` ne peut pas faire : tenir DANS une cellule de tableau, à
   côté de la donnée qu'elle qualifie. `.alert` est une boîte de pleine
   largeur ; posée dans une cellule, elle casse la hauteur de la ligne.

   Elle existe parce que le produit calculait déjà ses anomalies et les
   rendait invisibles. « Saisie incomplète : 3 séances déjà passées n'ont pas
   de présence saisie » s'écrivait en gris de 12 px sous un ratio, dans la
   colonne « Progression des leçons » de la fiche client : la bonne
   information, au bon endroit, sans rien qui la distingue du décor. Le
   contrôle qualité EduQua repose dessus.
   ------------------------------------------------------------------------- */
.notice {
  display: inline-flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: 1.35;
  padding: 2px var(--space-2) 2px var(--space-2);
  border-radius: var(--radius-sm);
  border-inline-start: 3px solid currentcolor;
  /* Le texte n'hérite pas de `currentcolor` du filet : chaque ton pose sa
     couleur de texte, mesurée sur son propre fond (verify-contraste-socle). */
}
.notice__icon {
  width: 14px;
  height: 14px;
  flex: none;
  /* Aligné sur la première ligne de texte, pas centré sur le bloc : sur deux
     lignes, une icône centrée flotte au milieu et ne désigne plus rien. */
  margin-top: 2px;
}
.notice__text {
  min-width: 0;
}
/* L'action est ce qui distingue une alerte d'un constat. Elle reste sur la
   même ligne que le texte tant que la place le permet. */
.notice__action {
  white-space: nowrap;
  font-weight: 600;
}
.notice--warning {
  background-color: var(--warning-soft);
  color: var(--warning);
}
.notice--danger {
  background-color: var(--danger-soft);
  color: var(--danger);
}
.notice--info {
  background-color: var(--info-soft);
  color: var(--info);
}

/* =========================================================================
   PAS DE RÉVÉLATION AU DÉFILEMENT DANS LES COCKPITS
   La classe .reveal vivait ici (opacité et translation rendues par un
   IntersectionObserver). Aucun cockpit ne l'a jamais posée : elle était
   arrivée par mimétisme avec la vitrine, où elle a du sens. Elle est partie
   le 12.08 avec la passe de mouvement.

   Un cockpit se relit toute la journée. Faire apparaître une carte quand elle
   entre dans le champ, c'est faire attendre une information déjà chargée, et
   l'attente est ici du travail retardé, pas du récit. La vitrine garde le
   motif (apps/vitrine/src/styles/global.css), l'outil ne le reprend pas ;
   scripts/verify-mouvement-socle.mjs empêche son retour.
   ========================================================================= */

/* =========================================================================
   UTILITAIRES MINIMAUX
   ========================================================================= */
.container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-6);
}
.stack > * + * {
  margin-top: var(--space-4);
}
.stack--tight > * + * {
  margin-top: var(--space-3);
}
/* Pile COMPACTE pour une cellule de tableau (chiffre, détail, commentaire).
   Contrairement à .stack, qui se contente d'espacer des blocs, celle-ci
   EMPILE réellement : des <span> inline resteraient sinon sur une seule
   ligne et la cellule deviendrait illisible. */
.stack-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  white-space: nowrap;
}
.row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
/* Le repli plutot que le debordement, EN MOBILE SEULEMENT : une rangee
   d'actions plus large que l'ecran (fiche cours en 375 px, 15.08) elargissait
   le DOCUMENT, et le viewport visuel dezoome deviait chaque tap de
   l'emulation. Au bureau, le retrecissement flex historique reste : la garde
   de densite mesure des rangees de tableau a 44 pixels, et le wrap
   inconditionnel les faisait replier. */
@media (max-width: 767px) {
  .row {
    flex-wrap: wrap;
  }
}
.row--between {
  justify-content: space-between;
}

/* Espacements verticaux courants (remplacent les style= inline structurels). */
.mt-2 {
  margin-top: var(--space-2);
}
.mt-4 {
  margin-top: var(--space-4);
}
.mt-5 {
  margin-top: var(--space-5);
}
.mt-6 {
  margin-top: var(--space-6);
}

/* Cible du lien d'évitement : focus posé sur le conteneur, sans halo visuel. */
[tabindex="-1"]:focus {
  outline: none;
}

/* Masquage visuel mais lisible au lecteur d'écran (en-têtes d'actions, etc.). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Liste de définition en deux colonnes : intitulé à gauche, valeur à droite.
   Remplace le `style="display:grid; grid-template-columns:auto 1fr"` répété
   dans huit écrans (identité, fiche entreprise, détail de leçon, compte).

   `minmax(0, 1fr)` et non `1fr` : la forme courte vaut `minmax(auto, 1fr)`,
   dont le minimum est le contenu le plus étroit possible de la piste. Une
   valeur insécable la fait donc GRANDIR au lieu de la contraindre, et c'est
   une adresse électronique qui a poussé l'accueil de l'espace entreprise à
   491 pixels pour 375 d'écran (mesuré au banc le 07.08). Le zéro rend à la
   piste le droit de rétrécir ; la coupure ci-dessous s'occupe du reste, car
   sans elle la valeur déborderait simplement de sa colonne. */
.deflist {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2);
}
.deflist dd {
  min-width: 0;
  overflow-wrap: anywhere;
}

.grid {
  display: grid;
  gap: var(--space-4);
}
.grid > * {
  min-width: 0;
} /* enfants de grille : rétrécir au lieu de déborder */
.grid--2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid--3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid--4 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}
.app-shell > * {
  min-width: 0;
} /* colonne de contenu : rétrécir au lieu de déborder */
.app-main {
  padding: var(--space-6);
  min-width: 0;
}

@media (max-width: 1024px) {
  .grid--4 {
    grid-template-columns: repeat(2, 1fr);
  }
  .grid--3 {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (max-width: 768px) {
  .grid--2,
  .grid--3,
  .grid--4 {
    grid-template-columns: 1fr;
  }
  .app-shell {
    grid-template-columns: 1fr;
  }
  .sidebar {
    display: none;
  } /* nav mobile gérée par les cockpits */
  .app-main {
    padding: var(--space-5);
  }
  :root {
    --text-display: 2.25rem;
    --text-h1: 1.75rem;
    --text-kpi: 1.625rem;
  }
}

/* =========================================================================
   ACCESSIBILITÉ - mouvement réduit
   ANNULER, PAS RACCOURCIR. Le filet posait `transition-duration: 0.001ms
   !important` sur le sélecteur universel. Une transition d'un millionième de
   seconde n'est pas une transition annulée : elle joue, elle émet ses
   évènements, et surtout ce `!important` sur `*` écrasait toute règle ciblée
   écrite plus bas dans la feuille. Le `.spinner { animation-duration: 2s }` du
   bloc « mouvement réduit », dont l'intention écrite était de RALENTIR la
   rotation pour la rendre supportable, ne s'appliquait pas : le spinner
   tournait à 0.001ms comme tout le reste, exactement ce que la règle voulait
   éviter. Une déclaration en !important bat une déclaration normale quelle que
   soit sa spécificité et quel que soit son rang dans la feuille.

   Les transitions sont donc annulées, une bonne fois. Les animations, elles,
   ne se traitent pas en bloc : une entrée se retire, une boucle d'attente se
   ralentit ou se remplace par un état fixe, un fondu de bord se garde. Chacune
   dit ce qu'elle devient, plus bas dans la feuille, et
   scripts/verify-mouvement-socle.mjs échoue si l'une d'elles se tait.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition: none !important;
    animation-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

/* =========================================================================
   LARGEUR DE CONTENU - respiration sur grands écrans
   Le contenu (--content-max) reste à 1240px jusqu'à 1600px, puis s'élargit
   par paliers pour éviter le vide latéral sur les très grands moniteurs, sans
   allonger les lignes de texte au point de nuire à la lecture.
   ========================================================================= */
@media (min-width: 1600px) {
  :root {
    --content-max: 1360px;
  }
}
@media (min-width: 1920px) {
  :root {
    --content-max: 1500px;
  }
}

/* ============ Board / kanban (CRM pipeline) ============ */
/* Le tableau de pipeline defile deja ; ce qui manquait, c'est de le DIRE.
   Releve du 11.08 a 375 pixels : les quatre colonnes depassent la fenetre de
   174, 426, 678 et 930 pixels, trois sur quatre sont invisibles et rien ne
   l'indique. Meme fondu de bord que la barre d'onglets, meme raison. */
.board {
  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  padding-bottom: var(--space-2);
}
@media (max-width: 768px) {
  .board {
    mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 1.5rem), transparent 100%);
    animation: tabnav-fade linear both;
    animation-timeline: scroll(self inline);
  }
}
.board__col {
  flex: 0 0 15rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  background: var(--ivory, #f6f1e7);
  border: 1px solid rgba(31, 48, 71, 0.1);
  border-radius: 0.5rem;
  padding: var(--space-2);
}
.board__col--over {
  border-color: var(--navy, #1f3047);
  background: #eee6d6;
}
.board__col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: 600;
  font-size: 0.85rem;
}
.board__col-total {
  color: var(--muted);
  font-weight: 500;
}
.board__card {
  background: var(--surface);
  border: 1px solid rgba(31, 48, 71, 0.12);
  border-radius: 0.375rem;
  padding: var(--space-2);
  cursor: grab;
  font-size: 0.8rem;
}
.board__card--drag {
  opacity: 0.5;
}
.board__card-title {
  font-weight: 600;
}
.board__card-meta {
  color: var(--muted);
  margin-top: 0.15rem;
}

/* =========================================================================
   COQUILLE APPLICATIVE v1.1 - commune aux quatre cockpits
   Console (admin) : sidebar marine + topbar collante + tiroir mobile.
   Portails (prof, apprenant, entreprise) : masthead clair + onglets.
   Un seul design system, meme grammaire visuelle partout.
   ========================================================================= */

/* Colonne de droite d'une coquille a sidebar : topbar collante puis contenu. */
.app-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 100vh;
}
.app-col > .app-main {
  flex: 1 1 auto;
}

/* Burger : masque en desktop, revele quand la sidebar disparait (<= 768px). */
@media (max-width: 768px) {
  .topbar__burger {
    display: inline-flex;
  }
}

/* Intitule de page dans la topbar : sur-titre discret + page courante. */
/* LE TITRE DE LA PAGE NE PEUT PLUS ETRE ECRASE A ZERO.
   `min-width: 0` autorisait la boite flexible a comprimer ce bloc sans plancher
   pendant que le bloc d'actions gardait sa taille. Releve du 11.08 sur staging,
   meme page, seule la largeur change : le sur-titre et le titre passent de 109
   pixels a 75 (1200), 25 (1150), 1 (1126) puis ZERO de 1100 a 769. Le titre
   etant en `overflow: hidden`, il ne debordait pas : il DISPARAISSAIT, sans
   rien laisser paraitre. La bande morte couvre un MacBook Air 13 pouces en
   resolution par defaut (1152 pixels CSS) et une fenetre non maximisee sur un
   ecran de 1440.

   Le plancher seul ne suffirait pas : il deplacerait la compression sur le bloc
   de droite, qui deborderait a son tour. C'est pourquoi les libelles des
   actions et le champ de recherche cedent la place AVANT le titre, plus haut
   dans les largeurs (voir le seuil de 1180 px). L'ordre de sacrifice est
   desormais explicite : d'abord les mots qu'une icone remplace, jamais le nom
   de la page ou l'on se trouve. */
.topbar__heading {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 7rem;
  /* Base flex NULLE, et c'est ce qui compte : sous le filet d'enroulement
     mobile, une ligne se compose sur les tailles de BASE des blocs, avant
     toute compression. Base auto, la largeur naturelle du titre decidait de
     l'enroulement, et le passage a Inter (plus large que la serif a corps
     egal) a couche la barre sur deux rangees dans les cinq cockpits : 27
     pixels perdus, la feuille de seance ne tenait plus sur le premier ecran.
     Base zero, le titre ne pese que son plancher de 7rem dans la decision et
     s'elide dans la place recue (.topbar__page sait le faire) ; le filet
     reste pour les ACTIONS ajoutees demain, son role d'origine. */
  flex: 1 1 0%;
}
.topbar__eyebrow {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
}
.topbar__page {
  font-family: var(--font-serif);
  font-size: var(--text-h4);
  color: var(--navy);
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Menu utilisateur : avatar + identite + role + deconnexion, homogene partout. */
.usermenu {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}
/* L'identite mene a « Mon compte » : lien discret, jamais souligne, qui reprend
   l'alignement de l'ancien groupe avatar + identite. */
.usermenu__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  text-decoration: none;
  border-radius: var(--radius-sm);
  padding: 2px 4px;
  margin: -2px -4px;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.usermenu__link:hover,
.usermenu__link:focus-visible {
  background: var(--greige);
}
.usermenu__id {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  min-width: 0;
}
.usermenu__name {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 20ch;
}
.usermenu__role {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
}
@media (max-width: 600px) {
  .usermenu__id {
    display: none;
  }
}

/* Pastille de marque reutilisable (masthead des portails, ecran de connexion).
   Même corail foncé que .sidebar__mark, pour la même raison de contraste. */
.brand-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-md);
  background: var(--coral-700);
  color: var(--on-dark);
  font-family: var(--font-serif);
  font-size: 1.05rem;
  letter-spacing: 0.01em;
  box-shadow: 0 4px 12px rgba(199, 49, 50, 0.35);
}

/* Bas de sidebar : selecteur d'espaces SSO, pousse en pied de colonne. */
.sidebar__foot {
  margin-top: auto;
  padding-top: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
/* Même correction de contraste que .sidebar__group-label : 3.34:1 -> 4.95:1. */
.sidebar__foot-label {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.58);
  padding: 0 var(--space-3);
}
/* Chips d'espaces sur fond marine (lisibles, etat courant en corail fonce AA). */
.space-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: rgba(255, 255, 255, 0.8);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.08);
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.space-chip:hover {
  background: rgba(255, 255, 255, 0.13);
  color: var(--on-dark);
}
.space-chip:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(246, 188, 179, 0.5);
}
.space-chip--current {
  background: var(--coral-700);
  border-color: var(--coral-700);
  color: var(--on-dark);
}
.space-chip--current:hover {
  background: var(--coral-700);
  color: var(--on-dark);
  filter: brightness(0.96);
}
.sidebar--collapsed .sidebar__foot {
  display: none;
}

/* -------------------------------------------------------------------------
   Ecrans d'authentification des cockpits : chargement, redirection (vers la
   connexion ou vers l'espace du role), deconnexion, refus.
   Meme famille visuelle que l'application de connexion SWISS LINGUA
   (apps/login) : marque en tete, carte centree sur fond creme, pied discret.
   Aucun masthead ni navigation : le layout ne monte le chrome qu'une fois
   l'acces accorde. C'est la premiere impression.
   ------------------------------------------------------------------------- */
.auth-screen {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--space-5) var(--space-4) var(--space-6);
  background: radial-gradient(130% 120% at 50% -20%, rgba(42, 49, 125, 0.1), transparent 55%),
    radial-gradient(90% 90% at 100% 100%, rgba(246, 188, 179, 0.22), transparent 60%), var(--ivory);
}
/* Marque sortie de la carte, comme sur l'ecran de connexion : ces ecrans
   restent des ecrans du site, pas des pages isolees. */
.auth-topbar {
  width: 100%;
  max-width: 1080px;
  display: flex;
  align-items: center;
}
.auth-topbar__logo {
  display: block;
  height: 34px;
  width: auto;
}
.auth-middle {
  flex: 1;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--space-5) 0;
}
.auth-card {
  width: 100%;
  max-width: 31.5rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: clamp(1.75rem, 5.5vw, 3.25rem) clamp(1.25rem, 5vw, 2.25rem) clamp(1.5rem, 5vw, 2.9rem);
  text-align: center;
}
.auth-card__title {
  font-family: var(--font-serif);
  font-weight: 600;
  color: var(--navy);
  font-size: clamp(1.45rem, 5.2vw, 1.8rem);
  line-height: 1.18;
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-2);
}
.auth-card__body {
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.55;
  margin: 0 auto;
  max-width: 40ch;
}
/* Rappel du role du compte : c'est l'information qui permet a la personne de
   dire au secretariat ce qui ne va pas. */
.auth-card__role {
  margin-top: var(--space-4);
}
.auth-card__cta {
  width: 100%;
  justify-content: center;
  margin-top: var(--space-5);
}
/* Etats transitoires (chargement, redirection) : une ligne sobre, sans carte.
   Rien a lire, rien a faire, l'ecran va disparaitre. */
.auth-note {
  color: var(--muted);
  font-size: var(--text-sm);
  text-align: center;
  margin: 0;
}
.auth-foot {
  color: var(--muted);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  margin: 0;
}
@media (max-width: 720px) {
  .auth-topbar {
    justify-content: center;
  }
}
@media (prefers-reduced-motion: no-preference) {
  .auth-card {
    animation: auth-rise var(--dur-slow) var(--ease-out) both;
  }
}
@keyframes auth-rise {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Masthead des portails (prof, apprenant, entreprise) : bloc de marque + actions. */
.masthead {
  margin-bottom: var(--space-6);
}
.masthead__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.masthead__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}
.masthead__space {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.masthead__eyebrow {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--navy);
}
.masthead__title {
  font-family: var(--font-serif);
  font-size: var(--text-h2);
  color: var(--navy);
  line-height: 1.1;
}
.masthead__switcher {
  margin-top: var(--space-4);
}
.masthead__lead {
  margin-top: var(--space-3);
}

/* Sous-navigation de section : onglets soulignes, scrollables en mobile. */
/* LA BARRE D'ONGLETS DIT QU'ELLE CONTINUE.
   Elle defile deja (overflow-x: auto) mais rien ne le signalait : au format
   telephone, « Finances », « Documents » et « Commercial » sont hors champ sur
   les fiches, et « Preferences de notification » a 534 pixels du bord droit sur
   l'espace client (releve du 11.08). Un onglet qu'on ne voit pas et dont rien
   n'annonce l'existence n'existe pas.

   Le fondu est pose en `mask-image` sur les deux bords, et il ne s'allume qu'au
   bord ou il reste quelque chose : `animation-timeline: scroll(self inline)`
   lit la position de defilement du conteneur lui-meme, sans une ligne de
   JavaScript ni un seul ecouteur. Les navigateurs qui ne connaissent pas les
   animations liees au defilement gardent le fondu des deux cotes, ce qui reste
   le bon message par defaut : « ca continue ». */
.tabnav {
  display: flex;
  gap: var(--space-1);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
  margin-top: var(--space-5);
  --tabnav-fade: 1.5rem;
}
/* Le fondu ne vaut QUE la ou la barre deborde vraiment. Au bureau elle tient,
   et un `mask-image` y attenuerait le halo de focus du premier et du dernier
   onglet : la lisibilite gagnee ne vaut pas un reperage clavier degrade. */
@media (max-width: 768px) {
  .tabnav {
    mask-image: linear-gradient(
      to right,
      transparent 0,
      #000 var(--tabnav-fade),
      #000 calc(100% - var(--tabnav-fade)),
      transparent 100%
    );
    animation: tabnav-fade linear both;
    animation-timeline: scroll(self inline);
  }
}
@keyframes tabnav-fade {
  /* Au depart, rien a gauche : le bord gauche reste net. */
  0% {
    mask-image: linear-gradient(
      to right,
      #000 0,
      #000 calc(100% - var(--tabnav-fade)),
      transparent 100%
    );
  }
  /* En fin de course, rien a droite. */
  100% {
    mask-image: linear-gradient(to right, transparent 0, #000 var(--tabnav-fade), #000 100%);
  }
}
.tabnav::-webkit-scrollbar {
  display: none;
}
.tabnav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  white-space: nowrap;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--muted);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  min-height: 44px;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.tabnav__link:hover {
  color: var(--navy);
}
.tabnav__link:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
  border-radius: var(--radius-sm);
}
.tabnav__link--active {
  color: var(--navy);
  border-bottom-color: var(--coral);
}

/* Cartes d'orientation : tuile d'icone + intitule + accroche + fleche. */
.orient-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.orient-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  text-align: left;
  color: inherit;
}
.orient-card__tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: var(--radius-md);
  background: var(--salmon-soft);
  color: var(--coral-700);
}
.orient-card__tile svg {
  width: 22px;
  height: 22px;
}
.orient-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}
.orient-card__title {
  font-family: var(--font-serif);
  font-size: var(--text-h4);
  color: var(--navy);
}
.orient-card__text {
  font-size: var(--text-sm);
  color: var(--muted);
}
.orient-card__arrow {
  margin-left: auto;
  color: var(--navy);
  flex: none;
  transition: transform var(--dur-fast) var(--ease-out);
}
.orient-card__arrow svg {
  width: 20px;
  height: 20px;
}
.orient-card:hover .orient-card__arrow {
  transform: translateX(3px);
}

/* Etat vide elegant : jamais un ecran brut "aucune donnee". */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-5);
}
.empty-state__tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-lg);
  background: var(--greige);
  color: var(--navy);
}
.empty-state__tile svg {
  width: 28px;
  height: 28px;
}
.empty-state__title {
  font-family: var(--font-serif);
  font-size: var(--text-h4);
  color: var(--navy);
}
.empty-state__text {
  font-size: var(--text-sm);
  color: var(--muted);
  max-width: 46ch;
}

/* Page 404 : marque, code, message, retour. */
.notfound {
  min-height: 60vh;
  display: grid;
  place-items: center;
  text-align: center;
  padding: var(--space-7) var(--space-4);
}
.notfound__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  max-width: 48ch;
}
.notfound__code {
  font-family: var(--font-serif);
  font-size: var(--text-display);
  line-height: 1;
  color: var(--coral);
}
.notfound__title {
  font-family: var(--font-serif);
  font-size: var(--text-h2);
  color: var(--navy);
}
.notfound__text {
  font-size: var(--text-body);
  color: var(--muted);
}

/* Tailles d'icone explicites (trait 24x24 mis a l'echelle du contexte). */
.ic {
  width: 20px;
  height: 20px;
  flex: none;
}
.ic--sm {
  width: 16px;
  height: 16px;
  flex: none;
}
.ic--lg {
  width: 24px;
  height: 24px;
  flex: none;
}
.topbar__burger svg {
  width: 22px;
  height: 22px;
}
.nav-drawer__close svg {
  width: 20px;
  height: 20px;
}

/* Tiroir mobile en teinte marine : reprend la sidebar (nav-item lisibles sur marine). */
.nav-drawer__panel--navy {
  background: var(--navy);
  border-right-color: rgba(255, 255, 255, 0.08);
}
.nav-drawer__panel--navy .nav-drawer__head {
  margin-bottom: var(--space-4);
}
.nav-drawer__panel--navy .sidebar__foot {
  margin-top: var(--space-5);
}

/* LA BARRE DE TETE REND SA PLACE AU TITRE AVANT DE L'ECRASER.
   Ce seuil n'est pas un format d'appareil, c'est la largeur a laquelle le bloc
   d'actions cesse de tenir a cote du titre. Releve du 11.08 : le titre du
   bureau tombait a zero de 1100 a 769 pixels, celui de l'espace entreprise et
   du studio de 900 a 769. Les deux libelles « Securite » et « Se deconnecter »
   pesent a eux seuls pres de 260 pixels : les remplacer par leurs icones rend
   exactement la place qui manquait.

   Le libelle n'est pas SUPPRIME, il cesse d'etre peint : chaque controle porte
   un `aria-label` de meme texte, si bien que le nom accessible ne change pas de
   part et d'autre du seuil. Le verrou est le test de comportement « les actions
   du menu restent nommees une fois reduites a leur icone », present dans les
   cinq fichiers e2e/tests/<cockpit>/rendu.mobile.spec.ts. */
@media (max-width: 1180px) {
  .usermenu .btn > span {
    display: none;
  }
  .usermenu .btn {
    padding-inline: var(--space-3);
  }
}

/* Filet de securite, pour la meme raison qu'au format telephone : une barre
   d'outils qui ne peut pas s'enrouler n'a qu'une issue quand son contenu
   grandit, et c'est le debordement du document entier. Une action ajoutee
   demain coutera une rangee, jamais la mise en page. */
@media (max-width: 1024px) {
  .topbar {
    flex-wrap: wrap;
    height: auto;
    min-height: var(--topbar-h);
  }
}

/* Topbar en mobile : respiration reduite. */
@media (max-width: 768px) {
  .topbar {
    /* 24 pixels de marge de chaque côté sur un écran de 375, c'est un huitième
       de la largeur donné au vide alors que la barre déborde. */
    padding-inline: var(--space-4);
    gap: var(--space-3);
    /* Filet de sécurité, et non le correctif : ce qui fait tenir la barre, ce
       sont les icônes seules du menu utilisateur. Mais une barre d'outils qui
       ne peut pas s'enrouler n'a qu'une seule issue quand son contenu grandit,
       et c'est le débordement du document entier, en travers de la page. Avec
       l'enroulement, une action ajoutée demain coûtera une rangée, jamais la
       mise en page. `height` devient un plancher, sinon la deuxième rangée
       serait peinte hors de la barre. */
    flex-wrap: wrap;
    height: auto;
    min-height: var(--topbar-h);
    padding-block: var(--space-2);
  }
  /* CE QUI FAIT TENIR LA BARRE SUR UNE SEULE RANGÉE, mesuré le 14.08.
     L'enroulement ci-dessus est un filet, pas un état normal : il coûtait une
     rangée entière SUR TOUS LES ÉCRANS des cinq cockpits, et la barre pesait
     109 pixels de haut pour 667 d'écran, un sixième de l'iPhone SE donné à un
     titre et à deux réglages. Deux blocs la faisaient déborder de 98 pixels
     (184 + 245 + 12 de gouttière pour 343 utiles), et aucun des deux n'a besoin
     d'être là au téléphone. */

  /* Le sur-titre d'espace redit où l'on se trouve, ce que disent déjà la barre
     d'onglets du bas et l'entrée surlignée. Il mesurait 184 pixels de large et
     FIXAIT à lui seul la largeur du bloc de titre : le titre de page, lui, sait
     s'élider (.topbar__page), il ne demandait qu'à être laissé faire. */
  .topbar__eyebrow {
    display: none;
  }

  /* Le sélecteur de langue est un DOUBLON au téléphone : la même langue se
     règle dans « Mon compte » > Préférences (shared/profile.tsx), écran présent
     dans les cinq cockpits et servi par leur navigation. Le masquer ici ne
     retire donc aucune fonction, et rend 109 pixels de largeur (97 plus sa
     gouttière) : c'est ce qui laisse le titre du cours lisible au lieu d'être
     tronqué à 7rem. Il reste peint dès 769 pixels, où la barre a la place. */
  .topbar__locale {
    display: none;
  }

  /* Jalon de vague de la feuille de séance : il relève du plan de cours et non
     du pointage. Voir le commentaire de sa pose dans l'écran de séance. */
  .lesson-wave {
    display: none;
  }
}

/* =========================================================================
   ÉTATS DE CHARGEMENT
   Deux registres, jamais mélangés :
   - le SQUELETTE (.skeleton) pour une première lecture, quand la forme du
     contenu est connue d'avance (tableau, tuile d'indicateur, ligne de texte) ;
   - l'INDICATEUR D'ACTIVITÉ (.spinner) pour une action déclenchée par
     l'utilisateur, dont on ne peut pas dessiner le résultat à l'avance.
   Le balayage du squelette et la rotation de l'indicateur s'arrêtent sous
   prefers-reduced-motion (voir le bloc dédié en fin de section).
   ========================================================================= */
.skeleton {
  display: block;
  border-radius: var(--radius-sm);
  background-color: var(--greige);
  background-image: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.65) 50%,
    rgba(255, 255, 255, 0) 100%
  );
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: skeleton-sweep 1.4s var(--ease-in-out) infinite;
}
@keyframes skeleton-sweep {
  0% {
    background-position: 150% 0;
  }
  100% {
    background-position: -50% 0;
  }
}
/* Variantes calées sur les hauteurs de texte réelles du design system. */
.skeleton--text {
  height: 0.875rem;
}
.skeleton--title {
  height: 1.375rem;
  border-radius: var(--radius-md);
}
.skeleton--kpi {
  height: 2.25rem;
  width: 60%;
  border-radius: var(--radius-md);
}
.skeleton--pill {
  height: 1.25rem;
  width: 5rem;
  border-radius: var(--radius-pill);
}
/* Largeurs décroissantes : un pavé de barres strictement égales se lit comme
   un bug d'affichage, pas comme du texte en cours d'arrivée. */
.skeleton-lines {
  display: grid;
  gap: var(--space-3);
}
.skeleton-lines > .skeleton:nth-child(2) {
  width: 92%;
}
.skeleton-lines > .skeleton:nth-child(3) {
  width: 78%;
}
.skeleton-lines > .skeleton:last-child:not(:only-child) {
  width: 64%;
}
/* Squelette de tableau : même gouttière que .table pour que le contenu réel
   prenne exactement la place du squelette (aucun saut à l'arrivée des données). */
.skeleton-table {
  display: grid;
  gap: var(--space-4);
  padding: var(--space-2) 0;
}
.skeleton-row {
  display: grid;
  grid-template-columns: repeat(var(--skeleton-cols, 4), minmax(0, 1fr));
  gap: var(--space-4);
  align-items: center;
}
.skeleton-row > .skeleton {
  height: 0.875rem;
}
.skeleton-row--head > .skeleton {
  height: 0.75rem;
  opacity: 0.7;
}

/* Indicateur d'activité. Hérite de currentColor : lisible sur un bouton
   primaire marine comme sur une surface ivoire, sans variante de couleur. */
.spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  flex: none;
  border-radius: var(--radius-pill);
  border: 2px solid currentColor;
  border-top-color: transparent;
  opacity: 0.85;
  animation: spinner-turn 700ms linear infinite;
}
.spinner--sm {
  width: 0.75rem;
  height: 0.75rem;
  border-width: 2px;
}
.spinner--lg {
  width: 1.5rem;
  height: 1.5rem;
  border-width: 3px;
}
@keyframes spinner-turn {
  to {
    transform: rotate(360deg);
  }
}
/* Bloc d'attente centré (chargement d'une carte entière sans forme connue). */
.loading-block {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  color: var(--muted);
  font-size: var(--text-sm);
}

/* Élément occupé : l'action est partie, le bouton reste lisible mais inerte.
   Le libellé ne bouge pas (l'indicateur s'ajoute), pour ne pas décaler la
   mise en page au clic. */
.btn[aria-busy="true"],
.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.btn[aria-busy="true"] {
  pointer-events: none;
}
[aria-busy="true"] > .skeleton-table,
[aria-busy="true"] > .skeleton-lines {
  margin-block: var(--space-2);
}

/* =========================================================================
   NOTIFICATIONS ÉPHÉMÈRES (toasts)
   Confirmation d'une action réussie, ou échec d'une action lancée depuis un
   endroit qui n'a pas de place pour une alerte en ligne (suppression dans un
   tableau, transfert, envoi). Les erreurs de saisie d'un formulaire restent
   en ALERTE EN LIGNE, près du champ fautif : un toast qui disparaît ne peut
   pas porter une correction à faire.
   La région est annoncée en aria-live="polite" (voir ToastRegion).
   ========================================================================= */
.toast-region {
  position: fixed;
  z-index: 80;
  right: var(--space-5);
  bottom: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: min(380px, calc(100vw - var(--space-6)));
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  padding-left: var(--space-5);
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  border-left: 3px solid var(--navy);
  background: var(--surface);
  box-shadow: var(--shadow-md);
  color: var(--ink);
  font-size: var(--text-sm);
  pointer-events: auto;
  /* Pas de fill-mode `both` : l'etat de depart de l'animation est opacity:0,
     et un moteur qui ne joue pas l'animation laisserait la notification
     invisible. Sans fill-mode, l'element est visible par defaut et
     l'animation n'ajoute que l'entree (constate au navigateur). */
  animation: toast-in var(--dur-base) var(--ease-out);
}
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.toast--success {
  border-left-color: var(--success);
}
.toast--danger {
  border-left-color: var(--danger);
}
.toast__icon {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  color: var(--navy);
}
.toast--success .toast__icon {
  color: var(--success);
}
.toast--danger .toast__icon {
  color: var(--danger);
}
.toast__body {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: break-word;
}
.toast__title {
  font-weight: 600;
}
.toast__text {
  color: var(--muted);
  margin-top: var(--space-1);
}
.toast__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin: -2px -2px 0 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.toast__close:hover {
  background: var(--greige);
  color: var(--ink);
}
.toast__close:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
.toast__close svg {
  width: 16px;
  height: 16px;
}
@media (max-width: 600px) {
  .toast-region {
    right: var(--space-4);
    left: var(--space-4);
    bottom: var(--space-4);
    width: auto;
  }
}

/* =========================================================================
   DIALOGUE DE CONFIRMATION
   Élément <dialog> natif ouvert par showModal() : le piège de focus, la
   fermeture par Échap et l'inertage du fond sont fournis par le navigateur,
   aucune réimplémentation maison. Réservé aux actions à conséquence
   (suppression, transfert de crédits, annulation d'une inscription).
   ========================================================================= */
.modal {
  width: min(480px, calc(100vw - var(--space-6)));
  /* Le reset de cette feuille pose margin:0 partout, ce qui ECRASE le
     `margin:auto` que le navigateur applique a dialog:modal : sans cette ligne,
     le dialogue se colle en haut a gauche du viewport (constate au navigateur). */
  margin: auto;
  max-height: calc(100vh - var(--space-7));
  overflow: auto;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
}
/* ENTRÉE ET SORTIE, par transition et non par animation.
   Une animation d'entrée ne sait rien faire de la sortie : `close()` retire la
   boîte au premier calcul de style et le panneau disparaît d'un coup, après
   être arrivé en douceur. Le dialogue s'ouvrait donc comme une porte et se
   fermait comme une trappe.

   Ce que fait `allow-discrete` sur `display` et `overlay` : il retient
   l'affichage ET la couche supérieure le temps de la sortie, sans quoi le
   panneau redescendrait dans le flux de la page avant d'avoir fini de partir.
   Le comportement de `<dialog>` est intact : `close()` rend la main tout de
   suite, `open` passe à faux et l'évènement `close` part au même instant. Rien
   n'est différé côté script, c'est le rendu seul qui prend son temps.

   Vérifié au navigateur sur les quatre chemins de sortie (bouton, touche
   d'échappement, clic sur le fond, fermeture par le parent). */
.modal {
  opacity: 0;
  transform: translateY(8px) scale(0.99);
  transition:
    opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out),
    display var(--dur-base) var(--ease-out) allow-discrete,
    overlay var(--dur-base) var(--ease-out) allow-discrete;
}
.modal[open] {
  opacity: 1;
  transform: none;
}
/* L'état de départ. Sans lui, un élément qui vient d'apparaître n'a pas d'état
   précédent et le navigateur calcule directement son état final : la
   transition ci-dessus ne jouerait qu'à la sortie. */
@starting-style {
  .modal[open] {
    opacity: 0;
    transform: translateY(8px) scale(0.99);
  }
}
.modal::backdrop {
  background: rgba(20, 20, 15, 0.45);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out), display var(--dur-base) var(--ease-out)
    allow-discrete, overlay var(--dur-base) var(--ease-out) allow-discrete;
}
.modal[open]::backdrop {
  opacity: 1;
}
@starting-style {
  .modal[open]::backdrop {
    opacity: 0;
  }
}
/* Panneau de lecture ou de saisie (DetailModal du socle), plus large que la
   confirmation : il porte une grille d'indicateurs, un historique ou un
   formulaire, là où la confirmation ne porte qu'une phrase. */
.modal--wide {
  width: min(760px, calc(100vw - var(--space-6)));
}
.modal__panel {
  padding: var(--space-6);
}
.modal__title {
  font-family: var(--font-serif);
  font-size: var(--text-h3);
  line-height: 1.2;
  color: var(--navy);
  margin: 0;
}
/* Titre à gauche, fermeture à droite : le bouton de sortie est visible sans
   défilement même quand le contenu est long. */
.modal__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}
.modal__header > * {
  min-width: 0;
}
.modal__header .modal__body {
  margin-top: var(--space-2);
}
.modal__body {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  color: var(--muted);
}
.modal__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-6);
}
.modal__actions > * {
  flex: 0 0 auto;
}

/* Mouvement réduit : on garde le changement d'état (donc l'information), on
   retire le déplacement et les boucles. Les transitions sont déjà annulées par
   le filet universel ; ce bloc ne traite que les animations, qui ne se
   gouvernent pas en bloc. La modale n'y figure plus : elle entre et sort
   désormais par transition, donc par le filet.

   Ces trois règles n'avaient aucun effet jusqu'au 12.08 : le filet universel
   posait `animation-duration: 0.001ms !important`, qui les écrasait toutes.
   Le squelette et le toast s'en tiraient (une animation d'un millionième de
   seconde ne se voit pas), mais le spinner, lui, était censé RALENTIR à 2s et
   tournait en réalité à la même vitesse folle que le reste. */
@media (prefers-reduced-motion: reduce) {
  .skeleton {
    /* Une pulsation continue pendant tout un chargement est ce qui gêne le
       plus : elle est remplacée par un aplat, qui occupe la même place et dit
       la même chose. */
    animation: none;
    background-image: none;
  }
  .spinner {
    /* Une rotation reste nécessaire : c'est elle qui dit « ça travaille ». Sa
       vitesse, en revanche, passe de 700ms à 2s par tour. */
    animation-duration: 2s;
  }
  .toast {
    animation: none;
  }
}

/* =========================================================================
   LIEN D'ÉVITEMENT (WCAG 2.4.1)
   Premier élément focusable de la page : il saute la navigation (jusqu'à 30
   entrées dans la console) pour poser le focus sur le contenu. Hors focus, il
   est masqué visuellement mais reste dans l'ordre de tabulation.
   ========================================================================= */
.skip-link {
  position: absolute;
  z-index: 100;
  top: var(--space-2);
  left: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--navy);
  color: var(--on-dark);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  /* Hors focus : retire du rendu visuel par la meme technique que .sr-only,
     sans quitter l'ordre de tabulation. Volontairement PAS un translate anime :
     la visibilite du lien ne doit dependre d'aucune transition (un moteur qui
     ne la joue pas laisserait le lien invisible au clavier). */
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}
.skip-link:focus-visible {
  width: auto;
  height: auto;
  padding: var(--space-3) var(--space-4);
  overflow: visible;
  clip-path: none;
  outline: none;
  box-shadow: 0 0 0 3px var(--salmon);
}

/* Titre de page de la topbar : c'est le <h1> du document, pas une simple
   etiquette. La regle reprend exactement l'ancienne apparence du <span>. */
h1.topbar__page {
  margin: 0;
  font-family: var(--font-serif);
}

/* =========================================================================
   TAILLES DE CIBLE (WCAG 2.2 AA, critere 2.5.8 : 24 x 24 px minimum)
   La pastille (.pill) tombait a 22px : le plancher est pose sans changer la
   silhouette. Sur un pointeur grossier (tactile), les commandes montent a
   44px, la recommandation ergonomique, sans alourdir l'affichage au bureau.
   ========================================================================= */
.pill {
  min-height: 24px;
}

/* Case a cocher : 13 pixels de cote au rendu par defaut du navigateur, releve
   sur cinq ecrans du bureau le 11.08, pour la moitie du minimum exige. La case
   monte a 20 et gagne 2 pixels de marge sur chaque bord, ce qui porte sa zone
   de clic a 24 sans deplacer les colonnes du tableau qui la contient. */
input[type="checkbox"],
input[type="radio"] {
  width: 20px;
  height: 20px;
  margin: 2px;
  accent-color: var(--navy);
}

/* Lien de cellule de tableau : sa hauteur etait celle de sa ligne de texte, 17
   a 22 pixels selon la taille. Le plancher est pose sur la BOITE, sans changer
   l'interligne du tableau : le lien reste sur sa ligne, sa cible ne l'est
   plus. */
.table tbody td a,
a.inactive {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

@media (pointer: coarse) {
  .btn,
  .pill,
  .nav-item,
  .tabnav__link,
  .space-chip,
  .toast__close,
  .scrollx__arrow {
    min-height: 44px;
  }
  .btn--sm {
    padding-block: var(--space-3);
  }
}

/* Coquille des espaces (mon, pro, prof) : porte la respiration et la largeur
   maximale qui vivaient jusqu'ici en style en ligne sur le <main>. Le <main>
   n'enveloppe plus que le contenu de page. */
.portal-shell {
  padding: var(--space-6);
  max-width: var(--content-max, 1240px);
  margin-inline: auto;
  width: 100%;
  min-width: 0;
}

/* =========================================================================
   RÉCAPITULATIF DE TOTAUX (détail d'une offre, « Mes achats »)
   Liste libellé / montant sous un tableau de lignes. Pas un tfoot : le tableau
   ne style que `tbody td`, et un tfoot à colSpan se désaligne des colonnes.
   ========================================================================= */
.order-totals {
  margin: var(--space-4) 0 0;
  display: grid;
  gap: var(--space-2);
  font-size: var(--text-sm);
}
.order-totals > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
}
.order-totals dt {
  color: var(--muted);
}
.order-totals dd {
  margin: 0;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.order-totals__total {
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}
.order-totals__total dt,
.order-totals__total dd {
  color: var(--ink);
  font-weight: 600;
}

/* =========================================================================
   SOCLE VISUEL v1.2 - passe de design d'ensemble des quatre cockpits
   Deux moitiés dans cette section :
   1. des CORRECTIONS RÉTROACTIVES, qui redressent l'existant sans toucher au
      balisage des écrans (cartes imbriquées, tableaux dans une carte, tuiles
      d'indicateurs, densité mobile) ;
   2. des CLASSES NEUVES (.page, .page-head, .stat-strip, .card--flush, .rows)
      que les écrans adoptent au fil de l'eau, à la place des style= en ligne.
   Aucune couleur, aucun rayon, aucune ombre hors des tokens du fichier.
   ========================================================================= */

/* -------------------------------------------------------------------------
   1. Corrections rétroactives
   ------------------------------------------------------------------------- */

/* Carte dans une carte : le fond blanc est déjà posé par la carte parente, la
   seconde bordure et la seconde ombre ne délimitent plus rien et brouillent la
   hiérarchie (tout pèse pareil). La carte fille garde sa bordure, perd son
   ombre et son rayon large. */
.card .card {
  box-shadow: none;
  border-radius: var(--radius-md);
}
/* Carte fille cliquable : l'affordance passe du relief au fond chaud. Le
   soulèvement d'une carte posée dans une autre donnait un effet de calque. */
.card .card--hover:hover {
  box-shadow: none;
  transform: none;
  background: var(--ivory);
  border-color: var(--line-strong);
}

/* Tuile d'indicateur posée dans une carte : fond ivoire, sans cadre. Les
   indicateurs de premier niveau (posés directement sur la page) gardent la
   carte blanche, ce qui rétablit la différence entre les deux rangs. */
.card .card.kpi {
  background: var(--ivory);
  border-color: transparent;
}

/* Tableau dans une carte : `.table-wrap` porte bordure, rayon et fond blanc,
   qui redoublaient ceux de la carte. Dans une carte, il n'est plus qu'une
   zone de défilement. Hors carte, il garde son cadre. */
.card .table-wrap {
  border: 0;
  border-radius: 0;
  background: transparent;
}
/* L'en-tête du tableau reprend alors le rôle de séparateur. */
.card .table-wrap > .table thead th {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

/* -------------------------------------------------------------------------
   2. Classes neuves
   ------------------------------------------------------------------------- */

/* Rythme vertical d'un écran. Remplace les `style="display:grid; gap:…"`
   répétés en tête de chaque route. */
.page {
  display: grid;
  gap: var(--space-6);
  align-content: start;
}

/* En-tête d'écran : ce qu'on regarde, pourquoi, et ce qu'on peut y faire.
   Se pose SUR le fond, jamais dans une carte : une carte annonce un contenu,
   pas un écran. Les actions primaires vivent ici, à droite. */
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--line);
}
.page-head__text {
  min-width: 0;
  display: grid;
  gap: var(--space-1);
}
.page-head__title {
  font-family: var(--font-serif);
  font-size: var(--text-h2);
  color: var(--navy);
  line-height: 1.1;
  letter-spacing: var(--tracking-tight);
  margin: 0;
}
.page-head__lead {
  color: var(--muted);
  font-size: var(--text-sm);
  max-width: 60ch;
}
.page-head__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* Bandeau d'indicateurs : une seule surface, des colonnes séparées par un
   filet, des valeurs alignées sur la même ligne de base. Remplace la rangée
   de cartes autonomes, dont les libellés sur deux lignes décalaient les
   chiffres les uns par rapport aux autres. */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.stat {
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: var(--space-2);
  padding: var(--space-5);
  border-left: 1px solid var(--line);
  min-width: 0;
  container-type: inline-size;
}
.stat:first-child {
  border-left: 0;
}
.stat__label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
  /* Le libellé passe à la ligne au lieu d'être tronqué : dans une colonne
     étroite (bandeau à deux colonnes sur mobile), « Crédit entreprise »
     devenait « Crédit entrepr… », de l'information perdue. */
  overflow-wrap: break-word;
}
.stat__value {
  font-family: var(--font-serif);
  font-size: clamp(1.5rem, 14cqi, var(--text-kpi));
  line-height: 1;
  color: var(--navy);
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: baseline;
  /* La devise passe sous le chiffre plutôt que de rester sur la même ligne et
     de sortir de la carte quand la place manque. Le montant lui-même est
     insécable (séparateurs de milliers U+202F) : c'est la largeur de colonne
     qui doit suivre (voir la bascule à une colonne sous 600px). */
  flex-wrap: wrap;
  gap: var(--space-2);
  min-width: 0;
}
/* Unité et devise : jamais dans le corps du chiffre, sinon la valeur casse sur
   deux lignes et la rangée perd son alignement. */
.stat__unit {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--muted);
  letter-spacing: var(--tracking-wide);
}
/* Ligne de contexte sous le chiffre : ce qui rend l'indicateur lisible (une
   échéance de jetons, une période, une part). Un nombre seul ne se lit pas. */
.stat__meta {
  font-size: var(--text-xs);
  color: var(--muted);
}
.stat__meta--warning {
  color: var(--warning);
}

/* Carte à contenu bord à bord : l'en-tête, la barre d'outils et le pied
   gardent la respiration, le tableau ou la liste prend toute la largeur. */
.card--flush {
  padding: 0;
  overflow: hidden;
}
.card--flush > .card__header,
.card__toolbar,
.card__foot {
  padding: var(--space-5) var(--space-6);
  margin: 0;
}
.card--flush > .card__header {
  border-bottom: 1px solid var(--line);
}
/* Barre d'outils d'une liste : recherche, filtres, compteur de résultats. */
.card__toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  background: var(--ivory);
}
/* Pied d'une liste : volumétrie et pagination. */
.card__foot {
  border-top: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  font-size: var(--text-sm);
  color: var(--muted);
}
.card--flush > .table-wrap,
.card--flush > .scrollx > .table-wrap {
  border: 0;
  border-radius: 0;
}
/* Contenus qui ne portent pas leur propre respiration : dans une carte a
   contenu bord a bord, ils colleraient aux aretes. Les listes, les tableaux et
   l'etat vide (qui a deja son cadrage) ne sont volontairement pas de la
   partie. */
.card--flush > .skeleton-lines,
.card--flush > .skeleton-table,
.card--flush > .alert,
.card--flush > p {
  margin: var(--space-5) var(--space-6);
}

/* Liste de lignes séparées par un filet. Remplace les sous-cartes empilées
   dans une carte (deux cadres, deux ombres, deux marges pour une ligne). */
.rows {
  list-style: none;
  margin: 0;
  padding: 0;
}
.rows > li + li {
  border-top: 1px solid var(--line);
}
.row-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-6);
  color: inherit;
  transition: background var(--dur-fast) var(--ease-out);
}
.row-link:hover {
  background: var(--ivory);
}
.row-link:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
.row-link__title {
  display: block;
  font-weight: 600;
  color: var(--navy);
}
.row-link__meta {
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Tableau de gestion : la variante dense sert les écrans qui listent des
   milliers de lignes (clients, factures, commandes). L'en-tête reste visible
   pendant le défilement de sa zone. */
.table--dense tbody td {
  padding: var(--space-3) var(--space-5);
}
.table--dense thead th {
  padding: var(--space-2) var(--space-5);
  position: sticky;
  top: 0;
  z-index: 1;
}

/* -------------------------------------------------------------------------
   3. Compacité des portails et du mobile
   ------------------------------------------------------------------------- */

/* Le bloc de tête des portails empilait marque, espaces et onglets sur trois
   rangées largement espacées : plus de 170 px avant la première donnée. */
.masthead {
  margin-bottom: var(--space-5);
}
.masthead__switcher {
  margin-top: var(--space-3);
}
.tabnav {
  margin-top: var(--space-4);
}

@media (max-width: 768px) {
  .portal-shell {
    padding: var(--space-4);
  }
  /* Le nom de l'espace ne doit pas être le plus gros élément de l'écran. */
  .masthead__title {
    font-size: var(--text-h4);
  }
  .masthead {
    margin-bottom: var(--space-4);
  }
  /* Le rôle est redit dans « Mon profil » : sur 375 px, il coûte une ligne. */
  .usermenu__role {
    display: none;
  }
  .usermenu__name {
    max-width: 12ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* LES DEUX ACTIONS DU MENU PASSENT EN ICÔNE SEULE. C'est la cause première
     du débordement des cinq cockpits relevé le 07.08 : « Sécurité » et « Se
     déconnecter » pèsent à eux deux près de 260 pixels de libellé, sur les 343
     disponibles à 375 d'écran. Le bloc entier mesurait 308 pixels, et la barre
     de tête 421 : rien ne pouvait tenir.

     Le libellé n'est pas SUPPRIMÉ, il cesse d'être peint : chaque contrôle
     porte un `aria-label` de même texte (shared/auth.tsx des cinq cockpits), si
     bien que le nom accessible reste identique avant et après le seuil. Sans
     cet appariement, l'icône seule deviendrait un bouton sans nom. Le verrou
     est un test de comportement, pas une garde statique : « les actions du menu
     restent nommées une fois réduites à leur icône », présent dans les cinq
     fichiers e2e/tests/<cockpit>/rendu.mobile.spec.ts, cherche ces contrôles
     PAR LEUR NOM au format téléphone. Retirer l'aria-label en gardant cette
     règle rend donc la suite rouge. */
  /* Rangées d'indicateurs à valeurs courtes : deux colonnes, comparables d'un
     coup d'œil, au lieu de 800 px de défilement pour quatre chiffres. Opt-in
     par `.grid--dense` UNIQUEMENT : une rangée qui affiche un montant ou un
     grand nombre reste sur une colonne, car un montant suisse est insécable
     (séparateurs de milliers U+202F) et déborderait une demi-colonne (cf. le
     même écueil corrigé sur `.stat`). L'ancien sélecteur `:has(> .kpi)` ne
     visait jamais rien : KpiCard rend `.card > .kpi`, donc `.kpi` n'est pas
     enfant direct de la grille. */
  .grid--dense {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .card.kpi {
    padding: var(--space-4);
  }
  /* Le bandeau d'indicateurs tombait a une colonne (auto-fit, minmax 180px),
     soit quatre pleins ecrans pour quatre chiffres. Deux colonnes, avec les
     filets recalcules pour la grille a deux colonnes. */
  .stat-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .stat {
    padding: var(--space-4);
  }
  .stat:nth-child(odd) {
    border-left: 0;
  }
  .stat:nth-child(n + 3) {
    border-top: 1px solid var(--line);
  }
  .kpi__label {
    font-size: var(--text-xs);
  }
  /* Onglets : des libellés plus serrés en montrent davantage avant défilement. */
  .tabnav__link {
    padding-inline: var(--space-3);
  }
}

/* =========================================================================
   UN TABLEAU DE DONNEES DEVIENT UNE LISTE AU TELEPHONE
   Releve du 11.08 : quarante ecrans du bureau gardent leur jeu de colonnes
   complet a 375 pixels. Ils defilent dans leur conteneur, donc la page ne
   deborde pas, mais on ne voit qu'une fraction des colonnes : le pilotage du
   personnel demande 1559 pixels dans une boite de 261, les salles 1436. Ce
   n'est pas un effet du volume de donnees : sur un compte VIDE, les factures
   de l'espace client reclament deja 872 pixels. Ce sont les colonnes.

   Chaque ligne devient une carte, chaque cellule une paire libelle / valeur.
   Le libelle vient de l'en-tete de colonne, recopie dans `data-label` par le
   conteneur a defilement (shared/table-cards.ts) : aucun ecran n'a a le
   declarer, et un tableau dont les colonnes changent reste etiquete.

   LA SEMANTIQUE DE TABLEAU EST PRESERVEE. Passer `table`, `tr` et `td` en
   `display: block` retire leur role implicite : un lecteur d'ecran cesse
   d'annoncer « ligne 3 sur 12, colonne Statut » et lit une suite de textes.
   C'est le piege classique de ce patron. Les roles sont donc poses
   explicitement en ARIA par le meme module, ou ils ne changent rien au bureau
   (ils y sont deja implicites) et sauvent la lecture au telephone.
   ========================================================================= */
@media (max-width: 600px) {
  .table--cards thead {
    /* Masquee, pas supprimee : `display: none` la retirerait aussi de l'arbre
       d'accessibilite, et les en-tetes servent encore de reference aux roles. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .table--cards,
  .table--cards tbody,
  .table--cards tr,
  .table--cards td {
    display: block;
    width: auto;
  }
  .table--cards tbody tr {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    padding: var(--space-3);
    background: var(--surface);
  }
  .table--cards tbody tr + tr {
    margin-top: var(--space-3);
  }
  .table--cards tbody tr:hover {
    background: var(--surface);
  }
  .table--cards tbody td {
    display: grid;
    grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
    gap: var(--space-3);
    align-items: baseline;
    padding: var(--space-2) 0;
    border-top: 0;
    /* Une adresse de courriel n'a aucun espace ou se couper : sans cette regle
       elle pousse la carte au-dela de l'ecran, et le tableau qu'on vient de
       ramener a 317 pixels en redemande 423. Cas trouve par la garde de ce
       releve, sur la liste des clients, et par elle seule : les quatre autres
       ecrans du controle n'ont aucune colonne de courriel. */
    overflow-wrap: anywhere;
    min-width: 0;
  }
  .table--cards tbody td + td {
    border-top: 1px solid var(--line);
  }
  /* Le libelle de colonne, repris devant la valeur. */
  .table--cards tbody td[data-label]::before {
    content: attr(data-label);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--muted);
  }
  /* Cellule sans libelle (colonne d'action, en-tete vide) ou cellule qui couvre
     toute la ligne (un etat vide « Aucun element ») : pleine largeur. */
  .table--cards tbody td:not([data-label]) {
    grid-template-columns: minmax(0, 1fr);
  }
  /* Une ligne d'etat vide n'a pas a ressembler a une carte de donnees. */
  .table--cards tbody tr:has(td[colspan]) {
    border: 0;
    padding: 0;
  }
}

/* =========================================================================
   GOUTTIERES DU FORMAT TELEPHONE
   Releve du 11.08, chaine mesuree sur le tableau de bord a 375 pixels : la
   coquille prend 24 pixels de chaque cote, la carte 32, et une carte imbriquee
   32 de plus. Le libelle d'un indicateur commencait a 90 pixels du bord de
   l'ecran, pour 195 pixels de contenu : 180 pixels de gouttiere, soit 48 % de
   la largeur donnes au vide.

   Aucun point de rupture ne reduisait ces marges : elles gardaient leur valeur
   de bureau jusqu'en bas. Ramenees a 12 et 16 pixels, la largeur utile passe
   de 195 a 287 pixels, +47 %. Le seuil est 600 et non 768 : entre les deux, la
   place ne manque pas encore et la respiration vaut mieux que la densite.
   ========================================================================= */
@media (max-width: 600px) {
  .app-main,
  .portal-shell {
    padding: var(--space-3);
  }
  .card {
    padding: var(--space-4);
  }
  /* Une carte posee DANS une carte double la gouttiere sans rien delimiter de
     plus : le fond et la bordure du parent tiennent deja ce role. */
  .card .card {
    padding: var(--space-3);
  }
  /* En-tete de carte : le titre et son action s'empilent plutot que de se
     disputer 280 pixels. */
  .card__header {
    flex-wrap: wrap;
    gap: var(--space-2);
  }
}

/* Bandeau d'indicateurs contenant un montant : sous 600px, les deux colonnes
   tombent sous la largeur d'un montant. Comme un montant suisse est insécable
   (séparateurs de milliers U+202F, devise accolée), il déborderait la colonne
   et serait rogné par l'overflow de la surface (« CHF » coupé sur le filet).
   On rend la pleine largeur à ces bandeaux ; ceux qui n'affichent que des
   compteurs courts gardent leurs deux colonnes, plus denses. Filets recalculés
   pour une pile verticale. */
@media (max-width: 600px) {
  .stat-strip:has(.stat__unit) {
    grid-template-columns: 1fr;
  }
  .stat-strip:has(.stat__unit) .stat {
    border-left: 0;
    border-top: 1px solid var(--line);
  }
  .stat-strip:has(.stat__unit) .stat:first-child {
    border-top: 0;
  }
}

/* =========================================================================
   BANDEAU DE CONSULTATION D UN ESPACE CLIENT (impersonation, arbitrage P5)
   Barre permanente en tete de cockpit, visible tant qu'une consultation est
   ouverte. Corail plein : c'est un etat d'exception, il doit se voir sans
   qu'on le cherche, y compris du coin de l'oeil en travaillant sur un autre
   ecran. Position sticky et non fixed : elle pousse le contenu au lieu de le
   recouvrir, sinon la topbar passerait dessous.
   ========================================================================= */
.impersonation-bar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-4);
  background: var(--coral);
  color: var(--on-dark);
  font-size: 0.9rem;
}
.impersonation-bar__icon {
  flex: none;
}
.impersonation-bar__text {
  flex: 1 1 auto;
  min-width: 0;
}
.impersonation-bar__name {
  font-weight: 600;
}
/* Mention de lecture seule et echeance : secondaires, jamais masquees. Le
   contraste reste tenu (blanc a 88 % sur corail plein). */
.impersonation-bar__note {
  color: rgba(255, 255, 255, 0.88);
}
.impersonation-bar__actions {
  display: flex;
  gap: var(--space-2);
  flex: none;
}
.impersonation-bar .btn {
  background: var(--on-dark);
  color: var(--coral-800);
  border-color: var(--on-dark);
}
.impersonation-bar .btn:hover {
  background: var(--ivory);
  color: var(--coral-800);
}
@media (max-width: 600px) {
  .impersonation-bar {
    align-items: flex-start;
    gap: var(--space-2);
  }
  .impersonation-bar__actions {
    width: 100%;
  }
}

/* =========================================================================
   BARRE D'ONGLETS BASSE (espaces mon, pro, prof, au format téléphone)
   Au bureau, la colonne de gauche montre tout l'inventaire d'un coup. Au
   téléphone cette colonne disparaît (`.sidebar` passe en display:none sous
   768px) : sans rien d'autre, la navigation entière repasserait derrière un
   bouton, et le geste le plus fréquent coûterait deux gestes de plus. Cette
   barre pose les trois destinations les plus fréquentes du rôle SOUS LE
   POUCE, plus l'accès au reste de l'inventaire. C'est le cas d'usage réel du
   professeur, qui fait l'appel debout en salle.
   Elle est FIXE en bas et le contenu lui réserve sa hauteur (padding-bas du
   <main>) : la fin d'une liste reste atteignable, rien n'est recouvert.
   ========================================================================= */
.tabbar {
  /* Fond OPAQUE, et blanc : le contenu défile dessous, une barre translucide
     laisserait passer le texte de la page derrière ses libellés. Le blanc des
     cartes plutôt que l'ivoire de la page pose en plus la barre sur une surface
     élevée, elle se détache donc du contenu sans qu'un filet suffise à le dire.
     Contrastes mesurés par verify-contraste-socle.mjs : 6.73:1 pour un onglet
     au repos, 11.45:1 pour l'onglet courant. */
  background: var(--surface);
  border-top: 1px solid var(--line);
  display: none;
}
.tabbar__item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--tabbar-h);
  min-width: 0;
  padding: var(--space-2) var(--space-1);
  border: 0;
  background: transparent;
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-2xs);
  font-weight: 600;
  color: var(--muted);
  text-align: center;
  transition: color var(--dur-fast) var(--ease-out);
}
.tabbar__item:hover {
  color: var(--navy);
}
.tabbar__item:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
/* Onglet courant. La couleur ne porte pas seule l'information (WCAG 1.4.1) :
   le filet corail en tête d'onglet reprend la signature de l'entrée active de
   la colonne (.nav-item--active::before), au même endroit du regard. */
.tabbar__item--active {
  color: var(--navy);
}
.tabbar__item--active::before {
  content: "";
  position: absolute;
  top: 0;
  left: 25%;
  width: 50%;
  height: 3px;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  background: var(--coral);
}
.tabbar__icon {
  width: 22px;
  height: 22px;
  flex: none;
}
/* Un libellé plus long que sa colonne se tronque : il ne pousse jamais la
   barre au-delà de l'écran, quelle que soit la langue affichée. */
.tabbar__label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 768px) {
  .tabbar {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 40;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    /* Encoche et barre gestuelle des téléphones récents. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-shadow: 0 -2px 12px rgba(20, 20, 15, 0.08);
  }
  .app-col > .app-main {
    padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--space-5));
  }
}

/* =========================================================================
   FICHES 360 : LIRE UNE IDENTITE, LIRE UNE CHRONOLOGIE
   Deux patrons ajoutes le 11.08, apres la mesure de la fiche entreprise au
   banc. Ils repondent au meme defaut : tout y pesait pareil. Une valeur
   d'identite et son libelle etaient deux fragments du meme paragraphe gris ;
   un fil d'activite etait une pile de lignes grises de longueur egale. L'oeil
   n'avait aucun point d'entree, et lire la fiche demandait de la lire en
   entier.
   ========================================================================= */

/* LISTE DE DEFINITIONS - le libelle devient un sur-titre discret, la valeur
   devient l'information. C'est l'inverse exact de ce qui existait : « Secteur:
   Industrie » en un seul <p class="text-sm muted">, ou le mot qu'on cherche a
   la meme couleur et la meme taille que celui qui l'annonce.

   La grille s'ajuste d'elle-meme (auto-fit) plutot que de fixer un nombre de
   colonnes : le bloc occupe toute la largeur qu'on lui donne et repartit ses
   paires selon la place, sans jamais avoir a declarer un point de rupture par
   ecran. Sous 375 pixels, il n'y a plus qu'une colonne, sans regle
   supplementaire. */
.deflist {
  display: grid;
  /* 20rem et non 15 : une adresse de courriel de quarante caracteres demande
     environ 340 pixels. Mesuree a 15rem, la fiche posait trois colonnes de 300
     et coupait « facturation.lac@demo.swisslingua.invalid » au milieu du
     dernier mot. Deux colonnes larges valent mieux que trois qui cassent. */
  /* min(100%, 20rem) et non 20rem seul : une piste minmax(20rem, 1fr) ne
     descend JAMAIS sous 20rem, meme quand son conteneur est plus etroit. A 375
     pixels d'ecran, la carte n'offre que 279 pixels utiles et la grille en
     reclamait 320 : le document debordait de deux pixels, releve par la garde
     de largeur au format telephone alors que le format bureau etait propre.
     Avec min(), la piste prend toute la largeur disponible des qu'elle manque,
     et retrouve son minimum des qu'elle existe. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--space-4) var(--space-6);
  margin: 0;
}
.deflist > div {
  min-width: 0;
}
.deflist dt {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 2px;
}
.deflist dd {
  margin: 0;
  color: var(--ink);
  /* Une adresse de courriel sans espace deborde sa colonne sans ce reglage :
     c'est le cas le plus courant d'une fiche, et il casse la mise en page a
     375 pixels avant de la casser ailleurs. */
  overflow-wrap: anywhere;
}
/* Une valeur ABSENTE se lit comme absente, jamais comme une valeur : elle
   garde le gris du libelle, la valeur reelle garde l'encre. */
.deflist dd.is-empty {
  color: var(--muted);
}

/* FIL CHRONOLOGIQUE - une suite d'evenements se lit par sa date avant de se
   lire par son contenu. Le jour est un intertitre, l'heure occupe une colonne
   fixe en chiffres tabulaires (donc alignes verticalement d'une ligne a
   l'autre), et l'auteur suit le titre sans lui disputer le regard.

   L'ordre reste celui de la source : les lectures rendent deja le plus recent
   en premier (ORDER BY occurred_at DESC). Retrier a l'ecran ferait diverger
   l'affichage de la pagination des que le fil en aura une. */
.feed {
  list-style: none;
  margin: 0;
  padding: 0;
}
.feed__day + .feed__day {
  margin-top: var(--space-5);
}
.feed__daylabel {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--navy);
  margin: 0 0 var(--space-2);
}
.feed__items {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}
.feed__item {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr);
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}
.feed__time {
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  color: var(--muted);
  white-space: nowrap;
}
.feed__body {
  min-width: 0;
}
.feed__title {
  color: var(--ink);
  font-weight: 600;
  overflow-wrap: anywhere;
}
.feed__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: 2px;
  font-size: var(--text-sm);
  color: var(--muted);
}
/* Sous 600 pixels, la colonne d'heure couterait un quart de la largeur utile.
   L'heure rejoint alors le corps, en tete de ligne : la chronologie reste
   lisible, la place revient au texte. */
@media (max-width: 600px) {
  .feed__item {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
  }
}

/* COLONNE DE CHIFFRES - un montant se compare a celui du dessus, donc il
   s'aligne a droite et se compose en chiffres de largeur fixe. Pose par
   cellule (et non par tableau, ce que fait deja .table--num) parce qu'un
   tableau melange des colonnes de texte et des colonnes de montants. */
.table th.num,
.table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* MESURE EN ALERTE - un impaye non nul n'est pas une mesure parmi les autres.
   Il gardait la couleur des soldes, au milieu d'une bande de six cellules de
   meme poids : la seule question a laquelle personne ne repondait d'un coup
   d'oeil etait « combien nous doit-elle ». La couleur ne porte AUCUNE
   information a elle seule (le libelle dit « Impayes », la valeur dit le
   montant) : elle ne fait que rendre saillant ce qui l'est deja par le texte,
   ce qui est la seule facon acceptable de s'en servir. */
.stat__value--danger {
  color: var(--danger);
}

/* =========================================================================
   ACCUEIL DE L'ESPACE CLIENT - les trois questions de l'élève
   L'écran répondait à « bonjour » avant de répondre à « quand » : 2 213 px de
   haut, sans une seule date ni une seule heure (audit du 11.08). Ces classes
   portent les trois réponses, dans l'ordre où l'élève se les pose : quand est
   mon prochain cours, où j'en suis, ai-je payé.
   ========================================================================= */

/* 1. QUAND. Le seul bloc de l'écran à inverser le contraste, et c'est ce qui
   le fait lire en premier sans avoir à grossir quoi que ce soit. La date y
   est en serif : c'est l'emploi prévu de la fonte de marque (titre de page,
   valeurs chiffrées), et ici la date EST la valeur. */
.next-session {
  position: relative;
  background: var(--navy);
  color: var(--on-dark);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
.next-session__eyebrow {
  font-size: var(--text-2xs);
  font-weight: var(--weight-strong);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  /* Blanc à 78 % sur le marine : 6.1:1, au-dessus du seuil AA du petit texte.
     Les 70 % de la maquette tombaient à 5.2:1, encore conformes, mais la
     marge disparaît dès qu'un écran est mal calibré. */
  color: rgba(255, 255, 255, 0.78);
  margin: 0;
}
/* La proximité (« Dans 27 jours ») se pose en haut à droite, hors du flux :
   elle qualifie tout le bloc, pas la ligne devant laquelle elle se trouve.
   Elle repasse dans le flux sous 480 px, où la place manque pour deux
   colonnes et où elle chevaucherait le libellé. */
.next-session__proximity {
  position: absolute;
  top: var(--space-5);
  right: var(--space-5);
  font-size: var(--text-2xs);
  font-weight: var(--weight-strong);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: rgba(255, 255, 255, 0.16);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
}
.next-session__when {
  font-family: var(--font-serif);
  font-size: var(--text-kpi);
  line-height: 1.15;
  margin: var(--space-2) 0 0;
  /* Une date en toutes lettres allemande (« Donnerstag, 18. September ») fait
     deux fois la longueur de son équivalent français : elle doit pouvoir se
     couper, sans quoi elle déborderait la carte à 375 px. */
  overflow-wrap: anywhere;
}
.next-session__schedule {
  font-size: var(--text-sm);
  color: rgba(255, 255, 255, 0.85);
  margin: var(--space-1) 0 0;
}
.next-session__time {
  font-variant-numeric: tabular-nums;
}
.next-session__course {
  font-weight: var(--weight-strong);
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  overflow-wrap: anywhere;
}
.next-session__place {
  font-size: var(--text-sm);
  color: rgba(255, 255, 255, 0.85);
  margin: var(--space-1) 0 0;
}
.next-session__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  color: var(--on-dark);
  font-size: var(--text-sm);
  font-weight: var(--weight-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}
@media (max-width: 480px) {
  .next-session__proximity {
    position: static;
    display: inline-block;
    margin-top: var(--space-2);
  }
}

/* 2. OÙ J'EN SUIS. Une barre de progression ne dit rien seule : 57 % est bon
   ou mauvais selon qu'on vise 50 ou 70. Le seuil d'attestation est donc porté
   PAR la barre, en repère vertical, et nommé sous elle. */
.gauge {
  margin-top: var(--space-4);
}
.gauge__row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
}
.gauge__value {
  font-family: var(--font-serif);
  font-size: var(--text-kpi);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--navy);
}
.gauge__value--below {
  color: var(--warning);
}
.gauge__value--reached {
  color: var(--success);
}
.gauge__detail {
  font-size: var(--text-sm);
  color: var(--muted);
}
/* Le repère déborde la barre de 3 px en haut et en bas pour rester visible
   quand le remplissage l'atteint : à l'intérieur, il disparaîtrait sous la
   couleur exactement au moment où il compte le plus. */
.progress--gauge {
  position: relative;
  overflow: visible;
}
.progress--gauge .progress__bar {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.progress__threshold {
  position: absolute;
  top: -3px;
  bottom: -3px;
  left: var(--threshold, 100%);
  width: 2px;
  background: var(--navy);
  border-radius: 1px;
}
.gauge__scale {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--text-xs);
  color: var(--muted);
  margin: var(--space-2) 0 var(--space-4);
}

/* 3. AI-JE PAYÉ. Deux mesures, jamais quatre : ce que j'ai, ce que je dois.
   Le rouge ne sert qu'à la seconde, et seulement s'il y a quelque chose à
   régler ; il ne porte aucune information à lui seul, le libellé et le
   montant la disent déjà. */
.money-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
}
.money-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.money-card__label {
  font-size: var(--text-2xs);
  font-weight: var(--weight-strong);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
}
.money-card__value {
  font-size: var(--text-stat);
  line-height: 1.1;
  color: var(--navy);
}
.money-card__unit {
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  color: var(--muted);
  margin-left: var(--space-1);
}
.money-card__note {
  font-size: var(--text-xs);
  color: var(--muted);
}
.money-card--due {
  border-color: var(--danger);
  background: var(--danger-soft);
}
.money-card--due .money-card__value {
  color: var(--danger);
}
.money-card__note--due {
  color: var(--danger);
  font-weight: var(--weight-strong);
}
/* Sous 380 px, deux montants côte à côte laissent moins de 150 px par carte :
   un solde à cinq chiffres s'y coupe. Les cartes passent alors l'une sous
   l'autre. */
@media (max-width: 380px) {
  .money-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}
/* Les deux actions de bas d'écran (« Voir mes factures », « Découvrir le
   catalogue ») occupent toute la largeur sous le pouce, où un bouton pleine
   largeur est la cible la plus sûre à atteindre d'une main. Au-delà, un bouton
   de 1240 px de large n'est plus un bouton : ils reprennent leur largeur
   naturelle et s'alignent au bord du contenu. */
@media (min-width: 600px) {
  [data-route="mon-home"] > .btn--block {
    width: fit-content;
  }
}
/* Mesure ABSENTE. Une absence n'est pas une valeur : elle ne se lit pas dans
   la serif des chiffres, à la taille d'un chiffre.

   Ce que ce sélecteur remplace, et pourquoi il doit vivre ICI. Le repli
   s'écrivait `class="stat__value muted text-sm"`, et ces deux correctifs ne
   s'appliquaient PAS : `.muted` (l. 445) et `.text-sm` (l. 437) sont déclarées
   avant `.stat__value`, à spécificité égale, donc perdent la cascade. Le
   développeur croyait écrire petit et gris ; « Aucune mesure » sortait en
   serif de 24 à 30 px et en marine, à la place du chiffre, dans trois cellules
   sur six d'une fiche sans données. Toute correction posée plus haut dans la
   feuille aurait été avalée de la même façon. */
.stat__value--empty {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 450;
  color: var(--muted);
}

/* =========================================================================
   VAGUE ERGONOMIE (recette du 16.08) : sections aerees, onglets visibles,
   fil d'activite lisible, en-tete de document. Des regles de VOISINAGE
   d'abord : elles rangent les fiches PARTOUT sans reprendre chaque ecran,
   et un style= local plus genereux reste prioritaire puisqu'il s'applique
   a l'element, pas a son voisin.
   ========================================================================= */

/* 1. Respiration des sections de fiche. Titres, rangees d'en-tete, tables et
   textes d'aide etaient poses bord a bord (fiche du collaborateur, pilotage,
   fiche client : recette du 16.08). */
.row + .table-wrap,
.row + .scrollx,
.row + table,
.row + .stat-strip {
  margin-top: var(--space-3);
}
.eyebrow + .table-wrap,
.eyebrow + .scrollx,
.eyebrow + table,
.eyebrow + .form-inline {
  margin-top: var(--space-3);
}
.table-wrap + .eyebrow,
.scrollx + .eyebrow,
table + .eyebrow,
.stat-strip + .eyebrow {
  margin-top: var(--space-6);
}
.table-wrap + .row,
.scrollx + .row,
table + .row {
  margin-top: var(--space-6);
}
.table-wrap + .form-inline,
.scrollx + .form-inline,
table + .form-inline,
.form-inline + .table-wrap,
.form-inline + .scrollx,
.form-inline + table {
  margin-top: var(--space-4);
}
.form-inline + .text-sm,
.form-inline + .muted {
  margin-top: var(--space-2);
}

/* 2. Onglets. Deux retours clients successifs ont fixe ce dessin, et les
   deux comptent : le lisere de 2 pixels sous un mot gris ne se voyait pas
   (16.08), puis le rail en CAPSULE flottait comme une piece rapportee, tout
   en courbes, mal espace (18.08). D'ou : l'onglet actif garde sa pilule
   navy tres visible (l'acquis du premier retour), mais la capsule disparait.
   Les onglets posent sur un filet pleine largeur qui les INTEGRE a la
   structure de la carte, avec de l'air entre eux et sous eux. Le debordement
   mobile garde son fondu, pose par les regles historiques du .tabnav. */
.tabnav {
  background: none;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  gap: var(--space-2);
  padding: 0 0 var(--space-3);
  margin-bottom: var(--space-4);
  width: 100%;
}
.tabnav__link {
  border-bottom: 0;
  margin-bottom: 0;
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-4);
  min-height: 38px;
}
.tabnav__link:hover {
  background: var(--greige);
  color: var(--navy);
}
.tabnav__link--active,
.tabnav__link--active:hover {
  background: var(--navy);
  color: var(--on-dark);
}

/* 3. Le fil d'activite devient une TIMELINE : un rail vertical, un repere par
   evenement. La version precedente empilait heure + titre + pilule sans
   dessin : trois inscriptions de suite se lisaient comme un bloc de texte
   (recette du 16.08). La pilule de famille passe en micro : c'est une
   metadonnee, pas un statut. */
.feed__items {
  border-top: 0;
  border-left: 2px solid var(--line);
  padding-left: var(--space-4);
}
.feed__item {
  border-bottom: 0;
  position: relative;
  padding: var(--space-2) 0;
}
.feed__item::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-4) - 5px);
  top: 0.6em;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--navy);
}
.feed__meta .pill {
  font-size: var(--text-2xs);
  min-height: 1.25rem;
  padding: 0 var(--space-2);
}
/* Les filtres de famille du fil, compacts : sept grosses pilules mangeaient
   quatre rangees de la colonne. */
.feed-filters .chip {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  min-height: 30px;
}

/* 4. En-tete de DOCUMENT (facture, et tout ecran qui presente une piece) :
   l'identite a gauche, les montants a droite, sur un fond qui detache le
   cartouche du reste. Remplace la pile de <p> serres du detail de facture. */
.doc-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin-top: var(--space-4);
  padding: var(--space-5);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.doc-head__meta {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}
.doc-head + .table-wrap,
.doc-head + .scrollx,
.doc-head + .stat-strip,
.stat-strip + .table-wrap,
.stat-strip + .scrollx,
.stat-strip + table {
  margin-top: var(--space-4);
}

/* Cercle-photo de la liste des collaborateurs (retour cliente du 17.08) :
   les initiales dans un rond, PLEIN quand la photo vit au coffre du dossier,
   en creux tant qu'elle manque. L'indicateur dit d'un regard qui reste a
   photographier, sans ouvrir chaque fiche. */
.avatar-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  border: 1.5px dashed var(--line-strong);
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.avatar-dot--done {
  border: 1.5px solid var(--navy);
  background: var(--navy);
  color: var(--on-dark);
}

/* Pastille d'identite en tete de fiche (client, entreprise, collaborateur).
   Meme grammaire que le cercle de la liste des collaborateurs (.avatar-dot),
   a la taille d'un en-tete : pleine quand une photo vit au dossier, en creux
   sinon. Elle donne a la fiche un point d'ancrage visuel, et dit d'un regard
   si la photo reste a faire. */
.avatar-rond {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  /* 5rem apres TROIS retours (17, 19.08 jour et 19.08 soir) : le cercle
     porte la PHOTO, il doit permettre de reconnaitre la personne, pas
     seulement d'exister. */
  width: 5rem;
  height: 5rem;
  border-radius: 50%;
  border: 1.5px dashed var(--line-strong);
  color: var(--muted);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  margin-right: var(--space-3);
}

.avatar-rond--done {
  border: 1.5px solid var(--navy);
  background: var(--navy);
  color: var(--on-dark);
}

/* La pastille en GRAND sur l'ecran de creation : le cercle y est le sujet
   (la cliente veut le voir des la creation), pas un rappel de coin de fiche. */
.avatar-rond--xl {
  width: 7rem;
  height: 7rem;
  font-size: 2.1rem;
}

/* La pastille qui DEPOSE : reset de bouton, l'allure reste celle de la
   pastille. Le liseré s'anime au survol pour dire « je me clique ». */
button.avatar-rond--bouton {
  appearance: none;
  padding: 0;
  background: var(--surface);
  cursor: pointer;
  font: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}

button.avatar-rond--bouton:hover {
  border-color: var(--navy);
  border-style: solid;
}

button.avatar-rond--bouton:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

button.avatar-rond--bouton.avatar-rond--done {
  background: var(--navy);
}

button.avatar-rond--bouton:disabled {
  cursor: progress;
  opacity: 0.7;
}

/* La photo dans le rond : elle COUVRE le cercle sans se deformer, comme
   partout ou le motif existe (le geste et l'attente viennent de la). */
.avatar-rond__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  display: block;
}

/* L'affordance du depot (le badge « + » du motif Instagram) : sans lui, le
   cercle cliquable ressemble au cercle statique et personne ne clique. Pose
   sur le BOUTON seulement : le cercle en lecture n'en porte pas. */
button.avatar-rond--bouton {
  position: relative;
}

button.avatar-rond--bouton::after {
  content: "+";
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 1.35rem;
  height: 1.35rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--navy);
  color: var(--on-dark);
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1;
  border: 2px solid var(--surface);
}

/* Les rangees-titres de section (intertitre + bouton d'action) COLLAIENT au
   tableau qui les suit : l'air vit ici, une fois, plutot que dans chaque
   ecran. */
.row--between + table,
.row--between + .scrollx,
.row--between + .stack {
  margin-top: var(--space-3);
}

/* =========================================================================
   BARRE DE CHARGEMENT DE NAVIGATION (retour du 18.08)
   =========================================================================
   Pendant qu'un ecran charge, RIEN ne le disait : un clic muet ressemble a
   une panne, surtout juste apres un deploiement, quand tous les morceaux se
   retelechargent a froid. Un filet marine anime court sous la barre du haut
   tant que la navigation travaille.

   Elle n'apparait qu'apres un court delai (l'ecran qui repond vite ne
   scintille pas) : le delai vit dans le composant, pas ici. Sous mouvement
   reduit, la barre reste visible mais IMMOBILE : l'information (ca charge)
   n'est pas une decoration, seule son animation en est une. */
.nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 60;
  background: color-mix(in srgb, var(--navy) 18%, transparent);
  overflow: hidden;
}

.nav-progress::before {
  content: "";
  position: absolute;
  inset: 0;
  width: 40%;
  background: var(--navy);
  border-radius: 999px;
}

@media (prefers-reduced-motion: no-preference) {
  .nav-progress::before {
    animation: nav-progress-course 1.1s var(--ease-out) infinite;
  }
}

@keyframes nav-progress-course {
  from {
    transform: translateX(-110%);
  }
  to {
    transform: translateX(360%);
  }
}
