/* Système de design de RootIT Lab Learn : docs/design/systeme.md.
   Fichier unique de l'interface : maquettes (docs/maquettes/) et application (seule feuille
   depuis l'étape 7, groupe G7). Référence visuelle de chaque composant et de chaque état :
   docs/maquettes/composants.html.

   Règles de la feuille :
   - couleurs, polices, rayons, ombres et durées : uniquement les jetons de la charte (bloc :root
     ci-dessous, copié tel quel de docs/charte-graphique.md) ; aucune couleur codée en dur ;
   - dimensions : jetons du bloc « html » (espacements en grille de 8 px, 6 tailles de texte,
     hauteurs de contrôle, largeurs) ; aucune couleur dans ce bloc ;
   - le rouge de la marque n'est jamais une couleur d'état ; tout état est écrit (texte ou
     icône), la couleur ne fait que le souligner ;
   - aucune animation ; transitions de couleur courtes, coupées par prefers-reduced-motion et
     pendant une épreuve (body.exam-page) ;
   - classes .is-hover, .is-focus, .is-active : états forcés pour les maquettes et la page de
     référence seulement, jamais dans un gabarit de l'application ;
   - une section par composant ; un sélecteur n'y est déclaré qu'une fois, ses variantes de
     largeur (@media) suivent immédiatement le composant ; une correction modifie la règle du
     composant, jamais une section ajoutée en fin de fichier.

   Couches (@layer), de la plus faible à la plus forte : tokens, base, layout, components,
   screens, utilities. Une couche plus forte l'emporte quelle que soit la spécificité ; dans une
   couche, la spécificité puis l'ordre décident. Les seuls !important (mouvement coupé,
   .phone-only, .desktop-only) l'emportent sur tout. Hors couche (une règle hors couche
   l'emporte sur toutes les couches) : le bloc :root de la charte, que rien ne redéfinit, et en
   fin de fichier l'habillage de CodeMirror. CodeMirror injecte ses propres styles hors couche ;
   dans une couche, nos règles perdraient toujours contre eux, hors couche elles leur restent
   comparées par spécificité.

   Sommaire
   hors couche 1  Jetons de la charte (:root, texte de docs/charte-graphique.md)
   tokens      1  Dimensions (html), points de rupture
   base        2  Base : éléments, focus, mouvement, texte masqué
   layout      3  Mise en page : cadre, colonnes, piles, grilles, sections, fond, page pleine
   components  4  Rôles de texte, lien d'évitement
               5  En-tête de page et de section
               6  Motifs de la charte : invite, segment, barre, numéros, puces, terminal
               7  Icônes
               8  Boutons, barre d'actions
               9  Liens
               10 Champs : libellé, aide, erreur, saisie, unité, liste déroulante
               11 Cases, boutons radio, interrupteur, cartes de choix
               12 Groupes de champs, dépôt de fichier, résumé d'erreurs, lignes éditables
               13 Badges et compteurs
               14 Alertes et notifications
               15 Cartes, chiffres clés, états vides
               16 Tableaux
               17 Listes denses, récapitulatif
               18 Navigation : en-tête, fil d'Ariane, onglets, filtres, pagination, barre basse
               19 Indicateur d'étapes
               20 Menus, menu de l'en-tête, replis
               21 Fenêtres de dialogue, feuilles, voile
               22 Progression et chargement
               23 Chrono, état d'enregistrement, navigateur de questions
               24 Question de la copie et types de réponse
               25 Code : coloration, barre de saisie, disposition, sorties, lecture seule, copie
               26 Live : code projeté, choix, verdict, classement, barre de pilotage
               27 Pied des pages publiques
   screens     28 Copie étudiante : bandeau, disposition, une question à l'écran
               29 Accueil et connexion
               30 Correction et aperçu
               31 Copies d'une session : cartes compactes
               32 Écran projeté : échelle, classement final, phase dense
   utilities   33 Affichage selon la largeur, couleurs forcées
   hors couche 34 CodeMirror : éditeur, rendu statique des maquettes

   Points de rupture (les @media ne lisent pas les variables) :
   < 48rem (768) téléphone · 48rem tablette · 64rem (1024) PC · 80rem (1280) PC large.
   Vérifiés à 360, 390, 430, 768, 1280, 1440 et 1920 px. */

@layer tokens, base, layout, components, screens, utilities;

@font-face {
  font-family: "Inter";
  src: url("../fonts/inter.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/space-grotesk.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* --- 1. Jetons ---------------------------------------------------------------------------- */

/* Couleurs, polices, formes et durées : bloc :root de docs/charte-graphique.md, copié tel quel
   (même texte, vérifié à l'identique). Hors couche : rien ne redéfinit ces jetons. */
:root {
  /* Fonds et surfaces */
  --color-bg: #0a0b0d;            /* fond de page */
  --color-surface: #131519;       /* cartes, panneaux */
  --color-surface-2: #1a1d22;     /* champ, ligne survolée, en-tête de tableau */
  --color-border: rgb(255 255 255 / 0.09);
  --color-border-strong: rgb(255 255 255 / 0.18);

  /* Texte */
  --color-text-strong: #f5f7f9;   /* titres */
  --color-text: #e9ebee;          /* texte courant */
  --color-text-2: #bbbec2;        /* paragraphes secondaires */
  --color-muted: #9ea2a6;         /* aides, métadonnées */

  /* Rouge RootIT */
  --red-300: #ffa195;
  --red-400: #ff6b5f;             /* liens et petit texte rouge */
  --red-500: #f0101d;             /* accent : mots mis en avant, barres, puces, numéros */
  --red-600: #d80015;             /* bouton principal */
  --red-700: #ad000e;             /* bouton principal survolé */
  --color-glow: rgb(240 16 29 / 0.45);

  /* États */
  --color-success: #50ba75;
  --color-warning: #d4940c;
  --color-error: #ff6a6a;
  --color-focus: #f5f7f9;

  /* Typographie */
  --font-display: "Space Grotesk", ui-sans-serif, system-ui, "Segoe UI", sans-serif;
  --font-body: "Inter", ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace;

  /* Formes et ombres */
  --radius-sm: 6px; --radius-md: 10px; --radius-lg: 16px; --radius-full: 999px;
  --shadow-md: 0 8px 24px -8px rgb(0 0 0 / 0.55);
  --shadow-glow: 0 0 0 1px rgb(240 16 29 / 0.35), 0 10px 40px -10px rgb(240 16 29 / 0.45);
  --dur: 280ms; --dur-fast: 160ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}

@layer tokens {
  /* Dimensions du système (aucune couleur) : grille de 8 px ; 4 px pour les seuls micro-écarts
     (icône et texte, libellé et champ, badge). */
  html {
    --space-xs: 0.25rem;  /* 4 */
    --space-s: 0.5rem;    /* 8 */
    --space-m: 1rem;      /* 16 */
    --space-l: 1.5rem;    /* 24 */
    --space-xl: 2rem;     /* 32 */
    --space-2xl: 3rem;    /* 48 */
    --space-3xl: 4rem;    /* 64 */

    /* Six tailles de texte, chacune avec son interlignage en multiple de 4 px. */
    --text-sm: 0.875rem;      --leading-sm: 1.25rem;      /* 14/20 méta, aides, en-têtes */
    --text-md: 1rem;          --leading-md: 1.5rem;       /* 16/24 corps, contrôles */
    --text-lg: 1.125rem;      --leading-lg: 1.75rem;      /* 18/28 énoncés, titres de section */
    --text-xl: 1.5rem;        --leading-xl: 2rem;         /* 24/32 titre de page, numéros */
    --text-2xl: 2rem;         --leading-2xl: 2.5rem;      /* 32/40 chiffres, chrono, projeté */
    --text-display: 4.5rem;   --leading-display: 5rem;    /* 72/80 code de session projeté */

    --size-icon: 1.25rem;          /* 20 */
    --size-icon-sm: 1rem;          /* 16 */
    --size-control: 2.5rem;        /* 40 : bouton, champ (pointeur fin) */
    --size-control-sm: 2rem;       /* 32 : bouton compact (tableaux, PC seulement) */
    --size-touch: 3rem;            /* 48 : toute cible au doigt */
    --size-header: 4rem;           /* 64 */
    --size-bar: 3.5rem;            /* 56 : bandeau de copie et en-tête au téléphone */
    --size-bottom-bar: 4rem;       /* 64 */

    --content-max: 75rem;          /* 1 200 : cadre unique des pages enseignant */
    --form-max: 45rem;             /* 720 : colonne des formulaires */
    --measure: 34rem;              /* ≈ 70 caractères : texte courant */
    --gutter: 1rem;                /* marge latérale : 16 / 24 / 40 selon la largeur */
    --nav-column: 14rem;           /* 224 : navigateur de questions */
    --question-column: 47.5rem;    /* 760 : colonne des questions */

    color-scheme: dark;
    font-size: 100%;
    scroll-padding-top: calc(var(--size-header) + var(--space-m));
    -webkit-text-size-adjust: 100%;
  }

  @media (min-width: 48rem) {
    html { --gutter: 1.5rem; }
  }

  @media (min-width: 64rem) {
    html { --gutter: 2.5rem; }
  }

  /* Au doigt : toute cible passe à 48 px. */
  @media (max-width: 47.9375rem), (pointer: coarse) {
    html {
      --size-control: var(--size-touch);
      --size-control-sm: var(--size-touch);
    }
  }
}

/* --- 2. Base ------------------------------------------------------------------------------ */

@layer base {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  body {
    margin: 0;
    background: var(--color-bg);
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: var(--text-md);
    line-height: 1.65; /* charte : texte courant */
    overflow-wrap: break-word;
  }

  h1,
  h2,
  h3 {
    margin: 0;
    color: var(--color-text-strong);
    font-family: var(--font-display);
    font-weight: 600;
  }

  h1 {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  h2 {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  h3 {
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  p,
  ul,
  ol,
  dl,
  figure,
  blockquote {
    margin: 0;
  }

  /* Fin de ligne : pas de mot isolé (règles, aides, énoncés). */
  p {
    max-width: var(--measure);
    text-wrap: pretty;
  }

  li {
    text-wrap: pretty;
  }

  ul,
  ol {
    padding-left: var(--space-l);
  }

  img,
  svg {
    display: block;
    max-width: 100%;
  }

  input,
  textarea,
  select,
  button {
    font: inherit;
  }

  code,
  kbd,
  samp,
  pre {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
  }

  kbd {
    display: inline-block;
    min-width: var(--space-l);
    padding: 0 var(--space-xs);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text);
    line-height: var(--leading-sm);
    text-align: center;
  }

  hr {
    height: 1px;
    margin: var(--space-l) 0;
    border: 0;
    background: var(--color-border);
  }

  strong,
  b {
    font-weight: 600;
  }

  /* Focus clavier : anneau de la charte, toujours visible. Un titre ou une région qui reçoit le
     focus par script (tabindex="-1", annonce d'une nouvelle phase) ne dessine rien : il n'est pas
     interactif (constat UI, « Question 01 » du live). */
  :focus-visible,
  .is-focus {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
  }

  [tabindex="-1"]:focus,
  [tabindex="-1"]:focus-visible {
    outline: none;
  }

  /* Mouvement : transitions de couleur seulement, coupées si l'utilisateur le demande et
     toujours pendant une épreuve. Aucune animation dans tout le système. */
  .btn,
  .link,
  a,
  .nav-link,
  .tab,
  .card-link,
  .item,
  .choice,
  .menu-item,
  .qnav-item,
  .session-indicator {
    transition:
      background-color var(--dur-fast) var(--ease-out),
      border-color var(--dur-fast) var(--ease-out),
      color var(--dur-fast) var(--ease-out),
      box-shadow var(--dur) var(--ease-out);
  }

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation: none !important;
      transition: none !important;
      scroll-behavior: auto !important;
      caret-animation: manual !important;
    }
  }

  .exam-page *,
  .exam-page *::before,
  .exam-page *::after {
    animation: none !important;
    transition: none !important;
    caret-animation: manual !important;
  }

  /* #timer-alert : région d'annonce du chrono (« Moins de 5 minutes »…), lue, jamais vue. */
  .visually-hidden,
  .timer-alert {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* --- 3. Mise en page ---------------------------------------------------------------------- */

@layer layout {
  /* Cadre unique : 1 200 px de contenu centré (marges de 120 px à 1440), marges de 40 / 24 /
     16 px en dessous. */
  .container {
    width: 100%;
    max-width: calc(var(--content-max) + 2 * var(--gutter));
    margin-inline: auto;
    padding-inline: var(--gutter);
  }

  .page {
    padding-block: var(--space-l) var(--space-3xl);
  }

  /* Colonne étroite : rejoindre, attente, erreurs, copie remise. */
  .page-narrow {
    max-width: 30rem;
    margin-inline: auto;
  }

  /* Colonne des écrans étudiant à contenu long (copie remise) : celle des questions. */
  .page-column {
    max-width: var(--question-column);
    margin-inline: auto;
  }

  .stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-m);
  }

  .stack-s {
    display: flex;
    flex-direction: column;
    gap: var(--space-s);
  }

  .stack-l {
    display: flex;
    flex-direction: column;
    gap: var(--space-l);
  }

  .stack-xl {
    display: flex;
    flex-direction: column;
    gap: var(--space-xl);
  }

  .cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-s);
  }

  .split {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-m);
  }

  .grid-2,
  .grid-3 {
    display: grid;
    gap: var(--space-m);
  }

  @media (min-width: 48rem) {
    .grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  }

  .section {
    display: flex;
    flex-direction: column;
    gap: var(--space-m);
  }

  /* L'espace entre sections ne s'ajoute pas dans une pile (elle gère déjà l'écart). */
  :where(:not(.stack, .stack-s, .stack-l, .stack-xl, .grid-2, .grid-3)) > .section + .section {
    margin-top: var(--space-xl);
  }

  .section-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-s) var(--space-m);
    min-height: var(--size-control);
  }

  /* Fond de page à grille et halo : accueil, connexion, tableau de bord, pilotage, projection.
     Jamais derrière un énoncé ni l'éditeur. */
  body.has-backdrop {
    min-height: 100vh;
    background:
      radial-gradient(circle at 100% 0, color-mix(in srgb, var(--color-glow) 30%, transparent), transparent 45rem),
      linear-gradient(color-mix(in srgb, var(--color-text-strong) 3%, transparent) 1px, transparent 1px) 0 0 / 48px 48px,
      linear-gradient(90deg, color-mix(in srgb, var(--color-text-strong) 3%, transparent) 1px, transparent 1px) 0 0 / 48px 48px,
      var(--color-bg);
    background-repeat: no-repeat, repeat, repeat;
  }

  /* Accueil : la composition occupe l'écran, le pied reste en bas de la fenêtre. */
  body.page-fill {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
  }

  body.page-fill > main {
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: center;
  }
}

@layer components {
  /* --- 4. Rôles de texte, lien d'évitement ------------------------------------------------ */

  /* Hauteur d'un contrôle : 48 px au doigt. */
  .skip-link {
    position: absolute;
    top: var(--space-s);
    left: var(--space-s);
    z-index: 30;
    display: inline-flex;
    align-items: center;
    min-height: var(--size-control);
    padding: 0 var(--space-m);
    border-radius: var(--radius-md);
    background: var(--color-surface-2);
    color: var(--color-text-strong);
    translate: 0 -200%;
  }

  .skip-link:focus {
    translate: 0 0;
  }

  /* Texte : rôles, pas de tailles en dehors de l'échelle. */
  .text-sm {
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .text-lg {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  .text-2 {
    color: var(--color-text-2);
  }

  .text-muted {
    color: var(--color-muted);
  }

  .text-strong {
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .mono {
    font-family: var(--font-mono);
  }

  .tabular {
    font-variant-numeric: tabular-nums;
  }

  .truncate {
    display: inline-block;
    max-width: 100%;
    vertical-align: bottom;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .display {
    color: var(--color-text-strong);
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 600;
    line-height: var(--leading-2xl);
  }

  .lead {
    color: var(--color-text-2);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  /* --- 5. En-tête de page et de section --------------------------------------------------- */

  /* En-tête de page : fil d'Ariane, titre d'une ligne, méta, action principale à droite. */
  .page-header {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-xs) var(--space-l);
    align-items: center;
    margin-bottom: var(--space-l);
  }

  .page-header > .breadcrumb,
  .page-header > .prompt-line {
    grid-column: 1 / -1;
  }

  .page-header > :is(.steps, .steps-adaptive, .tabs) {
    grid-column: 1 / -1;
    margin-top: var(--space-m);
  }

  .page-title {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-s) var(--space-m);
    min-width: 0;
  }

  .page-title h1 {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .page-meta {
    grid-column: 1;
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .page-actions {
    grid-column: 2;
    grid-row: span 2;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-s);
  }

  /* Bouton qui envoie un formulaire (Dupliquer) : le formulaire ne compte pas dans la rangée. */
  .page-actions > form {
    display: contents;
  }

  /* Sous 1 024 px : actions sous la méta, dans l'ordre du document, pour que le titre et la
     méta gardent toute la largeur (à côté, elles écrasaient le titre en tablette). */
  @media (max-width: 63.9375rem) {
    .page-header {
      grid-template-columns: minmax(0, 1fr);
    }

    .page-actions {
      grid-column: 1;
      grid-row: auto;
      justify-content: flex-start;
      margin-top: var(--space-s);
    }
  }

  /* Au téléphone : principal en premier, pleine largeur ; les autres boutons à libellé
     occupent toute la ligne restante, « … » à côté. */
  @media (max-width: 47.9375rem) {
    .page-title h1 {
      white-space: normal;
    }

    .page-actions .btn-primary {
      flex: 1 1 100%;
      order: -1;
    }

    .page-actions .btn:not(.btn-icon) {
      flex: 1 1 auto;
    }

    .page-actions .btn.btn-primary:not(.btn-icon) {
      flex-basis: 100%;
    }
  }

  /* Lien d'en-tête de section (« Tout voir ») : cible de 32 px au pointeur fin, 48 au doigt. */
  .section-header > a {
    display: inline-flex;
    align-items: center;
    min-height: var(--size-control-sm);
  }

  /* --- 6. Motifs de la charte (emploi réglé : docs/design/systeme.md, « Motifs ») ---------- */

  /* Invite de terminal : accueil, connexion, tableau de bord, écrans projetés seulement. */
  .prompt-line {
    margin: 0 0 var(--space-xs);
    color: var(--red-400);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  /* Segment rouge d'un titre de 24 px au moins. */
  .title-accent {
    color: var(--red-500);
  }

  /* Barre rouge de 40 × 4 px : un titre de section par écran de présentation. */
  .title-bar::before {
    content: "";
    display: block;
    width: 40px;
    height: 4px;
    margin-bottom: var(--space-s);
    background: var(--red-500);
  }

  /* Numéro mono rouge 01, 02 : questions, étapes, classement. 24 px (rouge 500 permis). */
  .number-tag {
    color: var(--red-500);
    font-family: var(--font-mono);
    font-size: var(--text-xl);
    font-weight: 600;
    line-height: var(--leading-xl);
  }

  /* Même motif en petit (listes denses) : rouge 400, lisible à 14 px. */
  .number-sm {
    color: var(--red-400);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: var(--leading-sm);
    font-variant-numeric: tabular-nums;
  }

  /* Puces « > » des listes de contenu. */
  .bullets {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding: 0;
    list-style: none;
  }

  .bullets > li {
    position: relative;
    max-width: var(--measure);
    padding-left: var(--space-l);
  }

  .bullets > li::before {
    content: ">" / "";
    position: absolute;
    left: 0;
    color: var(--red-500);
    font-family: var(--font-mono);
  }

  /* Fenêtre de terminal : sorties de code, résultats de test, code projeté. */
  .terminal {
    overflow: hidden;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
  }

  .terminal-bar {
    display: flex;
    align-items: center;
    gap: var(--space-m);
    min-height: var(--space-xl);
    padding: var(--space-xs) var(--space-m);
    border-bottom: 1px solid var(--color-border);
    background: var(--color-surface-2);
    color: var(--color-muted);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .terminal-dots {
    display: flex;
    flex: none;
    gap: var(--space-s);
  }

  .terminal-dots span {
    width: 12px;
    height: 12px;
    border-radius: var(--radius-full);
    background: var(--red-500);
  }

  .terminal-dots span:nth-child(2) {
    background: var(--color-warning);
  }

  .terminal-dots span:nth-child(3) {
    background: var(--color-success);
  }

  .terminal-title {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .terminal-body {
    padding: var(--space-m);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  /* --- 7. Icônes (Lucide, sprite static/icons/lucide-<version>.svg) ----------------------- */

  .icon {
    flex: none;
    width: var(--size-icon);
    height: var(--size-icon);
    stroke-width: 2;
  }

  .icon-sm {
    width: var(--size-icon-sm);
    height: var(--size-icon-sm);
  }

  .icon-success { color: var(--color-success); }
  .icon-warning { color: var(--color-warning); }
  .icon-error { color: var(--color-error); }
  .icon-muted { color: var(--color-muted); }

  /* --- 8. Boutons, barre d'actions -------------------------------------------------------- */
  /* Quatre rôles, une apparence chacun : principal (un par écran), secondaire, discret, danger
     (seulement dans une fenêtre de confirmation). Tailles : standard (40 px, 48 au doigt),
     compacte (32 px, tableaux et listes denses au PC), projetée (.btn-lg). */

  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-s);
    min-height: var(--size-control);
    padding: 0 var(--space-m);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: var(--text-md);
    font-weight: 600;
    line-height: var(--leading-md);
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
  }

  .btn-primary {
    border-color: var(--red-600);
    background: var(--red-600);
    color: var(--color-text-strong);
  }

  .btn-primary:hover,
  .btn-primary.is-hover {
    border-color: var(--red-700);
    background: var(--red-700);
  }

  /* Lueur de la charte au survol du principal, hors épreuve. */
  body:not(.exam-page) .btn-primary:not(:disabled, [aria-disabled="true"], [aria-busy="true"]):is(:hover, .is-hover) {
    box-shadow: var(--shadow-glow);
  }

  .btn-primary:active,
  .btn-primary.is-active {
    border-color: var(--red-700);
    background: var(--red-700);
    box-shadow: none;
  }

  .btn-secondary {
    border-color: var(--color-border-strong);
    background: transparent;
    color: var(--color-text);
  }

  .btn-secondary:hover,
  .btn-secondary.is-hover {
    border-color: var(--color-muted);
    background: var(--color-surface-2);
  }

  .btn-secondary:active,
  .btn-secondary.is-active {
    background: var(--color-surface);
  }

  .btn-quiet {
    color: var(--color-text-2);
  }

  .btn-quiet:hover,
  .btn-quiet.is-hover {
    background: var(--color-surface-2);
    color: var(--color-text-strong);
  }

  .btn-quiet:active,
  .btn-quiet.is-active {
    background: var(--color-surface);
  }

  .btn.btn-quiet:disabled,
  .btn.btn-quiet[aria-disabled="true"] {
    background: transparent;
  }

  /* Danger plein : texte sombre sur --color-error (7,1:1) ; survol assombri (5,3:1). Jamais le
     rouge de la marque. */
  .btn-danger {
    border-color: var(--color-error);
    background: var(--color-error);
    color: var(--color-bg);
  }

  .btn-danger:hover,
  .btn-danger.is-hover,
  .btn-danger:active,
  .btn-danger.is-active {
    border-color: color-mix(in srgb, var(--color-error) 85%, var(--color-bg));
    background: color-mix(in srgb, var(--color-error) 85%, var(--color-bg));
  }

  /* Désactivé : même rendu pour les quatre rôles, sans bordure (distinct d'un secondaire). Un
     bouton aria-disabled reste focalisable (exécution en cours, raison annoncée). */
  .btn:disabled,
  .btn[aria-disabled="true"] {
    border-color: transparent;
    background: var(--color-surface-2);
    color: var(--color-muted);
    box-shadow: none;
    cursor: not-allowed;
  }

  /* Chargement : aria-busy="true" ; le bouton garde sa couleur et sa largeur, l'icône
     « loader » remplace l'icône d'action, le libellé dit l'action en cours (« Exécution… »).
     Aucune rotation. */
  .btn[aria-busy="true"] {
    cursor: progress;
  }

  .btn-sm {
    min-height: var(--size-control-sm);
    padding: 0 var(--space-s);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .btn-lg {
    min-height: var(--space-3xl);
    padding: 0 var(--space-l);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  .btn-block {
    width: 100%;
  }

  /* Bouton icône seule : carré, nom accessible obligatoire (aria-label). */
  .btn-icon {
    width: var(--size-control);
    padding: 0;
  }

  .btn-icon.btn-sm {
    width: var(--size-control-sm);
  }

  /* Infobulle d'un bouton à icône seule (panel P1-55) : reprend son nom accessible, au survol
     (pointeur) et au focus clavier ; jamais d'attribut title. Au-dessus du bouton : elle ne
     passe pas sous la ligne suivante d'une liste ; sous lui dans l'en-tête. Décorative pour les
     lecteurs d'écran (le nom est déjà lu). Masquée par display : elle n'élargit jamais la page.
     WCAG 1.4.13 (décision I-14) : Échap la masque (data-tooltip="off", posé par
     static/js/ui.js tant que le pointeur ou le focus reste sur le bouton), comme un focus venu
     d'un clic du pointeur (data-tooltip="pointer", jusqu'à la perte du focus) ; écartée de 8 px,
     au-delà de l'anneau de focus (2 + 3 px) ; au survol, un pont transparent (::before) couvre
     l'écart : le pointeur passe du bouton à l'infobulle sans la perdre ; un clic sur
     l'infobulle ou le pont ne déclenche rien (static/js/ui.js annule tout clic du pointeur hors
     du bouton) ; masquée quand le bouton a ouvert son menu (aria-expanded), qu'elle
     couvrirait. */
  .btn-icon[aria-label] {
    position: relative;
  }

  .btn-icon[aria-label]::after {
    content: attr(aria-label) / "";
    position: absolute;
    right: 0;
    bottom: calc(100% + var(--space-s));
    z-index: 30;
    display: none;
    width: max-content;
    max-width: 16rem;
    padding: var(--space-xs) var(--space-s);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    box-shadow: var(--shadow-md);
    color: var(--color-text-strong);
    font-size: var(--text-sm);
    font-weight: 400;
    line-height: var(--leading-sm);
    text-align: left;
    white-space: normal;
  }

  .btn-icon[aria-label]::before {
    content: "";
    position: absolute;
    right: 0;
    bottom: 100%;
    left: 0;
    z-index: 30;
    display: none;
    height: var(--space-s);
  }

  .app-header .btn-icon[aria-label]::after {
    top: calc(100% + var(--space-s));
    bottom: auto;
  }

  .app-header .btn-icon[aria-label]::before {
    top: 100%;
    bottom: auto;
  }

  .btn-icon[aria-label]:not([data-tooltip], [aria-expanded="true"]):focus-visible::after,
  .btn-icon[aria-label].is-hover::after,
  .btn-icon[aria-label].is-focus::after {
    display: block;
  }

  @media (hover: hover) {
    .btn-icon[aria-label]:not([data-tooltip], [aria-expanded="true"]):hover::after,
    .btn-icon[aria-label]:not([data-tooltip], [aria-expanded="true"]):hover::before {
      display: block;
    }
  }

  /* Bouton discret qui commence (.btn-flush) ou termine (.btn-flush-end) une ligne : aligné
     optiquement sur la gouttière ; .btn-flush-phone : au téléphone seulement. */
  .btn-flush {
    margin-inline-start: calc(-1 * var(--space-m));
  }

  .btn-sm.btn-flush {
    margin-inline-start: calc(-1 * var(--space-s));
  }

  .btn-flush-end {
    margin-inline-end: calc(-1 * var(--space-m));
  }

  .btn-sm.btn-flush-end {
    margin-inline-end: calc(-1 * var(--space-s));
  }

  /* Au téléphone : action principale pleine largeur (.btn-phone-block) ; un bouton pleine
     largeur peut passer à la ligne. */
  @media (max-width: 47.9375rem) {
    .btn-flush-phone {
      margin-inline-start: calc(-1 * var(--space-m));
    }

    .btn-sm.btn-flush-phone {
      margin-inline-start: calc(-1 * var(--space-s));
    }

    .btn-flush-end {
      margin-inline-end: 0;
    }

    .btn-block {
      padding-block: var(--space-s);
      white-space: normal;
    }

    .btn-phone-block {
      width: 100%;
      padding-block: var(--space-s);
      white-space: normal;
    }
  }

  .btn-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-s);
  }

  /* Bouton qui envoie son propre formulaire (décision d'une demande de reprise) : compté dans
     le groupe comme les autres. */
  .btn-group > form {
    display: contents;
  }

  /* Choix à conséquence (demande de reprise) : au téléphone, boutons empilés pleine largeur,
     dans l'ordre inverse (l'action principale, écrite en dernier, en haut). */
  @media (max-width: 47.9375rem) {
    .btn-group-stack {
      flex-direction: column-reverse;
      align-items: stretch;
    }

    .btn-group-stack .btn {
      white-space: normal;
    }
  }

  /* Barre d'actions des formulaires longs : collante en bas, action principale à droite,
     « Annuler » en discret à gauche d'elle. */
  .action-bar {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-s);
    margin-top: var(--space-xl);
    padding: var(--space-m) 0;
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
  }

  .action-bar .action-bar-start {
    margin-right: auto;
  }

  /* Ligne d'état au-dessus des boutons (étape 3 de « Préparer » : « 2 exercices n'ont pas de
     test réussi. ») ; à droite, au-dessus de l'action principale. */
  .action-bar-note {
    flex: 1 1 100%;
    max-width: none;
    color: var(--color-text-2);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    text-align: right;
  }

  /* Au téléphone, barre non collante : principal, secondaires, puis « Précédent » et
     « Annuler », tous pleine largeur. */
  @media (max-width: 47.9375rem) {
    .action-bar {
      position: static;
      justify-content: stretch;
    }

    .action-bar > .btn {
      flex: 1 1 100%;
      margin: 0;
    }

    .action-bar > .btn-primary {
      order: -1;
    }

    .action-bar > .btn-quiet,
    .action-bar > .action-bar-start {
      order: 1;
    }

    .action-bar > .action-bar-note {
      order: -2;
      text-align: left;
    }
  }

  /* --- 9. Liens --------------------------------------------------------------------------- */

  /* Spécificité nulle (:where) : un lien stylé par une classe (bouton, carte, ligne, lien
     d'évitement) garde sa couleur au survol. */
  :where(a),
  .link {
    color: var(--red-400);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
  }

  :where(a:hover),
  .link:hover,
  .link.is-hover {
    color: var(--red-300);
  }

  /* Lien qui ne quitte pas l'interface (ligne cliquable, titre d'objet) : sans soulignement,
     qui apparaît au survol. */
  .link-quiet {
    color: var(--color-text-strong);
    text-decoration: none;
  }

  .link-quiet:hover,
  .link-quiet.is-hover {
    color: var(--color-text-strong);
    text-decoration: underline;
  }

  .link-external {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
  }

  /* --- 10. Champs ------------------------------------------------------------------------- */
  /* Ordre fixe : libellé, champ, aide ou erreur sous le champ. Pas de « : » après un libellé ;
     « (facultatif) » sur les champs facultatifs. */

  .field {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    max-width: var(--form-max);
  }

  /* Formulaire : colonne de 720 px, 24 px entre champs et groupes. */
  .form {
    display: flex;
    flex-direction: column;
    gap: var(--space-l);
    max-width: var(--form-max);
  }

  .label {
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: var(--leading-sm);
  }

  /* « (facultatif) » d'un libellé ou d'une légende (distracteurs) : texte du corps, atténué. */
  .label .optional,
  legend .optional {
    color: var(--color-muted);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 400;
  }

  .hint {
    max-width: var(--measure);
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    text-wrap: pretty;
  }

  .field-error {
    display: flex;
    align-items: flex-start;
    gap: var(--space-xs);
    color: var(--color-error);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: var(--leading-sm);
  }

  .field-error .icon {
    width: var(--size-icon-sm);
    height: var(--size-icon-sm);
    margin-top: 2px;
  }

  /* Écrans étudiant : une erreur est lue en 16 px, jamais en 14. */
  .student-page .field-error,
  .exam-page .field-error {
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  .input,
  .select select,
  .textarea {
    width: 100%;
    min-height: var(--size-control);
    padding: var(--space-s) var(--space-m);
    border: 1px solid var(--color-muted);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text);
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  .input::placeholder,
  .textarea::placeholder {
    color: var(--color-muted);
  }

  .input:hover,
  .input.is-hover,
  .select select:hover,
  .textarea:hover {
    border-color: var(--color-text-2);
  }

  .input:focus-visible,
  .select select:focus-visible,
  .textarea:focus-visible {
    border-color: var(--color-text-strong);
  }

  /* Erreur : bordure, message écrit sous le champ (aria-invalid + aria-describedby). */
  .input[aria-invalid="true"],
  .select select[aria-invalid="true"],
  .textarea[aria-invalid="true"] {
    border-color: var(--color-error);
  }

  .input:disabled,
  .select select:disabled,
  .textarea:disabled {
    border-color: var(--color-border-strong);
    background: transparent;
    color: var(--color-muted);
    cursor: not-allowed;
  }

  .input[readonly],
  .textarea[readonly] {
    border-style: dashed;
    border-color: var(--color-border-strong);
    background: transparent;
  }

  .textarea {
    min-height: calc(4 * var(--leading-md) + 2 * var(--space-s));
    resize: vertical;
  }

  /* Champs courts : nombres, codes ; recherche au-dessus d'une liste. */
  .input-short {
    max-width: 8rem;
  }

  .input-code {
    max-width: 12rem;
    font-family: var(--font-mono);
    letter-spacing: 0.1em;
    text-transform: uppercase;
  }

  .input-search {
    max-width: 20rem;
  }

  /* Recherche dans son propre formulaire (liste des sessions) : le formulaire prend la largeur
     du champ dans une rangée .split. */
  .split > form:has(> .input-search) {
    flex: 0 1 20rem;
  }

  /* Champ avec unité (« pt », « min », « s », « Mo », « % »). */
  .input-group {
    display: flex;
    align-items: stretch;
    max-width: 10rem;
  }

  .input-group .input {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
  }

  .input-addon {
    display: inline-flex;
    white-space: nowrap;
    align-items: center;
    padding: 0 var(--space-s);
    border: 1px solid var(--color-muted);
    border-left: 0;
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    background: var(--color-surface);
    color: var(--color-muted);
    font-size: var(--text-sm);
  }

  /* Champ à unité et bouton voisin : même hauteur, alignés sur le bas. */
  .field-inline {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-s);
  }

  /* Bouton au libellé long (« Revenir aux points automatiques ») : passe à la ligne plutôt
     que de dépasser à 320 px. */
  .field-inline > .btn {
    max-width: 100%;
    white-space: normal;
  }

  /* Champ qui prend la place libre (titre au renommage), dans la colonne des formulaires ; au
     téléphone, pleine largeur et bouton dessous. */
  .field-inline-grow {
    max-width: var(--form-max);
  }

  .field-inline-grow > .field {
    flex: 1 1 20rem;
  }

  /* Liste déroulante : <span class="select"><select>…</select><svg class="icon">chevron</svg></span>. */
  .select {
    position: relative;
    display: block;
    max-width: var(--form-max);
  }

  .select select {
    padding-right: var(--space-2xl);
    appearance: none;
    cursor: pointer;
  }

  .select > .icon {
    position: absolute;
    top: 50%;
    right: var(--space-m);
    color: var(--color-text-2);
    pointer-events: none;
    translate: 0 -50%;
  }

  /* --- 11. Cases, boutons radio, interrupteur, cartes de choix ---------------------------- */

  /* Cases à cocher et boutons radio : 20 px, dans une étiquette cible de 40 px (48 au doigt). */
  /* Case qui révèle un champ (« Je reprends un devoir… » de Rejoindre) : le champ ne
     s'affiche que case cochée ; sans :has(), il reste affiché (repli lisible). */
  .reveal:not(:has(> .check :checked)) > .reveal-body {
    display: none;
  }

  .check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-s);
    min-height: var(--size-control);
    padding-block: var(--space-s);
    color: var(--color-text);
    cursor: pointer;
  }

  .check-text {
    display: flex;
    flex-direction: column;
  }

  .check-text .hint {
    margin-top: 2px;
  }

  .checkbox,
  .radio {
    flex: none;
    width: 20px;
    height: 20px;
    margin: 2px 0 0;
    border: 1px solid var(--color-muted);
    background: var(--color-surface-2);
    appearance: none;
    cursor: pointer;
  }

  .checkbox {
    position: relative;
    border-radius: var(--radius-sm);
  }

  .radio {
    border-radius: var(--radius-full);
  }

  .checkbox:hover,
  .radio:hover,
  .check:hover .checkbox,
  .check:hover .radio {
    border-color: var(--color-text-2);
  }

  .checkbox:checked {
    border-color: var(--color-text-strong);
    background: var(--color-text-strong);
  }

  /* Coche dessinée par deux bordures : aucune image. */
  .checkbox:checked::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 6px;
    width: 6px;
    height: 10px;
    border: solid var(--color-bg);
    border-width: 0 2px 2px 0;
    rotate: 45deg;
  }

  .radio:checked {
    border-width: 6px;
    border-color: var(--color-text-strong);
    background: var(--color-bg);
  }

  .checkbox:indeterminate::after {
    content: "";
    position: absolute;
    top: 8px;
    left: 4px;
    width: 10px;
    height: 2px;
    background: var(--color-bg);
  }

  .checkbox:indeterminate {
    border-color: var(--color-text-strong);
    background: var(--color-text-strong);
  }

  .checkbox[aria-invalid="true"],
  .radio[aria-invalid="true"] {
    border-color: var(--color-error);
  }

  .check:has(:disabled) {
    color: var(--color-muted);
    cursor: not-allowed;
  }

  .checkbox:disabled,
  .radio:disabled {
    border-color: var(--color-border-strong);
    background: transparent;
    color: var(--color-muted);
    cursor: not-allowed;
  }

  /* Interrupteur oui/non (réglage immédiat) : <input type="checkbox" role="switch" class="switch">. */
  .switch {
    position: relative;
    flex: none;
    width: 40px;
    height: 24px;
    margin: 0;
    border: 1px solid var(--color-muted);
    border-radius: var(--radius-full);
    background: var(--color-surface-2);
    appearance: none;
    cursor: pointer;
  }

  .switch::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 16px;
    height: 16px;
    border-radius: var(--radius-full);
    background: var(--color-text-2);
  }

  .switch:checked {
    border-color: var(--color-text-strong);
    background: var(--color-text-strong);
  }

  .switch:checked::after {
    left: 19px;
    background: var(--color-bg);
  }

  .switch:disabled {
    border-color: var(--color-border-strong);
    background: transparent;
    cursor: not-allowed;
  }

  .check:has(.switch) {
    align-items: center;
    justify-content: space-between;
    max-width: var(--form-max);
  }

  /* Carte de choix : mode de session, langage, type de question, QCM de l'étudiant. Le bouton
     radio reste visible ; carte entière cliquable (étiquette). */
  .choice {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--space-m);
    min-height: var(--size-touch);
    padding: var(--space-m);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    cursor: pointer;
  }

  .choice:hover,
  .choice.is-hover {
    border-color: var(--color-muted);
    background: var(--color-surface-2);
  }

  .choice:has(:checked) {
    border-color: var(--color-text-strong);
    background: var(--color-surface-2);
  }

  .choice:has(:focus-visible) {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
  }

  .choice:has(:disabled) {
    border-style: dashed;
    border-color: var(--color-border-strong);
    background: transparent;
    color: var(--color-muted);
    cursor: not-allowed;
  }

  .choice:has([aria-invalid="true"]) {
    border-color: var(--color-error);
  }

  .choice-title {
    display: block;
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .choice-desc {
    display: block;
    color: var(--color-text-2);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .choice:has(:disabled) .choice-title,
  .choice:has(:disabled) .choice-desc {
    color: var(--color-muted);
  }

  /* Réponse d'un QCM côté étudiant : texte de l'énoncé (18 px), cible de 56 px. */
  .choice-answer {
    align-items: center;
    min-height: var(--size-bar);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  .choice-list {
    display: grid;
    gap: var(--space-s);
    margin: 0;
    padding: 0;
    border: 0;
    list-style: none;
  }

  /* Réponses en deux colonnes (vrai / faux) dès 480 px. */
  @media (min-width: 30rem) {
    .choice-list.cols-2 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  /* --- 12. Groupes de champs, dépôt de fichier, résumé d'erreurs, lignes éditables -------- */

  /* Groupe de champs sans cadre (remplace les fieldset imbriqués). */
  .form-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-m);
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
  }

  .form-section > legend:not(.visually-hidden) {
    float: left; /* légende dans le flux : alignée comme un titre */
    width: 100%;
    margin-bottom: var(--space-xs);
    padding: 0;
    color: var(--color-text-strong);
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 600;
    line-height: var(--leading-lg);
  }

  .form-section > legend:not(.visually-hidden) + * {
    clear: both;
  }

  /* Dépôt de fichier : <label class="file-drop"><input type="file" class="visually-hidden">…. */
  .file-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-s);
    min-height: 10rem;
    padding: var(--space-l);
    border: 1px dashed var(--color-muted);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
    color: var(--color-text-2);
    text-align: center;
    cursor: pointer;
  }

  .file-drop:hover,
  .file-drop.is-hover,
  .file-drop.is-dragover {
    border-color: var(--color-text-strong);
    background: var(--color-surface-2);
  }

  .file-drop:has(:focus-visible) {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
  }

  .file-drop.has-error {
    border-color: var(--color-error);
  }

  .file-drop-name {
    color: var(--color-text-strong);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
  }

  /* Résumé d'erreurs en tête de formulaire (alerte d'erreur + liens vers les champs, sans
     puces) ; liens à cible de 32 px au pointeur fin, 48 au doigt. */
  .error-summary ul {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    margin-top: var(--space-xs);
    padding-left: 0;
    list-style: none;
  }

  .error-summary li a {
    display: inline-flex;
    align-items: center;
    min-height: var(--size-control-sm);
  }

  /* Lignes éditables d'un formulaire (choix d'un QCM, paires, éléments d'un ordre) ; case
     « bonne réponse » centrée sur son champ. */
  .edit-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-s);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* Lignes faites de champs à libellé (trous, cas d'un exercice) : 24 px entre elles. */
  .edit-list-loose {
    gap: var(--space-l);
  }

  .edit-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--space-s);
    align-items: center;
  }

  .edit-row > .check {
    align-items: center;
    min-height: var(--size-control);
    padding: 0 var(--space-xs);
  }

  .edit-row-pair {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
  }

  /* Ligne sans numéro (distracteurs : éléments sans ordre, systeme.md § 9). */
  .edit-row-plain {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  @media (max-width: 47.9375rem) {
    .edit-row > .check {
      justify-content: center;
      min-width: var(--size-touch);
      min-height: var(--size-touch);
    }

    .edit-row-pair {
      grid-template-columns: minmax(0, 1fr) auto;
    }

    .edit-row-pair > .icon {
      display: none;
    }

    .edit-row-pair > .input:nth-of-type(2) {
      grid-column: 1;
    }
  }

  /* Zone de texte d'aspect .input (texte d'un choix, titre au renommage) : une ligne qui
     grandit avec son contenu. */
  textarea.input {
    field-sizing: content;
    min-height: var(--size-control);
    padding-block: var(--space-s);
    resize: none;
  }

  /* Erreur d'une ligne : sous la ligne, sur toute sa largeur. */
  .edit-row > .field-error {
    grid-column: 1 / -1;
  }

  /* « Retirer » d'une ligne enregistrée : case DELETE du formset dans un bouton à icône
     (fonctionne sans JavaScript) ; cochée, le champ passe en pointillé, texte atténué, et la
     ligne sera retirée à l'enregistrement. */
  .row-remove:has(:focus-visible) {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
  }

  .row-remove:has(:checked) {
    background: var(--color-surface-2);
    color: var(--color-error);
  }

  .edit-row:has(.row-remove :checked) > .input {
    border-style: dashed;
    color: var(--color-muted);
  }

  /* Exercice de code : réglages sans objet pour le langage, le mode ou la politique choisis,
     masqués (validation posée sur le formulaire par static/js/code-question.js ; sans
     JavaScript, tout reste affiché) ; jamais quand ils portent une erreur. Le serveur valide
     dans tous les cas. Lancement d'une session (T-14, .when-exam, .when-homework,
     .when-live) : seuls les réglages du mode coché, sans JavaScript aussi ; le serveur ignore
     ceux des autres modes (SessionStartForm.MODE_FIELDS). */
  form[data-validation]:not([data-validation="hidden_tests"]) .when-hidden-tests:not(:has(.field-error)),
  form[data-validation]:not([data-validation="expected_output"]) .when-expected-output:not(:has(.field-error)),
  form[data-validation]:not([data-validation="sql_result"]) .when-sql-result:not(:has(.field-error)),
  form[data-validation-choice="false"] .when-validation-choice:not(:has(.field-error)),
  form:not([data-validation-choice="false"]) .when-validation-fixed,
  form:not(:has([name="run_mode"][value="public_tests"]:checked)) .when-public-tests:not(:has(.field-error)),
  form:not(:has([name="attempt_policy"][value="max"]:checked)) .when-max:not(:has(.field-error)),
  form:not(:has([name="attempt_policy"][value="penalty"]:checked)) .when-penalty:not(:has(.field-error)),
  form:not(:has([name="mode"][value="exam"]:checked)) .when-exam,
  form:not(:has([name="mode"][value="homework"]:checked)) .when-homework,
  form:not(:has([name="mode"][value="live"]:checked)) .when-live {
    display: none;
  }

  /* --- 13. Badges et compteurs ------------------------------------------------------------ */
  /* État court, toujours écrit, avec icône pour les états d'alerte. Teinte à 12 % du jeton
     d'état, texte du jeton : ≥ 5,1:1 sur toutes les surfaces. */

  .badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    min-height: var(--space-l);
    padding: 0 var(--space-s);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-full);
    background: var(--color-surface-2);
    color: var(--color-text-2);
    font-size: var(--text-sm);
    font-weight: 500;
    line-height: var(--leading-sm);
    white-space: nowrap;
  }

  .badge .icon {
    width: 14px;
    height: 14px;
  }

  .badge-success {
    border-color: color-mix(in srgb, var(--color-success) 40%, transparent);
    background: color-mix(in srgb, var(--color-success) 12%, transparent);
    color: var(--color-success);
  }

  .badge-warning {
    border-color: color-mix(in srgb, var(--color-warning) 40%, transparent);
    background: color-mix(in srgb, var(--color-warning) 12%, transparent);
    color: var(--color-warning);
  }

  .badge-error {
    border-color: color-mix(in srgb, var(--color-error) 40%, transparent);
    background: color-mix(in srgb, var(--color-error) 12%, transparent);
    color: var(--color-error);
  }

  /* En cours (session ouverte, test en cours) : pastille pleine devant le texte. */
  .badge-live::before {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: var(--radius-full);
    background: currentcolor;
  }

  /* Compteur dans un onglet ou un bouton. */
  .count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--space-l);
    height: 20px;
    padding: 0 var(--space-xs);
    border-radius: var(--radius-full);
    background: var(--color-surface-2);
    color: var(--color-text);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1;
    font-variant-numeric: tabular-nums;
  }

  /* --- 14. Alertes et notifications ------------------------------------------------------- */

  .alert {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-xs) var(--space-s);
    align-items: start;
    padding: var(--space-m);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
  }

  .alert > .icon {
    margin-top: 2px;
    color: var(--color-text-2);
  }

  .alert-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    min-width: 0;
  }

  .alert-title {
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .alert-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-s);
    margin-top: var(--space-s);
  }

  .alert-success {
    border-color: color-mix(in srgb, var(--color-success) 40%, transparent);
    background: color-mix(in srgb, var(--color-success) 8%, var(--color-surface));
  }

  .alert-success > .icon {
    color: var(--color-success);
  }

  .alert-warning {
    border-color: color-mix(in srgb, var(--color-warning) 40%, transparent);
    background: color-mix(in srgb, var(--color-warning) 8%, var(--color-surface));
  }

  .alert-warning > .icon {
    color: var(--color-warning);
  }

  .alert-error {
    border-color: color-mix(in srgb, var(--color-error) 40%, transparent);
    background: color-mix(in srgb, var(--color-error) 8%, var(--color-surface));
  }

  .alert-error > .icon {
    color: var(--color-error);
  }

  /* Alerte d'une ligne, sous un bandeau (copie, live). */
  .alert-inline {
    align-items: center;
    min-height: var(--size-control);
    padding: var(--space-xs) var(--space-m);
    border-radius: 0;
    border-width: 0 0 1px;
  }

  /* Alerte d'une ligne avec son action (« Revenir en plein écran ») : bouton à droite ; au
     téléphone, texte complet puis bouton sur toute la ligne (panel P1-24). */
  .alert-inline:has(> .btn) {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }

  @media (max-width: 47.9375rem) {
    .alert-inline:has(> .btn) {
      grid-template-columns: auto minmax(0, 1fr);
      padding-block: var(--space-s);
    }

    .alert-inline > .btn {
      grid-column: 1 / -1;
    }
  }

  /* Alerte avec son action à droite (« Publication des résultats » + « Changer la règle ») ;
     au téléphone, le bouton passe sous le texte. */
  .alert:has(> .alert-action) {
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
  }

  @media (max-width: 47.9375rem) {
    .alert:has(> .alert-action) {
      grid-template-columns: auto minmax(0, 1fr);
      align-items: start;
    }

    .alert > .alert-action {
      grid-column: 2;
      justify-self: start;
    }
  }

  /* Message Django persistant (erreur, avertissement) en tête de la page (base.html). */
  .page-alert {
    margin-bottom: var(--space-l);
  }

  /* Alerte fermable : bouton de fermeture à droite. */
  .alert:has(> .alert-close) {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }

  .alert-close {
    margin: calc(-1 * var(--space-s)) calc(-1 * var(--space-s)) 0 0;
  }

  /* Notification transitoire : en bas, au-dessus d'une barre basse ; 4 s, sans animation. */
  .toast-region {
    position: fixed;
    right: var(--gutter);
    bottom: var(--space-l);
    left: var(--gutter);
    z-index: 40;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-s);
    pointer-events: none;
  }

  /* Maquettes seulement : notification posée en fin de page, jamais sur une ligne de la
     capture pleine page (panel P1-60). */
  .toast-region.is-static {
    position: static;
    padding: 0 var(--gutter) var(--space-l);
  }

  .toast:has(> [role="status"]:empty) {
    display: none;
  }

  .has-bottom-bar .toast-region {
    bottom: calc(var(--size-bottom-bar) + var(--space-m));
  }

  .toast {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    max-width: 30rem;
    min-height: var(--size-control);
    padding: var(--space-s) var(--space-m);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface-2);
    box-shadow: var(--shadow-md);
    color: var(--color-text-strong);
    pointer-events: auto;
  }

  /* --- 15. Cartes, chiffres clés, états vides --------------------------------------------- */
  /* Carte : seulement pour un groupe qui se lit à part (bloc du tableau de bord, demande de
     reprise, exercice de code, question de la copie). Jamais de carte dans une carte. */

  .card {
    padding: var(--space-l);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
  }

  .card-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-s) var(--space-m);
    margin-bottom: var(--space-m);
  }

  /* Carte cliquable : lien qui couvre la carte (<a class="card-link"> dans le titre). */
  .card-clickable {
    position: relative;
  }

  .card-clickable:hover,
  .card-clickable.is-hover {
    border-color: var(--color-border-strong);
  }

  .card-link {
    color: var(--color-text-strong);
    text-decoration: none;
  }

  .card-link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: var(--radius-lg);
  }

  .card-clickable:has(.card-link:focus-visible),
  .card-clickable:has(.card-link.is-focus) {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
  }

  .card-link:focus-visible,
  .card-link.is-focus {
    outline: none;
  }

  /* Carte entièrement cliquable écrite comme un lien (choix du type de question). */
  a.card {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    color: var(--color-text);
    text-decoration: none;
  }

  a.card:hover,
  a.card.is-hover {
    border-color: var(--color-border-strong);
    background: var(--color-surface-2);
    color: var(--color-text);
  }

  @media (max-width: 47.9375rem) {
    .card {
      padding: var(--space-m);
    }
  }

  /* Bloc sans cadre séparé par un filet (commentaire dans une carte). */
  .block-divided {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding-top: var(--space-m);
    border-top: 1px solid var(--color-border);
  }

  .block-divided h3 {
    color: var(--color-text-strong);
  }

  /* Chiffre clé : libellé, valeur, unité ; aligné en haut (colonne inversée). */
  .stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: var(--space-m) var(--space-l);
    margin: 0;
  }

  .stat {
    display: flex;
    flex-direction: column-reverse;
    justify-content: flex-end;
    gap: var(--space-xs);
    margin: 0;
  }

  .stat dt {
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  /* Rangée souple : l'unité reste accolée au chiffre (l'espace du balisage est ignoré) et ne
     passe dessous, entière, que si la colonne est trop étroite. */
  .stat dd {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--space-xs);
    margin: 0;
    color: var(--color-text-strong);
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 600;
    line-height: var(--leading-2xl);
    font-variant-numeric: tabular-nums;
  }

  .stat-unit {
    color: var(--color-muted);
    white-space: nowrap;
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 400;
    line-height: var(--leading-sm);
  }

  .stat-warning dd {
    color: var(--color-warning);
  }

  /* Au téléphone (décision I-19) : deux chiffres par rangée, le dernier seul prend toute la
     largeur ; toutes les unités se placent de la même façon (statistiques T-22). Variante
     compacte du suivi (.stats-compact, T-16, valeurs courtes) : trois par rangée. */
  @media (max-width: 47.9375rem) {
    .stats {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .stats:not(.stats-compact) > :last-child:nth-child(odd) {
      grid-column: 1 / -1;
    }

    .stats-compact {
      grid-template-columns: repeat(3, minmax(0, 1fr));
      column-gap: var(--space-m);
    }
  }

  /* Sous 360 px, trois colonnes de 85 px ne tiennent plus « 12,64 » : le suivi passe aussi à
     deux par rangée, le dernier seul prend toute la largeur. */
  @media (max-width: 22.4375rem) {
    .stats-compact {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      column-gap: var(--space-l);
    }

    .stats > :last-child:nth-child(odd) {
      grid-column: 1 / -1;
    }
  }

  /* État vide : explication en une phrase et l'action qui le remplit. */
  .empty {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-s);
    padding: var(--space-xl) var(--space-l);
    border: 1px dashed var(--color-border-strong);
    border-radius: var(--radius-lg);
  }

  .empty > .icon {
    width: var(--space-l);
    height: var(--space-l);
    color: var(--color-muted);
  }

  .empty-title {
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .empty-text {
    color: var(--color-text-2);
  }

  .empty .btn-group {
    margin-top: var(--space-s);
  }

  /* --- 16. Tableaux ----------------------------------------------------------------------- */

  /* Relatif : le texte masqué des en-têtes d'actions ne sort pas du cadre (tablette). */
  .table-wrap {
    position: relative;
    overflow-x: auto;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
  }

  /* Bordures séparées : le filet des lignes signalées ne décale pas la ligne. */
  .table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-variant-numeric: tabular-nums;
  }

  .table caption {
    padding-bottom: var(--space-s);
    color: var(--color-text-2);
    font-size: var(--text-sm);
    text-align: left;
  }

  .table th,
  .table td {
    padding: var(--space-s) var(--space-m);
    border-bottom: 1px solid var(--color-border);
    text-align: left;
    vertical-align: middle;
  }

  .table thead th {
    height: var(--size-control);
    background: var(--color-surface-2);
    color: var(--color-text-2);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: var(--leading-sm);
    white-space: nowrap;
  }

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

  .table td {
    height: var(--space-2xl);
    line-height: var(--leading-md);
  }

  .table .num {
    text-align: right;
    white-space: nowrap;
  }

  .table .actions {
    width: 1%;
    text-align: right;
    white-space: nowrap;
  }

  .table tbody tr:hover,
  .table tbody tr.is-hover {
    background: var(--color-surface-2);
  }

  /* Ligne cliquable : le lien du premier champ couvre la ligne. */
  .table .row-link {
    color: var(--color-text-strong);
    font-weight: 600;
    text-decoration: none;
  }

  .table tr:has(.row-link) {
    position: relative;
  }

  .table .row-link::after {
    content: "";
    position: absolute;
    inset: 0;
  }

  .table .row-link:focus-visible,
  .table .row-link.is-focus {
    outline: none;
  }

  .table tr:has(.row-link:focus-visible),
  .table tr:has(.row-link.is-focus) {
    outline: 3px solid var(--color-focus);
    outline-offset: -3px;
  }

  .table .actions .btn,
  .table .actions .menu-trigger {
    position: relative;
    z-index: 1; /* au-dessus du lien de ligne */
  }

  /* Titre d'une cellule principale : 40 caractères au plus. */
  .table .cell-primary .truncate {
    max-width: 40ch;
  }

  /* Tablette (tableau encore en lignes) : titre plus court, pour que les autres colonnes
     (détail du test à l'étape 3 de l'épreuve) ne s'écrasent pas. */
  @media (min-width: 48rem) and (max-width: 63.9375rem) {
    .table .cell-primary .truncate {
      max-width: 24ch;
    }
  }

  /* Ligne signalée (incident récent, doublon) : filet d'état à gauche, en plus du badge écrit. */
  .table tr.is-flagged > :first-child {
    padding-left: calc(var(--space-m) - 3px);
    border-left: 3px solid var(--color-warning);
  }

  .table tr.is-error > :first-child {
    padding-left: calc(var(--space-m) - 3px);
    border-left: 3px solid var(--color-error);
  }

  .table tfoot td,
  .table tfoot th {
    border-top: 1px solid var(--color-border-strong);
    border-bottom: 0;
    color: var(--color-text-strong);
    font-weight: 600;
  }

  /* Tri : bouton dans l'en-tête, état dans aria-sort ; cible de 32 px (48 au doigt). */
  .sort {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    min-height: var(--size-control-sm);
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }

  .sort:hover {
    color: var(--color-text-strong);
  }

  /* Libellé d'une cellule (panel P1-62) : au PC, l'en-tête de colonne nomme la cellule et le
     libellé n'existe pas ; en cartes, il est lu dans la cellule (texte masqué) et affiché par
     le ::before, décoratif (pas de double lecture). Rôles ARIA posés par le gabarit : une
     table en display: block ou grid perd sa sémantique. */
  .cell-label {
    display: none;
  }

  /* Au téléphone : chaque ligne devient une carte ; chaque cellule reprend son en-tête par
     data-label. Même balisage, aucun défilement horizontal. */
  @media (max-width: 47.9375rem) {
    .table-responsive .cell-label {
      display: block;
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }

    .table-wrap:has(.table-responsive) {
      overflow: visible;
      border: 0;
    }

    .table-responsive thead {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }

    .table-responsive,
    .table-responsive tbody,
    .table-responsive tfoot {
      display: block;
    }

    .table-responsive tr {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: var(--space-xs) var(--space-m);
      margin-bottom: var(--space-s);
      padding: var(--space-m);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      background: var(--color-surface);
    }

    .table-responsive tbody tr:last-child > *,
    .table-responsive th,
    .table-responsive td {
      height: auto;
      padding: 0;
      border: 0;
      text-align: left;
    }

    .table-responsive td[data-label] {
      grid-column: 1 / -1;
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      gap: var(--space-xs) var(--space-m);
    }

    .table-responsive td[data-label]::before {
      content: attr(data-label) / "";
      color: var(--color-muted);
      font-size: var(--text-sm);
    }

    .table-responsive .cell-primary {
      grid-column: 1;
      font-weight: 600;
      display: flex;
      gap: var(--space-s);
      align-items: baseline;
      min-width: 0;
    }

    .table-responsive .actions {
      grid-column: 2;
      grid-row: 1;
      width: auto;
    }

    .table-responsive tr.is-flagged {
      border-left-width: 3px;
      border-left-color: var(--color-warning);
    }

    .table-responsive tr.is-error {
      border-left-width: 3px;
      border-left-color: var(--color-error);
    }

    .table-responsive tr.is-flagged > :first-child,
    .table-responsive tr.is-error > :first-child {
      padding-left: 0;
      border-left: 0;
    }
  }

  /* Tableaux de six colonnes ou plus (.table-wide) : en cartes dès moins de 1 024 px (tablette
     comprise), jamais coupés ; mêmes règles qu'au téléphone. */
  @media (max-width: 63.9375rem) {
    .table-responsive.table-wide .cell-label {
      display: block;
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }

    .table-wrap:has(.table-responsive.table-wide) {
      overflow: visible;
      border: 0;
    }

    .table-responsive.table-wide thead {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }

    .table-responsive.table-wide,
    .table-responsive.table-wide tbody,
    .table-responsive.table-wide tfoot {
      display: block;
    }

    .table-responsive.table-wide tr {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: var(--space-xs) var(--space-m);
      margin-bottom: var(--space-s);
      padding: var(--space-m);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      background: var(--color-surface);
    }

    .table-responsive.table-wide tbody tr:last-child > *,
    .table-responsive.table-wide th,
    .table-responsive.table-wide td {
      height: auto;
      padding: 0;
      border: 0;
      text-align: left;
    }

    .table-responsive.table-wide td[data-label] {
      grid-column: 1 / -1;
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      gap: var(--space-xs) var(--space-m);
    }

    .table-responsive.table-wide td[data-label]::before {
      content: attr(data-label) / "";
      color: var(--color-muted);
      font-size: var(--text-sm);
    }

    .table-responsive.table-wide .cell-primary {
      grid-column: 1;
      font-weight: 600;
      display: flex;
      gap: var(--space-s);
      align-items: baseline;
      min-width: 0;
    }

    .table-responsive.table-wide .actions {
      grid-column: 2;
      grid-row: 1;
      width: auto;
    }

    .table-responsive.table-wide tr.is-flagged {
      border-left-width: 3px;
      border-left-color: var(--color-warning);
    }

    .table-responsive.table-wide tr.is-error {
      border-left-width: 3px;
      border-left-color: var(--color-error);
    }

    .table-responsive.table-wide tr.is-flagged > :first-child,
    .table-responsive.table-wide tr.is-error > :first-child {
      padding-left: 0;
      border-left: 0;
    }
  }

  /* --- 17. Listes denses, récapitulatif --------------------------------------------------- */

  /* Liste dense d'objets (questions d'une épreuve) : numéro, titre d'une ligne, méta, actions. */
  .item-list {
    margin: 0;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    list-style: none;
  }

  .item {
    position: relative;
    display: grid;
    grid-template-columns: 2.5rem minmax(0, 1fr) auto;
    gap: var(--space-xs) var(--space-m);
    align-items: center;
    min-height: var(--space-3xl);
    padding: var(--space-s) var(--space-m);
    border-bottom: 1px solid var(--color-border);
  }

  .item:last-child {
    border-bottom: 0;
  }

  .item:hover,
  .item.is-hover {
    background: var(--color-surface-2);
  }

  /* Surlignée après enregistrement : la ligne visée par l'ancre de retour (#question-N). */
  .item.is-highlighted,
  .item:target {
    background: color-mix(in srgb, var(--color-success) 8%, transparent);
  }

  .item-main {
    display: flex;
    flex-direction: column;
    min-width: 0;
  }

  .item-title {
    overflow: hidden;
    color: var(--color-text-strong);
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .item-title::after {
    content: "";
    position: absolute;
    inset: 0;
  }

  .item-title:focus-visible,
  .item-title.is-focus {
    outline: none;
  }

  .item:has(.item-title:focus-visible),
  .item:has(.item-title.is-focus) {
    outline: 3px solid var(--color-focus);
    outline-offset: -3px;
  }

  .item-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs) var(--space-s);
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .item-actions {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: var(--space-xs);
  }

  /* Monter, Descendre, Tester : chacun dans son formulaire POST, sans compter dans la rangée. */
  .item-actions > form {
    display: contents;
  }

  /* Ligne d'ajout intégrée en fin de liste dense (pas un second bouton). */
  .item-add {
    min-height: var(--space-2xl);
    border-top: 1px dashed var(--color-border-strong);
  }

  .item-add .item-title {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    color: var(--color-text-2);
  }

  .item-add:hover .item-title {
    color: var(--color-text-strong);
  }

  /* Libellé court du téléphone (décision I-01 b) : un seul élément avec .label-desktop dans
     le même bouton ou badge, aria-hidden ; affiché sous 768 px seulement (règle ci-dessous). */
  .label-phone {
    display: none;
  }

  /* Au téléphone : actions à droite du titre (ligne de 72 px au plus), cibles de 48 px ;
     Monter / Descendre passent dans le menu « … » de la ligne ; libellés courts. */
  @media (max-width: 47.9375rem) {
    .item {
      grid-template-columns: 2rem minmax(0, 1fr) auto;
    }

    .item-actions {
      grid-column: 3;
      justify-content: flex-end;
    }

    .item-actions .btn {
      min-width: var(--size-touch);
    }

    .item-actions .btn-move {
      display: none;
    }

    /* Tableau de bord (.item-list-wrap) : titres sur deux lignes au plus, jamais tronqués à
       une ligne (panel P1-53). */
    /* Badge plus large que la colonne du titre (320 px) : il passe à la ligne dans son cadre,
       jamais sous les actions. */
    .item-meta .badge {
      max-width: 100%;
      white-space: normal;
    }

    /* Sous 360 px (320 de vérification), un état seul (badge, sans bouton) passe sous le titre :
       le titre garde toute la largeur (remarque du chef design, G2). */
    @media (max-width: 22.4375rem) {
      .item-list-wrap .item-actions:has(> .badge:only-child) {
        grid-column: 2 / -1;
        justify-content: flex-start;
      }
    }

    .item-list-wrap .item-title {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      white-space: normal;
    }

    .label-desktop {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }

    .label-phone {
      display: inline;
    }

    /* Bouton dont le libellé est masqué (icône seule, « Tester ») : cible de 48 px. */
    .btn:has(> .label-desktop) {
      min-width: var(--size-touch);
    }
  }

  /* Liste de définitions en ligne (récapitulatif, méta d'une copie). */
  .summary {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--space-s) var(--space-l);
    margin: 0;
  }

  .summary dt {
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-md);
  }

  .summary dd {
    margin: 0;
    color: var(--color-text);
  }

  @media (max-width: 47.9375rem) {
    .summary {
      grid-template-columns: minmax(0, 1fr);
      gap: 0;
    }

    .summary dd + dt {
      margin-top: var(--space-s);
    }
  }

  /* Légende des états (« Que signifient ces états ? », T-16, décision I-06) : badge d'exemple
     dans sa colonne, phrase en face, aussi au téléphone (la phrase passe à la ligne dans la
     sienne). */
  .state-legend {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    align-items: baseline;
    gap: var(--space-s) var(--space-m);
    margin: 0;
  }

  .state-legend dd {
    max-width: var(--measure);
    margin: 0;
  }

  /* --- 18. Navigation --------------------------------------------------------------------- */

  /* En-tête de l'application : logo (vers le tableau de bord), 3 entrées, indicateur de
     session ouverte, compte. Opaque : le halo ne passe jamais sous le « IT » rouge. */
  .app-header {
    position: sticky;
    top: 0;
    z-index: 20;
    border-bottom: 1px solid var(--color-border);
    background: var(--color-bg);
  }

  .app-header-inner {
    display: flex;
    align-items: center;
    gap: var(--space-l);
    min-height: var(--size-header);
  }

  .brand {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: var(--space-s);
    min-height: var(--size-control);
    color: var(--color-text-strong);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 700;
    line-height: var(--leading-xl);
    text-decoration: none;
  }

  .brand:hover {
    color: var(--color-text-strong);
  }

  .brand-logo {
    width: var(--space-xl);
    height: var(--space-xl);
  }

  /* 24 px en 700 : grand texte, rouge 500 permis sur --color-bg (4,5:1). */
  .brand-accent {
    color: var(--red-500);
  }

  /* « LAB » plus fin, en capitales (visuels) ; même taille que la marque (échelle). */
  .brand-lab {
    font-weight: 400;
    text-transform: uppercase;
  }

  .nav {
    display: flex;
    align-items: stretch;
    gap: var(--space-xs);
    align-self: stretch;
  }

  .nav-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--size-control);
    padding: 0 var(--space-s);
    border-bottom: 2px solid transparent;
    color: var(--color-text-2);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
  }

  .nav-link:hover,
  .nav-link.is-hover {
    color: var(--color-text-strong);
  }

  .nav-link[aria-current="page"] {
    border-bottom-color: var(--red-500);
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .app-header-end {
    display: flex;
    align-items: center;
    gap: var(--space-s);
    min-width: 0;
    margin-left: auto;
  }

  /* Indicateur de session ouverte : pastille cliquable vers le Suivi. */
  .session-indicator {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    min-height: var(--size-control-sm);
    padding: 0 var(--space-m) 0 var(--space-s);
    border: 1px solid color-mix(in srgb, var(--color-success) 40%, transparent);
    border-radius: var(--radius-full);
    background: color-mix(in srgb, var(--color-success) 12%, transparent);
    color: var(--color-text-strong);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    text-decoration: none;
    white-space: nowrap;
  }

  .session-indicator::before,
  .menu-session::before {
    content: "";
    flex: none;
    width: 8px;
    height: 8px;
    margin-left: var(--space-xs);
    border-radius: var(--radius-full);
    background: var(--color-success);
  }

  .session-indicator:hover,
  .session-indicator.is-hover {
    border-color: var(--color-success);
    color: var(--color-text-strong);
  }

  /* Devoir à venir : pas encore ouvert, donc rien de vert ; icône calendrier au lieu du point. */
  .session-indicator-upcoming {
    border-color: var(--color-border-strong);
    background: var(--color-surface-2);
  }

  .session-indicator-upcoming::before,
  .menu-session-upcoming::before {
    content: none;
  }

  .session-indicator-upcoming:hover {
    border-color: var(--color-muted);
  }

  .session-indicator .mono {
    letter-spacing: 0.05em;
  }

  /* De 1 024 à 1 279 px, la navigation tient sur une ligne avec la pastille : sans son détail
     (« · 60 min », « · jusqu'au 30/10 »), toujours lu dans son nom accessible. */
  @media (max-width: 79.9375rem) {
    .session-indicator .session-extra {
      display: none;
    }
  }

  /* Menu de l'en-tête sous 1 024 px : entrées masquées, bouton menu visible, jamais rétréci
     (48 × 48 au doigt). */
  .app-header .menu-button {
    display: none;
    flex: none;
  }

  @media (max-width: 63.9375rem) {
    .app-header-inner {
      min-height: var(--size-bar);
      gap: var(--space-s);
    }

    .app-header .nav,
    .app-header .account {
      display: none;
    }

    .app-header .menu-button {
      display: inline-flex;
    }

    .session-indicator .session-detail {
      display: none;
    }
  }

  /* En-tête minimal (écrans étudiant hors composition, pages publiques) : logo seul. */
  .app-header-minimal .app-header-inner {
    min-height: var(--size-bar);
  }

  /* Au téléphone : logo seul (24 px) dans une cible de 48 px, le nom de la marque est lu ;
     pastille de session (« ● 2 ») de 48 × 48. */
  @media (max-width: 47.9375rem) {
    .app-header-minimal .brand {
      min-width: var(--size-touch);
      min-height: var(--size-touch);
    }

    .app-header-minimal .brand-logo {
      width: var(--space-l);
      height: var(--space-l);
    }

    .app-header-minimal .brand-text {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }

    .session-indicator {
      flex: none;
      min-width: var(--size-touch);
      min-height: var(--size-touch);
      justify-content: center;
      padding: 0 var(--space-s) 0 var(--space-xs);
    }
  }

  /* Sous 360 px, le point et le code ne tiennent plus entre la marque et le menu : la pastille
     passe dans le menu de l'en-tête (.menu-session). */
  @media (max-width: 22.4375rem) {
    .app-header .session-indicator {
      display: none;
    }
  }

  /* Fil d'Ariane unique : le dernier élément n'est pas un lien. */
  .breadcrumb ol {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
    margin: 0 0 var(--space-xs);
    padding: 0;
    list-style: none;
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .breadcrumb li {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
  }

  .breadcrumb li + li::before {
    content: "›" / "";
    color: var(--color-muted);
  }

  .breadcrumb a {
    display: inline-flex;
    align-items: center;
    min-height: var(--size-control-sm);
    color: var(--color-text-2);
    text-decoration: none;
  }

  .breadcrumb a:hover {
    color: var(--color-text-strong);
    text-decoration: underline;
  }

  .breadcrumb [aria-current="page"] {
    overflow: hidden;
    color: var(--color-text);
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Au téléphone, le fil se réduit au parent (« ‹ Copies », panel P1-49) : plus de dernier
     élément seul sur une ligne ; le titre de la page dit où l'on est. */
  @media (max-width: 47.9375rem) {
    .breadcrumb li {
      display: none;
    }

    .breadcrumb li:nth-last-child(2) {
      display: inline-flex;
    }

    .breadcrumb li:nth-last-child(2)::before {
      content: "‹" / "";
      color: var(--color-text-2);
    }
  }

  /* Onglets de navigation (espace session) : liens, aria-current="page" ; libellé à gauche
     dans sa cible (premier onglet aligné sur la colonne). Défilent au téléphone. */
  .tabs {
    display: flex;
    gap: var(--space-l);
    overflow-x: auto;
    border-bottom: 1px solid var(--color-border);
  }

  .tab {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: flex-start;
    min-width: var(--size-control);
    gap: var(--space-s);
    min-height: var(--size-control);
    margin-bottom: -1px;
    border-bottom: 2px solid transparent;
    color: var(--color-text-2);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
  }

  .tab:hover,
  .tab.is-hover {
    color: var(--color-text-strong);
  }

  .tab[aria-current="page"],
  .tab[aria-selected="true"] {
    border-bottom-color: var(--red-500);
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .tab[aria-disabled="true"] {
    color: var(--color-muted);
    cursor: not-allowed;
  }

  /* Les quatre onglets de session tiennent à 360 px. */
  @media (max-width: 47.9375rem) {
    .tabs {
      gap: var(--space-m);
    }

    .tab {
      font-size: var(--text-sm);
    }
  }

  /* Filtres segmentés (Toutes · Incidents · Remises) : boutons aria-pressed ; largeur de leur
     contenu. */
  .segmented {
    display: inline-flex;
    width: fit-content;
    max-width: 100%;
    flex-wrap: wrap;
    gap: var(--space-xs);
    padding: var(--space-xs);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
  }

  .segment {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    min-height: var(--size-control-sm);
    padding: 0 var(--space-s);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text-2);
    font-size: var(--text-sm);
    font-weight: 500;
    cursor: pointer;
  }

  .segment:hover,
  .segment.is-hover {
    color: var(--color-text-strong);
  }

  .segment[aria-pressed="true"] {
    background: var(--color-surface-2);
    color: var(--color-text-strong);
    font-weight: 600;
  }

  /* Au téléphone : une rangée qui défile, jamais trois. */
  @media (max-width: 47.9375rem) {
    .segmented {
      flex-wrap: nowrap;
      overflow-x: auto;
    }

    .segment {
      flex: none;
    }
  }

  /* Pagination d'objets (« Copie 3 sur 30 ») : précédente, position, suivante. */
  .pager {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    color: var(--color-text-2);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
  }

  /* Barre basse (téléphone, écrans étudiant) : trois boutons texte à portée du pouce
     (Précédente · Questions · Suivante, ou Questions · Tests · Soumettre pour un exercice,
     panel P1-02) ; marge intérieure de 12 px, libellés sur une ligne. */
  .bottom-bar {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 20;
    display: grid;
    grid-auto-columns: minmax(0, 1fr);
    grid-auto-flow: column;
    gap: var(--space-s);
    min-height: var(--size-bottom-bar);
    padding: var(--space-s) var(--gutter) calc(var(--space-s) + env(safe-area-inset-bottom));
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
  }

  .bottom-bar .btn {
    min-height: var(--size-touch);
    padding-inline: calc(var(--space-s) + var(--space-xs));
    min-width: 0;
    line-height: var(--leading-sm);
    white-space: nowrap;
  }

  /* Barre basse de la copie et de son aperçu : jusqu'à 1 023 px (.compact-only). */
  @media (max-width: 63.9375rem) {
    .has-bottom-bar {
      padding-bottom: calc(var(--size-bottom-bar) + env(safe-area-inset-bottom) + var(--space-m));
    }

    /* Le focus et le dernier champ ne passent jamais sous la barre (WCAG 2.4.11, I-08). */
    html:has(> body.has-bottom-bar) {
      scroll-padding-bottom: calc(var(--size-bottom-bar) + env(safe-area-inset-bottom) + var(--space-m));
    }
  }

  /* Très petit écran (moins de 400 px) : libellés seuls, sans icône, trois boutons lisibles. */
  @media (max-width: 24.9375rem) {
    .bottom-bar .btn:not(.btn-icon) > .icon {
      display: none;
    }
  }

  /* --- 19. Indicateur d'étapes ------------------------------------------------------------ */
  /* <ol class="steps"> ; étape courante aria-current="step" ; états .is-done, .is-error ;
     chaque étape faite ou en erreur reste un lien (retour arrière). */

  .steps {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-s) var(--space-l);
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: step;
  }

  .step {
    counter-increment: step;
    display: flex;
    align-items: center;
    gap: var(--space-s);
    min-height: var(--size-control);
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .step a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    min-height: var(--size-control);
    color: inherit;
    text-decoration: none;
  }

  .step a:hover {
    color: var(--color-text-strong);
  }

  .step-marker {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: var(--space-l);
    height: var(--space-l);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-full);
    font-family: var(--font-mono);
    font-weight: 600;
  }

  .step-marker::before {
    content: counter(step);
  }

  .step-marker .icon {
    width: 14px;
    height: 14px;
  }

  .step:has(.step-marker .icon) .step-marker::before {
    content: none;
  }

  .step + .step::before {
    content: "";
    width: var(--space-l);
    height: 1px;
    margin-right: var(--space-xs);
    background: var(--color-border-strong);
  }

  .step.is-done {
    color: var(--color-text-2);
  }

  .step.is-done .step-marker {
    border-color: color-mix(in srgb, var(--color-success) 40%, transparent);
    background: color-mix(in srgb, var(--color-success) 12%, transparent);
    color: var(--color-success);
  }

  .step[aria-current="step"] {
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .step[aria-current="step"] .step-marker {
    border: 2px solid var(--color-text-strong);
    color: var(--color-text-strong);
  }

  .step.is-error {
    color: var(--color-error);
  }

  .step.is-error .step-marker {
    border-color: var(--color-error);
    color: var(--color-error);
  }

  /* Étape courante d'un formulaire en étapes déjà enregistré : son numéro, pas la coche. */
  .step[aria-current="step"] .step-marker .icon {
    display: none;
  }

  .step[aria-current="step"]:has(.step-marker .icon) .step-marker::before {
    content: counter(step);
  }

  /* Formulaire en étapes (exercice de code) : une étape à la fois quand le JS d'interface pose
     .is-stepped (.is-current sur l'étape affichée) ; sans JavaScript, toutes à la suite. */
  .form-steps.is-stepped > .form-step:not(.is-current) {
    display: none;
  }

  /* Version compacte (téléphone) : « Étape 2 sur 4 · Questions » et une barre segmentée ;
     .is-visible : à toutes les largeurs (parcours de deux étapes : Rejoindre). */
  .steps-compact {
    display: none;
    width: 100%;
    flex-direction: column;
    gap: var(--space-s);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .steps-compact.is-visible {
    display: flex;
  }

  .steps-compact-label {
    color: var(--color-text-2);
  }

  .steps-compact-label strong {
    color: var(--color-text-strong);
  }

  .steps-bar {
    display: flex;
    gap: var(--space-xs);
  }

  .steps-bar span {
    flex: 1;
    height: 4px;
    border-radius: var(--radius-full);
    background: var(--color-border-strong);
  }

  .steps-bar .is-done {
    background: var(--color-success);
  }

  .steps-bar .is-current {
    background: var(--color-text-strong);
  }

  .steps-bar .is-error {
    background: var(--color-error);
  }

  @media (max-width: 47.9375rem) {
    .steps-adaptive .steps {
      display: none;
    }

    .steps-adaptive .steps-compact {
      display: flex;
    }
  }

  /* --- 20. Menus, menu de l'en-tête, replis ----------------------------------------------- */

  /* Menu « … » : popover natif ([popover]) ancré par JS d'interface ; éléments de 40 px. */
  .menu {
    min-width: 12rem;
    margin: 0;
    padding: var(--space-xs);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    box-shadow: var(--shadow-md);
    color: var(--color-text);
  }

  .menu-static {
    position: static;
    display: block;
    width: max-content;
  }

  /* Menu « … » ouvert (popover natif) : sous son bouton, bord droit aligné (le bouton
     popovertarget est son ancre implicite) ; sans positionnement par ancre, au centre de la
     fenêtre. */
  .menu[popover] {
    margin: auto;
  }

  @supports (position-area: bottom) {
    .menu[popover] {
      margin: var(--space-xs) 0 0;
      position-area: bottom span-left;
      position-try-fallbacks: flip-block;
    }
  }

  .menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-s);
    width: 100%;
    min-height: var(--size-control);
    padding: 0 var(--space-s);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text);
    font-size: var(--text-md);
    text-align: left;
    text-decoration: none;
    cursor: pointer;
  }

  .menu-item:hover,
  .menu-item.is-hover {
    background: var(--color-surface-2);
    color: var(--color-text-strong);
  }

  .menu-item:focus-visible {
    outline-offset: -3px;
    background: var(--color-surface-2);
    color: var(--color-text-strong);
  }

  .menu-item-danger {
    color: var(--color-error);
  }

  .menu-item-danger:hover,
  .menu-item-danger.is-hover {
    color: var(--color-error);
  }

  .menu-item:disabled,
  .menu-item[aria-disabled="true"] {
    color: var(--color-muted);
    cursor: not-allowed;
  }

  .menu-item[aria-current="page"] {
    color: var(--color-text-strong);
    font-weight: 600;
  }

  .menu-separator {
    height: 1px;
    margin: var(--space-xs) 0;
    border: 0;
    background: var(--color-border);
  }

  /* Menu de l'en-tête sous 1 024 px (Tableau de bord, Épreuves, Sessions, Se déconnecter) :
     panneau sous l'en-tête, à droite. */
  .app-menu {
    position: absolute;
    top: 100%;
    right: var(--gutter);
    z-index: 30;
    display: none;
    min-width: 16rem;
  }

  .app-menu.is-open {
    display: block;
  }

  @media (min-width: 64rem) {
    .app-menu.is-open {
      display: none;
    }
  }

  /* Session ouverte en tête du menu, sous 360 px seulement (la pastille de l'en-tête y est
     masquée) : point vert, mode, code, temps. */
  .menu-session {
    display: none;
  }

  @media (max-width: 22.4375rem) {
    .menu-session {
      display: flex;
    }
  }

  /* Repli : information secondaire seulement, jamais une action. */
  .disclosure > summary {
    display: flex;
    align-items: center;
    gap: var(--space-s);
    min-height: var(--size-control);
    color: var(--color-text);
    font-weight: 600;
    list-style: none;
    cursor: pointer;
  }

  .disclosure > summary::-webkit-details-marker {
    display: none;
  }

  .disclosure > summary:hover {
    color: var(--color-text-strong);
  }

  .disclosure > summary .icon {
    color: var(--color-text-2);
  }

  .disclosure[open] > summary .icon-chevron {
    rotate: 180deg;
  }

  .disclosure-body {
    padding: var(--space-xs) 0 var(--space-m) calc(var(--size-icon) + var(--space-s));
  }

  /* --- 21. Fenêtres de dialogue, feuilles, voile ------------------------------------------ */

  /* Fenêtre de dialogue (<dialog class="dialog">) : titre qui répète l'action, conséquences,
     bouton qui répète l'action à droite, « Annuler » discret. Focus piégé par le navigateur. */
  .dialog {
    width: min(40rem, calc(100% - 2 * var(--space-m)));
    max-width: none; /* celle du navigateur (100 % - 6 px - 2em) rognerait 6 px au téléphone */
    max-height: calc(100% - 2 * var(--space-m));
    padding: 0;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
    box-shadow: var(--shadow-md);
    color: var(--color-text);
  }

  .dialog::backdrop {
    background: color-mix(in srgb, var(--color-bg) 80%, transparent);
  }

  /* « Avant de commencer » (copie) : titre de l'épreuve au pas du titre de page, voile opaque :
     rien de l'épreuve n'est visible avant le choix de l'étudiant. */
  .dialog-start::backdrop {
    background: var(--color-bg);
  }

  .dialog-start .dialog-header {
    display: block;
  }

  .dialog-start .dialog-body > h2 {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  /* Fenêtre large (choix du type de question). */
  .dialog-wide {
    width: min(48rem, calc(100% - 2 * var(--space-m)));
  }

  .dialog-static {
    position: static;
    display: block;
    max-height: none;
    margin: 0;
  }

  /* Fenêtre ouverte dans une maquette (dans l'application : showModal() et ::backdrop). */
  .dialog.is-open {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: block;
    height: fit-content;
    margin: auto;
    overflow-y: auto;
  }

  .dialog-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-m);
    padding: var(--space-l) var(--space-l) 0;
  }

  .dialog-header > .btn {
    flex: none;
  }

  .dialog-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-m);
    padding: var(--space-m) var(--space-l);
  }

  .dialog-footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-s);
    padding: var(--space-m) var(--space-l) var(--space-l);
  }

  /* Au téléphone : boutons pleine largeur (libellé sur deux lignes au besoin), action en
     premier, « Annuler » en dernier. */
  @media (max-width: 47.9375rem) {
    .dialog-footer {
      flex-direction: column;
    }

    .dialog-footer .btn {
      flex: 1 1 100%;
      width: 100%;
      margin: 0;
      padding-block: var(--space-s);
      white-space: normal;
    }

    .dialog-footer > .btn-primary,
    .dialog-footer > .btn-danger {
      order: -1;
    }

    /* « Avant de commencer » : les choix restent au bas du panneau, seul le texte défile
       au-dessus (jamais d'action cachée sous la fenêtre d'un petit téléphone). */
    .dialog-start .dialog-footer {
      position: sticky;
      bottom: 0;
      border-top: 1px solid var(--color-border);
      background: var(--color-surface);
    }
  }

  /* Liste homogène de points à vérifier (dialogue de remise) : icône, texte, liens. */
  .issue-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-s);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .issue-list > li {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-s);
    align-items: start;
  }

  .issue-list > li > .icon {
    margin-top: 2px;
    color: var(--color-warning);
  }

  /* Feuille du bas (téléphone) : Questions de la copie, résultat d'exécution. */
  .sheet {
    width: 100%;
    max-width: 100%;
    max-height: 85vh;
    margin: auto 0 0;
    padding: 0;
    border: 1px solid var(--color-border-strong);
    border-bottom: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: var(--color-surface);
    color: var(--color-text);
  }

  .sheet::backdrop {
    background: color-mix(in srgb, var(--color-bg) 80%, transparent);
  }

  /* Feuille de résultat (exécution, tests, soumission) : 60 % de la hauteur au plus, non
     modale (show()) : le haut du code reste visible et défilable au-dessus (panel P1-31). */
  .sheet-result {
    max-height: 60vh;
    box-shadow: var(--shadow-md);
  }

  .sheet-static {
    position: static;
    display: block;
    max-height: none;
    margin: 0;
  }

  /* Feuille ouverte dans une maquette (dans l'application : showModal()). */
  .sheet.is-open {
    position: fixed;
    inset: auto 0 0;
    z-index: 50;
    display: flex;
    flex-direction: column;
    margin: 0;
  }

  .sheet-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-m);
    min-height: var(--size-bar);
    padding: 0 var(--space-xs) 0 var(--space-m);
    border-bottom: 1px solid var(--color-border);
  }

  .sheet-header > .btn {
    flex: none;
  }

  /* Titre de la feuille : h2 (« Questions »), ou h3 dans une question (résultat d'un exercice). */
  .sheet-header > :is(h2, h3) {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  .sheet-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-m);
    padding: var(--space-m);
    overflow-y: auto;
  }

  /* Voile d'une fenêtre ou d'une feuille ouverte dans une maquette ; opaque pour le panneau
     de démarrage (rien de l'épreuve n'est visible avant le choix de l'étudiant). */
  .scrim {
    position: fixed;
    inset: 0;
    z-index: 45;
    background: color-mix(in srgb, var(--color-bg) 80%, transparent);
  }

  .scrim.is-opaque {
    background: var(--color-bg);
  }

  /* --- 22. Progression et chargement ------------------------------------------------------ */

  /* Barre (<progress class="meter">) : avancement, répartition, réussite. La valeur est
     toujours écrite à côté ; la couleur se choisit par classe (variable locale --bar, jeton
     seulement). */
  .meter {
    --bar: var(--color-text-2);
    display: block;
    width: 100%;
    height: 8px;
    overflow: hidden;
    border: 0;
    border-radius: var(--radius-full);
    background: var(--color-surface-2);
    appearance: none;
  }

  .meter::-webkit-progress-bar {
    background: var(--color-surface-2);
  }

  .meter::-webkit-progress-value {
    border-radius: var(--radius-full);
    background: var(--bar);
  }

  .meter::-moz-progress-bar {
    border-radius: var(--radius-full);
    background: var(--bar);
  }

  .meter-brand { --bar: var(--red-500); }
  .meter-success { --bar: var(--color-success); }
  .meter-warning { --bar: var(--color-warning); }
  .meter-error { --bar: var(--color-error); }

  /* Ligne libellé · barre · valeur à largeur fixe : barres comparables. */
  .meter-row {
    display: grid;
    grid-template-columns: minmax(6rem, 10rem) minmax(0, 1fr) 9rem;
    gap: var(--space-m);
    align-items: center;
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .meter-value {
    color: var(--color-text-2);
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
  }

  @media (max-width: 47.9375rem) {
    .meter-row {
      grid-template-columns: minmax(0, 1fr) auto;
    }

    .meter-row .meter {
      grid-column: 1 / -1;
      grid-row: 2;
    }
  }

  /* Barre de réussite dans une cellule de tableau. */
  .meter-row-compact {
    grid-template-columns: minmax(6rem, 1fr) auto;
  }

  @media (max-width: 47.9375rem) {
    .meter-row-compact {
      min-width: 10rem;
    }
  }

  /* Indicateur de chargement : icône « loader » fixe + texte (aucune rotation). */
  .loading {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    color: var(--color-text-2);
  }

  /* --- 23. Chrono, état d'enregistrement, navigateur de questions ------------------------- */

  /* Chrono : mono, chiffres tabulaires ; sous 5 min avertissement, sous 1 min alerte,
     toujours avec le texte « Moins de 5 min » / « Moins d'1 min ». */
  .timer {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
    color: var(--color-text-strong);
    font-family: var(--font-mono);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  .timer-note {
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 600;
  }

  /* Mention abrégée (« < 5 min ») des petits téléphones, section 28 ; la mention complète
     reste lue. */
  .timer-note-short {
    display: none;
  }

  .timer.is-low {
    color: var(--color-warning);
  }

  .timer.is-critical {
    color: var(--color-error);
  }

  .timer-lg {
    font-size: var(--text-2xl);
    line-height: var(--leading-2xl);
  }

  .timer-lg .icon {
    width: var(--space-l);
    height: var(--space-l);
  }

  /* Jauge de temps (live) : barre fine sous le chrono. */
  .timer-gauge {
    --bar: var(--color-text-strong);
    height: 4px;
  }

  .timer-gauge.is-low { --bar: var(--color-warning); }
  .timer-gauge.is-critical { --bar: var(--color-error); }

  /* État d'enregistrement (par question et global) : icône + texte. */
  .save-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    color: var(--color-text-2);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .save-status.is-saved .icon {
    color: var(--color-success);
  }

  .save-status.is-saving .icon {
    color: var(--color-text-2);
  }

  .save-status.is-error {
    color: var(--color-error);
    font-weight: 600;
  }

  /* Écrans étudiant : un échec d'enregistrement est lu en 16 px, jamais en 14. */
  .student-page .save-status.is-error,
  .exam-page .question-foot .save-status.is-error {
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  /* Navigateur de questions : une pastille par question, état écrit dans aria-label et doublé
     d'une forme (contour plein, pointillé, icône). */
  .qnav {
    display: grid;
    grid-template-columns: repeat(auto-fill, var(--size-control));
    gap: var(--space-s);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .qnav-item {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--size-control);
    height: var(--size-control);
    border: 1px dashed var(--color-muted);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text-2);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-weight: 600;
    text-decoration: none;
  }

  .qnav-item:hover,
  .qnav-item.is-hover {
    border-color: var(--color-text-2);
    color: var(--color-text-strong);
  }

  /* Répondue : contour plein `text-2` (9:1), fond. */
  .qnav-item.is-answered {
    border-style: solid;
    border-color: var(--color-text-2);
    background: var(--color-surface-2);
    color: var(--color-text-strong);
  }

  /* Courante : contour épais clair. */
  .qnav-item[aria-current="true"] {
    border: 2px solid var(--color-text-strong);
    color: var(--color-text-strong);
  }

  /* Non enregistrée ou ordre à confirmer : contour plein et marque d'état dans le coin. */
  .qnav-item.is-error {
    border-style: solid;
    border-color: var(--color-error);
    color: var(--color-error);
  }

  .qnav-item.is-pending {
    border-style: solid;
    border-color: var(--color-warning);
    color: var(--color-warning);
  }

  /* Pastille « ! » : fond de l'état, point d'exclamation sombre (la couleur d'état ne doit pas
     écraser celle du texte, sinon le « ! » disparaît). */
  .qnav-item.is-error::after,
  .qnav-item.is-pending::after {
    content: "!" / "";
    position: absolute;
    top: -6px;
    right: -6px;
    display: grid;
    place-items: center;
    width: 16px;
    height: 16px;
    border-radius: var(--radius-full);
    color: var(--color-bg);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 700;
    line-height: 1;
  }

  .qnav-item.is-error::after { background: var(--color-error); }
  .qnav-item.is-pending::after { background: var(--color-warning); }

  /* Légende dessinée (panel P1-16) : une mini-pastille d'exemple devant chaque état. */
  .qnav-legend {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    margin: 0;
    padding: 0;
    list-style: none;
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .qnav-legend li {
    display: flex;
    align-items: flex-start;
    gap: var(--space-s);
  }

  .qnav-sample {
    position: relative;
    flex: none;
    width: var(--space-m);
    height: var(--space-m);
    margin-top: 2px;
    border: 1px dashed var(--color-muted);
    border-radius: var(--space-xs);
  }

  .qnav-sample.is-answered {
    border-style: solid;
    border-color: var(--color-text-2);
    background: var(--color-surface-2);
  }

  .qnav-sample.is-current {
    border: 2px solid var(--color-text-strong);
  }

  .qnav-sample.is-pending {
    border-style: solid;
    border-color: var(--color-warning);
  }

  .qnav-sample.is-pending::after {
    content: "";
    position: absolute;
    top: -4px;
    right: -4px;
    width: var(--space-s);
    height: var(--space-s);
    border-radius: var(--radius-full);
    background: var(--color-warning);
  }

  /* --- 24. Question de la copie et types de réponse --------------------------------------- */

  /* Question de la copie : numéro, énoncé 18 px (62 ch), réponse, état d'enregistrement. Au
     PC, toutes les cartes prennent la largeur élargie ; les réponses gardent la mesure de
     lecture (760 px). */
  .question {
    display: flex;
    flex-direction: column;
    gap: var(--space-m);
    min-width: 0;
    margin: 0;
    padding: var(--space-l);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
  }

  .question-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-xs) var(--space-m);
  }

  .question-points {
    color: var(--color-muted);
    font-size: var(--text-sm);
  }

  .question-prompt {
    max-width: 62ch;
    color: var(--color-text-strong);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
    white-space: pre-line;
  }

  .question-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-s);
  }

  /* Copie : le formulaire d'enregistrement de la question et l'emplacement de son état ne
     font pas de boîte (la carte garde ses écarts, le pied sa rangée). */
  .question > .question-form,
  .save-slot {
    display: contents;
  }

  /* Échec d'enregistrement : alerte sur toute la largeur du pied (panel P1-18). */
  .question-foot .alert {
    flex: 1 1 100%;
  }

  .question .choice-list,
  .question .match-list,
  .question .order-list,
  .question > .field,
  .question .blank-text {
    max-width: var(--question-column);
  }

  @media (max-width: 47.9375rem) {
    .question {
      padding: var(--space-m);
    }
  }

  /* Texte à trous : champs dans la phrase, interligne de 48 px pour eux. */
  .blank-text {
    max-width: var(--measure);
    color: var(--color-text-strong);
    font-size: var(--text-lg);
    line-height: var(--space-2xl);
  }

  .input-inline {
    display: inline-block;
    width: 9em;
    min-height: var(--size-control-sm);
    margin: 0;
    padding: 0 var(--space-s);
    border: 1px solid var(--color-muted);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text);
    font-size: var(--text-md);
    line-height: var(--leading-md);
    vertical-align: baseline;
  }

  .input-inline:hover {
    border-color: var(--color-text-2);
  }

  /* Appariement : élément à gauche, liste à droite ; l'un sous l'autre au téléphone. */
  .match-list {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .match-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-xs) var(--space-m);
    align-items: center;
    padding-block: var(--space-s);
    border-bottom: 1px solid var(--color-border);
  }

  .match-row:last-child {
    border-bottom: 0;
  }

  .match-term {
    color: var(--color-text-strong);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  @media (max-width: 47.9375rem) {
    .match-row {
      grid-template-columns: minmax(0, 1fr);
    }
  }

  /* Ordonnancement : position, élément, Monter / Descendre. */
  .order-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-s);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .order-item {
    display: grid;
    grid-template-columns: var(--space-xl) minmax(0, 1fr) auto;
    gap: var(--space-m);
    align-items: center;
    min-height: var(--size-bar);
    padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-m);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface-2);
    color: var(--color-text-strong);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  .order-item .btn-group {
    gap: var(--space-xs);
  }

  /* Monter / Descendre bien visibles ; désactivés seulement à leur limite (panel P1-20). */
  .order-item .btn-quiet:not(:disabled) {
    color: var(--color-text-strong);
  }

  /* Nombre de caractères d'une réponse libre, au bord du champ. */
  .char-count {
    max-width: none;
    color: var(--color-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    font-variant-numeric: tabular-nums;
    text-align: right;
  }

  /* --- 25. Code : coloration, barre de saisie, disposition, sorties, lecture seule -------- */
  /* L'éditeur lui-même (CodeMirror) est habillé hors couche, section 34. */

  /* Coloration : chaque couleur ≥ 4,5:1 sur --color-surface-2 (charte). */
  .tok-keyword { color: var(--red-400); }
  .tok-string,
  .tok-string2 { color: var(--color-success); }
  .tok-comment { color: var(--color-muted); font-style: italic; }
  .tok-number,
  .tok-bool,
  .tok-atom { color: var(--color-warning); }
  .tok-propertyName { color: var(--color-text-2); }
  /* Après .tok-propertyName : une méthode appelée (« console.log(…) ») est une fonction. */
  .tok-typeName,
  .tok-className,
  .tok-function { color: var(--red-300); } /* fonctions appelées et natives (panel P1-33) */
  .tok-definition { color: var(--color-text-strong); font-weight: 600; }
  .tok-meta { color: var(--color-muted); }
  .tok-invalid { color: var(--color-error); text-decoration: underline wavy; }

  /* Points de coupure du retour à la ligne (éditeur et code en lecture seule, panel P1-27) :
     marque posée par static/js/code-mirror.js après « . », « ( », « , » et avant « < » ;
     l'espace sans chasse ajoutée n'est ni affichée, ni copiée, ni lue. */
  .cm-wrap-after::after {
    content: "\200B";
  }

  .cm-wrap-before::before {
    content: "\200B";
  }

  /* En-tête d'un éditeur : libellé, état, raccourcis. */
  .code-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-s);
  }

  /* Copie : état d'enregistrement de l'exercice à droite, avant « Tab sort de l'éditeur » et
     « Raccourcis clavier » ; un échec d'enregistrement passe sous l'en-tête, pleine largeur. */
  .code-header > .save-slot > .save-status {
    margin-left: auto;
  }

  .code-header > .save-slot > .alert {
    flex: 1 1 100%;
    order: 1;
  }

  /* Au téléphone, un seul état d'enregistrement : celui du bandeau (décision I-08). */
  @media (max-width: 47.9375rem) {
    .code-header > .save-slot > .save-status {
      display: none;
    }
  }

  /* Barre de saisie au-dessus du clavier (téléphone) : touches de 48 px, défilement horizontal
     seul permis ici. */
  .code-keys {
    display: flex;
    gap: var(--space-xs);
    overflow-x: auto;
    padding: var(--space-xs);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
  }

  .code-key {
    flex: none;
    min-width: var(--size-touch);
    min-height: var(--size-touch);
    padding: 0 var(--space-s);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text-strong);
    font-family: var(--font-mono);
    font-size: var(--text-md);
    cursor: pointer;
  }

  .code-key:active,
  .code-key.is-active {
    background: var(--color-surface);
  }

  /* Barre de saisie fixée au-dessus du clavier (téléphone, clavier ouvert) : remplace la barre
     basse. Les caractères défilent (dernière touche coupée par le bord) ; « Exécuter » ou
     « Tests » reste fixé à droite, hors de la zone qui défile (panel P1-01). Jamais
     « Soumettre » ici. Sur iPhone, le clavier reste dans la fenêtre de mise en page : le JS
     d'interface pose sa hauteur dans --keyboard-inset (seule propriété écrite par un script,
     systeme.md § 12) ; ailleurs, 0. */
  .code-keys-bar {
    position: fixed;
    right: 0;
    bottom: var(--keyboard-inset, 0px);
    left: 0;
    z-index: 25;
    display: flex;
    align-items: center;
    gap: var(--space-s);
    padding: var(--space-s) var(--gutter);
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
  }

  .code-keys-bar .code-keys {
    flex: 1;
    min-width: 0;
    border: 0;
    padding: 0;
    background: transparent;
  }

  .code-keys-run {
    flex: none;
    min-height: var(--size-touch);
    padding-inline: calc(var(--space-s) + var(--space-xs));
  }

  .code-editor {
    min-width: 0;
  }

  /* Actions d'un exercice, sous l'éditeur : essai et raccourci à gauche, soumissions restantes
     et « Soumettre » à droite ; le terminal suit (panel P1-26). */
  .code-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-s) var(--space-m);
  }

  .code-actions-end {
    margin-left: auto;
  }

  /* Sortie d'un exercice (terminal) : sous les actions au PC, seulement après une première
     exécution (pas de terminal vide). Au téléphone, feuille du bas non modale ouverte à
     chaque lancement (.is-open, static/js/code-exercise.js) : 60 % de la hauteur au plus, à
     la hauteur de son contenu, au-dessus de la barre basse ; la page réserve cette hauteur
     (tout le code reste atteignable au-dessus, panel P1-31, décision I-17 c). */
  .code-output:not(:has(.run-state > *)) {
    display: none;
  }

  @media (max-width: 47.9375rem) {
    .code-output {
      display: none;
    }

    .question.is-current .code-output.is-open {
      position: fixed;
      inset: auto 0 0;
      z-index: 30;
      display: flex;
      flex-direction: column;
      max-height: 60vh;
      border: 1px solid var(--color-border-strong);
      border-bottom: 0;
      border-radius: var(--radius-lg) var(--radius-lg) 0 0;
      background: var(--color-surface);
      box-shadow: var(--shadow-md);
    }

    .code-output .code-run {
      padding: var(--space-m) var(--space-m) calc(var(--space-m) + env(safe-area-inset-bottom));
      overflow-y: auto;
    }

    /* « Voir le résultat » : seulement s'il y a un résultat et que la feuille est fermée. */
    .question-code:not(:has(.run-state > *)) .code-output-open,
    .question-code:has(.code-output.is-open) .code-output-open {
      display: none;
    }

    /* Soumission échouée : « Revenir au code » devient l'action principale, sous le terminal. */
    .code-output:has(.code-output-actions) .sheet-header > .btn {
      display: none;
    }

    .has-bottom-bar:has(.question.is-current .code-output.is-open) {
      padding-bottom: calc(60vh + var(--space-m));
    }

    html:has(.question.is-current .code-output.is-open) {
      scroll-padding-bottom: calc(60vh + var(--space-m));
    }
  }

  /* Barre basse d'un exercice (téléphone, une question à l'écran) : Questions · Exécuter ou
     Tests · Soumettre, à la place de Précédente · Questions · Suivante (panel P1-02) ; masquée
     avec les autres barres basses pendant la saisie (barre de caractères, .is-typing). Sans
     JavaScript (questions à la suite), aucune : ses boutons relaient ceux de .code-actions. */
  .code-bar {
    display: none;
  }

  @media (max-width: 47.9375rem) {
    .question-stack.is-paged > .question.is-current .code-bar {
      display: grid;
    }

    .exam-page:has(.question-stack.is-paged > .question.is-current .code-bar)
      .bottom-bar:not(.code-bar),
    .exam-page:has(.question.is-typing) .bottom-bar:not(.code-bar),
    .question-stack.is-paged > .question.is-typing .code-bar {
      display: none;
    }
  }

  /* Exercice au PC : le pied de carte ne porte que « Précédente » / « Suivante », masqués hors
     du mode une question à la fois ; il ne laisse pas d'écart vide sous le terminal. */
  .question-stack:not(.is-single) .question-code .question-foot {
    display: none;
  }

  /* Message d'une soumission dans le terminal : texte, aide, action. */
  .output-message {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-s);
    font-family: var(--font-body);
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  /* Sortie d'exécution dans le terminal : titre de section en texte, valeur en mono. */
  .output-status {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-s);
    margin-bottom: var(--space-m);
    font-family: var(--font-body);
  }

  .output-section + .output-section {
    margin-top: var(--space-m);
  }

  .output-label {
    display: block;
    margin-bottom: var(--space-xs);
    color: var(--color-muted);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: var(--leading-sm);
  }

  .output-value {
    max-height: 20rem;
    margin: 0;
    overflow: auto;
    color: var(--color-text);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
    white-space: pre-wrap;
    word-break: break-word;
  }

  .output-value.is-error {
    color: var(--color-error);
  }

  .output-empty {
    color: var(--color-muted);
    font-style: italic;
  }

  /* Résultat SQL : tableau mono défilant dans sa zone. */
  .sql-result {
    max-height: 20rem;
    overflow: auto;
  }

  .sql-result caption {
    padding-bottom: var(--space-xs);
    color: var(--color-muted);
    font-family: var(--font-body);
    text-align: left;
  }

  .sql-result table {
    border-collapse: collapse;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .sql-result th,
  .sql-result td {
    padding: var(--space-xs) var(--space-s);
    border: 1px solid var(--color-border-strong);
    text-align: left;
    white-space: pre;
  }

  .sql-result th {
    background: var(--color-surface-2);
    color: var(--color-text-strong);
  }

  /* Code en lecture seule (tests publics, base de données, correction) : retour à la ligne
     comme l'éditeur, jamais de texte coupé à droite (panel P1-32). */
  .code-view {
    margin: 0;
    padding: var(--space-s) var(--space-m);
    border: 1px solid var(--color-border-strong);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text);
    line-height: var(--leading-sm);
  }

  /* Code à copier (code de reprise, lien d'invitation, clé 2FA) : valeur mono + « Copier ». */
  .copy-field {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-s);
  }

  .copy-value {
    display: inline-flex;
    align-items: center;
    min-height: var(--size-control);
    padding: 0 var(--space-m);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text-strong);
    font-family: var(--font-mono);
    font-size: var(--text-md);
    letter-spacing: 0.08em;
    overflow-wrap: anywhere;
  }

  .copy-value-lg {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  /* --- 26. Live : code projeté, choix, verdict, classement, barre de pilotage ------------- */

  /* Code de session projeté : 72 px, dans un terminal (motif 7). */
  .code-display {
    max-width: none;
    color: var(--color-text-strong);
    font-family: var(--font-mono);
    font-size: var(--text-display);
    font-weight: 700;
    line-height: var(--leading-display);
    letter-spacing: 0.08em;
    text-align: center;
  }

  /* Choix projetés et choix révélés : carte, puce « > », valeur écrite. */
  .live-options {
    display: grid;
    gap: var(--space-m);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* Choix projetés : tous sur une ligne dès 1 024 px (jamais de case orpheline, panel P1-38). */
  @media (min-width: 64rem) {
    .live-options-row {
      grid-auto-columns: minmax(0, 1fr);
      grid-auto-flow: column;
    }
  }

  .live-option {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-s);
    padding: var(--space-m) var(--space-m) var(--space-m) var(--space-2xl);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface-2);
    color: var(--color-text-strong);
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  .live-option::before {
    content: ">" / "";
    position: absolute;
    top: var(--space-m);
    left: var(--space-m);
    color: var(--red-500);
    font-family: var(--font-mono);
  }

  .live-option.is-right {
    border: 2px solid var(--color-success);
  }

  .live-option .meter-row {
    grid-template-columns: minmax(0, 1fr) auto;
    font-size: var(--text-md);
  }

  /* Verdict de l'étudiant : icône, mot, points. */
  .verdict {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-xs) var(--space-m);
    align-items: center;
    padding: var(--space-m);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface);
  }

  .verdict > .icon {
    width: var(--space-xl);
    height: var(--space-xl);
  }

  .verdict-title {
    color: var(--color-text-strong);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 600;
    line-height: var(--leading-xl);
  }

  .verdict.is-right { border-color: var(--color-success); }
  .verdict.is-right > .icon,
  .verdict.is-right .verdict-title { color: var(--color-success); }
  .verdict.is-partial { border-color: var(--color-warning); }
  .verdict.is-partial > .icon,
  .verdict.is-partial .verdict-title { color: var(--color-warning); }
  .verdict.is-wrong { border-color: var(--color-error); }
  .verdict.is-wrong > .icon,
  .verdict.is-wrong .verdict-title { color: var(--color-error); }

  /* Classement : cartes numérotées 01, 02, 03 (rouge, mono, 32 px) ; podium en lueur sur
     l'écran projeté (section 32). Points et durée sur une ligne. */
  .ranking {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
    gap: var(--space-m);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .rank-card {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: 0 var(--space-m);
    align-items: center;
    padding: var(--space-m);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
  }

  .rank-number {
    grid-row: span 2;
    color: var(--red-500);
    font-family: var(--font-mono);
    font-size: var(--text-2xl);
    font-weight: 600;
    line-height: var(--leading-2xl);
  }

  .rank-name {
    overflow: hidden;
    color: var(--color-text-strong);
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .rank-points,
  .rank-time {
    color: var(--color-text-2);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  .rank-time {
    grid-column: 3;
    grid-row: 1 / span 2;
  }

  .rank-card.is-podium {
    border-color: var(--color-border-strong);
  }

  .rank-card.is-me {
    border: 2px solid var(--color-text-strong);
  }

  /* Barre de pilotage du live : en bas, sobre, état court et une action principale. */
  .pilot-bar {
    position: sticky;
    bottom: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-m);
    padding: var(--space-m) 0;
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
  }

  /* Suivi du live non projeté (T-17) : barre en tête de page, collée sous l'en-tête ; elle ne
     couvre jamais le haut de la liste des participants (panel P1-39). */
  .pilot-bar-top {
    top: var(--size-header);
    bottom: auto;
    z-index: 10;
    border-top: 0;
    border-bottom: 1px solid var(--color-border);
  }

  @media (max-width: 63.9375rem) {
    .pilot-bar-top {
      top: var(--size-bar);
    }
  }

  /* Écran projeté, question et révélation : rappel du code de session pour les retardataires,
     à droite de l'état court, au pas de la méta (chef design, G6) ; jamais sur deux lignes. */
  .pilot-code {
    margin-inline-start: var(--space-m);
    white-space: nowrap;
  }

  /* Au téléphone : action principale pleine largeur, « … » à sa droite, secondaire dessous
     (remarque du chef design, étape 5, panel P1-39) ; pas de rappel du code. */
  @media (max-width: 47.9375rem) {
    .pilot-bar {
      gap: var(--space-s);
    }

    .pilot-code {
      display: none;
    }

    .pilot-bar > .btn-group {
      flex: 1 1 100%;
      flex-wrap: wrap;
    }

    .pilot-bar .btn-primary {
      flex: 1 1 0;
      order: -2;
    }

    .pilot-bar .btn-icon {
      order: -1;
    }

    .pilot-bar .btn-secondary {
      flex: 1 1 100%;
    }
  }

  /* Zones remplacées par le live (#live-state du pilotage, parties de la barre de pilotage et
     formulaire de son action principale), sans boîte : l'en-tête, la zone de contenu et les
     boutons restent enfants de .projection et de .btn-group (règles du téléphone ci-dessus). */
  .live-zone,
  .pilot-slot,
  .pilot-slot > form {
    display: contents;
  }

  /* Révélation au téléphone (décision I-17 a) : les badges d'une réponse (« Bonne réponse »,
     « Votre choix ») passent ensemble sur une ligne sous le texte, dans le même ordre. */
  @media (max-width: 47.9375rem) {
    .live-option > .cluster > :first-child {
      flex-basis: 100%;
    }
  }

  /* Salle d'attente projetée : noms publics des arrivants, en colonnes, sans autre donnée
     (panel P1-40). */
  .arrivals {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0 var(--space-l);
    margin: 0;
    padding: 0;
    list-style: none;
    color: var(--color-text-2);
    font-family: var(--font-body);
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  /* --- 27. Pied des pages publiques : signature « rootit-lab.com » (décorative) ----------- */

  .site-footer {
    padding-block: var(--space-l);
    border-top: 1px solid var(--color-border);
    color: var(--color-muted);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }
}

@layer screens {
  /* --- 28. Copie étudiante : bandeau, disposition, une question à l'écran ----------------- */

  /* Copie et live : le contenu (<main>) précède le bandeau dans le document, pour que l'énoncé
     vienne avant « Remettre ma copie » au clavier et au lecteur d'écran ; l'ordre d'affichage
     remet le bandeau en haut (panel P1-05). */
  body.exam-page {
    display: flex;
    flex-direction: column;
  }

  body.exam-page > main {
    order: 1;
  }

  /* « Avant de commencer » ouvert (section 21) : le reste de la page n'est pas rendu, rien ne
     défile sous le voile opaque ; la copie apparaît à sa fermeture. */
  body:has(> .dialog-start[open]) > :not(.dialog-start) {
    display: none;
  }

  /* Écrans étudiant : méta et aides en texte secondaire (≈ 10:1 au lieu de 7:1), tailles
     inchangées (panel P1-17). */
  .student-page :is(.text-muted, .page-meta, .hint, .optional, .question-points, .char-count, .qnav-legend, .stat dt, .stat-unit, .summary dt, .output-label, .output-empty, .terminal-bar) {
    color: var(--color-text-2);
  }

  /* Bandeau de copie : 64 px au PC, 56 au téléphone ; identité, chrono (et tiers-temps),
     progression, état d'enregistrement, « Remettre ma copie ». Alerte d'une ligne dessous
     (.attempt-bar-sub). */
  .attempt-bar {
    position: sticky;
    top: 0;
    z-index: 20;
    border-bottom: 1px solid var(--color-border);
    background: var(--color-bg);
  }

  .attempt-bar-inner {
    display: flex;
    align-items: center;
    gap: var(--space-l);
    min-height: var(--size-header);
  }

  .attempt-identity {
    min-width: 0;
    overflow: hidden;
    color: var(--color-text-2);
    font-size: var(--text-sm);
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .attempt-bar .save-status {
    white-space: nowrap;
  }

  .attempt-progress {
    color: var(--color-text-2);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  .attempt-progress strong {
    color: var(--color-text-strong);
  }

  .attempt-bar-inner > .btn {
    margin-left: auto;
  }

  /* Échéance d'un devoir à la place du chrono. */
  .attempt-deadline {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
  }

  /* Chrono et, pour un tiers-temps, badge neutre juste à sa droite (panel P1-13). */
  .attempt-time {
    display: inline-flex;
    align-items: center;
    gap: var(--space-s);
  }

  /* État global : texte long au PC, court sous 1 024 px (« Enregistré », « Envoi… »,
     « Non enregistré », panel P1-14). */
  .attempt-bar .save-short {
    display: none;
  }

  /* Alerte d'une ligne affichée sous le bandeau (hors plein écran) : la marge de défilement
     suit sa hauteur (106 px mesurés au PC, 179 au téléphone, texte puis bouton) ; une ancre ou
     un champ focalisé n'est jamais caché dessous (WCAG 2.4.11). */
  html:has(.attempt-bar-sub:not([hidden])) {
    scroll-padding-top: calc(var(--size-header) * 2);
  }

  @media (max-width: 47.9375rem) {
    html:has(.attempt-bar-sub:not([hidden])) {
      scroll-padding-top: calc(var(--size-header) * 3 + var(--space-s));
    }
  }

  /* Ligne sous le bandeau du live (tiers-temps). */
  .attempt-bar-note {
    padding-block: 0 var(--space-s);
  }

  /* Jauge de temps sous le bandeau (live). */
  .attempt-bar > .timer-gauge {
    border-radius: 0;
  }

  /* Entre 1 024 et 1 279 px, le bandeau s'aligne sur la colonne de la copie (navigateur +
     questions) ; dès 1 280 px la copie prend tout le cadre (exercices de code élargis). */
  @media (min-width: 64rem) and (max-width: 79.9375rem) {
    .attempt-bar:not(.is-wide) :is(.attempt-bar-inner, .attempt-bar-sub) {
      max-width: calc(var(--nav-column) + var(--space-xl) + var(--question-column) + 2 * var(--gutter));
    }
  }

  @media (max-width: 47.9375rem) {
    .attempt-bar-inner {
      gap: var(--space-m);
      min-height: var(--size-bar);
    }

    .attempt-bar .attempt-identity,
    .attempt-bar-inner > .btn {
      display: none;
    }

    .attempt-bar .save-status {
      margin-left: auto;
    }
  }

  /* Sous 1 024 px : état d'enregistrement en texte court ; « À remettre avant le » lu, pas
     vu. */
  @media (max-width: 63.9375rem) {
    .attempt-bar .save-long {
      display: none;
    }

    .attempt-bar .save-short {
      display: inline;
    }

    .attempt-bar .attempt-deadline-label {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }
  }

  /* Sous 1 024 px, bandeau chargé (seuil du chrono, tiers-temps, échéance), et petits téléphones :
     l'état d'enregistrement prime, « répondues » est lu sans être vu (panel P1-14). */
  @media (max-width: 63.9375rem) {
    .attempt-bar:has(.timer-note, .attempt-badge, .attempt-deadline) .attempt-progress-label {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }
  }

  /* Sous 400 px : mention du chrono abrégée (« < 5 min », texte complet lu) ; sous 380 px :
     écart de 8 px et badge de tiers-temps sans icône ; sous 360 px, « répondues » lu
     seulement. */
  @media (max-width: 24.9375rem) {
    .attempt-bar .timer-note-long {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }

    .attempt-bar .timer-note-short {
      display: inline;
    }
  }

  @media (max-width: 23.6875rem) {
    .attempt-bar-inner {
      gap: var(--space-s);
    }

    .attempt-badge .icon {
      display: none;
    }
  }

  @media (max-width: 22.4375rem) {
    .attempt-bar .attempt-progress-label {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }

    /* Chrono d'une heure ou plus avec tiers-temps (« 1:20:00 », « +33 % ») : sans icône. */
    .attempt-bar .attempt-time:has(.attempt-badge) .timer > .icon {
      display: none;
    }
  }

  /* Disposition de la copie au PC : navigateur collant à gauche, questions à droite. Dès
     1 280 px, une seule règle : la colonne prend toute la largeur (exercice de code élargi). */
  .attempt-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xl);
    padding-block: var(--space-l) var(--space-3xl);
  }

  @media (min-width: 64rem) {
    .attempt-layout {
      grid-template-columns: var(--nav-column) minmax(0, var(--question-column));
      justify-content: center;
    }

    .attempt-layout.is-wide {
      grid-template-columns: var(--nav-column) minmax(0, 1fr);
    }

    .attempt-aside {
      position: sticky;
      top: calc(var(--size-header) + var(--space-l));
      align-self: start;
    }
  }

  @media (min-width: 80rem) {
    .attempt-layout {
      grid-template-columns: var(--nav-column) minmax(0, 1fr);
    }
  }

  @media (max-width: 63.9375rem) {
    .attempt-aside {
      display: none;
    }
  }

  /* Une question à l'écran sous 1 024 px (téléphone et tablette, avec la barre basse et la
     feuille « Questions », .compact-only) : le JS d'interface pose .is-paged sur la pile et
     .is-current sur la question affichée ; toutes restent dans la page (sans JS : à la
     suite). */
  .question-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-l);
  }

  @media (max-width: 63.9375rem) {
    .question-stack.is-paged > :not(.is-current) {
      display: none;
    }
  }

  /* « Une question à la fois » au PC (interrupteur de « Consigne et aide », choix gardé dans
     le navigateur ; panel P1-21) : même masquage qu'au téléphone, à toutes les largeurs. */
  .question-stack.is-single > :not(.is-current) {
    display: none;
  }

  .question-pager {
    margin-left: auto;
  }

  /* « Précédente » / « Suivante » en pied de carte : seulement une question à la fois, au PC
     (sous 1 024 px, la barre basse les porte). */
  .question-stack:not(.is-single) .question-pager {
    display: none;
  }

  @media (max-width: 63.9375rem) {
    .question-pager {
      display: none;
    }
  }

  /* Message d'erreur de la page (envoi sans JavaScript refusé) : sur toute la grille. */
  .attempt-layout > .page-alert {
    grid-column: 1 / -1;
  }

  /* Titre de la copie (nom de l'épreuve) : un h1 à toutes les largeurs, masqué visuellement
     au téléphone où le bandeau occupe le haut (panel P1-05). */
  @media (max-width: 47.9375rem) {
    .attempt-intro {
      display: contents;
    }

    .attempt-title {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
  }

  /* --- 29. Accueil et connexion : visuels de la charte, sans logo en double ------------- */

  /* Accueil (panel P1-58) : texte et action à gauche, visuel officiel de la charte à droite
     dès 1 024 px (logo-glow.png, noir fondu par mix-blend-mode: screen, bords adoucis par un
     masque : le carré de l'image ne se voit pas sur la grille) ; au-dessous, le
     visuel passe au-dessus du texte, réduit. */
  .hero {
    display: grid;
    gap: var(--space-l) var(--space-2xl);
    align-items: center;
    padding-block: var(--space-2xl);
  }

  .hero-text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-m);
  }

  .hero .display {
    margin-top: calc(-1 * var(--space-s));
  }

  .hero-kinds {
    color: var(--color-text-2);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .hero-visual {
    order: -1;
    width: 10rem;
    height: auto;
    mix-blend-mode: screen;
    -webkit-mask-image: radial-gradient(closest-side, var(--color-bg) 70%, transparent);
    mask-image: radial-gradient(closest-side, var(--color-bg) 70%, transparent);
  }

  @media (min-width: 64rem) {
    .hero {
      grid-template-columns: minmax(0, 1fr) minmax(0, 28rem);
      padding-block: var(--space-3xl);
    }

    .hero-visual {
      order: 0;
      width: 100%;
    }
  }

  /* Connexion (T-01 à T-03, panel P1-58) : colonne centrée dans la fenêtre (body.page-fill),
     logo complet de la charte (logo-complet-sombre.webp, noir fondu par mix-blend-mode:
     screen, recadré sur le logo et sa signature) au-dessus du titre, sans en-tête : un seul
     logo par écran. Activation de la double authentification : colonne de formulaire (720). */
  .login {
    gap: var(--space-l);
    max-width: calc(30rem + 2 * var(--gutter));
    padding-block: var(--space-xl);
  }

  .login-wide {
    max-width: calc(var(--form-max) + 2 * var(--gutter));
  }

  .login > h1 {
    text-align: center;
  }

  .login > .lead {
    align-self: center;
    text-align: center;
  }

  .login-logo {
    align-self: center;
    border-radius: var(--radius-md);
  }

  /* Bords fondus (masque) : le fond presque noir de l'image ne dessine aucun cadre sur la
     grille. */
  .login-logo img {
    width: 15rem;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    object-position: 50% 45%;
    mix-blend-mode: screen;
    -webkit-mask-image:
      linear-gradient(to right, transparent, var(--color-bg) 10%, var(--color-bg) 90%, transparent),
      linear-gradient(transparent, var(--color-bg) 8%, var(--color-bg) 92%, transparent);
    -webkit-mask-composite: source-in;
    mask-image:
      linear-gradient(to right, transparent, var(--color-bg) 10%, var(--color-bg) 90%, transparent),
      linear-gradient(transparent, var(--color-bg) 8%, var(--color-bg) 92%, transparent);
    mask-composite: intersect;
  }

  @media (max-width: 47.9375rem) {
    .login-logo img {
      width: 12rem;
    }
  }

  /* --- 30. Correction et aperçu ----------------------------------------------------------- */

  /* État et points d'une question corrigée (badge, « 1,5 / 3 pt ») : zone remplacée hors bande
     après des points fixés, sans boîte (l'en-tête de la question garde sa rangée). */
  .question-state {
    display: contents;
  }

  /* Réponse d'un étudiant affichée en lecture (correction). */
  .answer-view {
    max-width: var(--measure);
    margin: 0;
    padding: var(--space-s) var(--space-m);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface-2);
    color: var(--color-text);
    white-space: pre-wrap;
  }

  /* En-tête collant de la correction : identité, note, pagination, « Corrigée, copie
     suivante ». */
  .review-bar {
    position: sticky;
    top: var(--size-header);
    z-index: 10;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding-block: var(--space-m);
    border-bottom: 1px solid var(--color-border);
    background: var(--color-bg);
  }

  @media (max-width: 63.9375rem) {
    .review-bar {
      top: var(--size-bar);
    }
  }

  /* Refus de « Corrigée » (attempt.html) : toujours présent pour les réponses HTMX ; vide, il ne
     prend pas d'écart de la pile (l'en-tête suit la méta, comme la maquette). */
  .review-error:empty {
    display: none;
  }

  /* Aperçu côté étudiant : bandeau de l'aperçu, et cadre de 390 px « comme sur un
     téléphone ». */
  .preview-banner {
    position: sticky;
    top: 0;
    z-index: 30;
    padding-block: var(--space-s);
    border-bottom: 1px solid var(--color-border-strong);
    background: var(--color-surface);
  }

  /* Dans la copie de l'aperçu, le bandeau défile : seul le bandeau de copie reste collé. */
  .exam-page .preview-banner {
    position: static;
  }

  .preview-frame {
    display: block;
    width: 100%;
    max-width: 24.375rem;
    height: 52.75rem;
    margin: var(--space-l) auto;
    overflow: hidden;
    background: var(--color-bg);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-lg);
  }

  /* --- 31. Copies d'une session : cartes compactes ---------------------------------------- */
  /* Variante de .table-responsive.table-wide sous 1 024 px : nom et menu ; badges ; méta ;
     cellules vides masquées. Chaque valeur porte son libellé (« Résultats visibles : Oui »,
     « Arrivée : 15:53 », panel P1-47), jamais un « — » ou un « Oui » nus. Liste des sessions
     (.cell-inline, .cell-stat, panel P1-51) : titre et état ; « code · mode · date » sans
     libellé ; « copies · moyenne ». Une classe suffit : la couche screens l'emporte sur les
     cartes de la section 16. */
  @media (max-width: 63.9375rem) {
    .table-cards-compact tr {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: var(--space-s) var(--space-m);
    }

    .table-cards-compact tr::after {
      content: "";
      flex-basis: 100%;
      order: 3;
    }

    .table-cards-compact td[data-label] {
      display: inline-flex;
      flex-wrap: wrap;
      align-items: center;
      gap: var(--space-xs);
      max-width: 100%;
    }

    .table-cards-compact td[data-label]::before {
      content: attr(data-label) "\00a0:" / "";
      color: var(--color-muted);
      font-size: var(--text-sm);
      line-height: var(--leading-sm);
      white-space: nowrap;
    }

    .table-cards-compact .cell-primary {
      flex: 1 1 calc(100% - var(--size-touch) - var(--space-m));
      order: 0;
    }

    /* Sans menu de ligne (liste des sessions) : l'état sur la ligne du titre. */
    .table-cards-compact tr:not(:has(> .actions)) .cell-primary {
      flex-basis: auto;
    }

    .table-cards-compact .actions {
      order: 1;
    }

    .table-cards-compact .cell-badge {
      order: 2;
    }

    .table-cards-compact .cell-meta {
      order: 4;
      color: var(--color-text-2);
      font-size: var(--text-sm);
      line-height: var(--leading-sm);
    }

    .table-cards-compact td.cell-empty[data-label] {
      display: none;
    }

    .table-cards-compact td.cell-inline[data-label] {
      gap: var(--space-s);
    }

    .table-cards-compact td.cell-inline[data-label]::before {
      content: none;
    }

    .table-cards-compact td.cell-inline + td.cell-inline::before {
      content: "·" / "";
    }

    .table-cards-compact .cell-stat {
      order: 6;
    }

    .table-cards-compact tr:has(> .cell-stat)::before {
      content: "";
      flex-basis: 100%;
      order: 5;
    }
  }

  /* Au PC, le nom (« NOM Prénom ») garde de quoi tenir sur deux lignes au plus, à côté des
     colonnes de dates et de badges (T-16 devoir). */
  @media (min-width: 64rem) {
    .table-cards-compact .cell-primary {
      min-width: 14rem;
    }
  }

  /* --- 32. Écran projeté : échelle, classement final, phase dense ------------------------- */
  /* La racine --p suit la largeur ET la hauteur de la fenêtre ; texte, écarts, icônes et
     contrôles en dérivent (six pas de texte conservés, tout passe un cran au-dessus). Cadre
     stable (invite, titre, marges, barre de pilotage) en échelle normale ; seule la zone de
     contenu de la révélation (.projection-stage.is-dense) passe en échelle dense. Chaque
     écran tient sans défilement de 1024 × 768 à 1920 × 1080. */
  html:has(.projection) {
    --p: clamp(1rem, min(2.2vw, 2.9vh), 2.75rem);
  }

  /* La page réserve la hauteur de la barre de pilotage fixe (contenu et focus jamais
     masqués, WCAG 2.4.11). */
  html:has(.projection > .pilot-bar) {
    scroll-padding-bottom: calc(var(--size-touch) * 3);
  }

  .projection,
  .projection-stage.is-dense,
  .projection-stage.is-compact {
    --text-sm: calc(var(--p) * 0.875);
    --leading-sm: calc(var(--p) * 1.25);
    --text-md: var(--p);
    --leading-md: calc(var(--p) * 1.5);
    --text-lg: calc(var(--p) * 1.125);
    --leading-lg: calc(var(--p) * 1.75);
    --text-xl: calc(var(--p) * 1.5);
    --leading-xl: calc(var(--p) * 2);
    --text-2xl: calc(var(--p) * 2);
    --leading-2xl: calc(var(--p) * 2.5);
    --text-display: calc(var(--p) * 5.5);
    --leading-display: calc(var(--p) * 6);
    --space-xs: calc(var(--p) * 0.25);
    --space-s: calc(var(--p) * 0.5);
    --space-m: var(--p);
    --space-l: calc(var(--p) * 1.5);
    --space-xl: calc(var(--p) * 2);
    --space-2xl: calc(var(--p) * 3);
    --space-3xl: calc(var(--p) * 4);
    --size-icon: calc(var(--p) * 1.25);
    --size-icon-sm: var(--p);
    --size-control: max(var(--size-touch), calc(var(--p) * 2.5));
    --size-control-sm: max(var(--size-touch), calc(var(--p) * 2));
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  .projection {
    --pilot-h: calc(var(--size-control) + 2 * var(--space-m) + 1px);
    display: flex;
    flex-direction: column;
    gap: var(--space-l);
    min-height: 100vh;
    padding: var(--space-l) var(--space-3xl);
  }

  .projection:has(> .pilot-bar) {
    padding-bottom: calc(var(--pilot-h) + var(--space-l));
  }

  .projection-title {
    color: var(--color-text-strong);
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 600;
    line-height: var(--leading-2xl);
  }

  .projection-meta {
    font-family: var(--font-body);
    color: var(--color-text-2);
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  /* Question et révélation : le nom de l'épreuve à la taille de la méta, l'énoncé seul au
     pas « 2xl » (le plus grand texte de l'écran, panel P1-38) ; chrono au pas de la méta. */
  .projection-name {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  .projection-timer {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  .projection .projection-timer .icon {
    width: var(--leading-md);
    height: var(--leading-md);
  }

  .projection .arrivals {
    gap: 0 var(--space-l);
    font-size: var(--text-sm);
    line-height: var(--leading-sm);
  }

  .projection-meta strong {
    color: var(--color-text-strong);
    font-family: var(--font-mono);
  }

  /* Faits projetés côte à côte (connectés, temps restant). */
  .projection-facts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-m) var(--space-2xl);
  }

  .projection .projection-facts {
    justify-content: center;
  }

  /* Adresse de connexion : jamais coupée, sur sa propre ligne si besoin. */
  .projection-address {
    display: inline-block;
    white-space: nowrap;
  }

  .projection p {
    max-width: none;
  }

  .projection h2 {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  .projection .terminal-dots span {
    width: calc(var(--p) * 0.6);
    height: calc(var(--p) * 0.6);
  }

  .projection .terminal-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-m);
    padding: var(--space-l);
    text-align: center;
  }

  .projection .timer-lg .icon {
    width: var(--leading-lg);
    height: var(--leading-lg);
  }

  .projection .btn-lg {
    min-height: var(--size-control);
    padding-inline: var(--space-l);
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  .projection > .pilot-bar {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 20;
    min-height: var(--pilot-h);
    padding: var(--space-m) var(--space-3xl);
  }

  @media (max-width: 47.9375rem) {
    .projection {
      padding-inline: var(--space-m);
    }

    .projection-meta {
      font-size: var(--text-md);
      line-height: var(--leading-md);
    }

    .projection > .pilot-bar {
      padding-inline: var(--space-m);
    }

    /* « Plein écran » passe sous le titre : aligné sur la marge, comme lui. */
    .projection-tool .btn-flush-end {
      margin-inline: calc(-1 * var(--space-m)) 0;
    }
  }

  /* Code projeté sous 480 px : un cran plus petit, 72 px (--p y vaut 1rem), le pas du code
     hors projection (jeton --text-display) ; il garde ses marges dans sa carte. */
  @media (max-width: 29.9375rem) {
    .projection .code-display {
      font-size: calc(var(--p) * 4.5);
      line-height: calc(var(--p) * 5);
    }
  }

  /* Commande discrète d'un écran projeté (« Plein écran ») : à l'échelle de l'interface. */
  .projection-tool {
    --text-md: 1rem;
    --leading-md: 1.5rem;
    --space-s: 0.5rem;
    --space-m: 1rem;
    --size-icon: 1.25rem;
    --size-control: 2.5rem;
  }

  @media (max-width: 47.9375rem), (pointer: coarse) {
    .projection-tool {
      --size-control: var(--size-touch);
    }
  }

  /* Zone de contenu : suit l'en-tête (pas de centrage vertical). */
  .projection-stage {
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: flex-start;
    gap: var(--space-l);
  }

  /* Révélation : échelle dense, jusqu'à 28 px (le classement final reste en échelle normale). */
  .projection-stage.is-dense {
    --p: clamp(1rem, min(1.35vw, 2vh), 1.75rem);
    gap: var(--space-m);
  }

  /* Salle d'attente avec les noms des arrivants (panel P1-40) : échelle intermédiaire, le code
     reste au-dessus de 72 px (≈ 100 px à 1024 × 768, 118 px à 1440 × 900) et tout tient sans
     défilement. */
  .projection-stage.is-compact {
    --p: clamp(1rem, min(1.8vw, 2.4vh), 2.25rem);
  }

  /* Révélation : question et réponses à gauche, bilan et classement à droite. */
  .projection-split {
    display: grid;
    gap: var(--space-l) var(--space-2xl);
  }

  /* Classement projeté : cartes à largeur minimale (nom lisible avant troncature). */
  .projection .ranking {
    grid-template-columns: repeat(auto-fill, minmax(calc(var(--p) * 16), 1fr));
  }

  @media (min-width: 64rem) {
    .projection-split {
      grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
      align-items: start;
    }

    .projection-split .ranking {
      grid-template-columns: minmax(0, 1fr);
    }

    /* Bilan de la classe dans la colonne étroite : barre sous le libellé et la valeur (les
       barres restent visibles à 1024 × 768). */
    .projection-split .meter-row {
      grid-template-columns: minmax(0, 1fr) auto;
      row-gap: var(--space-xs);
    }

    .projection-split .meter-row .meter {
      grid-column: 1 / -1;
      grid-row: 2;
    }
  }

  .projection .rank-card.is-podium {
    box-shadow: var(--shadow-glow);
  }

  .projection .rank-name {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  .projection .rank-points,
  .projection .rank-time,
  .projection .meter-row {
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  /* Classement final : le podium domine (trois cartes larges : numéro, nom jamais tronqué,
     puis points et durée l'un sous l'autre) ; les suivants en cartes compactes, aussi
     employées dans la révélation. */
  .ranking-podium {
    grid-template-columns: minmax(0, 1fr);
  }

  @media (min-width: 48rem) {
    .projection .ranking-podium,
    .ranking-podium {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
  }

  .ranking-podium .rank-card {
    grid-template-columns: auto minmax(0, 1fr);
    padding: var(--space-m) var(--space-l);
  }

  .ranking-podium .rank-number {
    grid-row: span 3;
  }

  .ranking-podium .rank-name {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
    white-space: normal;
  }

  .ranking-podium .rank-time {
    grid-column: 2;
    grid-row: auto;
  }

  .ranking-rest .rank-card {
    padding: var(--space-s) var(--space-m);
  }

  .ranking-rest .rank-number {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  .ranking-rest .rank-name {
    font-size: var(--text-md);
    line-height: var(--leading-md);
  }

  /* Phase dense : réponses plus serrées (la révélation tient aussi à 1024 × 768). */
  .projection-stage.is-dense .stack-l {
    gap: var(--space-m);
  }

  .projection-stage.is-dense .projection-title {
    font-size: var(--text-xl);
    line-height: var(--leading-xl);
  }

  /* L'énoncé reste un pas au-dessus de la ligne de méta (« Question 2 sur 8 · correction »). */
  .projection-stage.is-dense .projection-meta {
    font-size: var(--text-lg);
    line-height: var(--leading-lg);
  }

  .projection-stage.is-dense .live-option {
    gap: var(--space-xs);
    padding-block: var(--space-s);
  }

  .projection-stage.is-dense .live-option::before {
    top: var(--space-s);
  }

  .projection-stage.is-dense .ranking {
    gap: var(--space-s);
  }
}

/* --- 33. Affichage selon la largeur, couleurs forcées ------------------------------------- */
/* .phone-only / .desktop-only / .compact-only : réservés aux éléments qui changent vraiment de
   forme selon l'écran (barre basse, feuilles, mention téléphone ; sortie de code et intro de
   l'épreuve au PC). */

@layer utilities {
  /* Attribut hidden (posé par le gabarit ou le JS d'interface) : l'emporte sur l'affichage de
     tout composant (.btn, .save-status…). */
  [hidden] {
    display: none;
  }

  @media (min-width: 48rem) {
    .phone-only {
      display: none !important;
    }
  }

  @media (max-width: 47.9375rem) {
    .desktop-only {
      display: none !important;
    }
  }

  /* .compact-only : téléphone et tablette (sous 1 024 px), barre basse et feuille « Questions »
     de la copie et de son aperçu, là où le navigateur de questions de la colonne n'a pas la
     place. */
  @media (min-width: 64rem) {
    .compact-only {
      display: none !important;
    }
  }

  /* Couleurs forcées (contraste élevé de Windows) : les formes qui portent un état gardent un
     contour visible, l'anneau de focus prend la couleur système (panel P1-36). Couleurs
     système seulement, aucune couleur de la charte. */
  @media (forced-colors: active) {
    .badge,
    .qnav-item,
    .qnav-sample,
    .choice,
    .live-option,
    .order-item {
      border-color: CanvasText;
    }

    .qnav-item.is-error::after,
    .qnav-item.is-pending::after,
    .qnav-sample.is-pending::after {
      border: 1px solid CanvasText;
    }

    .choice:has(:checked),
    .qnav-item[aria-current="true"] {
      border-width: 2px;
    }

    :focus-visible,
    .is-focus,
    .card-clickable:has(.card-link:focus-visible) {
      outline-color: Highlight;
    }
  }
}

/* --- 34. CodeMirror (hors couche, voir l'en-tête) ------------------------------------------ */

/* Éditeur (thème de la charte) : fond --color-surface-2, bordure des champs. La gouttière suit
   la police et l'interlignage du contenu (constat : numéros tassés). */
.code-editor .cm-editor {
  border: 1px solid var(--color-muted);
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
  color: var(--color-text);
  font-size: var(--text-md);
}

.code-editor .cm-editor.cm-focused {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
}

.code-editor .cm-scroller {
  overflow: auto;
  font-family: var(--font-mono);
  line-height: var(--leading-md);
}

.code-editor .cm-content {
  min-height: calc(8 * var(--leading-md));
  padding: var(--space-s) 0;
  caret-color: var(--color-text);
}

.code-editor .cm-line {
  padding: 0 var(--space-m) 0 var(--space-s);
}

/* Gouttière (panel P1-28, § 7.14 de systeme.md) : même police et même interlignage que le
   code, fond et filet sur toute la hauteur de l'éditeur, 48 px filet compris comme le rendu
   statique ; chaque numéro sur la première ligne visuelle de sa ligne (CodeMirror place les
   numéros d'après les lignes : aucune marge propre à la gouttière). Étirée par le défilement
   flex de CodeMirror : sa hauteur de 100 % (indéfinie) l'en empêcherait. */
.code-editor .cm-gutters {
  height: auto;
  align-self: stretch;
  border-right: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-muted);
  font-family: var(--font-mono);
  line-height: var(--leading-md);
}

.code-editor .cm-lineNumbers .cm-gutterElement {
  box-sizing: border-box;
  min-width: calc(var(--space-2xl) - 1px);
  padding: 0 var(--space-s) 0 var(--space-xs);
  text-align: right;
}

.code-editor .cm-activeLine {
  background: color-mix(in srgb, var(--color-text-strong) 4%, transparent);
}

.code-editor .cm-editor.cm-focused .cm-matchingBracket {
  outline: 1px solid var(--color-muted);
  background: var(--color-border-strong);
}

.code-editor .cm-editor.cm-focused .cm-nonmatchingBracket {
  outline: 1px solid var(--color-error);
  background: none;
}

.code-editor.is-readonly .cm-editor {
  border-style: dashed;
  border-color: var(--color-border-strong);
}

.code-editor.has-error .cm-editor {
  border-color: var(--color-error);
}

/* Éditeur en rendu statique (maquettes, page de référence) : même aspect que CodeMirror ;
   numéro de ligne posé sur la première ligne visuelle, juste aussi quand la ligne revient à
   la ligne. */
.code-editor .cm-static {
  overflow: auto;
  font-family: var(--font-mono);
  line-height: var(--leading-md);
}

.cm-static .cm-content {
  min-height: calc(8 * var(--leading-md) + 2 * var(--space-s));
  margin: 0;
  padding: var(--space-s) 0;
  border-left: var(--space-2xl) solid var(--color-surface);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.cm-static .cm-line {
  position: relative;
  min-height: var(--leading-md);
  padding: 0 var(--space-m) 0 var(--space-s);
}

/* Retour à la ligne (panel P1-27) : coupure aux frontières de jeton (espace, « . », « ( »,
   « , », avant « < » : marques .cm-wrap-*), dans le mot seulement si le jeton dépasse la
   ligne ; la suite reprend le retrait de sa ligne (data-indent : colonnes du retrait, 1 à 16,
   posé par l'éditeur), sans retrait artificiel ; un signe de suite discret (filet pointillé)
   longe les lignes visuelles de suite, au bord de la gouttière. */
.code-editor .cm-line[data-indent="1"] { padding-left: calc(var(--space-s) + 1ch); text-indent: -1ch; }
.code-editor .cm-line[data-indent="2"] { padding-left: calc(var(--space-s) + 2ch); text-indent: -2ch; }
.code-editor .cm-line[data-indent="3"] { padding-left: calc(var(--space-s) + 3ch); text-indent: -3ch; }
.code-editor .cm-line[data-indent="4"] { padding-left: calc(var(--space-s) + 4ch); text-indent: -4ch; }
.code-editor .cm-line[data-indent="5"] { padding-left: calc(var(--space-s) + 5ch); text-indent: -5ch; }
.code-editor .cm-line[data-indent="6"] { padding-left: calc(var(--space-s) + 6ch); text-indent: -6ch; }
.code-editor .cm-line[data-indent="7"] { padding-left: calc(var(--space-s) + 7ch); text-indent: -7ch; }
.code-editor .cm-line[data-indent="8"] { padding-left: calc(var(--space-s) + 8ch); text-indent: -8ch; }
.code-editor .cm-line[data-indent="9"] { padding-left: calc(var(--space-s) + 9ch); text-indent: -9ch; }
.code-editor .cm-line[data-indent="10"] { padding-left: calc(var(--space-s) + 10ch); text-indent: -10ch; }
.code-editor .cm-line[data-indent="11"] { padding-left: calc(var(--space-s) + 11ch); text-indent: -11ch; }
.code-editor .cm-line[data-indent="12"] { padding-left: calc(var(--space-s) + 12ch); text-indent: -12ch; }
.code-editor .cm-line[data-indent="13"] { padding-left: calc(var(--space-s) + 13ch); text-indent: -13ch; }
.code-editor .cm-line[data-indent="14"] { padding-left: calc(var(--space-s) + 14ch); text-indent: -14ch; }
.code-editor .cm-line[data-indent="15"] { padding-left: calc(var(--space-s) + 15ch); text-indent: -15ch; }
.code-editor .cm-line[data-indent="16"] { padding-left: calc(var(--space-s) + 16ch); text-indent: -16ch; }

.code-editor :is(.cm-static, .cm-lineWrapping) .cm-line::after {
  content: "";
  position: absolute;
  top: var(--leading-md);
  bottom: 0;
  left: 2px;
  border-left: 2px dotted var(--color-muted);
}

.cm-static .cm-line::before {
  content: attr(data-ln) / "";
  position: absolute;
  left: calc(-1 * var(--space-2xl));
  width: calc(var(--space-2xl) - var(--space-s));
  color: var(--color-muted);
  text-align: right;
}

.cm-static .cm-content.is-short {
  min-height: 0;
}

/* Retour à la ligne à toutes les largeurs (EditorView.lineWrapping) : aucune ligne coupée ;
   mêmes règles que le rendu statique (data-indent posé par une décoration de ligne, § 12 de
   docs/design/systeme.md). */
.code-editor .cm-lineWrapping {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.code-editor .cm-lineWrapping .cm-line {
  position: relative;
}
