/* Feuille de style de nsiaide.
   CSS écrit à la main, sans framework ni CDN : l'application doit fonctionner
   sur le réseau filtré d'un lycée et rester lisible en vidéoprojection. */

/* --- Les deux thèmes ---------------------------------------------------

   Tout ce qui est une couleur passe par un jeton, et le mode sombre ne fait
   que redéfinir les jetons : une couleur écrite en clair dans une règle est
   une couleur qui restera claire la nuit, sans que rien ne le signale. C'est
   la seule discipline à tenir en ajoutant du style ici.

   Le thème est porté par `data-theme` sur `<html>`, posé par le script en
   tête de `base.html` — jamais par une media query seule. Deux raisons : la
   bascule doit pouvoir contredire le système (c'est tout son objet), et un
   seul mécanisme vaut mieux que deux qui se disputent la priorité. Sans
   JavaScript, l'attribut n'est pas posé et la page reste claire, comme avant.

   `color-scheme` n'est pas décoratif : il donne au navigateur les barres de
   défilement, les champs natifs et les menus déroulants du bon côté. Sans
   lui, une liste déroulante s'ouvre en blanc au milieu d'une page sombre. */

:root {
  color-scheme: light;

  --encre: #1f2933;
  --encre-douce: #52606d;
  --discret: #9aa5b1;
  --fond: #f5f7fa;
  --carte: #ffffff;
  --bordure: #e4e7eb;
  --accent: #2a6fdb;
  /* L'accent renforcé : le survol d'un bouton, et le texte d'accent posé sur
     `--accent-fond`. Il s'appelait `--accent-sombre` — un nom qui ment la
     nuit, où « renforcer » veut dire éclaircir. */
  --accent-appuye: #1c4fa1;
  /* Les deux teintes de l'accent, comme `--succes` a son `--succes-fond` :
     elles habillaient déjà les indices, en clair dans la règle. */
  --accent-fond: #eef4fd;
  --accent-fond-survol: #e3ecfa;
  --accent-bordure: #cfe0f7;
  --succes: #0b8457;
  --succes-fond: #e6f6ef;
  --succes-bordure: #bfe6d4;
  --echec: #c0392b;
  --echec-fond: #fdecea;
  --echec-bordure: #f5c6c0;
  --attente: #b7791f;
  --attente-fond: #fdf6e3;
  --attente-bordure: #f0dfae;

  /* Ce qui s'écrit *sur* une surface pleine. Deux jetons plutôt qu'un `#fff`
     répété : la nuit, l'accent devient un bleu clair et le texte qu'il porte
     doit passer au sombre — un blanc sur bleu clair ne se lit plus. */
  --sur-accent: #ffffff;
  --sur-encre: #ffffff;

  /* La console de l'élève est sombre dans les deux thèmes — c'est un terminal.
     Mais pas de la même valeur : le gris d'ardoise qui se détachait sur une
     carte blanche se confond avec elle la nuit. */
  --console-fond: #1f2933;
  --console-encre: #e4e7eb;

  /* La coloration syntaxique côté serveur (`colorer_python`). Les autres
     jetons de couleur y suffisaient, sauf ces deux teintes-là. */
  --jeton-litteral: #a34d9c;
  --jeton-motcle: #7a3fa0;

  /* La sélection dans l'éditeur de code. `drawSelection()` la peint lui-même,
     donc c'est bien un fond opaque et non le `::selection` natif. */
  --selection: #d4e2fb;

  /* Le fond de la ligne du curseur dans l'éditeur, et le voile des modales.
     Translucides tous les trois, voir leurs règles pour la raison. */
  --ligne-active: rgba(31, 41, 51, 0.03);
  --voile: rgba(31, 41, 51, 0.45);
  --voile-fort: rgba(31, 41, 51, 0.55);

  --rayon: 8px;
  /* Hauteur de l'éditeur de code, et de la console qui lui fait face : les
     deux doivent s'aligner, donc la valeur est écrite une fois. */
  --hauteur-editeur: 22rem;
  /* Un rayon plus généreux pour les cartes : les blocs de contenu se
     détachent, les champs et les boutons gardent le rayon d'origine. */
  --rayon-large: 14px;
  /* Deux niveaux de profondeur, et pas un de plus. Le premier pose une carte
     sur le fond, le second la soulève au survol ou la fait flotter au-dessus
     du contenu (barre collante). */
  --ombre: 0 1px 2px rgba(31, 41, 51, 0.05), 0 1px 3px rgba(31, 41, 51, 0.06);
  --ombre-forte: 0 4px 14px rgba(31, 41, 51, 0.10), 0 1px 3px rgba(31, 41, 51, 0.06);
  /* La carte de code de la vitrine est la seule à porter une ombre longue :
     c'est ce qui la pose au-dessus du dégradé du hero. */
  --ombre-connexion: 0 1px 2px rgb(31 41 51 / 0.04), 0 12px 28px -12px rgb(31 41 51 / 0.12);

  /* Largeur de l'application, définie une seule fois. Le bandeau, la vitrine
     et les pages larges la partagent : sans elle, le nom de l'application et
     le lien enseignant se retrouvent collés aux deux bords de l'écran, à un
     mètre l'un de l'autre sur un moniteur large. */
  --shell: 76rem;
}

/* Le thème sombre ne redéfinit que des couleurs : ni les rayons, ni les
   hauteurs, ni la largeur de l'application ne changent la nuit.

   Les valeurs ne sont pas l'inverse arithmétique des claires. Un fond noir
   pur et un texte blanc pur font vibrer le texte long — l'énoncé d'un
   exercice se lit sur une demi-page — d'où une encre légèrement adoucie sur
   un fond bleuté plutôt que neutre. Et la carte est *plus claire* que le
   fond, à rebours du thème clair où elle est plus blanche que lui : dans le
   noir, c'est l'élévation qui éclaircit. */
:root[data-theme="sombre"] {
  color-scheme: dark;

  --encre: #e3e9ef;
  --encre-douce: #a4b0bd;
  --discret: #6f7d8c;
  --fond: #11161c;
  --carte: #1a2029;
  --bordure: #2c3540;
  --accent: #6ea8f5;
  --accent-appuye: #9ac4ff;
  --accent-fond: #17263c;
  --accent-fond-survol: #1e3150;
  --accent-bordure: #2c476e;
  --succes: #4fc38d;
  --succes-fond: #12271e;
  --succes-bordure: #1f4a38;
  --echec: #f2796b;
  --echec-fond: #2b1815;
  --echec-bordure: #552822;
  --attente: #e0a94a;
  --attente-fond: #2a2113;
  --attente-bordure: #4d3c1a;

  --sur-accent: #0e1620;
  --sur-encre: #11161c;

  --console-fond: #0b1015;
  --console-encre: #d6dee6;

  --jeton-litteral: #d78fd0;
  --jeton-motcle: #b98ae0;

  --selection: #2b4a70;

  --ligne-active: rgba(255, 255, 255, 0.045);
  --voile: rgba(0, 0, 0, 0.6);
  --voile-fort: rgba(0, 0, 0, 0.7);

  /* Une ombre portée ne se voit pas sur un fond sombre : ce qui détache une
     carte, la nuit, c'est sa bordure et son fond plus clair. On garde une
     ombre franche mais discrète, pour que la barre collante et les modales
     continuent de flotter au-dessus du contenu plutôt que d'y adhérer. */
  --ombre: 0 1px 2px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.35);
  --ombre-forte: 0 4px 16px rgba(0, 0, 0, 0.55), 0 1px 3px rgba(0, 0, 0, 0.4);
  --ombre-connexion: 0 1px 2px rgb(0 0 0 / 0.4), 0 12px 28px -12px rgb(0 0 0 / 0.7);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.55;
  color: var(--encre);
  background: var(--fond);
}

a { color: var(--accent); }

/* --- Bandeau --------------------------------------------------------- */

.bandeau {
  background: var(--carte);
  border-bottom: 1px solid var(--bordure);
}

.bandeau-interieur {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0.75rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.marque {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 700;
  font-size: 1.15rem;
  color: var(--encre);
  text-decoration: none;
  letter-spacing: -0.015em;
}

.marque svg { flex: none; color: var(--accent); }

.bandeau nav {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  margin-left: auto;
  font-size: 0.94rem;
}

.bandeau nav a { text-decoration: none; }
.bandeau nav a:hover { text-decoration: underline; }
.bandeau .discret { color: var(--encre-douce); }
.bandeau .identite { color: var(--discret); }

/* La bascule de thème. Elle hérite de la couleur du texte qui l'entoure et
   n'a ni fond ni bordure au repos : c'est un réglage, pas une destination, et
   elle ne doit pas peser autant qu'un onglet du bandeau. */
.bascule-theme {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--rayon);
  background: none;
  color: var(--encre-douce);
  cursor: pointer;
}

.bascule-theme:hover { background: var(--fond); color: var(--encre); }
.bascule-theme:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Une seule des deux icônes à la fois. Elle annonce la destination : la lune
   tant qu'on est en clair, le soleil une fois passé en sombre. */
.icone-vers-clair { display: none; }
:root[data-theme="sombre"] .icone-vers-clair { display: block; }
:root[data-theme="sombre"] .icone-vers-sombre { display: none; }

/* --- Structure ------------------------------------------------------- */

.contenu {
  max-width: 62rem;
  margin: 2rem auto;
  padding: 0 1.5rem;
}

.contenu-large {
  max-width: var(--shell);
  margin: 1.5rem auto;
  padding: 0 1.5rem;
}

h1 { font-size: 1.6rem; margin: 0 0 0.35rem; letter-spacing: -0.02em; }
h2 { font-size: 1.2rem; margin: 2rem 0 0.75rem; }
h3 { font-size: 1rem; margin: 1.25rem 0 0.5rem; }

.sous-titre { color: var(--encre-douce); margin: 0 0 1.75rem; }

/* Le bloc de contenu de base. Le léger relief n'est pas de la décoration :
   sur un fond gris très clair, une carte sans ombre ne se détache que par sa
   bordure d'un pixel, ce qui donne à toutes les pages le même aspect de
   grille de traits. */
.carte {
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre);
  padding: 1.25rem 1.5rem;
}

.pile > * + * { margin-top: 1rem; }

/* --- Boutons et formulaires ------------------------------------------ */

.bouton {
  display: inline-block;
  padding: 0.55rem 1.1rem;
  border: 1px solid transparent;
  border-radius: var(--rayon);
  background: var(--accent);
  color: var(--sur-accent);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.bouton:hover { background: var(--accent-appuye); }
.bouton:disabled { opacity: 0.55; cursor: not-allowed; }

.bouton-secondaire {
  background: var(--carte);
  color: var(--encre);
  border-color: var(--bordure);
}
.bouton-secondaire:hover { background: var(--fond); }

.bouton-petit { padding: 0.3rem 0.7rem; font-size: 0.86rem; }

label { display: block; font-weight: 600; font-size: 0.92rem; margin-bottom: 0.3rem; }

input[type="text"], input[type="email"], input[type="password"],
input[type="search"], textarea, select {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  font: inherit;
  background: var(--carte);
  color: var(--encre);
}

input:focus, textarea:focus, select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.champ + .champ { margin-top: 0.9rem; }
.aide { font-size: 0.86rem; color: var(--encre-douce); margin-top: 0.3rem; }

.champ-cache { display: none; }

/* Une case à cocher et son intitulé sur la même ligne : la règle générale des
   `label` les met en bloc, ce qui renverrait le texte sous la case. */
label.case { display: flex; align-items: center; gap: 0.5rem; font-weight: 500; }
label.case input { width: auto; }

/* Aides de saisie : exemples JSON et listes explicatives sous un champ.
   Les exemples portent des phrases entières à l'intérieur du JSON — repliés
   plutôt que débordants, c'est le texte qu'on vient y lire. */
pre.aide {
  background: var(--fond);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  padding: 0.7rem 0.9rem;
  margin: 0.4rem 0;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.82rem;
  line-height: 1.5;
  /* `pre` ne replie rien par défaut : une longue ligne d'exemple pousserait
     la page entière en largeur. Les exemples sont écrits courts, ces trois
     déclarations sont le filet — `max-width` et `overflow-x` bornent le
     débordement au bloc si un jour une ligne ne peut pas être repliée. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-width: 100%;
  overflow-x: auto;
}

ul.aide { margin: 0.3rem 0; padding-left: 1.3rem; }
ul.aide li { margin-bottom: 0.25rem; }

/* --- La mention du modèle d'IA ---------------------------------------- */

/* Elle dit qui répond : sous la colonne d'indices, en tête de l'échange, et
   sur la page de génération avant le clic. Discrète — c'est une note, pas un
   avertissement — mais toujours présente, y compris avant le premier indice :
   c'est précisément l'endroit où l'on décide si l'on va demander de l'aide.

   `border-top` plutôt qu'une couleur ou une icône : la mention n'appartient
   pas à ce qui la précède, elle en parle. Un filet le dit sans ajouter de
   teinte à une colonne qui en porte déjà trois. */
.mention-ia {
  border-top: 1px solid var(--bordure);
  padding-top: 0.5rem;
  margin-top: 0.7rem;
}

/* Le nom du modèle est un identifiant (`deepseek-v4-flash`) : sa graisse et
   sa casse sont celles du code, jamais celles de la phrase autour. Même
   correctif que pour les en-têtes de tableaux de saisie. */
.mention-ia .modele-ia {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.92em;
  text-transform: none;
  overflow-wrap: anywhere;
}

/* Dans l'échange, la mention n'est pas au bas d'une colonne mais au sommet de
   la conversation : le filet passe donc dessous, et l'alignement suit celui
   du fil, qui porte le même retrait. */
.mention-ia-echange {
  border-top: 0;
  border-bottom: 1px solid var(--bordure);
  margin: 0;
  padding: 0.7rem 1.25rem;
}

/* --- Tableaux de saisie (cas de test, grille d'interprétation) --------- */

/* Le nombre de colonnes suit la signature de la fonction : il peut dépasser
   la largeur disponible, d'où un défilement borné au tableau plutôt qu'une
   page qui s'élargit. */
.saisie-defilante {
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  overflow-x: auto;
  margin-top: 0.3rem;
}

.saisie-defilante table { margin: 0; }

/* Les en-têtes portent des noms de paramètres Python : la mise en majuscules
   du style général des tableaux les rendrait faux (`nombres` n'est pas
   `NOMBRES`), alors que c'est justement la casse écrite dans la signature
   qu'il faut lire. */
.saisie-defilante th {
  white-space: nowrap;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.85rem;
  color: var(--encre);
}

/* Le petit encadré des noms de code est défini une fois, avec l'énoncé et les
   indices — voir « Page exercice ». */
.saisie-defilante td { vertical-align: middle; padding: 0.4rem 0.5rem; }
.saisie-defilante tr:last-child td { border-bottom: none; }

.saisie-defilante input[type="text"],
.saisie-defilante select {
  padding: 0.35rem 0.5rem;
  font-size: 0.9rem;
  min-width: 7rem;
}

.saisie-defilante input.cellule-json {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.85rem;
}

/* Une valeur illisible ne peut pas être sérialisée : la signaler à la cellule
   évite d'envoyer un contenu qui n'est pas celui affiché. */
.cellule-invalide {
  border-color: var(--echec) !important;
  background: var(--echec-fond);
}

.colonne-etroite { width: 1%; white-space: nowrap; text-align: center; }

.barre-saisie { margin-top: 0.5rem; display: flex; gap: 0.6rem; }

/* --- Session interactive Python (cas de test) -------------------------- */

/* Les cas de test s'affichent comme dans l'interpréteur : l'élève y reconnaît
   ce qu'il voit en cours, sans avoir à décoder une notation propre au site. */
.console-python {
  background: var(--fond);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  padding: 0.8rem 1rem;
  margin: 0.5rem 0;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.88rem;
  line-height: 1.7;
  /* `max-width` en plus du défilement : le second ne sert à rien si le bloc a
     le droit de s'élargir. Les deux ensemble bornent le jeu de tests à sa
     colonne, quel que soit le plus long des appels. */
  max-width: 100%;
  overflow-x: auto;
}

/* Le `code` d'un bloc est neutralisé avec les autres, voir « Page exercice ». */

.console-python .invite { color: var(--encre-douce); user-select: none; }

.jeton-fonction { color: var(--accent); font-weight: 400; }
.jeton-nombre { color: var(--jeton-litteral); }
.jeton-chaine { color: var(--succes); }
.jeton-constante { color: var(--jeton-litteral); font-weight: 600; }
.jeton-motcle { color: var(--jeton-motcle); font-weight: 600; }
.jeton-operateur { color: var(--encre-douce); }
.jeton-nom { color: var(--encre); }
.jeton-commentaire { color: var(--discret); font-style: italic; }

/* --- Attente d'une action longue --------------------------------------- */

.indicateur-attente {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-right: 0.5em;
  vertical-align: -0.08em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: rotation 0.7s linear infinite;
}

@keyframes rotation { to { transform: rotate(360deg); } }

/* htmx masque `.htmx-indicator` avec `opacity: 0` : invisible, mais l'élément
   occupe toujours sa place et décale le texte du bouton tant qu'on n'a pas
   cliqué. On le retire du flux, et on le rétablit sur les deux sélecteurs que
   pose htmx — `htmx-request` atterrit sur l'indicateur lui-même quand il est
   désigné par `hx-indicator`, sur un ancêtre sinon. */
.indicateur-attente.htmx-indicator { display: none; }

.indicateur-attente.htmx-indicator.htmx-request,
.htmx-request .indicateur-attente.htmx-indicator { display: inline-block; }

/* Le mouvement n'est pas ce qui porte l'information — le compteur de secondes
   s'en charge : on le ralentit fortement plutôt que de le supprimer, pour que
   l'indicateur reste visiblement actif. */
@media (prefers-reduced-motion: reduce) {
  .indicateur-attente { animation-duration: 3s; }
}

.attente-etat { margin-top: 0.5rem; font-variant-numeric: tabular-nums; }
.message-saisie:empty { display: none; }
.message-erreur { color: var(--echec); font-weight: 600; }

/* --- En-tête de page avec actions -------------------------------------- */

.entete-page {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.entete-page .sous-titre { margin-bottom: 0.5rem; }
.entete-page-titre { min-width: 0; }

.entete-page-actions {
  display: flex;
  gap: 0.6rem;
  flex: none;
  flex-wrap: wrap;
  padding-top: 0.2rem;
}

details.aide summary { cursor: pointer; font-weight: 600; }
details.aide ul { margin: 0.5rem 0 0; padding-left: 1.3rem; }
details.aide li { margin-bottom: 0.3rem; }

/* --- Messages -------------------------------------------------------- */

.messages { list-style: none; max-width: 62rem; margin: 1rem auto 0; padding: 0 1.5rem; }
.message {
  padding: 0.7rem 1rem;
  border-radius: var(--rayon);
  border: 1px solid var(--bordure);
  background: var(--carte);
  margin-bottom: 0.5rem;
}
.message.success { background: var(--succes-fond); border-color: var(--succes-bordure); color: var(--succes); }
.message.error { background: var(--echec-fond); border-color: var(--echec-bordure); color: var(--echec); }
.message.warning { background: var(--attente-fond); border-color: var(--attente-bordure); color: var(--attente); }

.erreur {
  background: var(--echec-fond);
  border: 1px solid var(--echec-bordure);
  color: var(--echec);
  padding: 0.7rem 1rem;
  border-radius: var(--rayon);
  margin-bottom: 1rem;
}

/* --- Page d'accueil (vitrine) ----------------------------------------- */

/* La vitrine gère ses propres sections en pleine largeur : c'est la seule
   page qui alterne des bandes de fond différent d'un bord à l'autre. */
.accueil { width: 100%; }

.section { max-width: var(--shell); margin: 0 auto; padding: 0 1.5rem; }

/* Page ne portant qu'un formulaire (connexion enseignant) : une colonne
   étroite et centrée, sinon le champ s'étirerait sur toute la largeur. */
.page-etroite { max-width: 27rem; margin: 4rem auto; padding: 0 1.5rem; }
.page-etroite h1 { font-size: 1.6rem; }

/* Le rappel affiché à qui arrive ici en étant déjà connecté. Bande pleine
   largeur, comme les sections de la vitrine, et posée avant le hero : une
   carte flottante au milieu du contenu se prendrait pour une information de
   la page, alors que c'est du repérage. */
.rappel-espace {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem 1.2rem;
  padding: 0.7rem 1.5rem;
  background: var(--accent-fond);
  border-bottom: 1px solid var(--accent-bordure);
  font-size: 0.92rem;
}

.rappel-espace p { margin: 0; color: var(--accent-appuye); }

.hero { padding-top: 3rem; padding-bottom: 3.5rem; }

/* Le formulaire de code occupe la colonne de droite, à hauteur du titre :
   c'est la seule chose qu'un élève vient faire ici, elle ne doit jamais
   demander de faire défiler la page.

   Le placement est explicite plutôt que laissé au flux automatique. Sans
   cela, la hauteur de la carte de connexion impose celle de la première
   ligne, et les atouts — placés d'office en deuxième ligne — décrochent
   plusieurs centimètres sous le texte qu'ils prolongent. */
.hero-grille {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  column-gap: 3.5rem;
  row-gap: 2.25rem;
  align-items: start;
}

.hero-titre { grid-column: 1; grid-row: 1; }
.atouts { grid-column: 1; grid-row: 2; }
.hero-connexion { grid-column: 2; grid-row: 1 / span 2; }

.kicker {
  display: inline-block;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.85rem;
}

.hero h1 {
  font-size: clamp(2rem, 4.2vw, 3rem);
  line-height: 1.12;
  letter-spacing: -0.03em;
  margin: 0 0 1rem;
}

/* Pas de marge basse : l'écart avec les atouts est celui du `row-gap` de la
   grille. Les deux cumulés donnaient un trou de quatre rem sous le texte. */
.hero-texte {
  font-size: 1.08rem;
  color: var(--encre-douce);
  margin: 0;
  max-width: 34rem;
}

.atouts { list-style: none; padding: 0; margin: 0; display: grid; gap: 1.1rem; }

.atouts li { display: flex; gap: 0.85rem; align-items: flex-start; }
.atouts svg { flex: none; color: var(--accent); margin-top: 0.15rem; }
.atouts strong { display: block; font-weight: 600; }
.atouts span { color: var(--encre-douce); font-size: 0.94rem; }

/* --- Carte de connexion élève ----------------------------------------- */

.carte-connexion {
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: 14px;
  padding: 1.75rem;
  box-shadow: var(--ombre-connexion);
}

.carte-connexion h2 { margin: 0 0 0.25rem; font-size: 1.15rem; }
.carte-connexion .aide { margin: 0 0 1.1rem; }

.champ-code {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 1.45rem;
  letter-spacing: 0.2em;
  text-align: center;
  text-transform: uppercase;
  padding: 0.7rem;
}

.champ-code::placeholder { letter-spacing: 0.2em; color: var(--discret); }

.bouton-bloc { display: block; width: 100%; padding: 0.75rem; font-size: 1rem; }

.mention {
  font-size: 0.82rem;
  color: var(--discret);
  margin: 1rem 0 0;
  text-align: center;
}

.rappel-prof {
  display: block;
  margin-top: 1rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  background: var(--carte);
  text-decoration: none;
  color: inherit;
  font-size: 0.9rem;
}

.rappel-prof:hover { border-color: var(--accent); }
.rappel-prof strong { display: block; color: var(--encre); }
.rappel-prof span { color: var(--encre-douce); }

/* --- Comment ça marche ------------------------------------------------ */

.bande { background: var(--carte); border-top: 1px solid var(--bordure); }
.bande-interieur { padding-top: 3rem; padding-bottom: 3rem; }

.etapes {
  list-style: none;
  padding: 0;
  margin: 1.75rem 0 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  counter-reset: etape;
}

.etapes li {
  border: 1px solid var(--bordure);
  border-radius: 12px;
  padding: 1.25rem;
}

.etapes li::before {
  counter-increment: etape;
  content: counter(etape);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.85rem;
  height: 1.85rem;
  border-radius: 999px;
  background: var(--accent-fond);
  color: var(--accent-appuye);
  font-weight: 700;
  font-size: 0.9rem;
  margin-bottom: 0.7rem;
}

.etapes strong { display: block; margin-bottom: 0.25rem; }
.etapes p { margin: 0; color: var(--encre-douce); font-size: 0.93rem; }

/* Encart sur le choix du nom : « itération » se lit dans les deux sens,
   la boucle du cours et le fait de recommencer jusqu'à ce que ça marche. */
.encart-nom {
  margin-top: 2.5rem;
  border-left: 3px solid var(--accent);
  padding: 0.35rem 0 0.35rem 1.15rem;
  color: var(--encre-douce);
  max-width: 46rem;
}

.encart-nom strong { color: var(--encre); }

/* --- L'usage de l'IA, et son cadre ------------------------------------ */

/* Ce qu'un enseignant montre à un chef d'établissement avant la première
   séance : donc une carte en fin de vitrine, et non une note en bas de page.
   La bande blanche qui précède rend son fond à la page ; la carte s'en
   détache. */
.encart-ia { padding-top: 3rem; padding-bottom: 1rem; }
.encart-ia-carte { padding: 1.75rem; }
.encart-ia h2 { margin-top: 0; }

/* Les deux paragraphes d'introduction sont bornés en largeur : sur un
   moniteur large, la carte fait plus de soixante rem et une ligne de texte
   qui les traverse ne se relit plus d'un bout à l'autre. Les définitions,
   elles, sont déjà tenues par leur colonne. */
.encart-ia-intro { margin: 0 0 1.25rem; color: var(--encre-douce); max-width: 52rem; }

.encart-ia-cadre {
  margin: 0 0 1.5rem;
  font-size: 0.93rem;
  color: var(--encre-douce);
  max-width: 52rem;
}

/* Deux colonnes plutôt qu'une : les quatre points forment sinon une liste
   qu'on fait défiler, et l'on n'en lit alors que le premier. */
.points-cadre {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.35rem 2.5rem;
  margin: 0;
}

.points-cadre dt { font-weight: 600; margin-bottom: 0.3rem; }
.points-cadre dd { margin: 0; color: var(--encre-douce); font-size: 0.93rem; }

/* Le rappel que l'enseignant relit est séparé par un filet : il ne parle plus
   du cadre ministériel mais de ce que fait cette application-ci, et c'est le
   seul des cinq points qui engage l'élève vis-à-vis d'une personne. */
.encart-ia-pied {
  margin: 1.5rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--bordure);
  color: var(--encre-douce);
  font-size: 0.93rem;
}

.encart-ia-pied strong { color: var(--encre); }

/* Le pied porte deux lignes depuis qu'il invite à demander un compte. Les
   marges par défaut d'un paragraphe les écarteraient d'un interligne entier,
   alors qu'elles se lisent comme un seul bloc. */
.pied {
  text-align: center;
  font-size: 0.85rem;
  color: var(--discret);
  padding: 2.5rem 1.5rem;
}

.pied p { margin: 0; }
.pied p + p { margin-top: 0.45rem; }

@media (max-width: 62rem) {
  /* Le formulaire remonte juste sous le titre : sur un téléphone, les
     arguments ne doivent pas s'intercaler avant la seule action que l'élève
     vient faire. Les placements explicites sont annulés, sinon la carte
     resterait accrochée à une deuxième colonne qui n'existe plus. */
  .hero-grille { grid-template-columns: 1fr; row-gap: 2rem; }
  .hero-titre, .atouts, .hero-connexion { grid-column: 1; grid-row: auto; }
  .hero-titre { order: 1; }
  .hero-connexion { order: 2; }
  .atouts { order: 3; }
  .hero { padding-top: 2.25rem; }
  .etapes { grid-template-columns: 1fr; }
  .points-cadre { grid-template-columns: 1fr; }
}

/* --- Étiquettes de statut -------------------------------------------- */

.etiquette {
  display: inline-block;
  padding: 0.12rem 0.55rem;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
}

.etiquette-succes { background: var(--succes-fond); color: var(--succes); }
.etiquette-echec { background: var(--echec-fond); color: var(--echec); }
.etiquette-attente { background: var(--attente-fond); color: var(--attente); }
.etiquette-neutre { background: var(--fond); color: var(--encre-douce); }

/* --- Pastilles ------------------------------------------------------- */

/* Une pastille par notion : le thème, la difficulté, l'avancement. Dans une
   liste de trente exercices, c'est ce qui permet de retrouver « les listes,
   niveau facile » d'un coup d'œil, avant même d'avoir lu un titre.

   La couleur d'un thème est indexée sur son code et non sur son rang :
   ajouter une valeur à la nomenclature ne décale pas les autres. Elle
   n'apporte aucune information que le libellé ne donne pas — un daltonien lit
   « Récursivité » comme tout le monde. */

.chip {
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.65rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  background: var(--fond);
  color: var(--encre-douce);
}

.chip-bases { background: #e9edf2; color: #3b4a5c; }
.chip-liste { background: #e8f0fe; color: #1a4fa0; }
.chip-dict { background: #ebe6fb; color: #513a9e; }
.chip-tuple { background: #e2f3ef; color: #0f6b5c; }
.chip-chaine { background: #fdeee2; color: #99511a; }
.chip-recursivite { background: #fce8ef; color: #a01e57; }
.chip-tri { background: #eaf2df; color: #446b18; }
.chip-recherche { background: #e2f1f8; color: #0f5c7c; }

/* La difficulté monte du vert au rouge : quatre crans, lus comme une échelle
   alors que quatre pastilles de la même teinte ne se compareraient pas. Le
   mot reste le porteur de l'information — les étoiles qui l'ont précédé
   obligeaient à compter, et se ressemblaient toutes de loin. */
.chip-difficulte-1 { background: #e4f5ea; color: #14724a; }
.chip-difficulte-2 { background: #eff5dd; color: #55701a; }
.chip-difficulte-3 { background: #fdf0d9; color: #94640f; }
.chip-difficulte-4 { background: #fbe5e1; color: #ad3a2a; }

/* Le niveau : une seule pastille, puisqu'un exercice de première n'en porte
   aucune. Elle ne reprend ni la palette des thèmes ni celle des difficultés —
   elle ne se compare à rien, elle signale. Le violet est la seule teinte que
   ni l'une ni l'autre n'emploie, et la bordure la distingue d'un thème pour
   qui lit sans les couleurs. */
.chip-niveau-terminale {
  background: #f1ebfb;
  color: #5b3a9e;
  box-shadow: inset 0 0 0 1px #cdb8f0;
}

/* Les couleurs des trois séries ci-dessus — thème, difficulté, niveau — sont
   les seules à ne pas passer par un jeton : aucune ne sert ailleurs, et une
   douzaine de paires de jetons rendraient le bloc illisible pour rien. Elles
   sont donc redéclarées ici pour la nuit — teinte conservée, valeurs
   inversées : fond très sombre, texte clair. C'est le seul endroit de la
   feuille où une couleur s'écrit deux fois, et une pastille ajoutée doit
   l'être aux deux endroits. */
:root[data-theme="sombre"] .chip-bases { background: #1c232c; color: #a7b6c8; }
:root[data-theme="sombre"] .chip-liste { background: #16273f; color: #8db6ee; }
:root[data-theme="sombre"] .chip-dict { background: #221c3d; color: #b0a0ee; }
:root[data-theme="sombre"] .chip-tuple { background: #102b26; color: #6cc9b5; }
:root[data-theme="sombre"] .chip-chaine { background: #33200f; color: #e0a071; }
:root[data-theme="sombre"] .chip-recursivite { background: #331323; color: #e88bb2; }
:root[data-theme="sombre"] .chip-tri { background: #1d2a12; color: #a3c977; }
:root[data-theme="sombre"] .chip-recherche { background: #10262f; color: #78bcd8; }
:root[data-theme="sombre"] .chip-difficulte-1 { background: #0f2a1e; color: #63c79b; }
:root[data-theme="sombre"] .chip-difficulte-2 { background: #1f2a12; color: #aac97a; }
:root[data-theme="sombre"] .chip-difficulte-3 { background: #32260f; color: #ddad4f; }
:root[data-theme="sombre"] .chip-difficulte-4 { background: #331b16; color: #e58a78; }
:root[data-theme="sombre"] .chip-niveau-terminale {
  background: #241a3d;
  color: #bda6f0;
  box-shadow: inset 0 0 0 1px #43356b;
}

/* L'avancement de l'élève et, côté enseignant, le statut de l'exercice : deux
   nomenclatures, un seul attribut `data-etat`, donc une seule série de
   pastilles. Elles reprennent les couleurs des étiquettes de statut, pour que
   « Réussi » ait la même teinte dans la barre et sur la carte. */
.chip-etat-non_commence { background: var(--fond); color: var(--encre-douce); }
.chip-etat-en_cours, .chip-etat-valide { background: var(--attente-fond); color: var(--attente); }
.chip-etat-reussi, .chip-etat-publie { background: var(--succes-fond); color: var(--succes); }
.chip-etat-brouillon { background: var(--echec-fond); color: var(--echec); }

/* Les classes destinataires, dans le catalogue : même forme, mais neutre —
   elles ne sont pas une catégorie de contenu. */
.chip-classe {
  background: var(--carte);
  border: 1px solid var(--bordure);
  color: var(--encre-douce);
  font-weight: 500;
}

/* La pastille qui mène aux copies de la classe. Rien ne la distingue au
   repos : une colonne de quinze pastilles soulignées ferait du tableau un
   champ de liens, et masquerait le seul lien qui compte à cet endroit, le
   titre de l'exercice. L'affordance est au survol et au clavier — la bordure
   et le texte passent à l'accent, sans que la boîte ne change de taille, sans
   quoi la ligne sauterait sous le curseur.

   Le sélecteur est `a.chip-classe` et non une classe de plus : une pastille
   inerte reste un `span`, et c'est déjà ce qui les sépare. Il porte aussi la
   spécificité qu'il faut pour reprendre `a { color: var(--accent) }` au repos
   — même teinte que sa voisine en `span`. */
a.chip-classe {
  text-decoration: none;
  transition: border-color 0.12s ease, color 0.12s ease;
}

a.chip-classe:hover,
a.chip-classe:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
}

a.chip-classe:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Publié mais attribué à aucune classe : visible de personne, et pourtant
   indiscernable d'un exercice en ligne sans cette mise en garde. */
.manquant { color: var(--attente); font-weight: 600; }

/* --- Barre de filtres ------------------------------------------------- */

.filtres {
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre);
  padding: 1rem 1.15rem;
  margin-top: 1rem;
  margin-bottom: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

/* Le champ de recherche porte sa loupe en fond : une icône dessinée en SVG
   inline plutôt qu'un fichier ou une police, pour qu'il n'y ait toujours
   aucune requête réseau à l'affichage. */
.filtres-recherche input[type="search"] {
  padding: 0.6rem 0.8rem 0.6rem 2.4rem;
  border-radius: var(--rayon);
  background-color: var(--fond);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2352606d' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 0.7rem center;
  background-size: 1.05rem;
}

.filtres-recherche input[type="search"]:focus { background-color: var(--carte); }
.filtres-recherche input[type="search"]::placeholder { color: var(--discret); }

/* La croix native de Chrome se superpose mal au fond coloré du champ. */
.filtres-recherche input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

.filtres-groupes { display: flex; flex-direction: column; gap: 0.5rem; }

/* Une rangée par facette, précédée de son intitulé. Les intitulés partagent
   une largeur fixe : les pastilles s'alignent alors d'une rangée à l'autre,
   ce qui donne trois colonnes lisibles au lieu de trois lignes décalées. */
.filtres-groupe { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }

.filtres-intitule {
  flex: none;
  width: 5.5rem;
  font-size: 0.76rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--encre-douce);
}

.chip-filtre {
  border: 1px solid transparent;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0.28rem 0.75rem;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.12s ease;
}

.chip-filtre:hover { transform: translateY(-1px); box-shadow: var(--ombre); }
.chip-filtre:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* L'état retenu doit se voir sans comparer deux pastilles : fond plein,
   contraste inversé. Une simple bordure ne suffisait pas sur les teintes
   claires des thèmes. */
.chip-filtre[aria-pressed="true"] {
  background: var(--encre);
  color: var(--sur-encre);
  box-shadow: var(--ombre);
}

.filtres-fin {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  min-height: 1.3rem;
}

.filtres-compte { font-size: 0.85rem; color: var(--encre-douce); }

/* Un bouton qui doit se lire comme un lien : l'action est secondaire et ne
   mérite pas le poids visuel d'un bouton de plus dans la barre. */
.lien-bouton {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.85rem;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
}

/* `hidden` doit primer sur la mise en page : une carte passée en flex et une
   ligne de tableau réafficheraient sinon un élément que le filtre écarte. */
[hidden] { display: none !important; }

/* Visible d'un lecteur d'écran, invisible à l'écran : pour les intitulés que
   la mise en page rend superflus mais qu'une navigation au clavier réclame. */
.hors-ecran {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Sections par thème ----------------------------------------------- */

.groupe-theme { margin-top: 2rem; }

.groupe-theme h2 {
  margin: 0 0 0.9rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 1.05rem;
  letter-spacing: 0.01em;
}

/* Le nombre d'exercices du thème, en pastille discrète plutôt qu'en phrase :
   il se lit comme une quantité, pas comme une légende. */
.compteur {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding: 0 0.4rem;
  border-radius: 999px;
  background: var(--bordure);
  color: var(--encre-douce);
  font-size: 0.78rem;
  font-weight: 600;
}

/* --- Lignes d'exercice (liste de l'élève) ----------------------------- */

/* Une ligne pleine largeur par exercice, et non une vignette. Dans un thème,
   ce qui se compare d'un exercice au suivant est la difficulté et
   l'avancement : en colonnes alignées, la lecture est verticale et immédiate.
   Les largeurs fixes des trois dernières colonnes servent exactement à cela —
   sans elles, chaque ligne placerait ses éléments là où le titre l'a laissée,
   et il n'y aurait plus de colonne à lire. */

.liste-exercices {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.ligne-exercice {
  position: relative;
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.85rem 1.1rem 0.85rem 1.35rem;
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre);
  transition: box-shadow 0.12s ease, border-color 0.12s ease;
}

.ligne-exercice:hover {
  box-shadow: var(--ombre-forte);
  border-color: var(--accent-bordure);
}

/* Un liseré coloré sur la tranche : l'avancement se repère en balayant la
   colonne de gauche, sans lire une seule mention. */
.ligne-exercice::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  border-radius: var(--rayon-large) 0 0 var(--rayon-large);
  background: var(--bordure);
}

.ligne-exercice.etat-reussi::before { background: var(--succes); }
.ligne-exercice.etat-en_cours::before { background: var(--attente); }

.ligne-exercice-titre {
  margin: 0;
  flex: 1;
  min-width: 0;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.35;
}

.ligne-exercice-titre a { color: var(--encre); text-decoration: none; }

/* Le lien du titre est étiré sur toute la ligne : elle devient cliquable d'un
   bout à l'autre sans qu'un lecteur d'écran annonce deux fois la même
   destination — c'est pourquoi le bouton n'est qu'un `span`. */
.ligne-exercice-titre a::after { content: ""; position: absolute; inset: 0; }
.ligne-exercice:hover .ligne-exercice-titre a { color: var(--accent); }

/* L'étiquette de niveau suit le titre plutôt que les deux pastilles de la
   colonne fixe, qui débordait de 59 px sur l'avancement dès qu'une troisième
   s'y ajoutait. Reste à ce qu'elle ne fasse pas grandir la ligne : à sa taille
   ordinaire elle mesure 23,5 px pour une ligne de titre qui en fait 21,6, et
   les seules lignes de terminale auraient été 2,5 px plus hautes que leurs
   voisines — le tapis irrégulier que les colonnes fixes existent pour éviter.
   Mesuré dans Chrome à 1280 et 1024 px. */
.ligne-exercice-titre .chip {
  margin-left: 0.45rem;
  padding-block: 0.05rem;
  line-height: 1.35;
}

.ligne-exercice-chips {
  display: flex;
  gap: 0.4rem;
  flex: none;
  width: 15rem;
}

.etat-mention {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  flex: none;
  width: 7.5rem;
  font-size: 0.85rem;
  color: var(--encre-douce);
}

/* Où en est l'aide sur cet exercice. Largeur fixe comme les autres colonnes :
   c'est ce qui permet de balayer la liste du regard au lieu de la lire ligne à
   ligne. Sur une seule ligne — deux étages rendraient certaines lignes plus
   hautes que d'autres, et la liste deviendrait ce tapis irrégulier que les
   colonnes servent justement à éviter.

   8rem est mesuré, pas estimé : le plus long des trois libellés (« indices
   épuisés ») occupe 108 px dans un moteur réel. Le reste est la marge qu'il
   faut pour ne pas rogner au premier réglage de police. */
.ligne-exercice-suivi {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  flex: none;
  width: 8rem;
  overflow: hidden;
}

.ligne-exercice-suivi .etiquette { font-size: 0.72rem; }

.pastille {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--discret);
  flex: none;
}

.etat-reussi .pastille { background: var(--succes); }
.etat-en_cours .pastille { background: var(--attente); }

/* Le bouton n'est qu'un repère : sa largeur fixe aligne la colonne d'action,
   que le mot soit « Revoir » ou « Commencer ». */
.ligne-exercice .bouton { flex: none; width: 6.5rem; text-align: center; }

/* La colonne d'action tient deux boutons, et garde sa largeur quand elle n'en
   porte qu'un : « Revoir » reste calé à droite d'une ligne à l'autre. Le
   laisser flotter selon la présence de « Recommencer » ferait danser la
   colonne au fil de la liste. */
.ligne-exercice-actions {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
  flex: none;
  width: 14.5rem;
}

/* Le seul élément réellement cliquable de la ligne — tout le reste n'est que
   le repère visuel du lien étiré sur toute la surface (`::after { inset: 0 }`).
   Sans cette remontée, le lien passerait devant et « Recommencer » ouvrirait
   simplement l'exercice.

   Et sa largeur est libre, contre la règle qui fige celle des autres : le mot
   est plus long qu'eux tous, et les 6,5rem imposés à tous les boutons de la
   ligne laissaient « Recommencer » déborder de son propre cadre. Le sélecteur
   doit donc être aussi spécifique que celui qu'il corrige — `.ligne-exercice
   .bouton` l'emporterait sur une classe seule. */
.ligne-exercice .ligne-exercice-reprise {
  position: relative;
  z-index: 1;
  width: auto;
  white-space: nowrap;
}

/* Sous 70rem, les colonnes n'ont plus la place de s'aligner : la ligne se
   replie en deux étages plutôt que de comprimer le titre à trois mots.

   Le seuil était à 60rem quand la ligne n'avait que trois colonnes fixes.
   Elle en a cinq depuis qu'elle dit où en est l'aide et propose de
   recommencer, soit 838 px avant le titre : à 1024 px celui-ci tombait à
   135 px et « Recherche dichotomique dans un tableau trié » s'y repliait sur
   quatre lignes. Mesuré, pas estimé. */
@media (max-width: 70rem) {
  .ligne-exercice { flex-wrap: wrap; gap: 0.6rem 0.9rem; }
  .ligne-exercice-titre { flex: 1 0 100%; }
  .ligne-exercice-chips { width: auto; }
  .etat-mention { width: auto; margin-left: auto; }
  .ligne-exercice-suivi { width: auto; }
  .ligne-exercice-actions { width: auto; }
}

/* --- Feuille d'attribution -------------------------------------------- */

/* Une carte par thème, lignes séparées par un filet : trente rectangles
   bordés les uns sous les autres faisaient un mur de traits. */
.liste-attribution {
  list-style: none;
  padding: 0;
  margin: 0;
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre);
  overflow: hidden;
}

.liste-attribution li {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.15rem 1rem 0.15rem 0.4rem;
  border-top: 1px solid var(--bordure);
}

.liste-attribution li:first-child { border-top: none; }
.liste-attribution li:hover { background: var(--fond); }

/* La ligne entière est cliquable : trente cases à cocher visées une à une au
   pixel près, c'est le genre de formulaire qu'on n'ouvre pas deux fois. */
.liste-attribution label {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex: 1;
  margin: 0;
  padding: 0.6rem;
  font-weight: 400;
  cursor: pointer;
  min-width: 0;
}

.liste-attribution input[type="checkbox"] {
  width: 1.1rem;
  height: 1.1rem;
  flex: none;
  accent-color: var(--accent);
  cursor: pointer;
}

.liste-attribution .attribution-titre {
  font-weight: 600;
  flex: 1;
  min-width: 0;
}

/* Ce qui est déjà donné à la classe se distingue de ce qui reste à choisir,
   sans avoir à viser l'état de la case. */
.liste-attribution li:has(input:checked) { background: var(--accent-fond); }

.lien-fiche {
  font-size: 0.82rem;
  color: var(--encre-douce);
  text-decoration: none;
  white-space: nowrap;
  flex: none;
}
.lien-fiche:hover { color: var(--accent); text-decoration: underline; }

.barre-attribution {
  position: sticky;
  bottom: 0.75rem;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 1.5rem;
  padding: 0.85rem 1.15rem;
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre-forte);
}

.barre-attribution-outils { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.barre-attribution-outils .aide { margin: 0; }
.barre-attribution button[type="submit"] { margin-left: auto; flex: none; }

/* --- Sélection pour l'export ------------------------------------------ */

.barre-export {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin: 1rem 0 0;
  padding: 0.85rem 1.15rem;
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
}

.barre-export .aide { margin: 0; }
.barre-export button[type="submit"] { margin-left: auto; flex: none; }

/* La colonne des cases est étroite et ne porte pas d'en-tête visible : son
   libellé est hors écran, pour que la navigation au clavier sache ce qu'elle
   coche. */
.colonne-case { width: 2.75rem; }
.colonne-case label.case { gap: 0; }

/* Repliée par `export-json.js` seulement — sans lui, les cases restent
   visibles et le formulaire utilisable tel quel. */
.export-replie [data-colonne-export] { display: none; }

/* --- Grille de fiches (classes) --------------------------------------- */

.grille-fiches {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: 0.85rem;
}

.carte-fiche {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1.1rem 1.2rem;
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre);
  transition: box-shadow 0.12s ease, border-color 0.12s ease;
}

.carte-fiche:hover { box-shadow: var(--ombre-forte); border-color: var(--accent-bordure); }

.carte-fiche-titre { margin: 0; font-size: 1.05rem; }
.carte-fiche-titre a { color: var(--encre); text-decoration: none; }
.carte-fiche-titre a:hover { color: var(--accent); }
.carte-fiche-meta { margin: 0; font-size: 0.85rem; color: var(--encre-douce); }

/* Deux chiffres côte à côte, valeur au-dessus de son intitulé : ils se
   comparent d'une classe à l'autre sans lire les libellés. */
.chiffres { display: flex; gap: 1.75rem; margin: 0.4rem 0 0.2rem; }
.chiffres div { display: flex; flex-direction: column-reverse; }
.chiffres dt { font-size: 0.76rem; color: var(--encre-douce); text-transform: uppercase; letter-spacing: 0.04em; }
.chiffres dd { margin: 0; font-size: 1.35rem; font-weight: 700; line-height: 1.2; }
.chiffres dd.manquant { font-size: 1rem; font-weight: 600; padding-top: 0.28rem; }

.carte-fiche-actions {
  margin-top: auto;
  padding-top: 0.7rem;
  border-top: 1px solid var(--bordure);
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* --- Listes et tableaux ---------------------------------------------- */

table { width: 100%; border-collapse: collapse; background: var(--carte); }

th, td {
  padding: 0.55rem 0.75rem;
  border-bottom: 1px solid var(--bordure);
  text-align: left;
  font-size: 0.93rem;
}

th { font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--encre-douce); }

.tableau-cadre {
    background: var(--carte);
    border: 1px solid var(--bordure);
    border-radius: var(--rayon-large);
    box-shadow: var(--ombre);
    overflow: hidden;
    overflow-x: auto;
    margin-top: 1rem;
}

/* L'en-tête se démarque du corps, et la dernière ligne ne pose pas son filet
   contre le bord arrondi du cadre. */
.tableau-cadre thead th { background: var(--fond); }
.tableau-cadre tbody tr:last-child td { border-bottom: none; }
.tableau-cadre tbody tr:hover { background: var(--fond); }

/* --- Grille de suivi d'une classe ------------------------------------- */

/* Un exercice par ligne, un élève par colonne, et les deux bords qui portent
   la lecture — le nom de l'exercice, le score de l'élève — restent en place
   pendant qu'on défile. Sans cela, arrivé au trentième élève on ne sait plus
   quelle ligne on lit, et le score se trouve à un écran du dernier exercice.
   Les deux défilements vivent sur le même conteneur : `sticky` se cale sur
   l'ancêtre défilant le plus proche, il n'y en a donc qu'un. */
.tableau-grille {
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre);
  overflow: auto;
  max-height: min(70vh, 38rem);
  margin-top: 1rem;
}

/* `border-collapse: collapse` fusionne les bordures au niveau du tableau :
   celles d'une cellule figée restent alors à leur place d'origine et se
   décollent d'elle au défilement. En `separate`, chaque cellule garde la
   sienne et voyage avec. */
.tableau-grille table {
  border-collapse: separate;
  border-spacing: 0;
  width: auto;
  min-width: 100%;
}

.tableau-grille th,
.tableau-grille td {
  background: var(--carte);
  border-bottom: 1px solid var(--bordure);
  white-space: nowrap;
}

/* Une colonne par élève, de largeur fixe : c'est ce qui rend la grille
   lisible quel que soit le nombre d'exercices, là où des colonnes élastiques
   rétrécissaient à chaque ajout jusqu'à ne plus rien montrer. */
.tableau-grille thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--fond);
  min-width: 7rem;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.85rem;
  color: var(--encre);
  white-space: normal;
  vertical-align: bottom;
}

.tableau-grille tbody td { text-align: center; }

/* La colonne des exercices : figée à gauche, bornée en largeur, le titre
   coupé plutôt que replié — une ligne haute de trois lignes de texte casserait
   l'alignement de toute la grille. Le titre entier reste dans l'infobulle. */
.tableau-grille .colonne-figee {
  position: sticky;
  left: 0;
  z-index: 1;
  min-width: 13rem;
  max-width: 18rem;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.9rem;
  color: var(--encre);
  /* Le filet de séparation en ombre, et non en bordure : il doit rester
     collé à la colonne quand le reste défile dessous. */
  box-shadow: 1px 0 0 var(--bordure);
}

.tableau-grille thead .colonne-figee,
.tableau-grille tfoot .colonne-figee { z-index: 3; }

/* La colonne qui mène aux copies de la classe : la plus étroite possible.
   Elle s'ajoute entre l'exercice et le premier élève, et chaque rem qu'elle
   prend est prise à la grille — d'où le `min-width: 0`, qui défait le 7rem
   des colonnes d'élève, et le `width: 1%`, qui laisse la table la ramener à
   son contenu. Les trois sélecteurs sont écrits en entier : `thead th` est
   déjà à (0,2,1), une classe seule ne suffirait pas à le reprendre. */
.tableau-grille thead th.colonne-copies,
.tableau-grille tbody td.colonne-copies,
.tableau-grille tfoot td.colonne-copies {
  min-width: 0;
  width: 1%;
  text-align: center;
  font-size: 0.85rem;
  white-space: nowrap;
}

.tableau-grille .colonne-figee a { color: var(--encre); text-decoration: none; }
.tableau-grille .colonne-figee a:hover { color: var(--accent); text-decoration: underline; }

.tableau-grille tfoot th,
.tableau-grille tfoot td {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--fond);
  border-bottom: none;
  box-shadow: 0 -1px 0 var(--bordure);
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.tableau-grille tfoot .colonne-figee {
  text-align: left;
  box-shadow: 1px 0 0 var(--bordure), 0 -1px 0 var(--bordure);
}

/* Le survol suit la ligne entière, colonne figée comprise : sans la citer,
   elle garderait son fond et la ligne paraîtrait coupée en deux. */
.tableau-grille tbody tr:hover th,
.tableau-grille tbody tr:hover td { background: var(--fond); }

/* Le verdict est le lien vers le détail : la case d'une grille de classe, la
   colonne « État » du tableau de bord. C'est le geste qu'on fait en parlant
   d'un élève — on montre sa case — et il n'y avait rien derrière.

   Rien de visible ne s'ajoute au repos : une grille de six cents cases
   soulignées ou colorées en bleu ne se lit plus. L'affordance est au survol et
   au clavier, sur l'étiquette elle-même, qui est ce qu'on vise. */
.case-parcours {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  text-decoration: none;
  color: inherit;
}

.case-parcours:hover .etiquette,
.case-parcours:focus-visible .etiquette {
  box-shadow: 0 0 0 2px var(--accent);
}

/* Le contour est porté par l'étiquette (règle ci-dessus), qui est arrondie :
   celui du lien, rectangulaire, en doublerait un second à angles droits. */
.case-parcours:focus-visible { outline: none; }

/* Le nombre de tentatives, collé au verdict de la case. Discret par
   construction : sur six cents cases il ne doit se voir que là où il apparaît,
   c'est-à-dire chez les rares élèves qui ont recommencé. */
.reprises {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--encre-douce);
  font-variant-numeric: tabular-nums;
}

/* La grille compte quatre signes depuis qu'un ✓ peut être « aidé » : la
   légende n'est pas un ornement, elle est la seule chose qui distingue les
   deux réussites l'une de l'autre. */
.legende-grille {
  margin-top: 0.6rem;
  font-size: 0.84rem;
  color: var(--encre-douce);
  line-height: 1.9;
}

.code-acces {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  letter-spacing: 0.08em;
  font-weight: 600;
}

/* --- L'ordre des exercices d'une classe -------------------------------- */

/* L'intertitre de thème, dans la grille. Il dit où une ligne peut se
   déplacer : l'ordre ne vaut qu'à l'intérieur d'une section. Sa cellule reste
   figée avec le reste de la colonne, sinon la grille défilée vers le
   trentième élève ne nommerait plus aucun thème. */
.tableau-grille .ligne-theme th,
.tableau-grille .ligne-theme td {
  background: var(--fond);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--encre-douce);
  padding-top: 0.55rem;
  padding-bottom: 0.35rem;
}

/* Le survol de ligne ne doit pas s'appliquer aux intertitres : ils ne mènent
   nulle part, et les allumer ferait croire à une ligne cliquable. */
.tableau-grille tbody tr.ligne-theme:hover th,
.tableau-grille tbody tr.ligne-theme:hover td { background: var(--fond); }

/* La poignée et le titre dans la cellule figée. `min-width: 0` sur le lien :
   sans lui, un titre long refuserait de se laisser couper et pousserait la
   colonne au-delà de son `max-width`, ce qui décalerait toute la grille. */
.cellule-ordonnable {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.cellule-ordonnable a {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* La poignée est écrite `hidden` dans le gabarit et révélée par le script :
   sans lui elle ne glisserait rien, et une poignée qui ne glisse pas se lit
   comme une panne. `flex: none` pour qu'elle ne se laisse jamais comprimer
   par un titre long — c'est ce qu'on vise pour saisir la ligne. */
.poignee {
  flex: none;
  cursor: grab;
  color: var(--encre-douce);
  font-size: 0.95rem;
  line-height: 1;
  user-select: none;
}

.poignee:active { cursor: grabbing; }

/* La ligne en cours de déplacement s'efface sans quitter le flux : elle sert
   de repère pendant qu'on la déplace, et la faire disparaître ferait sauter
   les autres d'une hauteur de ligne au moment précis où l'on vise. */
.en-deplacement { opacity: 0.45; }

/* --- Le bloc « Ordre des exercices » ----------------------------------- */

.bloc-ordre { margin-top: 2rem; }

.bloc-ordre h2 {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

/* Le témoin d'enregistrement s'efface tout seul : il est re-rendu à chaque
   échange htmx, donc son animation rejoue sans qu'aucun minuteur ait à le
   retirer. `forwards` le laisse transparent à la fin plutôt que de le faire
   réapparaître d'un coup. Sous `prefers-reduced-motion` il reste affiché —
   un message qui ne clignote pas vaut mieux qu'un message qu'on rate. */
.ordre-enregistre {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--succes);
  animation: ordre-enregistre 2.4s ease-out forwards;
}

@keyframes ordre-enregistre {
  0%, 55% { opacity: 1; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ordre-enregistre { animation: none; }
}

/* L'avertissement de rangement perdu. Il reste jusqu'au prochain rangement
   réussi, qui remplace le bloc entier : rien ne doit le faire disparaître
   avant que l'ordre affiché soit redevenu celui de la base. */
.ordre-echec {
  margin: 0 0 0.8rem;
  padding: 0.5rem 0.7rem;
  border-radius: var(--rayon);
  background: var(--echec-fond);
  border: 1px solid var(--echec-bordure);
  color: var(--echec);
  font-size: 0.86rem;
}

.bloc-ordre .entete-page { margin-bottom: 0.5rem; }

.ordre-theme {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 1.4rem 0 0.6rem;
  font-size: 0.95rem;
}

.liste-ordre {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.ligne-ordre {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  background: var(--carte);
}

/* Le rang, en chiffres à chasse fixe : la colonne reste alignée du premier au
   dixième, ce qui est ce qu'on lit en vérifiant un ordre. */
.ordre-rang {
  flex: none;
  min-width: 1.4rem;
  color: var(--encre-douce);
  font-size: 0.82rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* Le titre prend toute la place restante et se coupe : c'est lui qui varie,
   et le laisser se replier ferait danser la hauteur des lignes d'un thème à
   l'autre — la comparaison verticale est justement ce qu'on vient faire. */
.ordre-titre {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--encre);
  text-decoration: none;
}

.ordre-titre:hover { color: var(--accent); text-decoration: underline; }

.ordre-fleches {
  flex: none;
  display: flex;
  gap: 0.25rem;
}

/* Les deux flèches gardent leur largeur, désactivées comprises : aux deux
   bouts d'une section, une colonne qui rétrécit ferait glisser la ligne sous
   le curseur au moment où l'on enchaîne les clics. */
.ordre-fleches .bouton {
  min-width: 2rem;
  padding-left: 0.4rem;
  padding-right: 0.4rem;
  line-height: 1.1;
}


/* --- Parcours d'un élève sur un exercice (fiche élève) ----------------- */

/* Une carte par exercice, un bloc par tentative. Le trait ne sépare que des
   tentatives entre elles — la première n'en a pas au-dessus, où il doublerait
   le titre de la carte. */
.bloc-tentative + .bloc-tentative {
  border-top: 1px solid var(--bordure);
  margin-top: 1.2rem;
  padding-top: 1rem;
}

.titre-tentative {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1rem;
  margin-bottom: 0.2rem;
}

/* La carte d'un exercice, repliée elle aussi. Vingt exercices travaillés font
   vingt écrans ; ce qu'on vient chercher — lesquels posent problème — se lit
   sur les seules étiquettes du `summary`. Le reste s'examine ensuite, un
   exercice à la fois. */
.carte-parcours {
  margin-bottom: 1rem;
  padding: 0;
}

.carte-parcours-entete {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 1rem 1.5rem;
  cursor: pointer;
  border-radius: var(--rayon-large);
}

.carte-parcours-entete:hover { background: var(--fond); }
.carte-parcours[open] > .carte-parcours-entete {
  border-bottom: 1px solid var(--bordure);
  border-radius: var(--rayon-large) var(--rayon-large) 0 0;
}

.carte-parcours-titre { font-size: 1.05rem; font-weight: 600; }

/* Arrivé depuis une case de grille ou une ligne du tableau de bord : la carte
   visée est déjà dépliée — le serveur pose `open` — mais sur une fiche de
   vingt exercices il reste à dire *laquelle* on vient d'ouvrir. Le lien du
   tableau de bord vise une tentative, d'où la seconde règle : sur un exercice
   recommencé trois fois, la carte seule laisserait chercher. */
.carte-parcours:target { border-color: var(--accent); }

/* Le filet est pris *dans* le retrait de la carte : marge, bordure et retrait
   valent ensemble les 1.5rem que porte tout enfant de `.carte-parcours`, d'où
   les 3px retranchés. Sans cette soustraction, la tentative visée se décalerait
   par rapport à ses voisines, et c'est le décalage qu'on lirait plutôt que le
   filet. */
.bloc-tentative:target {
  border-left: 3px solid var(--accent);
  margin-left: 0.6rem;
  padding-left: calc(0.9rem - 3px);
}

/* Le corps porte lui-même le retrait de la carte : le `summary` est collé aux
   bords pour que sa zone cliquable couvre toute la largeur. */
.carte-parcours > :not(summary) { margin-left: 1.5rem; margin-right: 1.5rem; }
.carte-parcours > :last-child { margin-bottom: 1.25rem; }

/* Le code de l'élève au moment où l'aide a été rédigée. Sans lui, un indice
   qui parle d'une boucle ne dit rien : c'est ce bloc qui permet d'en juger la
   cohérence. */
.code-lu {
  margin: 0.9rem 0 0.5rem;
  border-left: 3px solid var(--bordure);
  padding-left: 0.8rem;
}

.code-lu-entete,
.code-retenu-entete {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin: 0 0 0.3rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--encre-douce);
}

.code-lu-date,
.code-retenu-date { font-weight: 400; }
.code-lu .console-python { margin: 0; }

/* Le code retenu pour la tentative — celui qui a réussi, ou le dernier
   envoyé — en tête de celle-ci. Même en-tête que le code lu au titre de
   l'aide, c'est la même chose qu'on lit : un programme et ce qu'il valait.
   Sans le filet de gauche en revanche — celui-ci marque un aparté dans une
   conversation, alors qu'ici le code est ce qu'on est venu voir. */
.code-retenu { margin: 0.9rem 0 0.5rem; }
.code-retenu .console-python { margin: 0; }

/* « puis 2 autres envois » : la page a choisi de montrer la soumission
   réussie, elle doit dire ce qu'elle laisse de côté. En retrait de ton, parce
   que c'est une réserve sur ce qu'on lit, pas un verdict de plus. */
.code-retenu-suite { font-weight: 400; color: var(--discret); }

/* Indices et échange, repliés. Sans cela une fiche d'élève fait plusieurs
   écrans par exercice, et ce qu'on vient y chercher — où en est-il — se trouve
   noyé sous ce qu'il a lu. Le `summary` garde ce qui se lit sans déplier : les
   compteurs, et l'étiquette « solution révélée ». */
.bloc-repli {
  margin-top: 0.8rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  background: var(--fond);
}

.bloc-repli > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.8rem;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--encre-douce);
  border-radius: var(--rayon);
}

.bloc-repli > summary:hover { color: var(--encre); }
.bloc-repli[open] > summary { border-bottom: 1px solid var(--bordure); }

.bloc-repli-corps { padding: 0.3rem 0.8rem 0.8rem; }

/* Sur la fiche de l'élève, entre le sous-titre et les cartes. Le champ
   prend toute la largeur de son parent : borné ici, il garde la taille d'un
   nom plutôt que celle de la page. */
.bloc-renommer { max-width: 32rem; margin: 0 0 1.75rem; }
.bloc-renommer .bloc-repli-corps { padding-top: 0.8rem; }

/* Le même geste dans le tableau des codes de la fiche de classe : un nom et
   un crayon, que `renommer-eleve.js` change en champ. Le crayon reste visible
   sans survol — un écran tactile n'en a pas — mais discret : trente crayons
   à l'encre pleine feraient de la colonne une rangée de boutons. */
.nom-renommable { display: inline-flex; align-items: center; gap: 0.35rem; }

.crayon-renommer {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: var(--rayon);
  color: var(--discret);
}
.crayon-renommer:hover { background: var(--fond); color: var(--accent); }
.crayon-renommer:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* La colonne du nom a sa largeur à elle : laissée au calcul automatique, elle
   s'élargissait de 55 px à l'ouverture du champ, et le code comme le bouton
   « Régénérer » glissaient sous le curseur. */
.tableau-codes th:first-child { width: 50%; }

/* Ouvert, le champ prend la hauteur d'un `.bouton-petit`, sans quoi la ligne
   grandirait de la différence. */
.renommer-en-ligne { display: flex; align-items: center; gap: 0.4rem; }
.renommer-en-ligne input[type="text"] {
  width: 15rem;
  min-width: 8rem;
  padding: 0.3rem 0.5rem;
  font-size: 0.86rem;
}

/* Deux indices demandés sans resoumettre entre les deux n'ont pas de code
   lu pour les séparer : sans cette règle ils se touchaient, et l'on ne
   voyait plus où finissait l'un. Même écart que dans la colonne des
   indices, qui pose le sien sur son propre corps. */
.bloc-repli-corps > .indice + .indice { margin-top: 0.6rem; }

/* --- Les copies d'une classe sur un exercice --------------------------- */

/* La page réemploie la carte de la fiche d'un élève (`.carte-parcours`), et
   ce n'est pas une économie : les deux pages posent la même question à un
   `summary` — qui est-ce, et où en est-il — puis cachent derrière le même
   code. Deux apparences pour un même geste laisseraient croire à deux objets
   différents. Rien à ajouter donc, sinon les deux cas que la fiche d'un élève
   ne connaît pas. */

/* Un élève qui n'a rien commencé : le même bandeau, mais rien derrière. Ce
   n'est pas un `<details>` — un dépliage qui ne déplie rien se lit comme une
   panne — d'où le curseur et le survol à neutraliser, que `.carte-parcours`
   pose pour ce qui s'ouvre. */
.carte-parcours-inerte > .carte-parcours-entete {
  cursor: default;
  /* Le retrait de 1.5rem que `.carte-parcours` donne à tout enfant qui n'est
     pas un `summary` s'ajouterait ici à celui du bandeau lui-même, et le nom
     de cet élève-là serait décalé de ses voisins. */
  margin: 0;
}

.carte-parcours-inerte > .carte-parcours-entete:hover { background: none; }

/* Les envois et l'aide consommée, au bout du bandeau. En retrait de ton : ce
   sont des repères, pas le verdict, et sur trente lignes ils ne doivent pas
   disputer la place au nom de l'élève. `margin-left: auto` les pousse à
   droite du `summary`, qui est un conteneur flex. */
.copie-meta {
  margin-left: auto;
  font-size: 0.82rem;
  font-weight: 400;
  color: var(--encre-douce);
  white-space: nowrap;
}

/* --- Vide ------------------------------------------------------------ */

.vide {
  text-align: center;
  padding: 2.5rem 1.5rem;
  color: var(--encre-douce);
  background: var(--carte);
  border: 1px dashed var(--bordure);
  border-radius: var(--rayon);
}

/* --- Page exercice --------------------------------------------------- */

/* Trois bandes empilées : l'énoncé, puis le code et sa console, puis les
   tests et l'aide. Le même 1.5rem partout — entre les bandes comme entre les
   colonnes — plutôt que quatre valeurs voisines qui se ressemblent sans se
   valoir. */
.atelier-code, .atelier {
  display: flex;
  gap: 1.5rem;
  margin-top: 1.5rem;
}

.atelier { align-items: flex-start; }

/* On écrit à gauche, on lit à droite ce que ça produit. La console est bien
   plus étroite : elle affiche des valeurs, pas des programmes. */
.zone-code { flex: 7 1 0; min-width: 0; }
.zone-sortie { flex: 3 1 0; min-width: 0; }

/* Les tests sont un peu plus larges : ils portent des appels de fonction et
   deux valeurs à comparer, là où l'aide n'est que de la prose — qui se lit
   d'ailleurs mieux sur une colonne étroite. */
.colonne-tests { flex: 7 1 0; min-width: 0; }
.colonne-indices { flex: 5 1 0; min-width: 0; }

/* Les deux colonnes de la fiche enseignant : l'énoncé et son jeu de tests à
   gauche, l'attribution et les résultats à droite.

   Le `min-width: 0` n'est pas une précaution de style. Un élément flex ne
   descend jamais sous la largeur intrinsèque de son contenu tant qu'on ne le
   lui permet pas : un seul appel un peu long dans le jeu de tests élargissait
   la colonne de gauche jusqu'à chasser la droite hors de l'écran, et le
   `overflow-x` du bloc de tests ne pouvait pas s'enclencher — un conteneur
   qui accepte de grandir ne déborde pas. Les proportions fixent la largeur
   une fois pour toutes, le contenu défile à l'intérieur. */
.colonne-enonce { flex: 7 1 0; min-width: 0; }
.colonne-suivi { flex: 5 1 0; min-width: 0; }

/* En dessous, les colonnes s'empilent et l'aide passe au-dessus du verdict.
   Seul l'ordre visuel change : le document, lui, garde les tests en premier.
   D'où le repli du bloc d'indices — sans lui, il faudrait faire défiler toute
   l'aide reçue pour voir quel test échoue. */
@media (max-width: 60rem) {
  .atelier { flex-direction: column; }
  .colonne-tests, .colonne-indices,
  .colonne-enonce, .colonne-suivi { width: 100%; flex: none; }
  .colonne-indices { order: -1; }
}

/* La prose de l'énoncé ne suit pas la largeur de la carte : sur un moniteur
   large, une ligne de 180 caractères ne se lit pas. */
.enonce-texte { max-width: 80ch; }

.enonce { line-height: 1.6; }
.enonce pre {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.92em;
  background: var(--fond);
  padding: 0.75rem 1rem;
  border-radius: var(--rayon);
  overflow-x: auto;
}

/* Un nom de variable ou de fonction cité au fil d'une phrase. Le même petit
   encadré partout où il apparaît — énoncé, indice, en-tête de tableau de
   saisie — sinon `nombres` se lit différemment selon l'endroit de la page où
   on le rencontre. Seul le fond suit le support : gris sur du blanc, blanc
   sur le bleu d'un indice. */
.enonce code, .indice code, .echange-message code, .saisie-defilante th code {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.9em;
  background: var(--fond);
  border: 1px solid var(--bordure);
  border-radius: 4px;
  padding: 0.05rem 0.35rem;
}

.indice code { background: var(--carte); border-color: var(--accent-bordure); }

/* Dans une bulle de l'échange, même principe que dans un indice : le fond
   suit le support. Blanc sur le gris de l'assistant comme sur le bleu de
   l'élève, sans quoi le `nombres` cité en passant se confondrait avec sa
   bulle — et le modèle en cite à chaque phrase. */
.echange-message code { background: var(--carte); }
.echange-eleve code { border-color: var(--accent-bordure); }

/* Pas de cadre dans un cadre : dans un bloc de code, le `code` n'est plus une
   citation en ligne mais le contenu même du bloc. */
.enonce pre code, .indice pre code, .echange-message pre code, .console-python code {
  font: inherit;
  background: none;
  border: 0;
  padding: 0;
}

/* Balises produites par le rendu Markdown de l'énoncé. */
.enonce ul, .enonce ol { padding-left: 1.4rem; margin: 0.6rem 0; }
.enonce li { margin-bottom: 0.3rem; }
.enonce blockquote {
  margin: 0.8rem 0;
  padding-left: 0.9rem;
  border-left: 3px solid var(--bordure);
  color: var(--encre-douce);
}
.enonce table { margin: 0.8rem 0; display: block; overflow-x: auto; }

.editeur-hote {
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  overflow: hidden;
  background: var(--carte);
  min-height: var(--hauteur-editeur);
}

.editeur-hote .cm-editor { min-height: var(--hauteur-editeur); }

/* Pas de ligatures dans le code, ni ici ni sous aucune police à chasse fixe.
   `ui-monospace` n'est reconnu que de Safari : ailleurs, c'est Cascadia Code
   qui sert dès qu'elle est installée — Windows Terminal, Visual Studio,
   Windows 11 récent — et elle dessine par défaut `<=` en `≤`, `!=` en `≠`.
   D'un poste à l'autre de la même salle, l'élève voyait donc ou non un
   symbole qu'il ne peut pas taper et que Python refuserait, sans savoir s'il
   avait écrit `<` ou `<=`. D'où le `font-variant-ligatures: none` posé à côté
   de chaque `"Cascadia Code"` de cette feuille ; le thème de `entree.js` ne
   le porte pas, la règle s'hérite et évite de reconstruire le bundle. */
.editeur-hote .cm-editor { font-variant-ligatures: none; }

/* Le fond de la ligne où se trouve le curseur, et il doit rester translucide.
   CodeMirror garde ce soulignement pendant une sélection — la ligne active est
   celle de la *tête* de la sélection — et il le peint dans la couche du texte,
   au-dessus de celle qui dessine la sélection. Une couleur opaque effaçait donc
   la dernière ligne sélectionnée : celle où s'arrête le geste, en descendant
   comme en montant. On sélectionnait tout son code et la ligne qu'on venait
   d'atteindre n'était pas marquée.

   C'est pour cette raison que la valeur de CodeMirror a un canal alpha, et
   `outillage/editeur/entree.js` la redéfinissait en `#f9fafb`, opaque. La
   redéfinir ici évite de reconstruire le bundle pour un fond de ligne ; sur du
   blanc elle rend le même gris, sur une sélection elle la laisse voir. */
.editeur-hote .cm-activeLine { background-color: var(--ligne-active); }

/* --- Tentatives précédentes -------------------------------------------- */

/* Ce qu'on avait écrit avant de recommencer, replié sous l'éditeur. Replié,
   parce que ce n'est pas ce qu'on vient faire ici : recommencer, c'est repartir
   de la page blanche, et l'ancien code est un recours — la garantie qu'on ne
   perd rien en cliquant — pas un point de départ. */
.tentatives-precedentes {
  margin-top: 1rem;
  background: var(--carte);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  padding: 0.6rem 1rem;
}

.tentatives-precedentes > summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--encre-douce);
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.tentative-ancienne { margin-top: 0.9rem; }
.tentative-ancienne .meta { margin-bottom: 0.35rem; }

/* --- Console de l'élève ------------------------------------------------ */

.zone-sortie-titre {
  font-weight: 600;
  font-size: 0.92rem;
  margin: 0 0 0.3rem;
}

/* La console est bornée à la hauteur de l'éditeur et défile au-delà : sans
   cela, cinquante lignes affichées étireraient la rangée et repousseraient
   toute la page. Elle ne suit pas un éditeur devenu plus haut que ça — elle
   reste alors en tête de colonne, ce qui vaut mieux qu'une hauteur qui bouge
   à chaque ligne tapée. */
.zone-sortie .sortie-console {
  margin-top: 0;
  max-height: var(--hauteur-editeur);
  overflow: auto;
}

.zone-sortie .sortie-console-vide {
  background: none;
  border: 1px dashed var(--bordure);
  color: var(--encre-douce);
  font-family: inherit;
  font-size: 0.86rem;
  white-space: normal;
}

.zone-sortie .sortie-console-vide code {
  color: var(--encre);
  overflow-wrap: anywhere;
}

/* Sur téléphone la console s'intercale entre le code et les tests : vide, elle
   coûterait un écran de défilement pour ne rien dire. Sur grand écran elle
   garde sa place — sans quoi l'éditeur changerait de largeur à chaque
   correction. */
@media (max-width: 60rem) {
  .atelier-code { flex-direction: column; }
  .zone-code, .zone-sortie { width: 100%; flex: none; }
  .sortie-vide { display: none; }
}

.barre-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.9rem;
  flex-wrap: wrap;
}

.barre-actions .fin { margin-left: auto; }

/* --- Résultats de correction ----------------------------------------- */

/* La colonne des tests s'ouvre sur la barre d'action : elle démarre en haut,
   sans la marge que porte la barre partout ailleurs, pour rester à hauteur du
   « Ton code » de la colonne de gauche. */
.colonne-tests > .barre-actions { margin-top: 0; }

/* L'écart au bandeau se règle sur la paire, pour ne pas laisser un blanc sous
   la barre tant qu'aucune correction n'a tourné. */
.colonne-tests > .barre-actions + div { margin-top: 0.9rem; }

.bandeau-resultat {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1.1rem;
  border-radius: var(--rayon);
  font-weight: 600;
  border: 1px solid var(--bordure);
  background: var(--carte);
}

.bandeau-resultat.succes { background: var(--succes-fond); border-color: var(--succes-bordure); color: var(--succes); }
.bandeau-resultat.echec { background: var(--echec-fond); border-color: var(--echec-bordure); color: var(--echec); }
.bandeau-resultat.attente { background: var(--attente-fond); border-color: var(--attente-bordure); color: var(--attente); }

/* Ce que le recadrage d'`exercice.html` amène sous les yeux après une
   correction. `scrollIntoView({block: 'nearest'})` colle sa cible au bord de
   la fenêtre ; cette marge l'en décolle, et surtout laisse dépasser ce qui
   suit — un bandeau au ras du bas se lit comme la fin de la page, et le
   deuxième cas visé ne dit qu'il y en a d'autres que si le troisième pointe
   dessous. C'est la seule chose qui distingue « voilà ton résultat » de
   « voilà ton résultat, et il continue ». */
.bandeau-resultat, .tests li { scroll-margin-bottom: 2rem; }

.tests { list-style: none; padding: 0; margin: 0.9rem 0 0; }

.tests li {
  border: 1px solid var(--bordure);
  border-left-width: 4px;
  border-radius: var(--rayon);
  padding: 0.6rem 0.9rem;
  margin-bottom: 0.45rem;
  background: var(--carte);
  font-size: 0.92rem;
}

.tests li.reussi { border-left-color: var(--succes); }
.tests li.echoue { border-left-color: var(--echec); }

.tests .appel {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-weight: 600;
}

.tests dl { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.6rem; margin: 0.4rem 0 0; }
.tests dt { color: var(--encre-douce); font-size: 0.86rem; }
.tests dd {
  margin: 0;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.88rem;
  overflow-wrap: anywhere;
}

.sortie-console {
  background: var(--console-fond);
  color: var(--console-encre);
  padding: 0.75rem 1rem;
  border-radius: var(--rayon);
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.86rem;
  white-space: pre-wrap;
  overflow-x: auto;
  margin-top: 0.75rem;
}

/* --- Indices --------------------------------------------------------- */

/* Un panneau à part entière, et non une liste sous un titre : il occupe une
   colonne face aux tests, et doit se lire comme leur pendant. Le repli est
   son seul réglage, donc l'en-tête entier est la commande — pas un triangle
   de 8 pixels. */
.bloc-indices {
  border: 1px solid var(--accent-bordure);
  border-radius: var(--rayon);
  background: var(--carte);
  overflow: hidden;
}

.bloc-indices-entete {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.9rem;
  cursor: pointer;
  font-weight: 700;
  font-size: 0.92rem;
  color: var(--accent-appuye);
  background: var(--accent-fond);
  /* Un double clic sur l'en-tête surlignerait le titre au lieu de replier. */
  user-select: none;
  transition: background 0.12s ease-out;
}

.bloc-indices-entete:hover { background: var(--accent-fond-survol); }

/* Le contour par défaut se dessinerait autour du `summary` entier, débordant
   des coins arrondis du panneau : on le ramène à l'intérieur. */
.bloc-indices-entete:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.bloc-indices-icone { flex: none; opacity: 0.75; }
.bloc-indices-titre { margin-right: 0.1rem; }

/* Combien d'indices sont déjà là — utile surtout replié, où c'est la seule
   chose qui reste visible du contenu. */
.bloc-indices-compte {
  flex: none;
  min-width: 1.35rem;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--sur-accent);
  font-size: 0.75rem;
  line-height: 1.35rem;
  text-align: center;
}

/* `display: flex` sur un `summary` escamote le triangle du navigateur : sans
   ce chevron, rien n'annoncerait que le panneau se replie. */
.bloc-indices-entete { list-style: none; }
.bloc-indices-entete::-webkit-details-marker { display: none; }

.bloc-indices-chevron {
  flex: none;
  margin-left: auto;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  /* Fermé, la pointe est en bas — « déplier vers le bas ». Le décalage
     compense le centre de rotation, qui n'est pas la pointe du chevron. */
  transform: translateY(-0.1rem) rotate(45deg);
  transition: transform 0.18s ease-out;
}

.bloc-indices[open] .bloc-indices-chevron {
  transform: translateY(0.1rem) rotate(225deg);
}

.bloc-indices-corps {
  padding: 0.9rem;
  border-top: 1px solid var(--accent-bordure);
  animation: deploiement 0.18s ease-out;
}

/* Le contenu n'apparaît pas d'un coup : il glisse depuis l'en-tête, ce qui
   dit d'où il sort et rend le repli réversible à l'œil. */
@keyframes deploiement {
  from { opacity: 0; transform: translateY(-0.35rem); }
}

/* Sauf pendant une génération : le fragment est alors réécrit toutes les deux
   secondes, et le corps rejouerait son fondu à chaque tour. L'animation
   accompagne une ouverture, pas un rafraîchissement. */
.indices-en-cours .bloc-indices-corps { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .bloc-indices-corps { animation: none; }
  .bloc-indices-chevron { transition: none; }
}

/* La colonne garde sa place dans la mise en page avant le premier indice :
   un mot discret vaut mieux qu'un blanc, qui se lit comme un bogue. */
.indices-vide {
  margin: 0;
  padding: 0.9rem 1rem;
  border: 1px dashed var(--bordure);
  border-radius: var(--rayon);
  color: var(--encre-douce);
  font-size: 0.86rem;
}

.indice {
  border: 1px solid var(--accent-bordure);
  background: var(--accent-fond);
  border-left: 4px solid var(--accent);
  border-radius: var(--rayon);
  padding: 0.85rem 1.1rem;
}

.bloc-indices-corps > .indice + .indice { margin-top: 0.6rem; }

.indice .niveau {
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent-appuye);
  display: block;
  margin-bottom: 0.2rem;
}

/* Dans l'échange, `.indice` sert aussi bien à l'élève qu'à l'assistant — le
   texte du `niveau` le disait déjà, mais rien ne le donnait à voir sans le
   lire. Le message de l'élève passe en neutre pour se détacher du bleu de
   l'IA, qu'il garde par défaut : même teinte que les indices, puisque
   c'est la même voix qui écrit les deux. */
.indice-eleve {
  background: var(--carte);
  border-color: var(--bordure);
  border-left-color: var(--encre-douce);
}

.indice-eleve .niveau { color: var(--encre-douce); }

/* Le code sur lequel un indice a été rédigé, replié dans l'indice lui-même.
   Un bouton, et pas le bloc déroulé de la fiche de l'enseignant : le
   programme est déjà dans l'éditeur à quelques centimètres de là, et le
   dérouler d'office repousserait l'indice suivant d'un écran pour le
   recopier. Ce qu'on vient vérifier ici est le cas contraire — avoir
   resoumis depuis, et lire un indice qui parle d'autre chose.

   `list-style: none` et le marqueur WebKit escamotés : le triangle du
   navigateur se pose mal sur un `summary` qu'on veut lire comme un lien, et
   le chevron dessiné ci-dessous dit la même chose en suivant l'état. */
.code-indice { margin-top: 0.6rem; }

.code-indice > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--accent-appuye);
  list-style: none;
}

.code-indice > summary::-webkit-details-marker { display: none; }
.code-indice > summary:hover { text-decoration: underline; }

.code-indice > summary::before {
  content: "";
  width: 0.35rem;
  height: 0.35rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  /* Fermé, la pointe est à droite — « déplier vers la droite » ; ouvert, elle
     tombe, comme le chevron du panneau des indices. */
  transform: translateX(-0.05rem) rotate(-45deg);
}

.code-indice[open] > summary::before {
  transform: translateY(-0.1rem) rotate(45deg);
}

/* Le fond de l'indice est déjà teinté : le code y prend le fond des cartes
   pour se détacher, plutôt qu'un second bleu par-dessus le premier. */
.code-indice .console-python {
  margin: 0.4rem 0 0;
  background: var(--carte);
}

/* Un indice se génère en plusieurs secondes, pendant lesquelles la seule
   chose qui bouge est cette roue : le texte seul laissait croire à une page
   figée. Teinte atténuée — rien n'est encore là à lire. */
.indice-attente { color: var(--encre-douce); }
.indice-attente .indicateur-attente { margin-right: 0.15rem; }

/* Curseur de frappe, posé par `frappe.js` et par lui seul : sans JavaScript
   la phrase est déjà complète, et un bloc clignotant à sa suite n'aurait rien
   à annoncer. Pas de `content` textuel — un caractère ici serait lu à voix
   haute par les lecteurs d'écran. */
.frappe::after {
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 1em;
  margin-left: 0.08em;
  vertical-align: -0.15em;
  background: currentColor;
  animation: clignotement 1s steps(2, jump-none) infinite;
}

@keyframes clignotement { 50% { opacity: 0; } }

/* Balises produites par le rendu Markdown de l'indice. */
.indice p { margin: 0.5rem 0; }
.indice > p:first-of-type { margin-top: 0; }
.indice > :last-child { margin-bottom: 0; }
.indice ul, .indice ol { padding-left: 1.3rem; margin: 0.5rem 0; }
.indice li { margin-bottom: 0.25rem; }

/* --- Génération en série ---------------------------------------------- */

/* Rappel d'une série en cours, sous le bandeau de navigation. Même gabarit
   que lui — bord à bord, contenu borné à la largeur des pages — pour qu'il se
   lise comme une seconde ligne de l'en-tête et non comme un message de plus. */
.bandeau-serie {
  background: var(--attente-fond);
  border-bottom: 1px solid var(--bordure);
  color: var(--encre);
  font-size: 0.92rem;
}

.bandeau-serie-alerte { background: var(--echec-fond); }

.bandeau-serie-interieur {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0.5rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.bandeau-serie .fin { margin-left: auto; }

@media print {
  .bandeau-serie { display: none; }
}


.serie-etat {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
  flex-wrap: wrap;
}

.serie-etat .aide { margin-top: 0; }

/* Avancement d'une série : la jauge dit ce qui est fait sur ce qui est
   demandé — le seul avancement connaissable ici, puisque la durée d'un appel
   au modèle ne l'est pas. */
.jauge {
  height: 0.5rem;
  border-radius: 999px;
  background: var(--fond);
  border: 1px solid var(--bordure);
  overflow: hidden;
}

.jauge-remplie {
  height: 100%;
  background: var(--accent);
  transition: width 0.4s ease;
}

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

.serie-lignes { list-style: none; padding: 0; margin: 0; }

.serie-lignes li {
  padding: 0.7rem 0;
  border-top: 1px solid var(--bordure);
}

.serie-lignes li > .aide { display: inline; margin-left: 0.4rem; }
.serie-lignes li > p.aide { display: block; margin-left: 0; }
.serie-lignes .etiquette { margin-right: 0.5rem; }

/* --- Modale (prompt à emporter) --------------------------------------- */

/* `<dialog>` natif : ni bibliothèque, ni piège de focus à écrire — le
   navigateur s'en charge, y compris de la touche Échap.

   La première règle est un filet pour un navigateur qui ne connaîtrait pas
   `<dialog>` : il traiterait la balise comme inconnue, donc visible, et
   déverserait les cinquante lignes du prompt en plein milieu de la page. */
dialog:not([open]) { display: none; }

.modale {
  width: min(52rem, 92vw);
  max-height: 86vh;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  background: var(--carte);
  color: var(--encre);
  overflow-y: auto;
}

.modale::backdrop { background: var(--voile); }

.modale-entete {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.modale-entete h2 { margin: 0; }

.modale .barre-actions { margin-top: 0.9rem; }

/* Le prompt fait plusieurs dizaines de lignes : à lui la place, en
   monospace, pour qu'on repère d'un coup d'œil où finit la consigne. */
.modale-prompt {
  margin-top: 0.6rem;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.82rem;
  line-height: 1.5;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}

/* --- Impression des codes -------------------------------------------- */

.grille-codes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.75rem;
}

.carton {
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  padding: 0.9rem 1.1rem;
  background: var(--carte);
}

.carton .nom { font-weight: 600; }
.carton .code { font-size: 1.3rem; margin-top: 0.25rem; }

/* --- Lecteur pas à pas ------------------------------------------------ */

/* Un bouton qui ne réclame rien : il vit dans une liste de tests déjà
   chargée, et un bouton plein par ligne ferait de la page un tableau de
   bord. */
.bouton-discret {
  display: inline-block;
  margin-top: 0.5rem;
  padding: 0.2rem 0.55rem;
  font: inherit;
  font-size: 0.82rem;
  color: var(--accent);
  background: none;
  border: 1px solid var(--accent-bordure);
  border-radius: var(--rayon);
  cursor: pointer;
}

.bouton-discret:hover { background: var(--accent-fond); }

/* La modale. Le déroulé demande de la place, mais surtout il doit s'imposer :
   déplié au bas de la page, il passait inaperçu — on cliquait, rien ne
   semblait bouger. */
.modale-trace {
  width: min(78rem, calc(100vw - 2rem));
  max-width: none;
  /* Ancrée en haut, et non centrée : sa hauteur varie avec le tableau de
     suivi, et une modale centrée se recalerait verticalement à chaque étape
     — tout glisserait sous le curseur pendant qu'on avance. */
  margin: 2rem auto auto;
  padding: 0;
  border: none;
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre-forte);
  background: var(--carte);
  color: var(--encre);
  overflow: hidden;

  /* Hauteur **définie**, et non un simple plafond. Sans une hauteur connue
     de bout en bout, le tableau de suivi ne peut pas savoir jusqu'où
     s'étendre : il gardait donc une hauteur arbitraire, et sa barre de
     défilement apparaissait alors qu'il restait du vide sous lui. Tout ce
     qui suit — les `flex: 1` et les `min-height: 0` — sert à faire
     descendre cette hauteur jusqu'au tableau.

     Définie aussi pour une seconde raison : la modale s'ouvre sur l'attente
     du calcul, et une hauteur qui s'ajusterait au contenu ferait sauter
     toute la fenêtre au moment où le déroulé arrive. */
  display: flex;
  flex-direction: column;
  height: calc(100vh - 4rem);
  /* `dvh` tient compte des barres d'outils mobiles ; ignoré là où il n'existe
     pas, la ligne précédente ayant déjà fait le travail. */
  height: calc(100dvh - 4rem);
}

.modale-trace::backdrop { background: var(--voile-fort); }
.modale-trace:not([open]) { display: none; }

/* `min-height: 0` sur chaque maillon : un enfant de flex refuse par défaut
   de descendre sous sa taille de contenu, ce qui suffit à faire déborder
   toute la chaîne et à annuler l'effet recherché. */
.modale-trace > div,
.trace-enveloppe {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.trace-attente {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex: 1;
  min-height: 0;
  gap: 0.35rem;
  padding: 3rem 1.5rem;
  text-align: center;
}

.trace-attente-titre { margin: 0; font-weight: 600; }
.trace-attente .bouton { margin-top: 1rem; }

/* L'attente occupe une modale vide : un indicateur de la taille du texte y
   serait un détail perdu au milieu du blanc. */
.indicateur-large {
  width: 2rem;
  height: 2rem;
  border-width: 3px;
  margin: 0 0 0.75rem;
  color: var(--accent);
}

/* Le lecteur remplit la modale : c'est `.trace-corps` qui absorbe la place
   restante, et le reste (en-tête, verdict, alertes) garde sa taille propre.
   `overflow-y` est le filet, pour une fenêtre trop courte pour tout tenir. */
.lecteur-trace {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 1.25rem 1.5rem;
}
.lecteur-trace:focus { outline: none; }
.lecteur-trace h2 { margin: 0; font-size: 1.1rem; }

.trace-entete {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.trace-appel {
  margin: 0.3rem 0 0;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.95rem;
}

.trace-appel .etiquette { margin-left: 0.5rem; }

.trace-verdict {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.15rem 0.6rem;
  margin: 0.8rem 0 0;
  font-size: 0.9rem;
}

.trace-verdict dt { color: var(--encre-douce); }
.trace-verdict dd {
  margin: 0;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  overflow-wrap: anywhere;
}

/* Le code et les commandes à gauche, le suivi des variables à droite : c'est
   la disposition du tableau noir, où la trace s'écrit à côté de l'algorithme
   et non en dessous. Sous 64rem les deux se superposent, le code d'abord —
   c'est la ligne surlignée qui donne son sens à ce qu'on lit en face. */
.trace-corps {
    display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  /* La ligne est bornée à la hauteur disponible. Sans ce `minmax(0, 1fr)`,
     une ligne de grille se dimensionne sur son contenu le plus haut : le
     tableau de suivi gagnant une rangée par étape, la ligne grandissait avec
     lui, `align-items: stretch` étirait aussi la colonne de gauche, et
     `.trace-code` — qui est en `flex: 1` — absorbait le surplus en blanc sous
     le code. Les commandes descendaient d'autant. Mesuré sur un `maximum`
     de six lignes : 149 px de code aux huit premières étapes, 204 px à la
     dixième, et les boutons vingt-cinq pixels plus bas à chaque fois.

     Les colonnes s'étirent toujours — c'est ce qui donne au tableau de quoi
     aller jusqu'en bas — mais elles ne peuvent plus repousser leur ligne. */
  grid-template-rows: minmax(0, 1fr);
  align-items: stretch;
  flex: 1;
  min-height: 0;
  gap: 1.1rem;
  margin-top: 1rem;
}

.trace-colonne-code,
.trace-panneau {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Empilées, les deux colonnes ne peuvent plus se partager une hauteur : on
   rend la main au défilement de la modale, et chaque bloc reprend une taille
   propre. */
@media (max-width: 64rem) {
  .trace-corps { grid-template-columns: 1fr; flex: none; min-height: auto; }
  .trace-code { flex: none; max-height: 18rem; }
  .trace-vue { flex: none; height: 20rem; }
}

.trace-code {
  margin: 0;
  padding: 0.6rem 0;
  list-style: none;
  background: var(--fond);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.86rem;
  overflow: auto;
  /* Le cadre épouse le code, et se replie s'il n'y tient pas. Pas `flex: 1` :
     il absorberait toute la colonne, et six lignes de programme flotteraient
     dans quatre cents pixels de blanc. Sa hauteur ne dépend donc que du
     nombre de lignes écrites par l'élève — elle ne bouge pas d'une étape à
     l'autre, ce qui est tout ce qu'on demande aux commandes placées dessous. */
  flex: 0 1 auto;
  min-height: 0;
}

.trace-code li {
  display: flex;
  gap: 0.9rem;
  padding: 0 0.9rem;
  /* Une ligne vide n'a pas de contenu : sans hauteur imposée, elle
     disparaîtrait et décalerait toute la numérotation par rapport au code
     que l'élève a sous les yeux dans l'éditeur. */
  min-height: 1.5em;
  white-space: pre;
}

/* Le numéro vient de l'attribut, et non d'un compteur CSS : les lignes du
   programme d'essai sont retirées, et un compteur les renuméroterait de 1 à n.
   Le lecteur désigne la ligne courante par son numéro d'origine, celui que
   l'élève lit dans son éditeur — c'est celui-là qu'il faut afficher. */
.trace-code li::before {
  content: attr(data-ligne);
  flex: none;
  width: 2.5ch;
  text-align: right;
  color: var(--discret);
}

.trace-code li.trace-courante {
  background: var(--accent-fond);
  box-shadow: inset 3px 0 0 var(--accent);
}

.trace-code li.trace-fautive {
  background: var(--echec-fond);
  box-shadow: inset 3px 0 0 var(--echec);
}

/* Ce que valent les sous-expressions de la ligne courante. Sous le code, et
   non dans le tableau : elles changent de nature d'une ligne à l'autre, et
   en faire des colonnes en créerait autant qu'il y a de lignes. */
.trace-expressions {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.9rem;
    align-content: start;
  margin-top: 0.6rem;
  font-size: 0.84rem;
  /* Hauteur **fixe**, pas un minimum. Un `while` composé rend quatre
     expressions et cent vingt caractères — deux à trois lignes de pastilles
     selon la largeur. À hauteur libre, le bandeau repousserait les commandes
     à chaque étape, et la place resterait réservée sur les lignes qui n'ont
     rien à dire. Deux lignes tiennent, le reste défile ici. */
  height: 3.6rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.trace-expressions-titre { color: var(--encre-douce); }

.trace-expression {
  display: inline-flex;
  align-items: baseline;
  /* Une valeur va jusqu'à quatre-vingts caractères, un nom d'expression
     autant : sans repli, la pastille élargirait la colonne et ferait
     déborder la modale entière. */
  flex-wrap: wrap;
  max-width: 100%;
  overflow-wrap: anywhere;
  gap: 0.35rem;
  padding: 0.1rem 0.45rem;
  background: var(--fond);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
}

.trace-expression code {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  background: none;
  padding: 0;
}

.trace-expression-egal { color: var(--discret); }
.trace-expression-valeur { font-weight: 600; }
.trace-expression-valeur.trace-vrai { color: var(--succes); }
.trace-expression-valeur.trace-faux { color: var(--echec); }

.trace-onglets {
  display: flex;
  flex: none;
  gap: 0.3rem;
  margin-bottom: 0.6rem;
  border-bottom: 1px solid var(--bordure);
}

.trace-onglets button {
  padding: 0.35rem 0.7rem;
  font: inherit;
  font-size: 0.86rem;
  color: var(--encre-douce);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
}

.trace-onglets button[aria-selected="true"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}

/* Le cadre occupe tout ce que la modale lui laisse, et ne bouge plus : le
   tableau part d'une seule rangée et se remplit à mesure, mais c'est son
   contenu qui grandit, pas son cadre. */
.trace-vue { flex: 1; min-height: 6rem; overflow: auto; }

/* Le tableau de suivi : une ligne par étape, une colonne par variable. */
.trace-suivi {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.84rem;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
}

/* Les en-têtes portent des noms de variables Python : la mise en majuscules
   du style général des tableaux les rendrait faux — `nombres` n'est pas
   `NOMBRES`, et c'est la casse écrite par l'élève qu'il vient relire. Même
   raison que pour les tableaux de saisie, plus haut. */
.trace-suivi th,
.trace-variables th {
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.84rem;
  color: var(--encre);
}

/* L'en-tête reste visible : sur trois cents étapes on perd sinon de vue
   quelle colonne est quelle variable. */
.trace-suivi thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--carte);
  border-bottom: 1px solid var(--bordure);
  text-align: left;
  padding: 0.3rem 0.5rem;
  white-space: nowrap;
}

.trace-suivi td {
  padding: 0.22rem 0.5rem;
  border-bottom: 1px solid var(--bordure);
  white-space: nowrap;
  max-width: 14rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

.trace-suivi tbody tr { cursor: pointer; }
.trace-suivi tbody tr:hover td { background: var(--fond); }

.trace-suivi tr.trace-rangee-courante td {
  background: var(--accent-fond);
  font-weight: 600;
}

/* Répéter la valeur rend le tableau lisible de haut en bas ; l'atténuer
   quand elle n'a pas bougé fait ressortir ce qui change — la seule chose
   qu'on y cherche. */
.trace-suivi td.trace-inchange { color: var(--discret); }
.trace-suivi tr.trace-rangee-courante td.trace-inchange { color: var(--encre-douce); }

.trace-colonne-ligne { color: var(--discret); text-align: right; width: 3.4rem; }

/* La dernière ligne d'une fonction apparaît deux fois : avant de s'exécuter,
   puis en rendant sa valeur. Les deux rangées disent des choses différentes,
   mais rien ne le montrait — on n'y lisait qu'une répétition. */
.trace-suivi tr.trace-rangee-retour td { border-top: 1px solid var(--succes); }
.trace-suivi tr.trace-rangee-retour .trace-colonne-ligne { color: var(--succes); }
.trace-suivi tr.trace-rangee-exception td { border-top: 1px solid var(--echec); }
.trace-suivi tr.trace-rangee-exception .trace-colonne-ligne { color: var(--echec); }

.trace-frame {
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  padding: 0.5rem 0.75rem;
  margin-bottom: 0.6rem;
}

/* La frame du sommet est celle où l'on se trouve ; les autres disent d'où
   l'on vient, et doivent se lire comme un arrière-plan. */
.trace-frame-active { border-color: var(--accent-bordure); background: var(--accent-fond); }
.trace-frame:not(.trace-frame-active) { opacity: 0.7; }

.trace-frame-titre {
  display: flex;
  justify-content: space-between;
  gap: 0.6rem;
  margin: 0 0 0.4rem;
  font-size: 0.86rem;
}

.trace-frame-nom {
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
}
.trace-frame-ligne { color: var(--encre-douce); font-weight: normal; }

.trace-variables { width: 100%; border-collapse: collapse; font-size: 0.86rem; }

.trace-variables th,
.trace-variables td {
  text-align: left;
  padding: 0.2rem 0.4rem;
  vertical-align: top;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  overflow-wrap: anywhere;
}

.trace-variables th { font-weight: 600; width: 30%; }
.trace-variables td { color: var(--encre-douce); }

.trace-variables tr.trace-change td,
.trace-variables tr.trace-change th {
  background: var(--attente-fond);
  color: var(--encre);
}

.trace-vide { margin: 0; font-size: 0.85rem; color: var(--discret); }

.trace-retour,
.trace-exception {
  margin: 0.5rem 0 0;
  padding: 0.4rem 0.6rem;
  border-radius: var(--rayon);
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.86rem;
}

.trace-retour { background: var(--succes-fond); color: var(--succes); }
.trace-exception { background: var(--echec-fond); color: var(--echec); }

.trace-commandes {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.trace-curseur { display: flex; align-items: center; gap: 0.75rem; }
.trace-curseur input[type="range"] { flex: 1 1 auto; min-width: 0; }

.trace-boutons { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }

.trace-commandes .bouton { padding: 0.35rem 0.8rem; font-size: 0.88rem; }
.trace-commandes .bouton:disabled { opacity: 0.45; cursor: default; }

.trace-position {
  font-size: 0.85rem;
  color: var(--encre-douce);
  font-variant-numeric: tabular-nums;
}

.trace-clavier { margin-top: 0.4rem; }
.trace-alerte { color: var(--attente); }

/* La sortie du cas déroulé, en tête de la colonne de droite. Sa hauteur est
   fixée, et c'est tout l'intérêt de la placer là : le tableau de suivi gagne
   une rangée par étape, et ne doit pas la faire descendre pendant qu'on
   clique sur « Suivant ». `flex: none` la met hors du partage de hauteur —
   sans lui, elle se serrerait à mesure que le tableau réclame de la place. */
.trace-sortie { flex: none; margin-bottom: 0.7rem; }

.trace-sortie-titre {
  margin: 0 0 0.3rem;
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--encre-douce);
}

.trace-sortie .sortie-console {
  margin-top: 0;
  height: 6rem;
  overflow: auto;
  font-size: 0.82rem;
}

/* Une page sombre à l'impression, c'est une cartouche entière pour un fond
   qui n'aide personne, et un texte clair devenu illisible sur du papier
   blanc. Les cartons de codes d'accès sont faits pour être découpés : le
   thème clair est le seul qui vaille ici, quel que soit celui de l'écran. */
@media print {
  :root {
    color-scheme: light;
    --fond: #ffffff;
    --carte: #ffffff;
    --encre: #1f2933;
    --encre-douce: #52606d;
    --discret: #6b7684;
    --bordure: #b6bec7;
    --sur-accent: #ffffff;
    --ombre: none;
    --ombre-forte: none;
    --ombre-connexion: none;
  }
  .bandeau, .barre-actions, .messages { display: none; }
  body { background: var(--fond); }
  .carton { break-inside: avoid; }
}

/* --- Échange avec l'IA, après le dernier indice --------------------------- */

/* Même montage que la modale du déroulé, et pour la même raison : le fil des
   messages gagne un bloc à chaque échange. Sans une hauteur définie de bout en
   bout, la fenêtre grandirait sous le curseur et la zone de saisie
   descendrait pendant qu'on écrit dedans. */
.modale-echange {
  width: min(90rem, calc(100vw - 2rem));
  max-width: none;
  margin: 2rem auto auto;
  padding: 0;
  border: none;
  border-radius: var(--rayon-large);
  box-shadow: var(--ombre-forte);
  background: var(--carte);
  color: var(--encre);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  height: calc(100vh - 4rem);
  height: calc(100dvh - 4rem);
}

.modale-echange::backdrop { background: var(--voile-fort); }
.modale-echange:not([open]) { display: none; }

/* `min-height: 0` sur chaque maillon : sans lui, un enfant de flex refuse de
   descendre sous la taille de son contenu et toute la chaîne déborde. */
.modale-echange > div,
.echange-enveloppe {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.echange-entete {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem 1.25rem;
  border-bottom: 1px solid var(--bordure);
}

.echange-titre { margin: 0; font-size: 1.05rem; }

/* Deux colonnes : ce dont on parle à gauche, ce qu'on en dit à droite — plus
   la glissière entre les deux, qui porte elle-même le trait de séparation
   (`.echange-glissiere::before`, donc plus de `border-right` ici).

   Le premier partage, 5/12 - 7/12, est celui d'avant la glissière : elle ne
   change qu'au premier geste de l'élève. `--echange-colonne-gauche` est posée
   par `modale-echange.js` sur la modale, jamais ici — voir le commentaire du
   gabarit. `minmax(14rem, …)` protège les deux bords qu'un glissement trop
   franc, ou un `var()` qu'un vieux navigateur ignorerait, pourrait sinon
   réduire à rien. */
.echange-corps {
  display: grid;
  grid-template-columns:
    minmax(14rem, var(--echange-colonne-gauche, 41.7%))
    auto
    minmax(14rem, 1fr);
  flex: 1;
  min-height: 0;
}

.echange-contexte {
  min-height: 0;
  overflow-y: auto;
  padding: 1rem 1.25rem;
  background: var(--fond);
}

/* Une poignée de quelques pixels, avec une zone de prise plus large que le
   trait visible : sur un doigt ou une souris imprécise, viser 1 px est un
   geste raté d'avance. `touch-action: none` empêche le geste de faire
   défiler la page derrière, sur les écrans tactiles. */
.echange-glissiere {
  width: 9px;
  cursor: col-resize;
  position: relative;
  touch-action: none;
}

.echange-glissiere::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: var(--bordure);
  transform: translateX(-50%);
  transition: background 0.1s ease, width 0.1s ease;
}

.echange-glissiere:hover::before,
.echange-glissiere:focus-visible::before,
.echange-glissiere.echange-glissiere-active::before {
  background: var(--accent);
  width: 2px;
}

.echange-glissiere:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Pendant le glissement : le curseur ne doit pas changer au gré des éléments
   survolés — un texte, un bouton — sans quoi la poignée semble « lâcher » à
   chaque fois que le pointeur passe dessus. Et rien ne doit se sélectionner
   sur un mouvement rapide. */
body.echange-redimensionnement {
  cursor: col-resize;
  user-select: none;
}

.echange-bloc + .echange-bloc { margin-top: 1.25rem; }

.echange-bloc-titre {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--encre-douce);
}

.echange-contexte .console-python { margin: 0; }
.echange-contexte .indice + .indice { margin-top: 0.6rem; }

.echange-discussion {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* En tête de la colonne de contexte, comme un `.echange-bloc` de plus : la
   marge qui la sépare du bloc suivant vient donc de `.echange-bloc +
   .echange-bloc`, pas d'une règle à elle. Un fond et un cadre la distinguent
   quand même — c'est la pièce qu'on vient comparer au reste.

   Repliable, et dépliée par défaut : une fois lue, la replier rend la place
   au code et aux indices qui suivent dans la même colonne, sans qu'elle
   continue à les repousser plus bas. */
.echange-solution {
  background: var(--accent-fond);
  border: 1px solid var(--accent-bordure);
  border-radius: var(--rayon);
  padding: 0.75rem 0.9rem;
}

.echange-solution > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

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

/* Le chevron dit que le bloc se replie : sans lui, on ne cherche pas à
   cliquer sur un titre. */
.echange-solution > summary::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}

.echange-solution[open] > summary::before { transform: rotate(-135deg); }

.echange-solution-titre { margin: 0; font-size: 0.95rem; font-weight: 600; display: inline; }

/* Un plafond, et non une hauteur libre : une solution de cent lignes ne doit
   pas repousser le code et les indices hors du premier écran. Elle défile
   pour elle-même, plutôt que d'entraîner toute la colonne. */
.echange-solution-corps {
  margin-top: 0.6rem;
  max-height: 16rem;
  overflow-y: auto;
}
.echange-solution-corps .console-python { margin: 0; }

/* Le fil absorbe le reste et défile seul : c'est lui qui grandit, et lui seul
   qui doit bouger. */
.echange-fil {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

/* `min(46rem, 100%)` et non `46rem` seul. Une bulle est un élément flex
   aligné sur un bord : sa largeur est celle de son contenu, ramenée à la
   place disponible — sauf que « ramenée » s'arrête au contenu minimum, et le
   contenu minimum d'un bloc de code est sa plus longue ligne. Une bulle de
   848 px dans un fil de 704 px, et c'est la conversation entière qui défile
   latéralement, à l'instant où l'élève lit l'explication qu'il attendait.
   Seul un `max-width` passe outre ce plancher ; le `100%` le rend au fil.

   Mesuré dans Chrome sur la réponse type — code commenté, une ligne de
   88 caractères : le fil débordait de 72 px à 1280 px de fenêtre et de 222 px
   à 1024 px, largeur d'un portable de lycée. Au-delà de 46rem de fil,
   c'est-à-dire au-dessus de ~1400 px, le défaut ne se voyait pas : d'où le
   soin à remesurer toute retouche d'ici en fenêtre étroite.

   `min()` couvre aussi le petit écran, où la bulle prenait toute la largeur
   par une règle à part sous 40rem. `overflow-wrap` est le pendant du
   `overflow-x` des blocs de code : ce qui ne défile pas déborderait sinon
   pour un identifiant trop long cité au fil d'une phrase, et le fil se
   remettrait à glisser par cette porte-là. */
.echange-message {
  max-width: min(46rem, 100%);
  overflow-wrap: break-word;
}

.echange-message p { margin: 0.2rem 0; }

.echange-qui {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--encre-douce);
  margin-bottom: 0.2rem;
}

.echange-eleve {
  align-self: flex-end;
  background: var(--accent-fond);
  border: 1px solid var(--accent-bordure);
  border-radius: var(--rayon);
  padding: 0.6rem 0.85rem;
}

.echange-eleve .echange-qui { text-align: right; }

.echange-assistant {
  align-self: flex-start;
  background: var(--fond);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon);
  padding: 0.6rem 0.85rem;
}

/* Le modèle écrit du code dès que la solution est révélée, et une ligne
   commentée dépasse vite la bulle. Elle défile pour elle-même — mais cela ne
   vaut que parce que `.echange-message` est borné à la largeur du fil : un
   `max-width: 100%` sur un parent qui, lui, s'est élargi jusqu'à la ligne ne
   borne rien du tout. Les deux règles ne se séparent pas. */
.echange-assistant pre {
  overflow-x: auto;
  max-width: 100%;
}

.echange-attente { color: var(--encre-douce); }
.echange-alerte { color: var(--attente); }

.echange-saisie {
  display: flex;
  align-items: flex-end;
  gap: 0.6rem;
  padding: 0.9rem 1.25rem 0.4rem;
  border-top: 1px solid var(--bordure);
}

.echange-saisie textarea {
  flex: 1;
  resize: none;
  font-family: inherit;
}

/* L'étiquette n'est là que pour les lecteurs d'écran : le champ porte déjà
   son invite, et un libellé visible ferait une ligne de plus au-dessus de la
   saisie. */
.echange-etiquette {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.echange-compte {
  margin: 0;
  padding: 0 1.25rem 0.9rem;
}

/* Le bouton qui ouvre l'aide finale se distingue du reste de la barre : c'est
   la porte de sortie quand plus rien d'autre n'a marché. */
.bouton-aide-finale {
  border-color: var(--accent-bordure);
  background: var(--accent-fond);
}

/* Sous cette largeur les deux colonnes ne tiennent plus côte à côte : le
   contexte passe au-dessus, en hauteur bornée.

   En flex et non en grille, et en `vh` et non en pourcentage. Un
   `max-height: 30%` sur une rangée dimensionnée par son contenu se définit
   par elle-même : la rangée du contexte s'était réservé près de 600 px, et le
   fil de discussion tombait à 32 px — deux lignes de conversation, sous une
   colonne de code qu'on n'avait pas demandé de lire en entier. */
@media (max-width: 60rem) {
  .echange-corps {
    display: flex;
    flex-direction: column;
  }
  .echange-contexte {
    flex: 0 0 auto;
    max-height: 34vh;
    border-bottom: 1px solid var(--bordure);
  }
  /* Redimensionner horizontalement deux blocs empilés verticalement ne
     voudrait rien dire : la glissière disparaît avec les colonnes. */
  .echange-glissiere { display: none; }
  .echange-discussion {
    flex: 1;
    min-height: 0;
  }
}

@media (max-width: 40rem) {
  .modale-echange {
    width: 100vw;
    margin: 0;
    border-radius: 0;
    height: 100vh;
    height: 100dvh;
  }
}

/* --- Comptes enseignant ------------------------------------------------- */

/* Ce qui sert aux pages de gestion des comptes : liste des enseignants,
   première connexion, réinitialisation. Peu de règles — ces pages reprennent
   `.carte`, `.champ` et `.tableau-cadre` du reste de l'application. */

/* L'invitation à réclamer un compte, sous le formulaire de connexion. Plus
   discrète que le formulaire, mais pas au point de passer inaperçue : c'est la
   seule indication qu'il existe un moyen d'entrer sans compte. */
.mention-contact {
  margin: 1.25rem 0 0;
  font-size: 0.9rem;
  color: var(--encre-douce);
  text-align: center;
}

/* La colonne d'actions d'un tableau ne s'étire pas : sans cette largeur, elle
   prend ce que les autres colonnes lui laissent et le bouton flotte au milieu
   d'un vide. Même correctif que la ligne d'exercice de la liste élève. */
.colonne-action { width: 9rem; white-space: nowrap; }

/* Le lien de réinitialisation : un champ en lecture seule, parce qu'on vient
   le copier et non le suivre. Monospace et pleine largeur — une URL signée
   fait deux cents caractères, et coupée au milieu elle ne se vérifie pas. */
.etiquette-champ {
  display: block;
  font-weight: 600;
  font-size: 0.9rem;
  margin-bottom: 0.35rem;
}

.champ-lien {
  width: 100%;
  font-family: ui-monospace, "Cascadia Code", monospace;
  font-variant-ligatures: none;
  font-size: 0.84rem;
  background: var(--fond);
}

/* Les règles de mot de passe arrivent en `<ul>` dans le `help_text` de Django,
   donc à l'intérieur du bloc d'aide et non à sa place : la règle `ul.aide` ne
   les atteint pas. */
.aide-regles ul { margin: 0.3rem 0; padding-left: 1.3rem; }
.aide-regles li { margin: 0.1rem 0; }

/* La barre du générateur de mot de passe, posée par `mot-de-passe.js` sous le
   champ de mot de passe provisoire. Elle existe aussi dans l'administration
   Django, avec sa propre feuille (`admin-mot-de-passe.css`) : le script est
   partagé, l'habillage non — une barre doit ressembler à la page où elle est. */
.barre-mot-de-passe {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

/* Le compte rendu (« Engendré », « Copié »…). Il passe à la ligne plutôt que
   d'étirer la barre : le message qui explique une copie refusée fait deux
   lignes, et les boutons ne doivent pas se déplacer pour autant. */
.barre-mot-de-passe .aide {
  flex: 1 1 100%;
  margin-top: 0;
}
