/* ------------------------------------------------------------------------
   Thème « Ardoise & Abricot ».

   Chaque couleur est déclarée une fois en canaux (« 27 31 36 ») pour que
   Tailwind puisse y appliquer une opacité, et la variable lisible qui suit en
   est dérivée : « var(--accent) » reste utilisable dans du CSS ordinaire sans
   qu'il existe deux sources de vérité à tenir synchronisées.

   Répartition des rôles : l'ardoise porte l'interface (texte, surfaces,
   bordures) ; l'abricot est réservé aux points d'accroche — marque, bouton
   principal, élément de navigation actif, anneau de focus, bulle du chatbot.
   ------------------------------------------------------------------------ */

:root {
  /* Polices du système, sans rien télécharger : une police distante est un
     appel réseau de plus sur la connexion d'un élève, et le texte changerait
     de dessin selon qu'elle arrive ou non. */
  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-code: ui-monospace, "Cascadia Code", Consolas, monospace;

  --bg-rgb: 250 248 245;
  --panel-rgb: 255 255 255;
  --column-rgb: 240 237 232;
  --ink-rgb: 27 31 36;
  --muted-rgb: 103 110 120;

  /* L'abricot foncé : 4,6:1 sur le fond clair, il passe donc AA en texte.
     L'abricot vif de la version sombre ne le passerait pas ici. */
  --accent-rgb: 194 99 45;
  --accent-ink-rgb: 141 70 30;
  --accent-soft-rgb: 250 236 226;
  --accent-hover-rgb: 165 83 36;
  /* Encre posée SUR un aplat d'accent (bouton principal). */
  --sur-accent-rgb: 255 250 246;

  --border-rgb: 231 226 219;
  --border-strong-rgb: 199 192 182;

  --danger-rgb: 193 51 51;
  --danger-ink-rgb: 158 40 40;
  --danger-soft-rgb: 252 234 234;
  --success-rgb: 42 122 88;
  --success-ink-rgb: 30 90 65;
  --success-soft-rgb: 229 242 235;
  --warning-rgb: 146 96 12;
  --warning-ink-rgb: 112 74 9;
  --warning-soft-rgb: 250 241 224;

  --radius: 10px;
  --radius-large: 14px;
  --shadow: 0 1px 2px rgb(27 31 36 / .05), 0 1px 3px rgb(27 31 36 / .07);
  --shadow-forte: 0 4px 14px rgb(27 31 36 / .10), 0 1px 3px rgb(27 31 36 / .06);
  --ring: 0 0 0 3px rgb(var(--accent-rgb) / .40);
}

[data-theme="nuit"] {
  --bg-rgb: 21 24 28;
  --panel-rgb: 29 33 38;
  --column-rgb: 37 42 48;
  --ink-rgb: 236 238 241;
  --muted-rgb: 154 161 172;

  /* L'abricot s'éclaircit en nuit : le ton foncé du mode clair tomberait à
     3:1 sur l'ardoise et deviendrait illisible en texte. */
  --accent-rgb: 242 149 92;
  --accent-ink-rgb: 246 173 128;
  --accent-soft-rgb: 52 38 29;
  --accent-hover-rgb: 248 172 124;
  --sur-accent-rgb: 26 18 12;

  --border-rgb: 47 53 61;
  --border-strong-rgb: 72 80 90;

  --danger-rgb: 248 129 129;
  --danger-ink-rgb: 250 154 154;
  --danger-soft-rgb: 56 28 28;
  --success-rgb: 108 205 158;
  --success-ink-rgb: 134 216 175;
  --success-soft-rgb: 20 45 35;
  --warning-rgb: 229 178 96;
  --warning-ink-rgb: 236 194 129;
  --warning-soft-rgb: 51 40 18;

  --shadow: 0 1px 3px rgb(0 0 0 / .45);
  --shadow-forte: 0 4px 16px rgb(0 0 0 / .55);
  --ring: 0 0 0 3px rgb(var(--accent-rgb) / .35);
}

/* Formes lisibles des mêmes couleurs, pour le CSS écrit à la main. */
:root, [data-theme="nuit"] {
  --bg: rgb(var(--bg-rgb));
  --panel: rgb(var(--panel-rgb));
  --column: rgb(var(--column-rgb));
  --ink: rgb(var(--ink-rgb));
  --muted: rgb(var(--muted-rgb));
  --accent: rgb(var(--accent-rgb));
  --accent-ink: rgb(var(--accent-ink-rgb));
  --accent-soft: rgb(var(--accent-soft-rgb));
  --accent-hover: rgb(var(--accent-hover-rgb));
  --sur-accent: rgb(var(--sur-accent-rgb));
  --border: rgb(var(--border-rgb));
  --border-strong: rgb(var(--border-strong-rgb));
  --danger: rgb(var(--danger-rgb));
  --danger-ink: rgb(var(--danger-ink-rgb));
  --danger-soft: rgb(var(--danger-soft-rgb));
  --success: rgb(var(--success-rgb));
  --success-ink: rgb(var(--success-ink-rgb));
  --success-soft: rgb(var(--success-soft-rgb));
  --warning: rgb(var(--warning-rgb));
  --warning-ink: rgb(var(--warning-ink-rgb));
  --warning-soft: rgb(var(--warning-soft-rgb));
}

body { font-family: var(--font-ui); }
code, kbd, pre, samp { font-family: var(--font-code); }

/* La barre de défilement ne doit pas décaler la page : son emplacement est
   réservé en permanence. Sans cela, une liste filtrée qui devient assez courte
   pour tenir dans la fenêtre fait disparaître l'ascenseur, et tout le contenu
   centré saute latéralement d'une quinzaine de pixels. */
html { scrollbar-gutter: stable; background-color: var(--bg); }

/* Largeur de l'application, définie une seule fois. L'en-tête, les messages et
   le contenu s'alignent parce qu'ils partagent cette classe : trois
   « max-w-* » recopiés dans trois gabarits finissent toujours par diverger. */
.shell {
  width: 100%;
  max-width: 76rem;
  margin-inline: auto;
  padding-inline: 1rem;
}
/* Colonne étroite : la conversation, qui se lit d'un trait. Une bulle de chat
   étirée sur 1 200 px ne se lit plus. */
.shell-etroit { max-width: 46rem; }

/* --- « hidden » veut dire caché -------------------------------------------
   Le seul `!important` du projet, et il en évite une classe entière.

   Tailwind écrit `[hidden]{display:none}` dans son preflight, avec la
   spécificité d'un sélecteur d'attribut — exactement celle d'une classe. Toute
   règle d'affichage à une classe fait donc jeu égal avec lui, et c'est l'ordre
   des feuilles qui départage : `.flex`, écrit plus loin dans le MÊME fichier
   que le preflight, l'emporte toujours ; `.btn`, écrit dans style.css,
   l'emporte en production (chargé après tailwind.css) et perd en développement
   (le CDN s'injecte à l'exécution, donc après).

   Autrement dit, un bloc rendu `hidden` par le serveur pouvait s'afficher
   quand même, et pas de la même façon en local et en ligne. Mesuré : un
   `class="flex" hidden` calcule `display: flex`, un `class="btn" hidden`
   calcule `inline-flex`. C'est ce qui rendait visibles sans JavaScript les
   commandes de `_trame.html`, que le gabarit écrit pourtant `hidden`
   précisément pour qu'elles n'apparaissent que si `trame.js` les prend en
   charge — des boutons qui n'agissent sur rien.

   `hidden` est une déclaration de l'HTML, pas un choix de mise en forme : il
   n'y a rien à arbitrer, et cette ligne remplace les rustines au cas par cas
   (`.voile[hidden]` en était une). Les scripts qui révèlent un bloc retirent
   l'attribut, ce que `!important` ne gêne en rien. */
[hidden] { display: none !important; }

/* --- Encoches et barre d'accueil ------------------------------------------
   Installée sur un téléphone, l'application occupe l'écran entier : plus de
   barre de navigateur pour la tenir à l'écart de l'encoche et de la barre
   d'accueil. « viewport-fit=cover » (cf. base.html) est ce qui donne accès aux
   marges sûres, et il faut alors les reprendre soi-même.

   Les marges latérales sont posées sur « body » et non sur « .shell » :
   l'en-tête collant d'une séance annule la gouttière de « .shell » par un
   « -mx-4 » pour que son fond aille d'un bord à l'autre, et le décaler ici
   laisserait une bande de fond de page le long de l'encoche. Elles valent zéro
   partout ailleurs qu'en paysage sur un téléphone encoché.

   La marge basse, elle, va au pied de page — le dernier élément de la colonne.
   Il porte « pt-6 » et non « py-6 » : Tailwind écrirait sinon la même
   déclaration que celle d'en dessous, et laquelle l'emporte dépendrait de
   l'ordre des feuilles, qui n'est pas le même en développement et en
   production. */
body {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
.pied { padding-bottom: calc(1.5rem + env(safe-area-inset-bottom)); }

/* Le focus se voit, et il se voit en abricot. */
a:focus-visible, button:focus-visible, summary:focus-visible,
[role="button"]:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: 0.375rem;
}

/* --- Les pièces de la charte -------------------------------------------- */

.carte {
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-large);
  box-shadow: var(--shadow);
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  border-radius: 0.5rem;
  border: 1px solid transparent;
  padding: 0.55rem 1rem;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  background-color: var(--panel);
  color: var(--ink);
  text-decoration: none;
}
.btn:disabled, .btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }

/* Bouton principal : l'aplat abricot est la signature du thème, et il est
   réservé à UNE action par écran. */
.btn-principal { background-color: var(--accent); color: var(--sur-accent); }
.btn-principal:hover:not(:disabled) { background-color: var(--accent-hover); }

.btn-neutre { border-color: var(--border); }
.btn-neutre:hover:not(:disabled) { border-color: var(--border-strong); background-color: var(--column); }

.btn-danger { border-color: var(--danger); color: var(--danger-ink); background-color: var(--danger-soft); }

.btn-petit { padding: 0.3rem 0.6rem; font-size: 0.8125rem; }

/* Élément de navigation. Inactif il est transparent ; actif il prend l'aplat
   d'accent. */
.nav-item {
  padding: 0.35rem 0.7rem;
  border-radius: 0.5rem;
  color: var(--ink);
  white-space: nowrap;
  text-decoration: none;
}
.nav-item:hover { background-color: var(--column); }
.nav-item-actif, .nav-item-actif:hover {
  background-color: var(--accent);
  color: var(--sur-accent);
  font-weight: 600;
}

/* Liens de contenu : encre d'accent, souligné abricot. Le trait est lisible là
   où un texte abricot clair ne le serait pas — c'est la façon de faire entrer
   la couleur dans du texte courant sans perdre le contraste. */
.lien-charte {
  color: var(--accent-ink);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

.pastille {
  display: inline-block;
  border-radius: 999px;
  padding: 0.15rem 0.55rem;
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid transparent;
}
.pastille-neutre { background-color: var(--column); color: var(--muted); }
.pastille-accent { background-color: var(--accent-soft); color: var(--accent-ink); border-color: var(--accent); }
.pastille-succes { background-color: var(--success-soft); color: var(--success-ink); }
.pastille-attente { background-color: var(--warning-soft); color: var(--warning-ink); }
.pastille-echec { background-color: var(--danger-soft); color: var(--danger-ink); }

.chiffre-cle {
  color: var(--ink);
  font-weight: 800;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.progression {
  height: 0.4rem;
  border-radius: 999px;
  background-color: var(--column);
  overflow: hidden;
}
.progression > span {
  display: block;
  height: 100%;
  border-radius: 999px;
  background-color: var(--accent);
  transition: width .25s ease;
}

/* --- Jauge de trame : une case par notion, la couleur dit le motif ------- */

/* Un pourcentage unique confond « compris » et « sauté ». Ici l'avancement se
   lit à la longueur remplie, la maîtrise à la couleur : une notion acquise est
   en aplat, une notion traversée sans être comprise reste en creux. Sauter fait
   donc avancer sans remplir — c'est un frein plus honnête qu'un bouton grisé,
   et l'élève voit d'un coup d'œil ce qu'il lui reste à revoir. */
.jauge {
  display: flex;
  gap: 2px;
  height: 0.4rem;
}
.jauge-case {
  flex: 1;
  border-radius: 2px;
  background-color: var(--column);
  transition: background-color .25s ease;
}
.jauge-case:first-child { border-top-left-radius: 999px; border-bottom-left-radius: 999px; }
.jauge-case:last-child { border-top-right-radius: 999px; border-bottom-right-radius: 999px; }

.jauge-acquise { background-color: var(--accent); }

/* À consolider : l'élève a fini par trouver, mais après plusieurs tours. La
   case est donc à moitié pleine, littéralement — un aplat d'accent sur la
   moitié basse, le creux au-dessus.

   Le demi-plein se lit d'emblée comme « presque », là où les hachures disent
   « on est passé outre » et le contour « c'était un choix ». C'est la seule
   variation qui restait dans une famille de couleurs déjà prise trois fois :
   une teinte intermédiaire aurait exigé de distinguer deux abricots sur une
   case de 0,4 rem de haut, ce que personne ne fait. La coupe franche
   (`0 50%`, `50%`) et non un dégradé : à cette hauteur, un dégradé se lit
   comme une couleur unie plus pâle. */
.jauge-consolider {
  background-color: var(--accent-soft);
  background-image: linear-gradient(
    to top,
    rgb(var(--accent-rgb)) 0 50%,
    transparent 50%
  );
}

/* Passée après blocage : l'abricot en filet, le fond reste creux. La rayure
   dit « on est passé dessus » sans prétendre que c'est acquis. */
.jauge-bloquee {
  background-color: var(--accent-soft);
  background-image: repeating-linear-gradient(
    -45deg,
    rgb(var(--accent-rgb) / .55) 0 2px,
    transparent 2px 4px
  );
}
/* Passée par l'élève : même creux, sans rayure — c'est un choix, pas un échec. */
.jauge-passee {
  background-color: var(--accent-soft);
  box-shadow: inset 0 0 0 1px rgb(var(--accent-rgb) / .45);
}
.jauge-avenir { background-color: var(--column); }

/* La notion en cours est désignée par la **hauteur**, pas par une couleur : les
   trois motifs occupent déjà la teinte, et un liseré de plus se perdait sur une
   case de quatre pixels de haut — essayé, invisible. Le débordement vertical
   est voulu, `.jauge` ne coupe pas. */
.jauge-courante {
  transform: scaleY(2);
  background-color: var(--border-strong);
}
.jauge-courante.jauge-acquise { background-color: var(--accent); }
.jauge-courante.jauge-consolider { background-color: var(--accent-soft); }

/* --- La jauge de répartition (le bilan d'une classe) --------------------- */

/* La jauge segmentée dit le sort de CHAQUE notion pour UN élève, une case par
   notion. Celle-ci dit le sort d'UNE notion pour toute la classe, une bande
   par sort, large de sa part. Une case par élève ne tenait pas ici : à trente
   élèves les cases descendent sous le pixel, et personne ne compte trente
   cases du regard — ce qu'on lit sur une classe, c'est une proportion.

   Les couleurs restent celles des `.jauge-*` : un carré, une case et une bande
   doivent dire la même chose. La hauteur, elle, monte à 1,25 rem parce que les
   bandes portent leur pourcentage. */
.repartition {
  display: flex;
  height: 1.25rem;
  border-radius: 999px;
  overflow: hidden;
  /* Le fond vaut « pas abordée » : une somme d'arrondis à 99 % laisse voir un
     cheveu de fond, qui dit alors la bonne chose plutôt qu'un trou blanc. */
  background-color: var(--column);
}
.repartition-part {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  color: var(--accent-ink);
  white-space: nowrap;
  /* La largeur est posée dans le gabarit, en pourcentage : elle EST la donnée.
     Sans ce `flex: none`, flex la renégocierait et les bandes ne diraient plus
     la proportion qu'elles portent. */
  flex: none;
}

/* Trois grounds différents sous le même chiffre, donc trois encres.

   Sur l'aplat d'accent, celle qui est faite pour ça : `--sur-accent` bascule
   avec le thème et reste lisible des deux côtés — clair sur l'abricot foncé du
   mode jour, sombre sur l'abricot vif de la nuit. L'encre du texte ordinaire,
   elle, n'y passerait pas le contraste. */
.repartition-part.jauge-acquise { color: rgb(var(--sur-accent-rgb)); }
.repartition-part.jauge-avenir { color: var(--muted); }

/* « À consolider » perd ici son demi-plein, et c'est mesuré, pas esthétique.
   Sur une case de jauge, muette, la moitié pleine se lit d'un coup ; sur une
   bande de 1,25 rem qui porte son pourcentage, le texte traverse la coupe et
   tombe donc à la fois sur l'accent plein et sur le creux. Aucune encre ne
   passe les deux : la meilleure tenait 4,03:1 en clair et **1,96:1 en nuit**.

   D'où l'aplat, à 45 % d'accent sur le creux. L'encre ordinaire y tient
   8,4:1 en clair et 5,1:1 en nuit — la même encre que la bande hachurée, pour
   la même raison. Un aplat plus dense (55 %) retombe à 4,2:1 en nuit, un plus
   clair (35 %) se confond avec le creux voisin.

   Le carré, la case et la bande disent toujours la même chose : même famille,
   même position entre l'acquis et le subi. C'est le seul endroit où la forme
   change, parce que c'est le seul où il y a du texte par-dessus. */
.repartition-part.jauge-consolider {
  background-color: var(--accent-soft);
  background-image: linear-gradient(
    rgb(var(--accent-rgb) / .45) 0 100%
  );
  color: var(--ink);
}

/* La bande hachurée est le cas dur : les rayures courent SOUS le texte, donc
   une même encre tombe tantôt sur la rayure, tantôt sur le fond, et il faut
   qu'elle passe sur les deux. Contrastes mesurés sur la rayure — l'encre
   d'accent y tombe à 2,6:1 en nuit (l'abricot clair sur des rayures claires),
   l'encre ordinaire tient 7,5:1 en clair et 4,1:1 en nuit. C'est donc elle,
   et c'est le seul endroit du site où un chiffre posé sur l'accent ne prend
   pas une encre de la famille : ici il ne s'agit pas d'accorder une couleur
   mais de rester lisible sur un motif.

   Le halo achève le travail. Il dégage la rayure autour des glyphes sans
   effacer la hachure ailleurs, et comme il prend la couleur de fond de la
   bande, il bascule tout seul avec le thème. */
.repartition-part.jauge-bloquee {
  color: var(--ink);
  text-shadow:
    0 0 2px var(--accent-soft),
    0 0 3px var(--accent-soft),
    0 0 4px var(--accent-soft);
}

/* Les comptes écrits sous la jauge. Le point médian est posé par la feuille et
   non par le gabarit : les bandes vides ne sont pas rendues, et une boucle de
   gabarit ne sait pas laquelle est la dernière VISIBLE — elle mettrait un
   séparateur en fin de ligne un tableau sur deux. `:last-child` le sait, lui,
   puisque seules les bandes non vides sont écrites.

   Il est posé APRÈS et non avant : la ligne se replie sur les colonnes
   étroites, et un séparateur en tête de ligne repliée se lit comme une puce
   là où, en fin de ligne, il annonce la suite. */
.comptes { display: flex; flex-wrap: wrap; column-gap: 0.4rem; }
.comptes > span:not(:last-child)::after {
  content: " ·";
  color: var(--border-strong);
}

/* Un témoin de motif isolé — la puce d'une liste de notions, une légende.
   `.jauge-case` ne convient pas hors d'une jauge : son `flex: 1` l'étire sur
   toute la ligne, ce qui désalignait les intitulés du parcours de trame. Les
   couleurs, elles, restent celles des `.jauge-*` ci-dessus : un carré et une
   case de jauge doivent dire la même chose. */
.temoin {
  flex: none;
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 3px;
  vertical-align: -1px;
}

/* --- L'avis de l'élève sur sa séance ------------------------------------- */

/* Django rend un groupe de cases ou de boutons radio en `<div>` imbriqués —
   un conteneur portant l'`id` du champ, puis un `<div>` par option. **Pas en
   `<ul>`** : il l'a fait jusqu'à la version 4, et une feuille écrite sur cette
   idée ne s'applique à rien, sans erreur ni avertissement. Le sélecteur se
   vérifie sur le HTML rendu, il ne se déduit pas du souvenir qu'on a du
   widget.

   La mise en forme vit ici et non en classes Tailwind : le gabarit ne voit pas
   ces `<div>`, c'est le widget qui les produit.

   Les cases s'empilent sur téléphone et se rangent en deux colonnes dès qu'il
   y a la place — huit libellés d'une ligne chacun feraient un écran entier de
   défilement, là où c'est la dernière chose qu'on demande à l'élève. */
.avis-choix > div {
  display: grid;
  gap: 0.4rem 1.25rem;
  grid-template-columns: 1fr;
}
@media (min-width: 40rem) {
  .avis-choix > div { grid-template-columns: 1fr 1fr; }
}

/* Toute la ligne est cliquable, pas seulement la case de 13 px : au doigt,
   viser une case à cocher est le geste le plus raté d'un formulaire. */
.avis-choix label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  cursor: pointer;
  line-height: 1.35;
}
.avis-choix input { flex: none; }

/* L'appréciation — trois boutons radio — se range TOUJOURS en ligne : ce sont
   trois mots courts sur une échelle, et les empiler la perdrait. */
.avis-note > div {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  grid-template-columns: none;
}

/* --- La dernière séance d'un chapitre ------------------------------------ */

/* Sous « Tes séances précédentes », la ligne du haut est la plus récente —
   celle qu'on vient de quitter, et celle qu'on revient presque toujours
   chercher. Un filet abricot suffit à la désigner : la liste est déjà rangée
   de la plus récente à la plus ancienne, le repère ne fait que confirmer où
   l'œil doit tomber.

   La règle est posée APRÈS `.carte` et dans le même fichier : à égalité de
   spécificité, c'est l'ordre qui départage, et deux règles de la même feuille
   se départagent de la même façon en développement et en production. Une
   classe Tailwind `border-orange-500` aurait, elle, dépendu de l'ordre des
   feuilles. */
.seance-derniere { border-color: var(--accent); }

/* --- Les pictogrammes des trois modes ------------------------------------ */

/* Dessinés dans `templates/revision/_picto_mode.html`, sur l'écran où l'élève
   choisit comment réviser un chapitre. Les couleurs sont celles des `.jauge-*`
   et du fil : le pictogramme du mode guidé est la barre segmentée qu'il verra
   pendant sa séance, et la bulle claire est la sienne. Un dessin qui
   inventerait ses teintes annoncerait un écran qui n'existe pas.

   Les classes sont préfixées `picto-` parce qu'elles entrent dans un SVG
   inliné : les noms courts de cette feuille (`.carte`, `.bulle`, `.point`,
   `.anneau`…) y descendent aussi, et `.anneau` est le rouage de chargement —
   les cases d'une figure du mode d'emploi s'étaient mises à tourner sur
   elles-mêmes pour cette raison.

   La taille est posée ici et non en classes Tailwind : deux tailles et un
   point de bascule tiennent en trois lignes, et la feuille compilée n'a rien
   de nouveau à apprendre. */
.picto-mode { flex: none; width: 5rem; height: 2.5rem; }
@media (min-width: 640px) { .picto-mode { width: 6rem; height: 3rem; } }

.picto-acquise { fill: var(--accent); }
.picto-avenir  { fill: var(--column); }
.picto-bulle   { fill: var(--column); }
.picto-bulle-eleve { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1; }
.picto-trait { stroke: var(--border-strong); stroke-width: 2; stroke-linecap: round; }
.picto-coche {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.picto-attente { fill: none; stroke: var(--border-strong); stroke-width: 2; }

/* --- Colonne de navigation (l'explorateur de disciplines) ---------------- */

/* L'élément actif prend l'accroche douce et un filet abricot, pas l'aplat de
   `.nav-item-actif` : l'aplat rendrait illisible la ligne de comptes, qui est
   en gris sous le nom. */
.item-liste {
  display: block;
  padding: 0.45rem 0.65rem;
  border-radius: 0.5rem;
  border-left: 3px solid transparent;
  color: var(--ink);
  text-decoration: none;
}
.item-liste:hover { background-color: var(--column); }
.item-liste-actif, .item-liste-actif:hover {
  background-color: var(--accent-soft);
  border-left-color: var(--accent);
}
.item-liste-actif .item-liste-nom { color: var(--accent-ink); }

/* --- Le rangement de l'enseignant : niveaux, puis matières --------------- */

/* Deux étages, et il faut qu'on les voie **avant** de lire les mots. Trois
   signaux se cumulent, parce qu'aucun ne suffit seul :

     - le niveau porte une bande teintée, la matière non ;
     - le niveau est en gras à la taille du texte, la matière un cran en dessous ;
     - les matières sont en retrait derrière un rail vertical, qui rend le
       rattachement lisible même en balayant la colonne du regard.

   La première version n'avait que la troisième moitié du deuxième signal — un
   `font-bold text-sm` contre un `font-semibold` — et l'on ne distinguait pas
   « Première » de « NSI ». */

.rangement-titre {
  padding: 0.25rem 0.65rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

/* Le filet ne sépare que ce qui se suit : sur le dernier niveau il doublerait
   le bord de la carte. */
.niveau + .niveau { border-top: 1px solid var(--border); }

/* L'en-tête d'un niveau reprend **la grammaire** de celui d'une matière côté
   élève — la bande, le retrait du marqueur natif, l'anneau de focus intérieur —
   par une liste de sélecteurs et non par une copie des valeurs : deux blocs
   identiques divergeraient au premier réglage changé. Seuls les écarts voulus
   sont écrits ici. */
.niveau-nom {
  font-weight: 700;
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Le compte des matières ferme la ligne, poussé à droite : il dit ce que le
   repli cache, ce qui est la seule information qu'on cherche sur un niveau
   fermé. */
.niveau-compte {
  margin-left: auto;
  flex-shrink: 0;
  font-size: 0.75rem;
  color: var(--muted);
}

/* Le rail. C'est lui qui fait le rattachement, et il continue derrière le
   dépliant de réglages : sans cela, « Modifier ce niveau » flotterait entre
   deux niveaux sans qu'on sache auquel il appartient. */
.niveau-corps {
  margin: 0 0 0.35rem 1.15rem;
  padding-left: 0.5rem;
  border-left: 2px solid var(--border);
}

/* Les commandes d'un niveau — crayon, monter, descendre — posées EN SURCOUCHE
   de sa ligne d'en-tête, et non dedans.
   
   C'est la contrainte qui gouverne toute cette structure : l'en-tête est un
   `<summary>`, et un bouton placé dans un `summary` bascule le dépliant en
   même temps qu'il agit — l'HTML n'offre aucun moyen propre de l'en empêcher.
   Les commandes sont donc **sœurs** du `<details>`, ramenées sur sa première
   ligne par un positionnement absolu. Elles restent ainsi cliquables sur un
   niveau replié, ce qu'un placement dans le corps du dépliant interdirait.
   
   Corollaire : `.niveau-entete` réserve à droite la place qu'elles occupent,
   sans quoi un nom long passerait dessous. */
.niveau { position: relative; }

.niveau-actions {
  position: absolute;
  top: 0.15rem;
  right: 0.3rem;
  display: flex;
  align-items: center;
  gap: 0.1rem;
  padding: 0.4rem 0;
}

/* Des icônes, pas des glyphes : « ↑ » et « ✎ » dépendent de la police
   installée, sautent d'une plateforme à l'autre et ne s'alignent sur rien. */
.niveau-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 0.375rem;
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}
.niveau-action:hover { color: var(--accent-ink); background-color: var(--panel); }
.niveau-action:focus-visible { outline: none; box-shadow: var(--ring); }
/* Aux extrémités, la flèche reste RENDUE mais inerte : la faire disparaître
   ferait sauter la position de l'autre d'un niveau à l'autre, et l'on
   cliquerait « descendre » en croyant viser « monter ». */
.niveau-action:disabled { opacity: 0.3; cursor: default; }
.niveau-action:disabled:hover { color: var(--muted); background: none; }
.niveau-action svg { width: 0.875rem; height: 0.875rem; }

/* Ajouter une matière : la seule commande de création de la colonne, et elle
   se perdait en gris parmi les comptes de chapitres. Elle prend l'encre
   d'accent et un « + », ce qui la sort de la liste sans lui donner le poids
   d'un bouton plein. */
.niveau-ajout > summary {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.65rem;
  border-radius: 0.5rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--accent-ink);
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.niveau-ajout > summary::-webkit-details-marker { display: none; }
.niveau-ajout > summary:hover { background-color: var(--accent-soft); }
.niveau-ajout > summary:focus-visible { outline: none; box-shadow: var(--ring); }
.niveau-ajout > summary svg { width: 0.75rem; height: 0.75rem; flex-shrink: 0; }

.niveau-outils {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.5rem 0.65rem 0.65rem;
}

/* Le panneau du crayon : renommer, et supprimer. Il s'ouvre par l'adresse
   (`?renommer=<id>`) et non par un dépliant, pour la même raison que les
   commandes sont hors du `summary` — un champ de saisie posé dans un `summary`
   replierait le niveau au premier clic dedans. */
.niveau-renommage {
  margin: 0.35rem 0 0.5rem;
  padding: 0.6rem 0.65rem;
  border-radius: 0.5rem;
  background-color: var(--column);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

/* --- Matières repliables, et l'ordre de leurs chapitres ------------------ */

/* La liste des chapitres de l'élève range chaque matière dans un `<details>` :
   le repli est natif, il survit à l'absence de JavaScript. `chapitres.js` ne
   fait que s'en souvenir d'une visite à l'autre, et révéler la barre de tri.

   La carte rogne ce qui dépasse de ses coins arrondis, faute de quoi la bande
   teintée de l'en-tête déborderait aux quatre angles. */
.matiere { overflow: hidden; }

.matiere-entete,
.niveau-entete {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1rem;
  background-color: var(--column);
  cursor: pointer;
  user-select: none;
  /* Deux façons de retirer le triangle natif, parce qu'aucune ne suffit
     partout : la propriété standard, et le pseudo-élément de WebKit. */
  list-style: none;
}
.matiere-entete::-webkit-details-marker,
.niveau-entete::-webkit-details-marker { display: none; }

/* L'anneau de focus est posé à l'intérieur : `.matiere` rogne ce qui dépasse,
   et un anneau extérieur serait coupé — le parcours au clavier n'aurait plus
   de repère sur la seule poignée de la page. */
.matiere-entete:focus-visible,
.niveau-entete:focus-visible { outline: none; box-shadow: var(--ring) inset; }

/* Le filet ne sépare que s'il y a quelque chose à séparer : sur une matière
   repliée, il doublerait le bord bas de la carte. */
.matiere[open] > .matiere-entete { border-bottom: 1px solid var(--border); }

/* Le `>` n'est pas décoratif sur le niveau : son corps porte un second
   `<details>` — « Ajouter une matière » —, et un sélecteur descendant ferait
   pivoter un chevron qui n'est pas le sien.

   `.niveau` est l'enveloppe (elle porte les commandes en surcouche), `.niveau-pli`
   le dépliant : les deux ne peuvent pas être le même élément, un positionnement
   absolu n'ayant aucun sens relativement à un `<details>` dont le contenu se
   replie. */
.matiere[open] .chevron { transform: rotate(90deg); }
.niveau-pli[open] > .niveau-entete .chevron { transform: rotate(90deg); }

/* Les écarts du niveau sur la grammaire commune, posés APRÈS elle : à égalité
   de spécificité c'est l'ordre qui départage, et écrits avant ils étaient
   silencieusement écrasés par le `padding` de la règle partagée. Un en-tête de
   niveau est une ligne de colonne, pas la bande pleine largeur d'une carte :
   il porte des coins arrondis et un pas plus serré. */
.niveau-entete {
  border-radius: 0.5rem;
  /* La marge droite est celle des trois commandes en surcouche : sans elle, un
     nom long ou un compte de matières passerait dessous. */
  padding: 0.5rem 5.1rem 0.5rem 0.65rem;
  margin: 0.15rem 0;
}
.niveau-entete:hover { background-color: var(--border); }

/* La barre de tri d'une matière. Elle porte sa mise en ligne ici et non en
   classes Tailwind : rendue `hidden` tant que `chapitres.js` ne l'a pas
   révélée, une classe `flex` y ferait jeu égal avec `[hidden]`. */
.tri-barre {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  border-bottom: 1px solid var(--border);
}

/* L'ordre en vigueur reste enfoncé : sans cela, trois libellés côte à côte ne
   diraient pas lequel des trois décrit la liste qu'on est en train de lire. */
.tri-groupe {
  display: inline-flex;
  border-radius: 0.5rem;
  border: 1px solid var(--border-strong);
  background-color: var(--panel);
  overflow: hidden;
}
/* Le segment sert des `<button>` côté élève — le rangement s'y fait dans la
   page — et des `<a>` côté enseignant, où il se fait côté serveur. D'où le
   `text-decoration`, sans effet sur les premiers et indispensable aux seconds. */
.tri-option {
  font-size: 0.8125rem;
  padding: 0.25rem 0.625rem;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  text-decoration: none;
}
.tri-option + .tri-option { border-left: 1px solid var(--border-strong); }
.tri-option:hover { color: var(--ink); }
.tri-option[aria-pressed="true"] {
  background-color: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 600;
}

/* --- Champs de saisie ---------------------------------------------------- */

/* Le préfixe « body » est nécessaire : le preflight de Tailwind pose
   « padding: 0 » sur input/select/textarea avec la même spécificité, et il est
   injecté après cette feuille. On passe donc à 0,0,2 pour primer.
   « input:not([type]) » est indispensable : les champs écrits sans attribut
   « type » n'entrent dans aucun des sélecteurs typés ci-dessous et se
   retrouveraient sans bordure ni padding. */
body input:not([type]),
body input[type="text"], body input[type="email"], body input[type="password"],
body input[type="search"], body input[type="number"], body input[type="url"],
body input[type="date"], body select, body textarea {
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  padding: 0.55rem 0.75rem;
  background-color: var(--panel);
  color: var(--ink);
  line-height: 1.45;
  width: 100%;
}
body textarea { padding: 0.65rem 0.75rem; }
body input[type="file"] {
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  padding: 0.45rem 0.6rem;
  background-color: var(--panel);
  color: var(--ink);
  font-size: 0.875rem;
  width: 100%;
}
body input:focus, body select:focus, body textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}
body input[type="checkbox"], body input[type="radio"] {
  padding: 0;
  width: auto;
  accent-color: var(--accent);
}
input::placeholder, textarea::placeholder { color: var(--muted); opacity: .75; }

/* Le champ « nombre de notions » de la trame ne porte que deux chiffres : il
   n'a pas à s'étirer sur toute la largeur du formulaire comme le « width: 100% »
   de la règle générale ci-dessus l'y forcerait. Le préfixe « body » aligne la
   spécificité sur celle de cette règle ; venant après, celle-ci l'emporte. */
body input.nombre-court {
  width: 6rem;
}

/* Le code d'accès se lit caractère par caractère : chasse fixe, lettres
   espacées, majuscules imposées à la saisie. */
.champ-code {
  font-family: var(--font-code);
  font-size: 1.5rem;
  letter-spacing: 0.22em;
  text-align: center;
  text-transform: uppercase;
}

/* --- La conversation ----------------------------------------------------- */

.bulle {
  border-radius: var(--radius-large);
  padding: 0.8rem 1rem;
  max-width: 88%;
  line-height: 1.55;
}
/* Le chatbot prend l'accroche douce et s'aligne à gauche ; l'élève prend le
   panneau bordé et s'aligne à droite. La couleur distingue les deux voix plus
   vite qu'une étiquette. */
.bulle-assistant {
  background-color: var(--accent-soft);
  border: 1px solid rgb(var(--accent-rgb) / .35);
  align-self: flex-start;
  border-bottom-left-radius: 0.3rem;
}
.bulle-eleve {
  background-color: var(--panel);
  border: 1px solid var(--border);
  align-self: flex-end;
  border-bottom-right-radius: 0.3rem;
}

/* Les paragraphes d'une bulle : le preflight de Tailwind leur retire toute
   marge, et deux paragraphes se colleraient en un bloc illisible. */
.bulle p + p { margin-top: 0.6rem; }
.bulle ul, .bulle ol { margin: 0.5rem 0 0.5rem 1.25rem; list-style: revert; }
.bulle code { background-color: rgb(var(--ink-rgb) / .07); padding: 0.1em 0.3em; border-radius: 4px; }
.bulle pre { background-color: rgb(var(--ink-rgb) / .07); padding: 0.7rem; border-radius: 8px; overflow-x: auto; margin: 0.5rem 0; }
.bulle pre code { background: none; padding: 0; }

/* Trois points qui respirent pendant que le modèle rédige. Sous
   « prefers-reduced-motion », ils restent immobiles : l'information (« ça
   travaille ») est portée par leur présence, pas par leur mouvement. */
.points span {
  display: inline-block;
  width: 0.4rem; height: 0.4rem;
  margin-right: 0.2rem;
  border-radius: 999px;
  background-color: var(--accent);
  animation: respirer 1.2s infinite ease-in-out;
}
.points span:nth-child(2) { animation-delay: .15s; }
.points span:nth-child(3) { animation-delay: .3s; }
@keyframes respirer {
  0%, 60%, 100% { opacity: .3; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) {
  .points span { animation: none; opacity: .6; }
  .progression > span { transition: none; }
}

/* --- La séance : une conversation, pas une page qui défile ----------------

   Un élève révise sur un téléphone, et une messagerie n'y défile pas comme un
   article : la saisie tient le bas de l'écran, l'en-tête tient le haut, et
   seules les bulles bougent entre les deux. La page de séance prend donc la
   hauteur exacte de la fenêtre et distribue le reste en colonne.

   Le pied de page n'y est pas (cf. le bloc `pied` de base.html) : il pousserait
   la saisie hors de l'écran, ce qui est exactement ce qu'on veut éviter.

   La hauteur est écrite deux fois. `100vh` est le repli des navigateurs qui ne
   connaissent pas `dvh` ; `100dvh` suit les barres d'outils qui se rétractent
   au défilement sur mobile, là où `100vh` reste figé sur la fenêtre la plus
   grande et fait passer la saisie sous la barre d'adresse. Le clavier natif,
   lui, n'entre dans aucune des deux unités : c'est `seance.js` qui recale la
   hauteur sur la fenêtre visuelle. */

/* La page elle-même ne défile pas. Sans cela, iOS fait glisser le document
   entier à l'ouverture du clavier et la barre de saisie part avec lui. */
.corps-seance { overflow: hidden; }

.cadre-seance {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  height: 100vh;
  height: 100dvh;
}
/* Rien ne se comprime : ni le bandeau, ni les bandeaux d'alerte. Seule la
   colonne de la conversation prend la place qui reste — et `min-height: 0`
   est ce qui l'autorise à devenir plus courte que son contenu, faute de quoi
   un élément en `overflow: auto` imbriqué ne défile jamais. */
.cadre-seance > *:not(.seance-main) { flex: none; }
.seance-main {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding-top: 0.25rem;
}

/* L'en-tête de séance : matière, titre, notion en cours et jauge. Il n'a pas
   besoin d'être `sticky` ici — dans une colonne à hauteur fixe, il ne bouge
   pas parce que rien ne le pousse. */
.seance-entete {
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--border);
}

/* Le tiroir d'information de la séance : quel modèle répond, et le rappel que
   le professeur relit. Un `<details>` natif, comme l'encart « Pour commencer »
   du tableau de bord — le repli ne dépend d'aucun script, et il n'y a rien à
   révéler puisque le contenu est vrai dès le serveur.

   Il est posé dans l'en-tête et non dans le fil : ce dernier est remplacé
   toutes les 1,5 s tant qu'une réponse est en vol, et le panneau se
   refermerait tout seul sous les yeux de qui le lit. */
.info-seance { position: relative; }

.info-seance > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
}
.info-seance > summary::-webkit-details-marker { display: none; }
.info-seance > summary:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 999px; }

/* La pastille « i ». Une lettre dans un rond plutôt qu'un glyphe SVG : le
   projet n'a pas de jeu d'icônes, et un dessin de plus se paierait deux fois —
   à écrire, puis à tenir en accord avec les autres. */
.info-pastille {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.15rem;
  height: 1.15rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  /* Le texte de la ligne est en majuscules avec un interlettrage : la pastille
     n'en veut ni l'un ni l'autre, sinon le « i » se décale dans son rond. */
  text-transform: none;
  letter-spacing: normal;
}
.info-seance > summary:hover .info-pastille {
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* Le panneau flotte au-dessus du fil plutôt que de pousser l'en-tête : la
   séance est une colonne à hauteur de fenêtre, et tout ce qui grandit en haut
   rogne le bas — c'est-à-dire la barre de saisie. Ancré à droite, parce que la
   pastille l'est aussi et qu'un panneau ouvert vers la droite sortirait de
   l'écran sur un téléphone. */
/* Fermé, le panneau doit disparaître — explicitement.
   Mesuré : un enfant en **position absolue** d'un `<details>` fermé garde une
   boîte de 352 × 129 px. Chrome le neutralise par `::details-content`, donc il
   n'est ni peint ni cliquable ; mais c'est un comportement d'agent utilisateur
   qu'on ne contrôle pas et qui ne vaut pas partout, et un panneau flottant
   au-dessus du fil « qui marche par accident » est le genre de chose qui se
   révèle sur le téléphone d'un élève. On le cache soi-même. */
.info-seance:not([open]) .info-panneau { display: none; }

.info-panneau {
  position: absolute;
  right: 0;
  top: calc(100% + 0.4rem);
  z-index: 20;
  width: max-content;
  max-width: min(22rem, calc(100vw - 2rem));
  padding: 0.7rem 0.85rem;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  /* La ligne qui le porte est en petites majuscules espacées : le panneau
     revient à du texte ordinaire, sans quoi il serait illisible. */
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--ink);
  white-space: normal;
}
.info-panneau p + p { margin-top: 0.5rem; }

/* La mention sous la première bulle. Elle se range à gauche, sous la bulle du
   modèle, et reste volontairement discrète : c'est une information, pas un
   message de la conversation — la donner l'apparence d'une bulle ferait croire
   que le modèle se présente lui-même. */
.mention-modele {
  margin: -0.25rem 0 0.35rem;
  font-size: 0.7rem;
  line-height: 1.35;
  color: var(--muted);
  max-width: 32rem;
}
.info-panneau .font-mono { word-break: break-all; }

.fil {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* Le seul élément qui défile de toute la page. `overscroll-behavior: contain`
   empêche le geste de continuer sur le document une fois le fil au bout —
   c'est ce qui déclenche le rechargement par traction sur Android et le
   rebond élastique sur iOS, deux façons de perdre sa place en lisant. */
.fil-defilement {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  padding-block: 0.85rem;
  /* L'ascenseur du fil prend les couleurs du thème. Sans cela il reste blanc
     en mode nuit — le projet ne déclare aucun `color-scheme` —, et une barre
     claire au milieu d'une conversation sombre se voit plus que la
     conversation. Le réglage est posé ici et nulle part ailleurs : c'est le
     seul ascenseur que l'application dessine à l'intérieur d'une page. Sur un
     téléphone il ne se voit pas, l'ascenseur y est flottant. */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
/* `margin-top: auto` colle la conversation au bas tant qu'elle est courte, ce
   qu'un `justify-content: flex-end` ferait aussi — mais celui-ci, posé sur
   l'élément qui défile, met les premières bulles hors d'atteinte dès que la
   conversation dépasse la hauteur disponible. */
.fil-pile {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* La barre de saisie, posée sur le fond de la page pour que les bulles ne
   transparaissent pas derrière elle. La marge basse reprend la zone sûre du
   téléphone, rôle que tenait le pied de page partout ailleurs. */
.composeur {
  border-top: 1px solid var(--border);
  background-color: var(--bg);
  padding-top: 0.6rem;
  padding-bottom: calc(0.6rem + env(safe-area-inset-bottom));
}
.composeur-commandes {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
/* `font-size: 1rem` n'est pas décoratif : sous 16 px, iOS zoome sur le champ à
   la mise au point et ne dézoome jamais tout à fait. La hauteur est menée par
   `seance.js`, qui la fait grandir avec le texte jusqu'à ce plafond ; passé
   lui, c'est le champ qui défile. */
/* Le champ et le micro sur une ligne, alignés en BAS : quand `seance.js` fait
   grandir le `textarea`, le bouton reste à hauteur de la dernière ligne plutôt
   que de flotter au milieu. */
.composeur-champ {
  display: flex;
  align-items: flex-end;
  gap: 0.4rem;
}
.composeur-champ textarea {
  resize: none;
  overflow-y: auto;
  max-height: 8rem;
  font-size: 1rem;
  /* Un enfant de `flex` vaut au moins sa largeur minimale de contenu : sans
     `min-width: 0`, une réponse d'un seul mot très long pousserait le micro
     hors de l'écran. */
  flex: 1 1 auto;
  min-width: 0;
}

/* --- Le micro de la dictée ------------------------------------------------

   Il se pose au bord du champ, comme sur n'importe quelle messagerie, et non
   dans la rangée de commandes en dessous : celle-ci porte déjà « Terminer » et
   « Envoyer », et `serrer()` la compte dans la hauteur qu'il arbitre.

   Trois états, et **tous tiennent dans l'empreinte du bouton** : au repos, en
   enregistrement (avec le temps qui court), en transcription. Jamais une rangée
   de plus — une rangée qui apparaît change la hauteur de la zone qui défile,
   donc fait sauter un fil collé en bas.

   L'abricot marque l'enregistrement plutôt qu'un rouge d'alerte : c'est la
   couleur des éléments actifs dans toute l'application, et le rouge y dit
   « erreur ». */
.bouton-micro,
.bouton-lecture {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  height: 2.5rem;
  min-width: 2.5rem;
  padding: 0 0.55rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background-color: var(--panel);
  color: var(--muted);
  cursor: pointer;
  transition: background-color .15s, color .15s, border-color .15s;
}
.bouton-micro:hover:not(:disabled),
.bouton-lecture:hover:not(:disabled) {
  color: var(--ink);
  border-color: var(--muted);
}
.bouton-micro:focus-visible,
.bouton-lecture:focus-visible { outline: none; box-shadow: var(--ring); }
.bouton-micro:disabled,
.bouton-lecture:disabled { opacity: .5; cursor: not-allowed; }

.micro-glyphe {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
}
.micro-glyphe rect { fill: currentColor; stroke: none; }

/* Le minuteur ne prend de la place que lorsqu'il en a besoin : au repos le
   bouton est un rond, en enregistrement il s'allonge du temps écoulé. */
.micro-minuteur {
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.micro-minuteur:empty { display: none; }

.bouton-micro[data-etat="enregistre"] {
  background-color: var(--accent);
  border-color: var(--accent);
  color: var(--sur-accent);
  animation: micro-pouls 1.6s ease-in-out infinite;
}
.bouton-micro[data-etat="enregistre"]:hover { color: var(--sur-accent); }

.bouton-micro[data-etat="transcrit"] {
  color: var(--accent);
  border-color: var(--accent);
}
.bouton-micro[data-etat="transcrit"] .micro-glyphe {
  animation: micro-tourne 0.9s linear infinite;
  transform-origin: 50% 50%;
}

@keyframes micro-pouls {
  0%, 100% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / .45); }
  50%      { box-shadow: 0 0 0 6px rgb(var(--accent-rgb) / 0); }
}
@keyframes micro-tourne {
  to { transform: rotate(360deg); }
}

/* Une animation qui pulse n'est pas une décoration ici — elle dit « ça
   enregistre » —, mais elle ne doit pas être le SEUL signal : la couleur pleine
   et le minuteur suffisent quand on ne veut pas de mouvement. */
@media (prefers-reduced-motion: reduce) {
  .bouton-micro,
  .bouton-micro .micro-glyphe,
  .bouton-lecture,
  .bouton-lecture .lecture-onde,
  .bulle-lire .lecture-onde { animation: none; }
}

/* --- La lecture à voix haute ----------------------------------------------

   Le jumeau du micro. Il en reprend la grammaire par une LISTE DE SÉLECTEURS
   plutôt que par une copie des valeurs : deux blocs identiques divergeraient au
   premier réglage changé, et l'on aurait deux boutons ronds de tailles
   différentes côte à côte dans la même barre. C'est la règle de `_suivi.html`
   et de `trame._consigne`, appliquée à une feuille de style.

   Trois états, et tous tiennent dans l'empreinte du bouton — jamais une rangée
   de plus, sous peine de faire sauter un fil collé en bas :

     mode allumé  contour d'accent   l'auto-lecture est active, rien ne parle
     lit          aplat d'accent     une réponse est en train d'être dite
     muet         glyphe barré       aucune voix française sur cet appareil

   Les deux premiers reprennent exactement le partage du micro entre
   « transcrit » (contour) et « enregistre » (aplat). */
.lecture-glyphe {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.lecture-corps { fill: currentColor; stroke: none; }

/* La barre oblique est dessinée en permanence et seulement révélée : un glyphe
   qui change de nombre d'éléments se recompose visiblement sous l'oeil. */
.lecture-barre { display: none; }
.bouton-lecture[data-etat="muet"] .lecture-onde,
.bulle-lire[data-etat="muet"] .lecture-onde { display: none; }
.bouton-lecture[data-etat="muet"] .lecture-barre,
.bulle-lire[data-etat="muet"] .lecture-barre { display: inline; }
.bouton-lecture[data-etat="muet"],
.bulle-lire[data-etat="muet"] { opacity: .6; }

/* Le mode allumé sans que rien ne parle. Sans cet état, l'interrupteur serait
   indiscernable de son repos entre deux tours — l'élève ne saurait pas que la
   prochaine réponse va être lue à voix haute dans la salle. */
.bouton-lecture[aria-pressed="true"] {
  color: var(--accent-ink);
  border-color: var(--accent);
}

.bouton-lecture[data-etat="lit"],
.bulle-lire[data-etat="lit"] {
  background-color: var(--accent);
  border-color: var(--accent);
  color: var(--sur-accent);
}
.bouton-lecture[data-etat="lit"]:hover,
.bulle-lire[data-etat="lit"]:hover { color: var(--sur-accent); }

/* Les ondes respirent pendant la lecture. Le mouvement n'est jamais le SEUL
   signal — l'aplat plein dit déjà « ça parle » —, ce qui permet de le couper
   entièrement sous « prefers-reduced-motion ». */
.bouton-lecture[data-etat="lit"] .lecture-onde,
.bulle-lire[data-etat="lit"] .lecture-onde {
  animation: lecture-ondes 1.4s ease-in-out infinite;
}
.bouton-lecture[data-etat="lit"] .lecture-onde + .lecture-onde,
.bulle-lire[data-etat="lit"] .lecture-onde + .lecture-onde { animation-delay: .2s; }

@keyframes lecture-ondes {
  0%, 100% { opacity: .4; }
  50%      { opacity: 1; }
}

/* Le bouton d'écoute d'une bulle, révélé au clic sur elle par `lecture.js`.

   **Hors flux, et c'est tout l'enjeu.** Posé dans le flux — une rangée sous
   chaque bulle, ou un bouton dans la ligne du texte —, il changerait la hauteur
   de la zone qui défile : l'ancre du bas de `seance.js` sauterait, et `serrer()`
   recalculerait son arbitrage à chaque révélation. En `position: absolute`, il
   ne déplace pas un pixel.

   Il déborde le coin bas-droit plutôt que de se ranger à droite de la bulle :
   la marge laissée là par `max-width: 88%` fait environ 34 px sur un écran de
   320, soit tout juste la largeur du bouton — s'y loger entièrement le collerait
   au bord de l'écran. Le coin, lui, est le point de la bulle où le rayon écarte
   déjà le texte. La position se MESURE (recette `--dump-dom` + une sonde
   `getBoundingClientRect()`), elle ne se retouche pas à l'oeil. */
.bulle-assistant { position: relative; }

.bulle-lire {
  position: absolute;
  right: -0.5rem;
  bottom: -0.5rem;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background-color: var(--panel);
  color: var(--muted);
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: background-color .15s, color .15s, border-color .15s;
}
.bulle-lire .lecture-glyphe { width: 1rem; height: 1rem; }
.bulle-lire:hover { color: var(--ink); border-color: var(--muted); }
.bulle-lire:focus-visible { outline: none; box-shadow: var(--ring); }

/* Sur le bilan, le bouton porte son libellé : la page ne défile pas au pixel,
   rien n'y contraint à l'empreinte d'un rond, et « Écouter » se comprend sans
   qu'on ait à deviner un pictogramme. */
.bouton-lecture-ligne { height: 2.1rem; gap: 0.35rem; }
.lecture-libelle { font-size: 0.8rem; }

/* Et c'est le seul bouton de lecture qui porte un LIBELLÉ, ce qui lui interdit
   l'aplat d'accent des autres. Mesuré sur la page : l'encre `--sur-accent` sur
   `--accent` donne **3,96:1** en clair, sous le seuil de 4,5:1 qu'exige un
   texte de 12,8 px — et elle tient 8,1:1 en nuit, donc le mode clair seul ne
   montre pas le défaut. L'état « lit » prend ici le CONTOUR au lieu de
   l'aplat : 6,9:1, et les ondes qui bougent disent déjà que ça parle.

   C'est le second endroit du projet où un motif change de forme selon l'objet,
   après `.repartition-part.jauge-consolider`, et la raison est exactement la
   même : le texte par-dessus, jamais l'esthétique. Les valeurs se remesurent,
   elles ne se retouchent pas à l'oeil. Deux classes dans le sélecteur, pour
   qu'il l'emporte sur `.bouton-lecture[data-etat="lit"]` par la spécificité et
   non par l'ordre des règles. */
.bouton-lecture.bouton-lecture-ligne[data-etat="lit"] {
  background-color: var(--panel);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.bouton-lecture.bouton-lecture-ligne[data-etat="lit"]:hover { color: var(--accent-ink); }

/* Le paragraphe où le bilan dit pourquoi la lecture se tait, sur le modèle de
   `.micro-minuteur:empty` : vide par défaut, donc sans hauteur ni marge — il
   n'apparaît que si `lecture.js` y écrit une raison. Sans lui, la page devrait
   réserver une ligne vide en permanence pour un message qui ne sert presque
   jamais. */
[data-lecture-explication]:empty { display: none; }

/* --- Le choix de la voix --------------------------------------------------

   « Voix : Thomas — Changer », écrit par le script dans le tiroir de la séance
   et sous le bouton du bilan. Le bouton se lit comme un lien : c'est un
   réglage, pas une action de la page, et un `.btn` de plus dans le tiroir le
   ferait passer pour une commande de la séance. */
.lecture-changer {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--accent-ink);
  text-decoration: underline;
  cursor: pointer;
}
.lecture-changer:hover { color: var(--ink); }
.lecture-changer:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 3px; }

/* Le panneau. Un `<dialog>` natif — le premier du projet —, donc dans la
   couche supérieure : il passe au-dessus de la colonne d'une séance réglée au
   pixel et du tiroir d'information, sans qu'aucun `z-index` n'ait à être
   arbitré. Il n'entre pas non plus dans le flux, ce qui est la contrainte de
   toute la page.

   `margin: auto` le centre dans cette couche ; sans lui un `<dialog>` se colle
   en haut. La hauteur est bornée et la LISTE défile, pas le panneau : un
   appareil qui porte huit voix ne doit pas repousser « Utiliser cette voix »
   hors de l'écran. */
.dialogue-voix {
  margin: auto;
  width: min(22rem, calc(100vw - 2rem));
  max-height: min(28rem, calc(100dvh - 3rem));
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: var(--panel);
  color: var(--ink);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.dialogue-voix::backdrop { background-color: rgb(0 0 0 / .45); }

.dialogue-voix-titre { font-weight: 700; margin-bottom: 0.15rem; }
.dialogue-voix-aide {
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 0.7rem;
}

.voix-liste {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  max-height: 16rem;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

.voix-option {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: var(--bg);
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.voix-option:hover { border-color: var(--muted); }
.voix-option:focus-visible { outline: none; box-shadow: var(--ring); }

/* La ligne retenue prend le contour d'accent et non l'aplat : elle porte du
   texte, et c'est le même arbitrage que `.bouton-lecture-ligne` — mesuré là-bas,
   l'encre `--sur-accent` sur `--accent` tombe à 3,96:1 en clair. */
.voix-option[aria-checked="true"] {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.voix-option[aria-checked="true"] .voix-nom { color: var(--accent-ink); font-weight: 600; }

/* Le nom vient du système et peut être long (« Français (France) — amélioré »)
   ou technique (« fr-fr-x-frb-local ») : il prend la place qui reste et se
   coupe, la balise de langue ne se coupant jamais — c'est elle qui distingue
   la France du Québec quand le nom ne dit rien. */
.voix-nom { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.voix-langue {
  flex: 0 0 auto;
  font-size: 0.72rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.dialogue-voix-pied {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.9rem;
}

/* --- Le bandeau du site n'entre pas dans une séance sur téléphone --------

   Une séance est une tâche bornée, comme un tunnel de paiement : ce genre
   d'écran ne porte pas la navigation globale du site, il porte une sortie et
   l'état de la tâche. Sur un téléphone, le bandeau et l'en-tête de séance
   prenaient ensemble un quart de l'écran avant même que le clavier ne monte —
   pour un logo, un menu à deux entrées et un lien « Mes chapitres » que
   l'en-tête de séance donne déjà.

   Il est retiré, **franchement**, et non révélé au défilement. Remonter dans
   une conversation veut dire « je relis », pas « je cherche la navigation » :
   l'élève remonte précisément quand il n'a pas compris, et faire glisser une
   barre par-dessus la conversation à cet instant-là ne rend service à
   personne. S'y ajoute un coût mécanique : toute barre qui apparaît change la
   hauteur de la zone qui défile, donc fait sauter un fil collé en bas.

   Ce qu'on y perd — le thème et « Quitter » — est à un geste, par
   « Mes chapitres », qui reste dans l'en-tête et devient la sortie unique
   (d'où sa flèche). Sur grand écran le bandeau reste : la hauteur n'y manque
   pas, et il situe la page. Rien ne devient inatteignable d'un côté ni de
   l'autre. */
@media (max-width: 39.9375rem) {
  .cadre-seance [data-bandeau] { display: none; }
  /* L'en-tête de séance devient le haut de l'écran : il lui faut la marge que
     le bandeau lui tenait lieu, et la zone sûre d'un téléphone encoché — nulle
     tant que la barre d'état reste opaque, non nulle en paysage. */
  .cadre-seance .seance-main { padding-top: calc(0.5rem + env(safe-area-inset-top)); }
}

/* --- Quand il ne reste presque rien de haut ------------------------------

   Un téléphone en paysage, clavier natif ouvert, laisse parfois deux cents
   pixels : le bandeau, l'en-tête de séance et la barre de saisie en demandent
   trois cents, et c'est le dernier de la colonne — la saisie, avec son bouton
   « Envoyer » — que `overflow: hidden` rogne. La page a l'air normale et ne
   permet plus rien d'envoyer.

   `seance.js` mesure ce qui reste au fil et pose le serrage : d'abord le
   bandeau du site, puis l'en-tête de séance si cela ne suffit pas. La
   conversation et la saisie ne s'effacent jamais, et tout revient dès que le
   clavier se referme.

   Trois sélecteurs de classe et d'attribut : les règles restent hors de portée
   des `.flex` et `.block` de Tailwind quel que soit l'ordre des feuilles, ce
   qui est la convention du projet pour toute règle d'affichage. */
.cadre-seance[data-serrage] [data-bandeau] { display: none; }
.cadre-seance[data-serrage="tout"] .seance-entete { display: none; }

/* Effacer deux blocs ne suffit pas : les marges qui les séparaient restent, et
   elles se comptent en dizaines de pixels sur un écran qui n'en a que deux
   cents. La respiration de la page est le dernier réglage avant de rogner
   quelque chose d'utile. */
.cadre-seance[data-serrage] .seance-main { padding-top: 0; }
.cadre-seance[data-serrage] .fil-defilement { padding-block: 0.4rem; }
.cadre-seance[data-serrage="tout"] .composeur {
  padding-top: 0.35rem;
  padding-bottom: calc(0.35rem + env(safe-area-inset-bottom));
}
.cadre-seance[data-serrage="tout"] .composeur-commandes { margin-top: 0.35rem; }

/* Sur un écran tactile, la touche Entrée va à la ligne et n'envoie rien :
   l'astuce y serait un mensonge. La condition est la même que celle de
   `seance.js` — un pointeur grossier sans survol —, et elle doit le rester :
   deux critères différents finiraient par promettre l'inverse de ce que fait
   le clavier. */
@media (hover: none) and (pointer: coarse) {
  .astuce-clavier { display: none; }
}

/* --- Le texte canonique et son aperçu ------------------------------------ */

/* L'éditeur et l'aperçu se font face et doivent s'aligner : la hauteur est
   écrite une seule fois. */
.hauteur-relecture { height: min(60vh, 34rem); }

.rendu { line-height: 1.65; }
.rendu > * + * { margin-top: 0.85rem; }
.rendu h2 { font-size: 1.3rem; font-weight: 700; margin-top: 1.6rem; }
.rendu h3 { font-size: 1.1rem; font-weight: 700; margin-top: 1.3rem; }
.rendu h4 { font-weight: 700; }
.rendu ul { list-style: disc; margin-left: 1.4rem; }
.rendu ol { list-style: decimal; margin-left: 1.4rem; }
.rendu li + li { margin-top: 0.25rem; }
.rendu code { background-color: rgb(var(--ink-rgb) / .07); padding: 0.1em 0.3em; border-radius: 4px; font-size: 0.9em; }
.rendu pre { background-color: rgb(var(--ink-rgb) / .07); padding: 0.8rem; border-radius: 8px; overflow-x: auto; }
.rendu pre code { background: none; padding: 0; }
.rendu table { border-collapse: collapse; display: block; overflow-x: auto; }
.rendu th, .rendu td { border: 1px solid var(--border); padding: 0.35rem 0.6rem; text-align: left; }
.rendu th { background-color: var(--column); }
/* Une citation, dans un texte de cours, c'est une transcription de figure :
   le filet abricot la désigne comme « à vérifier » sans un mot de plus. */
.rendu blockquote {
  border-left: 3px solid var(--accent);
  background-color: var(--accent-soft);
  padding: 0.6rem 0.9rem;
  border-radius: 0 8px 8px 0;
}
.rendu blockquote > * + * { margin-top: 0.5rem; }
.rendu a { color: var(--accent-ink); text-decoration: underline; }

/* --- Le bilan écrit par le modèle ---------------------------------------- */

/* Deux textes se suivent dans la même carte et n'ont pas le même auteur : la
   phrase qui explique ce qu'est ce bilan est de l'application, ce qui suit est
   du modèle. Sans rien pour les séparer, le second se lit comme la suite du
   premier — et un texte de modèle qu'on prend pour une affirmation du site
   n'est pas une confusion anodine sur une page qui, partout ailleurs, ne
   montre que des chiffres vérifiables.

   Le filet abricot à gauche dit déjà cela dans le reste du projet : c'est
   celui de `.rendu blockquote`, qui désigne une transcription de figure — du
   texte produit par une machine, à relire. Même signe, même sens. Le cadre
   reste au filet d'un pixel et sans aplat : ce bilan fait vingt lignes, un
   fond teinté sur cette hauteur pèserait plus que le texte. */
.bilan-modele {
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 1rem 1.1rem;
}

/* Plus petit que le corps de page : ce n'est pas le texte principal de
   l'écran, ce sont les chiffres au-dessus. Les titres du Markdown suivent —
   laissés à leur taille de `.rendu`, les `##` du modèle passaient au-dessus du
   « Bilan rédigé » qui coiffe la carte, et la hiérarchie s'inversait. */
.bilan-modele .rendu { font-size: 0.875rem; }
.bilan-modele .rendu h2 { font-size: 1rem; margin-top: 1.2rem; }
.bilan-modele .rendu h3 { font-size: 0.9375rem; margin-top: 1rem; }

/* --- Le générateur de mot de passe --------------------------------------- */

/* La barre est posée par `mot-de-passe.js`, sous le champ marqué
   `data-mot-de-passe` : ces deux classes n'apparaissent dans aucun gabarit.
   Sans JavaScript, rien de tout cela n'existe et le champ reste une saisie
   ordinaire — le formulaire fonctionne toujours.

   L'admin Django a sa propre feuille (`admin-mot-de-passe.css`) : la barre y
   emprunte ses boutons, pour ressembler à la page où elle se trouve. */
.barre-mot-de-passe {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.aide { font-size: 0.8125rem; color: var(--muted); }
.barre-mot-de-passe .aide { flex: 1 1 12rem; min-width: 0; }

/* Le lien de réinitialisation fait deux cents caractères : il se lit en
   chasse fixe, à taille réduite, et on vient ici pour le sélectionner. */
.champ-lien {
  font-family: var(--font-code);
  font-size: 0.8125rem;
}

/* --- Le voile d'attente -------------------------------------------------- */

/* La génération d'une trame est synchrone : trente secondes pendant lesquelles
   la page ne bouge pas d'un pixel. Le voile dit que quelque chose se passe, et
   il barre l'écran — c'est ce qui empêche le second clic, qui partirait pour
   rien sur un quota compté en requêtes par jour. */
.voile {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background-color: rgb(var(--bg-rgb) / .84);
  backdrop-filter: blur(3px);
  animation: voile-apparait .2s ease-out;
}

.voile-carte {
  width: min(27rem, 100%);
  padding: 2rem 1.75rem 1.5rem;
  text-align: center;
  box-shadow: var(--shadow-forte);
}

/* L'anneau tourne, le halo respire : deux mouvements de vitesses différentes,
   sans quoi l'ensemble ressemble à une image figée mal chargée. */
.anneau {
  position: relative;
  width: 3.25rem;
  height: 3.25rem;
  margin: 0 auto 1.2rem;
  border-radius: 999px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  animation: tourner .9s linear infinite;
}
.anneau::after {
  content: "";
  position: absolute;
  inset: -0.7rem;
  border-radius: 999px;
  background: radial-gradient(closest-side, rgb(var(--accent-rgb) / .22), transparent);
  animation: respirer-halo 2.2s ease-in-out infinite;
}

/* Les phrases s'échangent sur place : une hauteur réservée évite que la carte
   ne saute d'une ligne à chaque changement d'étape. */
.etapes { min-height: 3rem; }
.etape { display: none; font-weight: 600; }
.etape-visible { display: block; animation: etape-entre .45s ease-out; }

.ruban {
  height: 0.35rem;
  border-radius: 999px;
  background-color: var(--column);
  overflow: hidden;
  margin: 1rem 0 0.85rem;
}
.ruban > span {
  display: block;
  height: 100%;
  width: 45%;
  border-radius: 999px;
  background: linear-gradient(
    90deg, rgb(var(--accent-rgb) / 0), var(--accent), rgb(var(--accent-rgb) / 0)
  );
  animation: glisser 1.8s ease-in-out infinite;
}

@keyframes voile-apparait { from { opacity: 0; } to { opacity: 1; } }
@keyframes tourner { to { transform: rotate(360deg); } }
@keyframes respirer-halo {
  0%, 100% { opacity: .45; transform: scale(.92); }
  50% { opacity: 1; transform: scale(1.06); }
}
@keyframes glisser {
  0% { transform: translateX(-110%); }
  100% { transform: translateX(240%); }
}
@keyframes etape-entre {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* Sans mouvement, l'information « ça travaille » doit rester lisible : elle
   passe alors par la présence des éléments, pas par leur animation. */
@media (prefers-reduced-motion: reduce) {
  .voile, .etape-visible, .anneau, .anneau::after { animation: none; }
  .ruban > span { animation: none; width: 100%; opacity: .55; }
}

/* --- La poignée de réordonnancement de la trame -------------------------- */

/* Posée par `trame.js`, jamais par un gabarit : sans JavaScript il n'y a rien
   à glisser. Le curseur est le seul indice qu'une poignée se prend — d'où
   `grab` au survol et `grabbing` pendant le geste. */
.poignee {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.6rem;
  color: var(--muted);
  cursor: grab;
  /* Le geste appartient au glisser-déposer : sans cela, un appui prolongé
     sélectionne les six points au lieu de prendre la ligne. */
  user-select: none;
  touch-action: none;
}
.poignee:hover { color: var(--ink); }
.poignee:active { cursor: grabbing; }
.poignee svg { fill: currentColor; }

/* La ligne prise s'efface et se teinte : elle reste visible — c'est elle qu'on
   déplace — mais cesse de se confondre avec les lignes en place. */
tr.ligne-saisie { opacity: .55; }
tr.ligne-saisie td { background-color: var(--accent-soft); }

/* --- Divers -------------------------------------------------------------- */

.messages { 
  list-style: none; 
  /* margin: 0; 
  padding: 0;  */
}
.message {
  border-radius: var(--radius);
  padding: 0.7rem 1rem;
  border: 1px solid var(--border);
  background-color: var(--panel);
}
.message.success { background-color: var(--success-soft); border-color: var(--success); color: var(--success-ink); }
.message.error { background-color: var(--danger-soft); border-color: var(--danger); color: var(--danger-ink); }
.message.warning { background-color: var(--warning-soft); border-color: var(--warning); color: var(--warning-ink); }

/* htmx pose « htmx-request » sur l'élément indicateur pendant la requête. */
.indicateur { display: none; }
.htmx-request .indicateur, .htmx-request.indicateur { display: inline-flex; }

/* --- Le bandeau replié ---------------------------------------------------

   Quand la barre n'a plus la place d'aligner ses commandes, `menu.js` les
   range dans un panneau ouvert par un bouton à trois points. Le script ne
   fait que poser des classes et des attributs : tout ce qui se voit est
   décidé ici.

   Chaque règle d'affichage porte **deux** sélecteurs de classe, jamais un.
   Tailwind pose « display » avec des classes simples (« .flex »,
   « .inline-flex ») : à égalité de spécificité, c'est l'ordre des feuilles
   qui tranche — style.css l'emporte en production, où il est chargé après
   tailwind.css, mais le CDN de développement injecte sa feuille à l'exécution,
   donc après. Une règle à une seule classe se comporterait autrement en
   développement et en production, ce qui est précisément le genre d'écart
   qu'on ne voit qu'une fois en ligne. */

.nav-item.bandeau-bouton { display: none; }
.nav-item.bandeau-bouton[data-visible] { display: inline-flex; }

.bandeau-nav.bandeau-nav-repliee {
  display: none;
  position: absolute;
  top: 100%;
  right: 1rem;          /* aligné sur le padding du « shell » */
  z-index: 40;
  margin-top: 0.35rem;
  min-width: 12rem;
  flex-direction: column;
  align-items: stretch;
  gap: 0.1rem;
  padding: 0.4rem;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-large);
  box-shadow: var(--shadow-forte);
}
.bandeau-nav.bandeau-nav-repliee[data-ouvert] { display: flex; }

/* Dans le panneau, chaque commande prend une ligne entière : une cible large
   comme son seul libellé se rate au pouce. */
.bandeau-nav-repliee .nav-item { display: block; padding: 0.5rem 0.7rem; }

/* Le pseudonyme n'est pas une commande : il coiffe le panneau et s'en sépare
   par un filet, sinon il se lit comme un lien qui ne réagit pas. */
.bandeau-nav-repliee .identite {
  padding: 0.35rem 0.7rem 0.5rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0.2rem;
}

.bandeau-nav-repliee .bascule-theme {
  width: auto;
  height: auto;
  justify-content: flex-start;
  gap: 0.5rem;
  padding: 0.5rem 0.7rem;
  opacity: 1;
}

.etiquette-repli { display: none; }
.bandeau-nav-repliee .etiquette-repli { display: inline; }
