/* Modus — design system "Classical" (tokens repris de la maquette) */
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600&family=Lora:wght@400;600&display=swap');

:root {
  /* Déclare à l'agent utilisateur DANS QUEL SCHÉMA il peint ses propres
     widgets — ceux que cette feuille ne peint pas et ne peut pas peindre :
     la piste des trois curseurs (input[type="range"] plus bas ne pose que
     `accent-color` et `width`, aucun `appearance:none`, aucun
     ::-webkit-slider-*), le glyphe et le panneau du sélecteur de date
     (« Modifier la séance »), les ascenseurs des six conteneurs qui défilent,
     la surbrillance d'autofill et le fond de sélection. Sans cette
     déclaration la valeur utilisée est `normal`, c'est-à-dire clair : en
     thème sombre l'app affichait donc des pistes de curseur grises claires,
     un glyphe de calendrier presque noir sur #242220 et des ascenseurs
     clairs, alors que css/landing.css porte ces trois mêmes déclarations
     depuis toujours, sur exactement les trois mêmes sélecteurs.
     `light dark` = « les deux me vont », donc prefers-color-scheme décide,
     comme pour les jetons ci-dessous. */
  color-scheme: light dark;

  --color-bg: #f3f2f2;
  --color-surface: #eae9e9;
  --color-text: #201f1d;
  --color-accent: #b68235;
  --color-accent-2: #ac803e;
  --color-divider: color-mix(in srgb, #201f1d 16%, transparent);
  /* Rouge d'erreur : jeton de RÔLE, donc redéfini dans les blocs de thème
     sombre plus bas (contrairement à l'échelle --color-accent-N, qui décrit
     des teintes). Le brique sombre du papier tombait à 1,62:1 sur encre, ce
     qui rendait les messages d'erreur quasiment invisibles en thème sombre :
     c'est le seul endroit de l'app où un défaut de contraste empêchait de
     lire ce qui compte le plus. --color-error-bg dérive du jeton (et non
     d'une valeur littérale) pour suivre ce renversement tout seul. */
  --color-error: #8a2e2e;
  --color-error-bg: color-mix(in srgb, var(--color-error) 10%, transparent);
  /* Priorité d'exercice (priority.js) — trois jetons de RÔLE, donc redéfinis
     par thème comme --color-error, jamais des crans de l'échelle
     --color-accent-N (qui décrit des teintes et ne suit aucun thème). Ce sont
     les trois seules couleurs du projet hors palette papier/encre/or, et
     elles sont là parce que le fait qu'elles portent est lui-même une
     couleur : la méthode Rouge / Jaune / Vert de Gebrian (Appendix A p. 213).
     Elles ne portent JAMAIS le fait toute seule — le libellé est écrit en
     toutes lettres à côté (voir priorityTagHtml) — mais elles tiennent le
     seuil AA de 4,5:1 sur les trois fonds de page dans les deux thèmes, et
     audit-lisibilite.mjs les contrôle au même titre que les six autres. */
  --color-prio-haute: #a8322c;
  --color-prio-moyenne: #856010;
  --color-prio-basse: #2f6b3d;

  --color-neutral-100: #f8f4f4;
  --color-neutral-200: #eae7e7;
  --color-neutral-300: #d7d3d3;
  --color-neutral-400: #bab6b6;
  --color-neutral-500: #9b9797;
  --color-neutral-600: #7d7979;
  --color-neutral-700: #605d5d;
  --color-neutral-800: #444141;
  --color-neutral-900: #2d2b2b;

  /* Échelle de teintes, jamais des rôles : le nombre décrit une clarté, pas
     un usage, et AUCUN de ces jetons n'est redéfini en thème sombre. Ils ne
     conviennent donc qu'aux surfaces qui ne suivent pas le thème (plaques
     fixes, pastilles, filets) et JAMAIS à du texte posé sur le fond de page.
     Pour du texte doré, prendre --color-accent-ink / --color-accent-ink-strong
     ci-dessous, qui se retournent seuls ; sur une plaque dorée fixe, prendre
     --color-on-accent-plate. Les crans 700/800/900 ont été retirés en même
     temps que la passe de lisibilité : ils ne servaient plus qu'à du texte,
     et les laisser déclarés en aurait fait des pièges à rouvrir. */
  --color-accent-100: #fff3e4;
  --color-accent-200: #ffe3bf;
  --color-accent-300: #facb8d;
  --color-accent-400: #e1ad66;
  --color-accent-500: #c28d41;
  --color-accent-600: #a06f24;

  --font-heading: "Cormorant Garamond", system-ui, sans-serif;
  --font-heading-weight: 600;
  --font-body: "Lora", system-ui, sans-serif;

  --space-1: 4.6px;
  --space-2: 9.2px;
  --space-3: 13.8px;
  --space-4: 18.4px;
  --space-5: 23px;
  --space-6: 27.6px;
  --space-8: 36.8px;

  --radius-sm: 2px;
  --radius-md: 4px;
  --radius-lg: 7px;

  /* ─ socle mobile ─ Ce qui vit SOUS le contenu, pour les deux seuls éléments
     de l'app posés en position:absolute contre le bord bas (le bouton
     flottant d'« Aujourd'hui » et le toast). Leurs offsets ont été posés sur
     la maquette pour une barre d'onglets nue, et cette barre n'a pas de
     hauteur constante : son rembourrage bas vaut max(--space-2,
     env(safe-area-inset-bottom)) (voir .tabbar), donc elle grandit de tout ce
     que le home indicator réclame — c'est-à-dire sur le téléphone même où
     cette PWA s'installe. --bottom-bar-grow est exactement cet écart, nul
     partout ailleurs (desktop, téléphone sans indicateur), ce qui rend son
     ajout neutre par construction.
     --session-resume-h est l'autre chose qui s'intercale, la bande « Séance
     en cours » (.session-resume, rendue par tabbar()) : sa hauteur réelle
     mesure 38,4px (1px de filet + 9+9 de rembourrage + une ligne de 12,5px à
     1,55), et 37px est la valeur que le bouton flottant employait déjà en
     dur (121 - 84). Un seul jeton pour les deux, plutôt qu'un second littéral
     dans le rendu du toast. */
  --bottom-bar-grow: max(0px, env(safe-area-inset-bottom, 0px) - var(--space-2));
  --session-resume-h: 37px;

  /* --floating-clearance exclut, au bas de tout conteneur défilant, la bande
     que couvre le bouton flottant posé dessus (le "+" d'« Aujourd'hui », le
     signalement générique, présent sur CHAQUE écran).

     Deux mécaniques la portent, et ce n'est pas un choix arbitraire — c'est
     un compromis mesuré entre deux défauts qui se corrigent l'un l'autre.

     PAR DÉFAUT (`.screen-scroll`/`.screen-scroll--pad` de base,
     `.teacher-roster`) : en PADDING-BOTTOM, sur le conteneur défilant
     lui-même. Une marge REDUIT la boîte du conteneur flex:1 en permanence,
     à TOUTE position de défilement — pas seulement à la fin —, puisqu'elle
     vit HORS de la zone défilable, dans le flux du parent (.screen). Sur un
     écran dont le contenu dépasse largement un écran (Compte, Sécurité,
     Nouveautés, Installer…), cela revient à amputer la zone utile d'environ
     144px en PERMANENCE, quel que soit l'endroit où l'on a défilé — mesuré
     le 23 août 2026 (Lucas) : "Financer la suite" tranché aux trois quarts
     de l'écran, un large bandeau vide et immobile avant la barre d'onglets.
     Un padding-bottom, lui, ne réserve l'espace qu'à la QUEUE du contenu
     réel : le conteneur garde toute sa hauteur disponible pendant tout le
     défilement normal, et la réserve ne devient visible qu'une fois qu'on a
     défilé jusqu'au bout — exactement là où elle doit l'être.

     EXCEPTION, EN MARGIN-BOTTOM : les panneaux compacts dont le contenu
     réel peut tenir SANS défiler (l'écran d'évaluation, views/eval.js ; les
     quatre .sheet de l'espace enseignant, teacherFeedback.js/
     teacherStudentDetail.js). Sur ceux-là, un padding-bottom ne protège
     RIEN à scrollTop=0 : si le contenu réel est plus court que la boîte
     pleine hauteur, il est peint EN ENTIER sans le moindre défilement, et
     sa fin peut retomber dans la bande que le padding est censé exclure —
     mesuré le 23 août 2026, sur le HTML réellement rendu : « Valider »
     chevauchait le bouton de signalement dès le premier rendu sur l'écran
     d'évaluation, et la fiche d'un élève sans aucun exercice/séance/routine
     faisait de même. Une marge, elle, RÉDUIT la boîte elle-même : son
     contenu ne peut plus jamais être peint dans cette bande, à AUCUNE
     position de défilement — et ce coût est NUL pour un contenu qui ne
     défile de toute façon jamais (l'espace resté vide sous le contenu est
     rigoureusement le même, marge ou padding, tant que le contenu tient
     dans un seul écran). C'est donc la seule mécanique qui n'ait AUCUNE
     contrepartie sur ces écrans-là, contrairement aux écrans qui défilent
     réellement, où elle en a une — d'où le partage : padding par défaut
     pour ne pas amputer un contenu qui grandit, marge sur ce qui ne
     grandit pas assez pour jamais en avoir besoin.

     L'ancienne valeur (84px) ne couvrait que l'OFFSET du bouton (sa distance
     au bord bas), jamais sa propre HAUTEUR : le bouton occupait donc la bande
     84-124px (ou 84-136px pour le "+", le plus grand des deux à 52px), soit
     précisément la zone que ce jeton était censé exclure et qu'il laissait
     au contraire ouverte au contenu. La valeur inclut donc maintenant cette
     hauteur (52px, le plus grand des deux boutons) et --bottom-bar-grow,
     pour rester synchronisée avec le bouton — qui l'inclut lui aussi dans
     son propre offset (voir .app-feedback-trigger) — plus une douzaine de
     pixels de respiration. Le cas où une bande "Séance en cours" s'intercale
     (.session-resume) est traité par la règle :has() qui suit, exactement
     comme .app-feedback-trigger et .fab le font déjà pour eux-mêmes : un
     second jeton dérivé plutôt qu'un calcul recopié — également en
     padding-bottom, pour la même raison que la règle de base. */
  --floating-clearance: calc(52px + 84px + var(--bottom-bar-grow) + 8px);

  /* --teacher-floating-clearance est la même garantie que --floating-
     clearance ci-dessus (une boîte flex:1 réduite d'avance, jamais un espoir
     fondé sur le défilement), mais dimensionnée sur le SEUL bouton flottant
     que l'espace enseignant puisse jamais montrer : .fab (le "+", 52px) n'est
     rendu que par views/today.js, qu'aucun écran de cet espace n'atteint —
     donc 40px (.app-feedback-trigger) suffit ici, pas le pire cas des deux
     boutons. Ce n'est pas une réduction au jugé : le contenu de « Tes
     élèves »/« Retours de pratique » n'est structurellement PAS plus court
     que celui d'un carnet élève (le nombre d'élèves ou de demandes n'est pas
     borné, et grandit avec l'usage — rien n'y interdit un jour un défilement
     aussi long qu'ailleurs), donc réduire la marge sur cette base rouvrirait
     un jour le défaut que --floating-clearance ferme. Seule l'ABSENCE
     garantie de .fab ici change la valeur réellement nécessaire. Aucun
     second jeton pour --session-resume-h : .teacher-roster ne coexiste
     jamais avec .session-resume, qui n'est rendue que par tabbar() (jamais
     appelée dans l'espace enseignant, qui a son propre bandeau). */
  --teacher-floating-clearance: calc(40px + 84px + var(--bottom-bar-grow) + 8px);

  --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);

  /* ─ texte doré ─ Les deux seuls jetons dorés pour du texte posé sur le fond
     de page. Ils partent du doré et l'entraînent vers la couleur de texte du
     thème courant : ils foncent en clair, s'éclaircissent en sombre, tout
     seuls. C'est ce qui manquait à l'ancien --color-accent-700 (#7d5411,
     jamais redéfini en sombre), parfait sur crème à 5,50:1 mais tombé à
     2,38:1 sur encre, et à --color-accent-800, descendu jusqu'à 1,34:1.
     "strong" est plus proche de l'encre, donc plus appuyé, dans les DEUX
     thèmes : la hiérarchie tient au rapprochement vers le texte, jamais à
     une teinte plus sombre, qui ne voudrait rien dire en thème sombre.
     Pires cas mesurés : ink 4,63:1, ink-strong 6,74:1 (voir
     tools/audit-lisibilite.mjs). */
  --color-accent-ink: color-mix(in srgb, var(--color-accent) 60%, var(--color-text));
  --color-accent-ink-strong: color-mix(in srgb, var(--color-accent) 38%, var(--color-text));
  /* Texte posé sur une plaque dorée FIXE (--color-accent-100, qui ne suit pas
     le thème : .tag-accent, .toast--success, .sync-badge--pending, les deux
     puces sélectionnées du desktop). Cette paire s'accorde avec elle-même
     dans les deux thèmes, 9,30:1 ; elle ne doit surtout pas devenir
     --color-accent-ink, qui s'éclaircirait en sombre et poserait alors du
     doré pâle sur une plaque crème. */
  --color-on-accent-plate: #5a3b0a;

  /* Anneau de focus clavier. Dérivé de --color-accent-ink, jamais du doré pur
     --color-accent : celui-ci ne pèse que 3,02:1 sur le fond de page et
     2,78:1 sur une carte, sous le seuil de 3:1 des éléments non textuels, et
     l'anneau se posait justement le plus souvent sur une carte. Le doré pur
     reste la couleur des filets et des remplissages, qu'on n'a qu'à
     percevoir ; un anneau de focus, lui, doit se voir depuis l'autre bout de
     la pièce, parfois sur un lavis doré qui rapproche le fond de sa teinte.
     Il suit le thème tout seul, --color-accent-ink mélangeant vers
     --color-text. */
  --color-focus: var(--color-accent-ink);

  /* ─ texte gris ─ Trois niveaux de texte, et trois seulement : --color-text
     (100 %), puis ces deux-ci. Les valeurs intermédiaires choisies au jugé
     qu'on trouvait auparavant partout (30 % à 78 % de --color-text) ne
     tenaient pas AA en thème CLAIR, à l'inverse de l'intuition : le pire cas
     à 55 % tombe à 3,54:1 sur --color-surface. 65 % est le plancher réel
     (4,75:1) ; en dessous de 63 %, tout petit texte échoue. Descendre sous
     ce plancher n'est donc jamais la réponse : la hiérarchie sous "muted"
     passe par la taille, la casse ou l'italique, jamais par un gris plus
     clair. */
  --color-text-soft: color-mix(in srgb, var(--color-text) 75%, transparent);
  --color-text-muted: color-mix(in srgb, var(--color-text) 65%, transparent);

  /* Courbe d'apparition/disparition unique de l'app. Les easings natifs
     (`ease-out`) sont trop mous pour un changement d'état qu'on veut voir
     arriver ; celle-ci démarre franchement et se pose en douceur. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  /* Courbe des panneaux qui montent depuis un bord (voir .tools-sheet). Elle
     démarre plus vite et se pose plus longuement que --ease-out : une carte
     centrée parcourt huit pixels, une feuille qui monte du bas en parcourt
     toute la hauteur, et la même courbe y paraît sèche. Reprise d'Ionic,
     référence de fait pour ce geste. */
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}

*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400;
  background: var(--color-bg); color: var(--color-text); font-family: var(--font-body);
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior-y: none;
}
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
}
h1 { font-size: 32px; }
h2 { font-size: 26px; }
h3 { font-size: 21px; }
h4 { font-size: 18px; }
h5 { font-size: 16px; }
h6 { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; }
/* Le surtitre doré posé au-dessus du titre d'un écran (« Compte » au-dessus
   de « Se connecter », la date au-dessus d'« Aujourd'hui », « Maîtrisés »
   au-dessus d'« Archivés »…) : c'est une étiquette, pas un titre. Il ne coiffe
   aucun contenu qui lui soit propre, et le VRAI titre vient juste après —
   écrit en h6, il faisait donc commencer la liste des titres de presque tous
   les écrans par un niveau 6 suivi d'un niveau 1 ou 2, c'est-à-dire à
   l'envers. L'app rendait déjà exactement le même objet SANS en faire un
   titre, 19 fois (.exercise-card-kicker, .celebrate-eyebrow,
   .teacher-homework-eyebrow) — cette classe étend l'idiome au surtitre
   d'écran. Elle reproduit au trait près ce que le couple `h1..h6` + `h6`
   donnait (mêmes police, graisse, interligne, marge, corps, interlettrage et
   capitales), donc rien ne bouge à l'écran ; la couleur reste en ligne, comme
   avant. Voir le contrôle SURTITRE de tools/audit-clavier.mjs. */
.screen-kicker {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: 1.12; margin: 0 0 var(--space-2);
  font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase;
}
/* Le RANG d'un titre (h1..h6) dit sa place dans la hiérarchie du document ;
   sa TAILLE dit son poids visuel. Les deux étaient confondus, et le rang
   suivait donc la place disponible : le titre d'un même écran était un <h2>
   au téléphone et un <h1> sur ordinateur (« Aujourd'hui », « Progression »,
   « Archivés », la fiche exercice, « Compte »), un <h4> contre un <h2> sur
   le formulaire et « Démarrer », un <h3> contre un <h2> sur l'évaluation et
   sur les deux écrans de sortie de l'onboarding — et 27 des 36 états rendus
   n'avaient de <h1> dans AUCUNE des deux coques. Ces classes découplent les
   deux : le titre d'un écran est désormais toujours
   un <h1>, et la classe lui rend exactement le corps de son ancien rang.
   Seule la `font-size` distingue h1..h5 dans le bloc ci-dessus, donc rien
   d'autre n'a à être reproduit — contrairement à `.screen-kicker`, qui
   remplace un <h6> et doit en reprendre aussi l'interlettrage et les
   capitales. C'est la moitié qui peut dériver en silence : retoucher le
   corps de `h2` sans retoucher `.h2-size` ferait diverger les titres
   d'écran des titres de section. Voir le contrôle TITRE de
   tools/audit-clavier.mjs, qui compare les deux dans les deux sens. */
.h2-size { font-size: 26px; }
.h3-size { font-size: 21px; }
.h4-size { font-size: 18px; }
/* Même découplage, un rang plus bas : les titres de SECTION d'un écran (les
   cinq cartes de « Sécurité », les dix chapitres du guide, « Journal » de la
   fiche exercice, les groupes de mois du changelog, « Exercices actifs » de la
   fiche élève, les états vides des trois listes…) démarraient à h3, h4, h5 ou
   h6 selon le corps voulu, donc SOUS un <h1> sans jamais passer par h2 : 40
   sauts de rang sur 23 des 45 états rendus, et le rang h2 vide dans toute
   l'app. Ils sont désormais tous des <h2>, chacun avec la classe qui lui rend
   le corps de son ancien rang. `.h6-size` est la seule à déclarer plus qu'une
   `font-size`, pour la même raison que `.screen-kicker` : l'interlettrage et
   les capitales appartiennent à la règle `h6` elle-même, pas au bloc commun.
   Pas de `.h5-size` : les quatre seuls titres remontés depuis h5 (les étapes
   d'enrôlement TOTP et celles d'« Installer ») épinglent déjà leur corps dans
   `.mfa-step-title`, et deux classes de même spécificité se départageraient
   sur leur ordre dans ce fichier. Voir le contrôle SECTION de
   tools/audit-clavier.mjs. */
.h6-size { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; }
/* Trois écrans n'écrivaient pas leur titre dans un <hN> DU TOUT : l'écran de
   séance (nom de l'exercice travaillé, .session-exercise), la liste d'élèves
   (« Vos élèves », .teacher-banner-title) et la fiche élève (nom de l'élève,
   .teacher-header-name) — soit les deux seuls écrans de l'espace enseignant
   et celui où l'on passe le plus de temps. Leur titre était un <div> stylé de
   bout en bout, donc absent de la hiérarchie du document : rien à annoncer,
   rien à survoler. Ces trois classes-là ne peuvent pas se contenter d'une
   `font-size` comme .h2-size ci-dessus, parce qu'un <div> ne recevait AUCUNE
   des déclarations du bloc `h1, h2, h3, h4, h5, h6` : chacune doit donc les
   redéclarer toutes, y compris `letter-spacing: normal` et `margin: 0`, qui
   sont exactement les deux que ces trois-là n'avaient pas. C'est ce qui rend
   le passage en <h1> visuellement neutre. Corollaire assumé : les trois
   propriétés typographiques qui vivaient en style EN LIGNE dans les gabarits
   des deux bandeaux enseignant remontent ici — un titre dont la police est
   écrite moitié en classe, moitié en ligne, est la moitié qui dérive.
   Voir le contrôle ÉCRAN de tools/audit-clavier.mjs. */
p { margin: 0 0 var(--space-3); }
.text-muted { color: var(--color-text-muted); }
:focus { outline: none; }
/* Rien à écrire sur le rayon : un `outline` suit de lui-même le
   border-radius de l'élément qu'il entoure (CSS UI 4), et surtout pas
   `border-radius: inherit` ici, qui écraserait le rayon propre de l'élément
   par celui de son parent au moment précis où il reçoit le focus. */
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
/* Anneau de focus sur une PLAQUE FIXE. --color-focus dérive de
   --color-accent-ink, donc de --color-text : il se retourne avec le thème, ce
   qui est juste sur un fond qui se retourne aussi. Le bandeau de l'espace
   enseignant, lui, reste volontairement sombre dans les DEUX thèmes
   (--color-neutral-900, même esprit que --color-accent-* : voir le commentaire
   de banner(), views/teacherRoster.js) — l'anneau y tombait donc à 2,23:1 en
   thème CLAIR, sous le seuil de 3:1 des éléments non textuels, alors qu'il
   tient 5,65:1 sur le fond de page du même thème. Mesuré sur le HTML
   réellement rendu des dix états de cet espace : dix plaques, ONZE contrôles
   focalisables posées dessus (les deux « Retour à mon carnet » et les deux
   « Vos élèves »), et neuf états concernés sur dix.

   L'idiome vient de la landing, qui le tient depuis toujours sur son unique
   contre-ton (section[data-tone="dark"] :focus-visible, css/landing.css) : le
   seul bloc de la page qui ne suit pas le thème y redéfinit la couleur de son
   anneau. Ici c'est le JETON qui est redéfini, pas la déclaration `outline`,
   pour que tout contrôle posé plus tard sur ces plaques en hérite sans qu'on
   ait à y penser — la règle :focus-visible ci-dessus reste la seule à dessiner
   l'anneau.

   La valeur est --color-accent-300, c'est-à-dire l'encre même des contrôles
   qui reçoivent ce focus (backToNotebookLink et le retour « Vos élèves »
   l'écrivent tous deux en style en ligne) et celle du surtitre du bandeau :
   9,38:1 sur cette plaque, fixe comme elle. Elle change donc aussi l'anneau du
   thème sombre, où il passe de 6,49:1 à 9,38:1 — un seul comportement pour les
   deux thèmes plutôt qu'une surcharge qui ne vaudrait qu'en clair. */
.teacher-banner,
.teacher-detail-header,
.teacher-plate { --color-focus: var(--color-accent-300); }
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }

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

/* — buttons — */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  cursor: pointer; text-decoration: none; user-select: none;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 15px; line-height: 1.2; color: var(--color-text);
  background: transparent; border: 1px solid transparent;
  padding: 12px calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}
.btn svg { display: block; }
/* Un contrôle DÉSACTIVÉ se voit, quelle que soit sa classe.
   Cette déclaration était scopée à `.btn:disabled`, donc elle décrivait une
   CLASSE et non un ÉTAT : les deux contrôles désactivés de l'app qui ne
   portent pas `.btn` gardaient l'apparence exacte d'un contrôle vivant.
   Mesuré sur le HTML réellement rendu de 22 états (45 contrôles désactivés,
   4 sans marque) :
     — « Annuler » du recadrage de la photo (views/account.js), désactivé
       PENDANT l'envoi, et seul contrôle par lequel on quitte ce dialogue
       (Échap y appelle le même cancelAvatarCrop, qui porte la même garde) :
       pleine opacité et curseur en main, alors qu'il ne répond plus. C'est
       « un bouton qui ne fait rien », que ce projet interdit ailleurs ;
     — le lien « Se déconnecter » de la reliure desktop
       (.desktop-signout-link), désactivé dès qu'une action est en vol —
       donc y compris pendant un « Enregistrer » de profil, où son libellé,
       lui, ne change pas. Son jumeau MOBILE porte `.btn` et s'estompait
       depuis toujours : les deux coques disaient deux choses du même geste.

   Sélecteurs d'ÉLÉMENT plutôt que de classe : c'est ce qui rend la marque
   impossible à manquer pour un contrôle futur, même parti que la règle
   `::placeholder` écrite nue et que `.screen-topbar`.

   `!important` sur le seul `cursor` : le bouton « Annuler » ci-dessus écrit
   `cursor:pointer` en style EN LIGNE, et seul un !important d'auteur bat une
   déclaration en ligne — même idiome que `[style*="lb-soft"]` du bloc de
   mouvement réduit. L'opacité, elle, n'est déclarée en ligne nulle part :
   l'y ajouter serait une garde qui ne peut pas être fausse.
   `node tools/audit-clavier.mjs` (contrôle DÉSACTIVÉ) le MESURE sur le rendu. */
button:disabled,
input:disabled,
select:disabled,
textarea:disabled { opacity: 0.45; cursor: not-allowed !important; }
/* Lettrage sur --color-accent-ink et non --color-accent : le doré pur ne
   pèse que 2,78:1 sur le crème (il a été choisi pour des filets et des
   remplissages, où le seuil est de 3:1, pas pour du texte). Le filet, lui,
   reste sur le doré pur : c'est lui qui porte la couleur de la marque, et
   il n'a pas à être lisible, seulement perceptible. Même partage pour
   .btn-danger, .tag-outline, le bouton flottant et .link-btn. */
.btn-primary { color: var(--color-accent-ink); border-color: var(--color-accent); }
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.btn-danger { color: var(--color-error); border-color: var(--color-error); }
.btn-danger:hover { background: color-mix(in srgb, var(--color-error) 12%, transparent); }
.btn-danger:active { background: color-mix(in srgb, var(--color-error) 22%, transparent); }
.btn-icon { width: 40px; height: 40px; padding: 0; flex: none; }
.btn-block { width: 100%; margin-top: var(--space-2); }
/* Croix de fermeture de la carte « Exercice maîtrisé » (dons, views/today.js).
   Taille fixe 40×40 (≥ 24px, WCAG 2.5.8), jamais en media query, pour que le
   contrôle CIBLE (audit-clavier) puisse la mesurer sur cette classe. */
.mastery-close { position: absolute; top: var(--space-2); right: var(--space-2); width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: none; border: none; border-radius: var(--radius-sm); cursor: pointer; color: var(--color-text-muted); }

/* — forms — */
.field > label {
  display: block; font-size: 12px; margin-bottom: 5px;
  color: var(--color-text-soft);
}
/* 16px et non les 15px du `body` : sous ce seuil exact, WebKit ZOOME la page
   à la prise de focus d'un champ de saisie, et ne dézoome pas au flou. Le
   téléphone est le cas nominal de cette PWA (voir l'écran « Installer le
   carnet ») et tous les navigateurs d'iOS sont WebKit, donc les 37 champs
   qui héritaient de cette règle emmenaient l'app dans un état zoomé dont
   rien ne la sortait — `.tabbar`, `.fab` et le toast sont en `position:fixed`,
   donc dimensionnés sur le viewport de MISE EN PAGE : zoomés, ils débordent
   du viewport VISUEL et les quatre onglets cessent d'être tous visibles.
   `viewport-fit=cover` sans `maximum-scale` (app/index.html) laisse
   volontairement le zoom disponible — le brider serait un échec WCAG 1.4.4,
   donc la seule parade est de ne pas le DÉCLENCHER.
   Croissance mesurée : un champ d'une ligne passe de 40 à 40,8px (23,25 →
   24,8px de line-height, `font: inherit` posant 1.55 sur sa propre taille),
   0 pour les cinq champs qui portent min-height:44px en ligne, 0 pour un
   textarea `rows="3"` (sous les 96px de min-height dans les deux cas),
   +6,2px sur `rows="4"` et +12,4px sur `rows="8"` — ces deux derniers dans
   un `.screen-scroll`, qui l'absorbe.
   `node tools/audit-lisibilite.mjs` (contrôle CHAMP) mesure le seuil sur le
   HTML réellement rendu, jamais sur cette seule déclaration. */
.input {
  width: 100%; min-height: 40px; padding: 8px 11px; font: inherit;
  font-size: 16px; color: var(--color-text); caret-color: var(--color-accent);
  background: transparent;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
/* Bascule « voir le mot de passe » (app/js/passwordField.js) — l'enveloppe
   n'existe que pour donner un référentiel au bouton posé DANS le champ.
   `display: block` par défaut et aucune marge : elle ne déplace rien, `.input`
   étant déjà en width:100% et `.field` un simple bloc.
   Taille fixe 36×36 (≥ 24px, WCAG 2.5.8), jamais en media query, pour que le
   contrôle CIBLE (audit-clavier) puisse la mesurer sur cette seule classe —
   même parti que `.mastery-close` et `.btn-icon`. Centrage par `margin: auto`
   entre top:0 et bottom:0 plutôt que par une translation : le champ mesure
   40px ou 44px selon l'écran (min-height en ligne sur « Sécurité » et
   « Nouveau mot de passe »), et rien n'a à connaître laquelle des deux.
   Le rembourrage droit du champ dégage exactement la boîte du bouton : sans
   lui, un mot de passe long passerait dessous une fois révélé. */
.pw-field { position: relative; }
.pw-field .input { padding-right: 42px; }
.pw-reveal {
  position: absolute; right: 2px; top: 0; bottom: 0; margin: auto 0;
  width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: var(--radius-sm);
  color: var(--color-text-muted); cursor: pointer;
}
.pw-reveal:hover { color: var(--color-text); }
/* Comme `.input` juste au-dessus : l'anneau est dessiné à 2px du bord par la
   règle générale, et il déborderait ici sur le filet du champ qui l'entoure. */
.pw-reveal:focus-visible { outline-offset: 0; }
/* Le texte d'AMORCE est peint par le navigateur tant que le projet ne le
   déclare pas, et il n'était déclaré nulle part : 27 attributs `placeholder`
   dans app/js, zéro règle `::placeholder` dans les deux feuilles. Ce n'est pas
   une couleur oubliée mais une couleur de texte qui ÉCHAPPAIT aux six jetons —
   la seule du projet. Mesuré sur les fonds réels, en composant l'opacité que
   la feuille de l'agent utilisateur applique à currentColor :
     clair / --color-bg      3,53:1   clair / --color-surface  3,44:1
     (Safari, plus sombre encore : 2,40:1 et 2,37:1)
     sombre / --color-bg     5,04:1   sombre / --color-surface 4,83:1
   c'est-à-dire le seuil qui mord en thème CLAIR et pas en sombre, exactement
   ce que le commentaire des trois gris du bloc :root écrit déjà.
   Le repo avait pourtant tenu le raisonnement UNE fois, sur un champ qui n'a
   justement PAS de placeholder : voir `.mfa-code` plus bas, dont le commentaire
   refuse un « 000000 » parce qu'il « tomberait sous le plancher AA ». La règle
   était donc connue, et appliquée au seul endroit où elle n'avait rien à
   protéger.
   `opacity: 1` n'est pas décoratif et c'est la moitié qui pourrait se perdre à
   la relecture : Firefox pose `opacity: 0.54` sur son ::placeholder, donc sans
   cette remise à 1 notre jeton se composerait à 65 % × 54 % et retomberait à
   2,13:1 — plus bas que le défaut qu'on vient de fermer.
   --color-text-muted et pas un gris plus clair : c'est le plancher du projet,
   et il laisse tout de même l'amorce se distinguer d'une valeur saisie, qui est
   à 100 % de l'encre (`.input` ci-dessus) — la hiérarchie se fait donc AU
   plancher, jamais en dessous.
   `node tools/audit-lisibilite.mjs` (contrôle AMORCE) mesure ce contraste sur
   les placeholders réellement rendus, jamais sur cette seule déclaration. */
::placeholder { color: var(--color-text-muted); opacity: 1; }
/* white-space:pre-wrap est déjà le défaut UA de <textarea> dans tous les
   navigateurs (c'est ce qui préserve nativement ses retours à la ligne) :
   la rendre EXPLICITE ici n'a rien de cosmétique — c'est ce qui permet à
   `tools/audit-lisibilite.mjs` (contrôles MULTILIGNE/DÉBORDEMENT) de
   confronter au rendu un champ dont la fidélité multiligne est aussi
   mesurée là où le brouillon lui-même est réaffiché en direct (aperçu live
   d'une demande, requestPreviewHtml), et non plus seulement là où il est
   relu une fois soumis. */
textarea.input { min-height: 96px; resize: vertical; font-family: var(--font-body); white-space: pre-wrap; overflow-wrap: break-word; }
input[type="range"] { accent-color: var(--color-accent); width: 100%; }

/* — modèles d'exercice (écran "Nouvel exercice", voir exerciseTemplates.js) —
   Une seule déclaration pour les deux dispositions : la rangée de pastilles
   se replie d'elle-même (flex-wrap), elle n'a donc rien à basculer en @media.
   Reprend l'idiome .tools-chip du panneau des outils de pratique (mêmes
   rayon, corps, transition et état actif) plutôt qu'une septième famille de
   pastilles : ce sont les deux seules rangées de choix multiples de l'app. */
.tpl-block { display: flex; flex-direction: column; gap: 9px; }
.tpl-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.tpl-label { font-size: 12px; color: var(--color-text-soft); }
.tpl-clear {
  padding: 2px 0; border: none; background: none; cursor: pointer;
  font-family: var(--font-body); font-size: 12px; color: var(--color-text-muted);
  text-decoration: underline; text-underline-offset: 3px;
  transition: color 140ms ease;
}
@media (hover: hover) and (pointer: fine) {
  .tpl-clear:hover { color: var(--color-text); }
}
.tpl-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.tpl-chip {
  padding: 8px 12px; cursor: pointer;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent;
  font-family: var(--font-body); font-size: 12.5px; line-height: 1.1;
  color: var(--color-text-soft);
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .tpl-chip:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
.tpl-chip:active { transform: scale(0.96); }
.tpl-chip--active {
  border-color: var(--color-accent); color: var(--color-accent-ink);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}
.tpl-hint { margin: 0; font-size: 11.5px; line-height: 1.55; color: var(--color-text-muted); }

/* — double authentification (views/mfa.js) —
   Une seule famille pour l'écran de gestion et l'écran de défi, les deux
   n'ayant qu'un élément vraiment particulier : le champ de code. */

/* Six chiffres qu'on recopie à la main d'un écran à l'autre : chasse fixe
   (tabular-nums, sinon un 1 rétrécit la suite et fait perdre le compte),
   interlettrage large, et centrage. Le corps monte à 22px parce que c'est
   précisément le moment où l'on regarde deux écrans en alternance. */
.mfa-code {
  font-size: 22px; letter-spacing: 0.22em; text-align: center;
  font-variant-numeric: tabular-nums; min-height: 48px;
}
/* Pas de placeholder "000000" : posé assez clair pour ne pas se lire comme une
   valeur déjà saisie, il tomberait sous le plancher AA, et la règle du projet
   est de faire la hiérarchie par la taille ou la casse, jamais par un gris
   plus clair. L'intitulé du champ dit déjà "Code à six chiffres". */

.mfa-body { margin: var(--space-3) 0 0; font-size: 13.5px; line-height: 1.7; color: var(--color-text-soft); max-width: 60ch; text-wrap: pretty; }
.mfa-note { margin: var(--space-3) 0 0; font-size: 12.5px; line-height: 1.6; color: var(--color-text-muted); max-width: 60ch; text-wrap: pretty; }

/* Étapes numérotées plutôt qu'un paragraphe : l'idiome des deux seules
   séquences de l'app où l'utilisateur doit faire quelque chose HORS de l'app
   au milieu d'un écran, et où perdre le fil revient à devoir tout reprendre
   (l'enrôlement du second facteur dans une application tierce, views/mfa.js,
   et la pose de l'icône sur l'écran d'accueil depuis le menu d'iOS,
   views/install.js). Le préfixe .mfa- est historique, comme .cooldown- pour
   les pastilles « i » : c'est la famille d'étapes numérotées de l'app, pas
   une famille réservée à la double authentification. Ne pas en créer une
   seconde, identique, sous un autre nom. */
.mfa-steps { margin: 0; padding: 0 0 0 1.35em; list-style: decimal; }
.mfa-steps > li { margin-top: var(--space-5); }
.mfa-steps > li:first-child { margin-top: 0; }
.mfa-steps > li::marker { color: var(--color-text-muted); font-size: 12.5px; }
.mfa-step-title { margin: 0; font-size: 15px; }
.mfa-step-body { margin: 7px 0 0; font-size: 12.5px; line-height: 1.65; color: var(--color-text-muted); max-width: 56ch; text-wrap: pretty; }

/* Fond blanc épinglé, jamais dérivé du thème : le QR rendu par Supabase est un
   tracé noir, et l'unique chose qu'il ait à faire est de rester scannable sur
   fond sombre comme sur fond clair. Aucun texte n'y est posé, donc rien à
   déclarer dans FIXED_PAIRS (tools/audit-lisibilite.mjs), contrairement aux
   trois plaques qui, elles, portent du lettrage. */
.mfa-qr {
  display: inline-block; margin: var(--space-3) 0 var(--space-1);
  padding: 10px; background: #ffffff;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.mfa-qr img { display: block; width: 164px; height: 164px; }

/* La clé en clair est le chemin normal sur téléphone (impossible de scanner un
   code affiché par l'appareil qui le montre), pas un repli : elle est donc
   sélectionnable d'un geste, groupée par quatre et sur une chasse fixe. */
.mfa-secret {
  margin: 7px 0 0; padding: var(--space-2) var(--space-3);
  font-size: 14px; line-height: 1.5; letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums; word-break: break-all;
  color: var(--color-text); background: color-mix(in srgb, var(--color-text) 6%, transparent);
  border-radius: var(--radius-md); user-select: all;
}

.mfa-confirm {
  margin-top: var(--space-4); padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
}

/* — catégories (categories.js) —
   Une seule famille sert les trois emplois : le champ du formulaire, la
   rangée de filtre en tête des listes, et l'étiquette en lecture seule posée
   sur les cartes. .cat-chip reprend l'idiome de .tpl-chip (mêmes corps,
   transition, état actif) mais en pilule plutôt qu'en rectangle : une
   catégorie est une valeur, un modèle est une action. */
.cat-field { display: flex; flex-direction: column; gap: 7px; }
.cat-field-label { font-size: 12px; color: var(--color-text-soft); }
.cat-field-optional { font-style: italic; color: var(--color-text-muted); }
.desktop-field-label .cat-field-optional { text-transform: none; letter-spacing: 0; }
.cat-field-hint { margin: 0; font-size: 11.5px; line-height: 1.55; color: var(--color-text-muted); }

.cat-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.cat-chip {
  padding: 7px 12px; cursor: pointer; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  border: 1px solid var(--color-divider); border-radius: 999px; background: transparent;
  font-family: var(--font-body); font-size: 12.5px; line-height: 1.15;
  color: var(--color-text-soft);
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .cat-chip:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
.cat-chip:active { transform: scale(0.96); }
.cat-chip--active {
  border-color: var(--color-accent); color: var(--color-accent-ink);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}

/* Rangée de filtre : défile latéralement plutôt que de se replier sur trois
   lignes. Un carnet bien rangé peut avoir dix catégories, et une barre de
   filtre qui pousse la première carte hors de l'écran ferait perdre plus
   qu'elle n'aide. */
.cat-filter {
  display: flex; gap: 6px; margin-bottom: var(--space-3); padding-bottom: 2px;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.cat-filter::-webkit-scrollbar { display: none; }
.cat-filter .cat-chip { flex: none; max-width: 42vw; }
/* Les écrans dont le .screen-scroll n'a pas de padding vertical (Progression,
   Archivés) ont besoin de cet espace en tête ; "Aujourd'hui" (--pad) l'a déjà. */
.screen-scroll:not(.screen-scroll--pad) > .cat-filter { margin-top: var(--space-3); }
.desktop-content-inner > .cat-filter { margin-bottom: var(--space-4); }

/* Étiquette en lecture seule. inline-block plutôt qu'inline-flex : elle vit
   aussi bien dans une rangée flex (bas de carte, ligne de méta de la fiche)
   que dans un bloc de texte (.list-row-sub), où seul un bloc sait tronquer. */
.cat-tag {
  display: inline-block; vertical-align: baseline; flex: none; max-width: 16ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--color-text) 18%, transparent);
  font-size: 11px; line-height: 1.45; letter-spacing: 0.01em; text-transform: none;
  color: var(--color-text-muted);
}
.list-row-sub .cat-tag, .desktop-archive-row-meta .cat-tag { margin-left: 7px; }
.exercise-card--cooldown .cat-tag { border-color: color-mix(in srgb, var(--color-text) 12%, transparent); }


/* — priorité d'exercice (priority.js) — Rouge / Jaune / Vert de Gebrian.

   Deux surfaces et une seule pastille de couleur, .prio-dot, partagée par les
   deux : la rangée de choix du formulaire (.prio-chip, qui reprend .cat-chip
   au trait près — mêmes corps, même transition, même état actif) et
   l'étiquette en lecture seule des listes et de la fiche (.prio-tag, qui
   reprend .cat-tag de la même façon).

   LA COULEUR N'EST JAMAIS SEULE À PORTER LE FAIT : le libellé est écrit à
   côté du point, dans les deux surfaces, et c'est lui que lit un lecteur
   d'écran comme quelqu'un qui ne distingue pas le rouge du vert. Le point est
   donc aria-hidden côté gabarit, et il ne porte aucune couleur de TEXTE — les
   trois jetons tiennent tout de même le seuil AA sur les trois fonds de page
   dans les deux thèmes (voir le commentaire de :root), pour qu'ils restent
   distincts les uns des autres et du fond.

   L'état actif de .prio-chip garde la bordure dorée de .cat-chip--active
   plutôt que de reprendre la couleur du cran : le doré dit « choisi » partout
   ailleurs dans l'app, et deux vocabulaires de couleur sur la même pastille
   (l'un pour la valeur, l'autre pour la sélection) ne se distingueraient
   plus. */
.prio-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.prio-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; cursor: pointer;
  border: 1px solid var(--color-divider); border-radius: 999px; background: transparent;
  font-family: var(--font-body); font-size: 12.5px; line-height: 1.15;
  color: var(--color-text-soft);
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .prio-chip:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
.prio-chip:active { transform: scale(0.96); }
.prio-chip--active {
  border-color: var(--color-accent); color: var(--color-accent-ink);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}

/* inline-flex et non inline-block, contrairement à .cat-tag : cette
   étiquette-ci ne tronque jamais (trois libellés courts et clos), et elle a
   deux enfants à aligner. */
.prio-tag {
  display: inline-flex; align-items: center; gap: 5px; vertical-align: baseline; flex: none;
  white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--color-text) 18%, transparent);
  font-size: 11px; line-height: 1.45; letter-spacing: 0.01em; text-transform: none;
  color: var(--color-text-muted);
}
.list-row-sub .prio-tag, .desktop-archive-row-meta .prio-tag { margin-left: 7px; }
.exercise-card--cooldown .prio-tag { border-color: color-mix(in srgb, var(--color-text) 12%, transparent); }

.prio-dot {
  flex: none; width: 7px; height: 7px; border-radius: 50%;
  /* Un cran clair sur un fond clair (le vert du thème sombre posé sur une
     pastille) resterait un disque sans contour : le filet reprend la couleur
     du point lui-même, assombrie, plutôt qu'une couleur d'encre qui le
     ferait paraître troué. */
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
}

/* — durée d'une routine quotidienne (routines.js, migration 0026) —
   Reprend .prio-chip au trait près (même corps, même transition, même état
   actif) : chaque famille de pastilles de ce repo porte sa propre feuille
   dupliquée plutôt qu'un sélecteur partagé, pour ne jamais faire dépendre
   l'une des trois d'une retouche destinée à une autre. */
.routine-duration-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.routine-duration-chip {
  padding: 7px 12px; cursor: pointer;
  border: 1px solid var(--color-divider); border-radius: 999px; background: transparent;
  font-family: var(--font-body); font-size: 12.5px; line-height: 1.15;
  color: var(--color-text-soft);
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .routine-duration-chip:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
.routine-duration-chip:active { transform: scale(0.96); }
.routine-duration-chip--active {
  border-color: var(--color-accent); color: var(--color-accent-ink);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}

.cat-reset-link {
  margin-top: var(--space-2); padding: 6px 0; border: none; background: none; cursor: pointer;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 13.5px; color: var(--color-accent-ink);
}

/* — tags — */
.tag {
  display: inline-flex; align-items: center; font-size: 11px;
  letter-spacing: 0.02em; padding: 3px 10px;
  border-radius: calc(var(--radius-md) * 0.75);
}
.tag-accent { background: var(--color-accent-100); color: var(--color-on-accent-plate); }
/* Littéral plutôt que var(--color-neutral-100) : badge auto-cohérent (fond
   clair + texte sombre) dans le même esprit que .tag-accent/.sync-badge--pending
   (accent-100 + on-accent-plate, eux aussi fixes) : ne doit jamais suivre le
   nouveau comportement sombre de --color-neutral-100 (voir media query
   plus bas), sous peine de désaccorder ce fond avec son propre texte fixe. */
.tag-neutral { background: #f8f4f4; color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent-ink); }

/* — bandeau environnement non-production (voir index.html) — bloc réel,
   jamais position:fixed : #app/.phone/.desktop-shell se dimensionnent tous
   en vh/dvh sur eux-mêmes, indépendamment de leur parent — les recalculer
   pour compenser un bandeau fixe aurait fallu toucher cette mécanique
   partout. En flux normal, il pousse juste le reste de la page de sa propre
   hauteur (quelques px de défilement de page en plus tout au plus), sans
   jamais recouvrir un contrôle. Couleurs littérales (jamais --color-*) et
   volontairement absentes du reste de la palette "Classical" — ne doit
   jamais suivre le thème clair/sombre ni se confondre avec un toast/badge de
   synchro existant. */
.env-banner {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 6px var(--space-3); text-align: center;
  background: #1c2b45; color: #f3d99a; font-size: 11.5px; line-height: 1.4;
  border-bottom: 1px solid #0e1b30;
}
.env-banner svg { flex: none; }
.env-banner strong { font-weight: 700; }

/* ═══ app shell ═══ */
#app { min-height: 100vh; min-height: 100dvh; display: flex; justify-content: center; background: var(--color-bg); }
.phone {
  width: 100%; max-width: 480px; min-height: 100vh; min-height: 100dvh;
  background: var(--color-surface); display: flex; flex-direction: column; position: relative;
  box-shadow: var(--shadow-md);
}
.screen { display: flex; flex-direction: column; height: 100dvh; }
.screen-header {
  padding: max(var(--space-4), env(safe-area-inset-top)) var(--space-4) var(--space-3);
  border-bottom: 1px solid var(--color-divider); flex: none;
}
.screen-header--row {
  display: flex; align-items: center; gap: var(--space-2);
  padding-top: max(var(--space-3), env(safe-area-inset-top));
}
/* Scopé à detail.js (seul écran à combiner un bouton aligné à droite dans un
   screen-header--row) : .sync-badge (position:absolute sur .phone, jamais
   masqué ici puisque cet écran n'a pas encore de phase desktop — voir
   main.js/DESKTOP_PHASED_VIEWS) partage sinon la même case que le bouton
   d'édition, les deux démarrant au même top:max(--space-3, safe-area-inset-
   top). Même principe de dégagement que .teacher-banner-trailing plus bas :
   la hauteur du badge (~25.5px, mesurée, constante quel que soit son texte
   puisqu'il reste toujours sur une seule ligne) + une marge de respiration
   sont ajoutées au padding-top plutôt qu'au seul bouton, pour que les 3
   éléments du header (retour/titre/édition) restent alignés entre eux. */
.screen-header--between { justify-content: space-between; padding-top: calc(max(var(--space-3), env(safe-area-inset-top)) + 34px); }
/* Bande supérieure des écrans EN PILE (Sécurité, Partage, Installer, Le guide,
   Nouveautés, la double authentification et le questionnaire d'onboarding) :
   un lien de retour au-dessus du surtitre et du titre, d'où un rembourrage
   haut plus généreux que celui de .screen-header. Elle était écrite SEPT fois
   en style en ligne, mot pour mot (`padding:var(--space-5) var(--space-4)
   var(--space-3);border-bottom:1px solid var(--color-divider)`), et aucune des
   sept ne dégageait l'encoche — alors que les vingt bandes qui passent par
   .screen-header ci-dessus le font depuis toujours.

   Le viewport de cette app est déclaré `viewport-fit=cover` et le manifeste
   demande `display: standalone` : dans l'app INSTALLÉE — le cas nominal, voir
   l'écran « Installer le carnet » — la fenêtre couvre donc la barre d'état et
   l'encoche, et `env(safe-area-inset-top)` vaut jusqu'à 59px sur un téléphone
   à Dynamic Island. À 23px de rembourrage, le lien « ← Compte » et le titre de
   ces sept écrans passaient entièrement dessous. Le contrôle « ENCOCHE » de
   tools/audit-clavier.mjs MESURE ce dégagement sur toutes les coques mobiles.

   Le filet est porté ici plutôt qu'en ligne : le questionnaire d'onboarding le
   déclarait sur le bord HAUT de sa liste (border-top) et non sur le bord bas
   de sa bande — deux façons d'écrire la même ligne d'un pixel, au même y. */
.screen-topbar {
  padding: max(var(--space-5), env(safe-area-inset-top)) var(--space-4) var(--space-3);
  border-bottom: 1px solid var(--color-divider);
}
.screen-scroll { flex: 1; overflow-y: auto; padding: 0 var(--space-4) var(--floating-clearance); -webkit-overflow-scrolling: touch; }
.screen-scroll--pad { padding: var(--space-4) var(--space-4) var(--floating-clearance); }
/* Six écrans dégénérés (« Non connecté. », « Aucune séance en cours. ») n'ont
   aucune bande supérieure : leur unique ligne de texte est posée directement
   dans le défilement, donc sous l'encoche. Le rembourrage n'est relevé que
   quand ce conteneur est le PREMIER enfant de .screen — partout ailleurs il
   suit un en-tête, et l'y ajouter doublerait l'écart. Neutre à inset nul :
   max(--space-4, 0) vaut exactement le --space-4 de la règle ci-dessus. */
.screen > .screen-scroll--pad:first-child { padding-top: max(var(--space-4), env(safe-area-inset-top)); }
/* Même exclusion que .screen-scroll, sur le seul autre conteneur défilant en
   pleine largeur de l'app (les listes de l'espace enseignant, .phone--full-
   width n'ouvrant aucune colonne de sidebar qui l'en écarterait
   horizontalement, contrairement à .desktop-content — voir plus bas) —
   --teacher-floating-clearance et non --floating-clearance : voir sa
   déclaration plus haut, qui documente pourquoi le bouton flottant "+" ne
   peut jamais y apparaître. */
/* Le bouton flottant grandit de --session-resume-h quand la bande "Séance en
   cours" s'intercale au-dessus de la barre d'onglets (voir .app-feedback-
   trigger et .screen:has(.session-resume) .fab plus bas) : la réserve doit
   grandir d'autant. .teacher-roster n'est PAS de la partie : .session-resume
   n'est rendue que par tabbar(), jamais appelée dans l'espace enseignant, qui
   a son propre bandeau — un sélecteur qui la couvrirait quand même décrirait
   un cas qui ne peut pas se produire. */
.screen:has(.session-resume) .screen-scroll,
.screen:has(.session-resume) .screen-scroll--pad {
  padding-bottom: calc(var(--floating-clearance) + var(--session-resume-h));
}

/* — today/archive list — */
.list-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-3) 0; border-bottom: 1px solid var(--color-divider); cursor: pointer;
}
.list-row-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 17px; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.list-row-sub { font-size: 12px; color: var(--color-text-muted); margin-top: 2px; }
/* provenance "Devoir de {prof}" en archivés (phase 5/5) — simple souvenir en
   10.5px, au-dessus du titre, jamais de fond ni de cadre. */
.list-row-provenance {
  display: flex; align-items: center; gap: 4px; margin-bottom: 2px;
  font-size: 10.5px; font-style: italic;
  color: var(--color-text-muted);
  overflow: hidden; white-space: nowrap;
}
.pastilles { display: flex; gap: 5px; flex: none; }
.pastille { width: 6px; height: 6px; border-radius: 50%; border: 1px solid var(--color-accent-500); }
.pastille--on { background: var(--color-accent-500); }
.pastille--off { background: transparent; }

/* — carte d'exercice (Aujourd'hui) — */
.exercise-card {
  background: var(--color-neutral-100); border: 1px solid var(--color-divider);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-4);
  cursor: pointer; display: flex; flex-direction: column; gap: var(--space-3);
  margin-bottom: var(--space-3);
  /* overflow intentionnellement laissé à sa valeur par défaut (visible) :
     le popover "Pourquoi une heure" est positionné en absolute à l'intérieur
     et ne doit jamais être rogné. */
}
.exercise-card:hover { box-shadow: var(--shadow-md); border-color: color-mix(in srgb, var(--color-accent) 45%, transparent); }
/* Retour au pressé. La carte se tasse à peine (0.985, pas le 0.97 des
   boutons) : le geste porte ici sur une surface large comme l'écran, où le
   même ratio se lirait comme un saut. Exclut .exercise-card--static (bloc
   "Identité" du Compte, plus bas), qui réutilise l'écrin sans être
   cliquable. */
.exercise-card:not(.exercise-card--static) {
  transition: transform 140ms var(--ease-out), box-shadow 140ms var(--ease-out), border-color 140ms var(--ease-out);
}
.exercise-card:not(.exercise-card--static):active { transform: scale(0.985); }
/* En cooldown, la carte reste cliquable : elle ouvre la fiche (voir le
   commentaire de renderCard, views/today.js). Seul son survol perd le doré,
   qui promettrait "Travailler" alors que la séance, elle, attend l'heure. */
.exercise-card--cooldown {
  background: repeating-linear-gradient(135deg, transparent 0 7px, color-mix(in srgb, var(--color-text) 3.5%, transparent) 7px 8px), var(--color-surface);
  border-color: color-mix(in srgb, var(--color-text) 11%, transparent);
  box-shadow: none;
}
.exercise-card--cooldown:hover {
  box-shadow: var(--shadow-sm);
  border-color: color-mix(in srgb, var(--color-text) 22%, transparent);
}
.exercise-card-top { display: flex; align-items: flex-start; gap: var(--space-3); position: relative; }
.exercise-card-kicker {
  font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--color-text-muted); margin-bottom: 4px;
}
.exercise-card--cooldown .exercise-card-kicker { color: var(--color-text-muted); }
/* provenance "Devoir de {prof}" (phase 5/5) — occupe la même ligne que le
   kicker (jamais les deux ensemble sur une même carte), doré à l'état
   normal ; en cooldown, perd le doré et suit le recul de la carte comme le
   reste — le doré reste l'exclusivité de l'heure de retour du cooldown. */
.exercise-card-provenance {
  display: flex; align-items: center; gap: 5px; margin-bottom: 4px;
  font-size: 11.5px; font-style: italic; color: var(--color-accent-ink);
  overflow: hidden; white-space: nowrap;
}
.exercise-card--cooldown .exercise-card-provenance { color: var(--color-text-muted); }
.exercise-card-title { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 22px; line-height: 1.2; }
.exercise-card--cooldown .exercise-card-title { color: var(--color-text-soft); }
.exercise-card-dots { display: flex; gap: 5px; padding-top: 7px; flex: none; }
.exercise-card-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--color-accent-600); }
.exercise-card-dot--off { background: transparent; border: 1px solid color-mix(in srgb, var(--color-text) 30%, transparent); box-sizing: border-box; }
.exercise-card--cooldown .exercise-card-dot { background: var(--color-accent-400); }
.exercise-card--cooldown .exercise-card-dot--off { border-color: color-mix(in srgb, var(--color-text) 22%, transparent); }
.exercise-card-divider { height: 1px; background: var(--color-divider); }
.exercise-card--cooldown .exercise-card-divider { background: color-mix(in srgb, var(--color-text) 10%, transparent); }

/* flex-wrap + marge auto plutôt que justify-content:space-between : le
   libellé de phase (flex-shrink:0) ne doit jamais être rogné ni passer à la
   ligne — c'est le groupe de droite (CTA ou pastille+i) qui bascule sous lui
   si l'écran est trop étroit pour tenir les deux sur une ligne. */
.exercise-card-bottom { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
.exercise-card-phase {
  font-size: 12.5px; color: var(--color-text-soft);
  white-space: nowrap; flex-shrink: 0;
}
.exercise-card--cooldown .exercise-card-phase { color: var(--color-text-muted); }
.exercise-card-cta {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 14px;
  color: var(--color-accent-ink); white-space: nowrap; flex-shrink: 0; margin-left: auto;
}
/* "Voir la fiche →" d'une carte en cooldown : suit immédiatement le groupe
   badge + "i" (margin-left annulée, sinon les deux marges automatiques se
   partageraient l'espace libre et écarteraient le badge de son libellé), et
   reste gris — le doré est réservé à ce qu'on peut faire tout de suite. */
.exercise-card-cta--muted { color: var(--color-text-muted); margin-left: 0; }
.exercise-card-right {
  display: flex; align-items: center; gap: 8px; flex-shrink: 0; margin-left: auto; position: relative;
}

.cooldown-badge {
  display: flex; align-items: center; gap: 6px; flex: none;
  background: color-mix(in srgb, var(--color-accent) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-accent) 26%, transparent);
  border-radius: 999px; padding: 4px 10px 4px 8px;
}
.cooldown-badge-label { font-size: 12px; color: var(--color-accent-ink); white-space: nowrap; }
.cooldown-info-wrap { position: relative; display: flex; align-items: center; flex: none; }
.cooldown-info-btn {
  width: 17px; height: 17px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--color-accent) 40%, transparent);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 11px; line-height: 1; color: var(--color-accent-ink); user-select: none;
  background: none; padding: 0;
  /* La pastille pose déjà sa police, sa graisse, son corps, son interligne et
     sa couleur : ces deux-là complètent la remise à zéro, et elles sont
     nécessaires depuis que le titre du bloc « Bourdon » en porte une.
     .tools-label est en `text-transform: uppercase` + `letter-spacing: 0.14em`,
     et les deux s'HÉRITENT — le « i » s'y afficherait « I », suivi d'un blanc
     d'interlettrage à l'intérieur d'un cercle de 17px. Neutre aux huit autres
     points d'appel, dont aucun ancêtre ne déclare l'un ou l'autre (vérifié sur
     le HTML réellement rendu, chaîne d'ancêtres remontée jusqu'à la racine). */
  text-transform: none; letter-spacing: normal;
}
.cooldown-info-btn:hover { border-color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 9%, transparent); }
/* Cible tactile. La pastille mesure 17px pour ne pas alourdir les rangées
   denses où elle vit (carte "Aujourd'hui", ligne de méta d'une fiche), très en
   dessous de ce qui s'attrape au doigt. Ce pseudo-élément l'étend à 35px sans
   toucher à la mise en page ni pousser ses voisins. 9px et pas davantage :
   c'est l'écart le plus serré entre la pastille et le contrôle d'à côté (la
   pilule de micro-pause, views/active.js), et une zone plus large lui volerait
   ses taps de bord. */
.cooldown-info-btn { position: relative; }
.cooldown-info-btn::before { content: ''; position: absolute; inset: -9px; border-radius: 50%; }
.cooldown-popover {
  position: absolute; top: 26px; right: -6px; width: 284px; z-index: 5;
  background: var(--color-neutral-100); border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
  border-radius: var(--radius-md); box-shadow: var(--shadow-md); padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-2); text-align: left;
}
.cooldown-popover-label { font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent-ink); }
.cooldown-popover-body { font-size: 12.5px; line-height: 1.6; color: var(--color-text); }
.cooldown-popover-divider { height: 1px; background: var(--color-divider); }
.cooldown-popover-source { font-size: 10.5px; line-height: 1.45; color: var(--color-text-muted); }
/* Popover micro-pause desktop (écran "Session en cours") — rendu hors de
   .desktop-stack-card (voir desktopMicroPopoverHtml, views/active.js) pour
   échapper à son overflow:hidden (nécessaire aux coins arrondis de la carte,
   jamais à retirer), qui rognait le bas du popover. position:fixed plutôt
   qu'absolute : coordonnées top/left posées en JS (positionDesktopMicroPopover,
   main.js) par rapport au viewport, à partir de la position réelle du bouton
   "i" — jamais relatives à un parent, qui n'est plus le bon repère une fois
   le popover sorti de la carte. */
.cooldown-popover--floating { position: fixed; top: 0; left: 0; right: auto; transform: translateX(-50%); width: 320px; }

/* — bloc d'attente du cooldown, fiche exercice (voir cooldownWaitHtml,
   views/detail.js) — Prend la place du bouton "Démarrer une session"
   pendant l'heure d'écart entre deux répétitions bonus, en mobile comme en
   desktop, et lui emprunte donc sa largeur pleine (align-self, la colonne
   desktop alignant ses enfants sur flex-start). Un lavis doré et non un
   fond gris : on attend une heure, on n'a rien fait de mal. L'explication
   s'ouvre en encart sous le texte, jamais en popover absolu, qui serait
   rogné par l'overflow:hidden de .desktop-stack-card. */
.cooldown-wait {
  align-self: stretch; width: 100%; box-sizing: border-box;
  border: 1px solid color-mix(in srgb, var(--color-accent) 26%, transparent);
  background: color-mix(in srgb, var(--color-accent) 7%, transparent);
  border-radius: var(--radius-md); padding: var(--space-4);
}
.cooldown-wait-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.cooldown-wait-hour {
  display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 17px;
  color: var(--color-accent-ink-strong);
}
.cooldown-wait-body {
  margin: 7px 0 0; font-size: 13px; line-height: 1.65;
  color: var(--color-text-soft); text-wrap: pretty;
}
.cooldown-wait-note {
  margin-top: var(--space-3); background: var(--color-neutral-100);
  border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
  border-radius: var(--radius-md); padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-2); text-align: left;
}

.empty-state {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 80px 20px 0; gap: var(--space-2);
}
.empty-ring { width: 44px; height: 44px; border-radius: 50%; border: 1.5px solid var(--color-accent); margin-bottom: var(--space-2); }
.empty-text { text-align: center; margin-top: 60px; color: var(--color-text-muted); font-size: 13px; }

/* — tabbar — */
.tabbar { display: flex; border-top: 1px solid var(--color-divider); padding: var(--space-2) 0 max(var(--space-2), env(safe-area-inset-bottom)); flex: none; }
/* Enveloppes de la sémantique de liste (voir tabbar, render.js) : le <nav> ne
   peut pas porter role="list" sans perdre son repère de page, et le <button>
   ne peut pas porter role="listitem" sans cesser d'être un bouton. Le
   `flex: 1` qui donne son quart de barre à un onglet vivait donc sur un
   élément qui n'est plus enfant direct de .tabbar, et il est repris à chaque
   cran : la liste remplit la barre, chaque item en prend le quart, et
   `.tab { flex: 1 }` reste vivante (le bouton est le seul flex item de son
   enveloppe, qu'il remplit). Deux flex imbriqués à `1 1 0%` avec le même
   `min-width: auto` en dessous donnent exactement la géométrie d'avant. */
.tabbar-list { display: flex; flex: 1; }
.tabbar-item { display: flex; flex: 1; }
.tab {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
  cursor: pointer; font-size: 11px; padding: 4px 0;
  color: var(--color-text-muted); background: none; border: none;
  font-family: var(--font-body);
}
.tab--active { color: var(--color-accent-ink); }

/* — rappel de séance en cours (mobile) —
   Bande d'une seule ligne intercalée entre le contenu et la barre d'onglets
   (voir sessionResumeBar, render.js), donc au-dessus d'elle et jamais
   par-dessus le contenu : .screen est une colonne flex de 100dvh qui ne
   défile pas, un encart flottant y recouvrirait la liste. Vrai <button>
   (nom accessible complet, aucun contrôle imbriqué : rien à marquer pour
   audit-clavier.mjs). */
.session-resume {
  flex: none; display: flex; align-items: center; gap: 9px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 9px var(--space-4); border: none;
  border-top: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
  background: color-mix(in srgb, var(--color-accent) 8%, transparent);
  color: var(--color-accent-ink); font-family: var(--font-body); font-size: 12.5px;
}
.session-resume-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.session-resume-ex { color: var(--color-text-soft); }
.session-resume-cta { flex: none; color: var(--color-accent-ink-strong); }

/* — fab — */
.fab {
  position: absolute; right: var(--space-4); bottom: calc(84px + var(--bottom-bar-grow));
  border-radius: 50%; width: 52px; height: 52px;
  background: var(--color-surface); box-shadow: var(--shadow-md);
  display: flex; align-items: center; justify-content: center;
  color: var(--color-accent-ink); cursor: pointer; border: 1px solid var(--color-accent);
}
/* Le bouton flottant est posé à 84px du bas, c'est-à-dire juste au-dessus de
   la barre d'onglets — la seule chose qui vivait là. Quand la bande de reprise
   de séance s'intercale entre les deux, il faut le remonter de sa hauteur,
   sinon il recouvre son bord droit ("Reprendre"). « Aujourd'hui » est le seul
   écran à porter les deux (le .fab n'est rendu que par views/today.js), d'où
   ce :has() plutôt qu'une hauteur portée par .screen.
   Les deux offsets passent par --bottom-bar-grow et --session-resume-h (voir
   :root) plutôt que par les littéraux 84/121 : 84 + 37 vaut exactement le 121
   d'avant, mais la barre d'onglets grandit du home indicator et le
   raisonnement de ce commentaire ("juste au-dessus de la barre d'onglets") ne
   tenait donc pas sur le téléphone même où cette PWA s'installe — le bouton y
   traversait le filet supérieur de la barre. */
.screen:has(.session-resume) .fab { bottom: calc(84px + var(--session-resume-h) + var(--bottom-bar-grow)); }

/* — signalement générique (volet 4) — */
/* Déclencheur persistant, rendu une fois par currentViewHtml (main.js) et
   présent sur CHAQUE écran, mobile et desktop. D'abord posé au bord droit,
   centré verticalement : mesuré sur le rendu réel, un texte pleine largeur
   (la consigne d'un champ, une carte de routine) passe alors SOUS le
   bouton à mi-hauteur d'écran, sur un état ou un autre selon l'écran — le
   milieu de l'écran est justement la zone la plus dense en contenu
   arbitraire, contrairement aux deux coins déjà réservés par ce fichier.

   Repris à l'identique de .fab (bas GAUCHE cette fois, pour ne jamais
   coïncider avec lui) : mêmes 84px depuis le bas, mêmes jetons
   --bottom-bar-grow/--session-resume-h.

   Deux défauts corrigés ensemble, parce qu'ils ont la même cause — ce
   bouton posé SUR du contenu dont la hauteur totale n'est jamais connue
   d'avance — et se manifestaient chacun d'un côté de cette même cause :

   1. CHEVAUCHEMENT (mobile, et desktop sans sidebar) : le commentaire
      précédent tenait pour acquis qu'« un écran sans tabbar n'a alors
      qu'un peu d'air sous le bouton, jamais une collision » — mesuré faux
      sur Compte, Sécurité, Installer, l'écran de séance, l'évaluation et
      Nouveautés, où une carte ou un bouton se retrouvait DERRIÈRE lui dès
      le premier rendu, sans le moindre défilement. La réserve qui l'évite
      vit sur chaque conteneur défilant réel (--floating-clearance sur
      .screen-scroll/.screen-scroll--pad, aussi sur les .sheet de l'espace
      enseignant, en style en ligne faute de règle dédiée ;
      --teacher-floating-clearance, plus étroit, sur le seul
      .teacher-roster, voir sa déclaration plus haut) : ce n'est plus ce
      bouton qui doit deviner s'il couvre quelque chose, c'est le
      conteneur qui ne PEUT plus y peindre de contenu à cet endroit.

      EN PADDING-BOTTOM par défaut (aucune amputation permanente de la
      zone visible pendant le défilement normal), sauf sur les quelques
      panneaux dont le contenu réel peut tenir sans jamais défiler — voir
      le raisonnement complet, avec les deux défauts mesurés qui ont
      chacun décidé de la mécanique employée, à la déclaration de
      --floating-clearance plus haut.

   2. DISPARITION (desktop SANS sidebar — Sécurité, Nouveautés, Installer,
      Le guide — dont la mise en page n'a pas de .desktop-content interne :
      c'est le DOCUMENT entier qui défile). `position: absolute` ancre ce
      bouton à .phone, qui couvre le viewport sur les écrans à sidebar
      (hauteur fixe, défilement interne) mais grandit avec son contenu sur
      ceux-ci (rien n'y défile en interne) : mesuré, .phone y atteint
      1273px de haut pour un viewport de 900px, et bottom:84px plaçait donc
      le bouton à 84px du bas d'une page presque deux fois plus haute que
      l'écran — invisible tant qu'on n'a pas défilé jusqu'au bout. `fixed`
      l'ancre au viewport réel plutôt qu'à cet ancêtre de hauteur variable :
      sans effet sur les écrans où .phone fait déjà exactement la hauteur
      du viewport (mobile, desktop à sidebar), puisque les deux valent
      alors la même chose ; et cela suffit à le rendre réellement flottant
      sur les écrans qui n'ont pas de sidebar. Aucun ancêtre de .phone ne
      pose de `transform`/`filter`/`contain`, qui changerait le référentiel
      de `fixed` — vérifié, sans quoi ce correctif ne tiendrait pas.

   z-index sous .modal-backdrop (10) : tout autre panneau ouvert le couvre
   déjà, donc ce bouton ne peut jamais être cliqué par-dessus une autre
   surcouche. */
.app-feedback-trigger {
  position: fixed; left: var(--space-4); bottom: calc(84px + var(--bottom-bar-grow));
  z-index: 5;
  width: 40px; height: 40px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--color-surface); box-shadow: var(--shadow-sm);
  border: 1px solid var(--color-accent);
  color: var(--color-accent-ink); cursor: pointer;
}
/* .phone:has(...) > .toast et non .screen:has(...) .fab : .app-feedback-
   trigger est un enfant DIRECT de .phone (comme .toast), jamais de .screen
   (comme .fab, qui n'existe que sur "Aujourd'hui") — même sélecteur
   exactement que .toast un peu plus haut. `position: fixed` ne change rien
   à la validité de ce sélecteur : `:has()` porte sur la relation DOM,
   jamais sur la propriété `position` de l'élément positionné. */
.phone:has(.session-resume) > .app-feedback-trigger { bottom: calc(84px + var(--session-resume-h) + var(--bottom-bar-grow)); }

/* — detail screen — */
.detail-title { margin: 0 0 4px; }
.detail-meta { font-size: 12px; color: var(--color-text-muted); margin-bottom: var(--space-3); }
/* overflow-wrap: break-word — la consigne est un texte que l'UTILISATEUR
   tape, et rien ne borne la longueur d'un mot qu'il y colle. Le cas n'est pas
   théorique : linkifyText (render.js) existe précisément pour rendre cliquable
   une URL collée dans ce champ, et une URL est le jeton insécable par
   excellence. Sans cette déclaration, la valeur initiale `normal` interdit
   toute coupure ailleurs qu'à une espace, et le bloc déborde de sa boîte —
   ici dans .screen-scroll, dont l'`overflow-y: auto` fait calculer
   `overflow-x: auto` (CSS Overflow), donc une barre de défilement horizontale
   sur l'écran entier ; sur le site desktop de la même consigne, dans
   .desktop-stack-card, dont l'`overflow: hidden` ROGNE la fin du lien, qui
   n'est alors ni lisible ni cliquable. `break-word` et non `anywhere` : la
   spécification l'exclut du calcul des tailles intrinsèques min-content, donc
   il ne peut par construction modifier aucune mise en page qui tient
   aujourd'hui — il n'agit que là où le texte débordait déjà. Le repo
   connaissait le problème (.mfa-secret porte `word-break: break-all`) pour le
   seul jeton que l'app ENGENDRE, jamais pour celui que l'utilisateur colle. */
.detail-notes { font-size: 13px; font-style: italic; opacity: 0.8; margin: 0 0 var(--space-3); white-space: pre-wrap; overflow-wrap: break-word; }
/* Liens détectés dans un texte libre (Consignes, voir linkifyText dans
   render.js) — mobile (.detail-notes) et desktop (.desktop-notes-text,
   views/detail.js), plus la fiche élève de l'espace enseignant
   (.teacher-notes-text, déclarée plus bas), troisième et dernier site de rendu
   d'une consigne. Classe séparée plutôt que de réutiliser .detail-notes
   sur le bloc desktop : celui-ci n'est ni italique ni atténué, contrairement
   au style mobile. Un site de rendu de consigne qui manquerait à cette liste
   n'échouerait nulle part : ses liens y prendraient simplement le bleu
   souligné du navigateur. */
.detail-notes a, .desktop-notes-text a, .teacher-notes-text a { color: var(--color-accent-ink); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--color-accent) 40%, transparent); }
.detail-notes a:hover, .desktop-notes-text a:hover, .teacher-notes-text a:hover { text-decoration-color: currentColor; }
.journal-heading { color: var(--color-text-soft); margin-bottom: var(--space-3); }
/* cursor:pointer + survol doré : édition d'une séance passée (voir
   views/sessionEdit.js) — même effet de survol que les lignes desktop
   .desktop-archive-row (marge négative pour étendre la teinte jusqu'aux
   bords de la carte, sans changer la mise en page environnante). */
.journal-entry {
  padding: var(--space-3) var(--space-2); margin: 0 calc(var(--space-2) * -1) var(--space-4);
  border-bottom: 1px solid var(--color-divider); border-radius: var(--radius-sm);
  cursor: pointer; transition: background 0.15s;
}
.journal-entry:hover { background: color-mix(in srgb, var(--color-accent) 5%, transparent); }
/* `.journal-entry:last-child { border-bottom: none; margin-bottom: 0 }` vivait
   ici et ne matchait RIEN : `.detail-danger-zone` suit toujours le journal dans
   .screen-scroll (views/detail.js), donc aucune entrée n'est le dernier enfant
   de son parent — mesuré sur le HTML réellement rendu, 0 sur 29 entrées dans 12
   états. La règle a été supprimée AVANT d'envelopper les entrées dans un
   role="list", qui aurait sinon fait de la dernière un vrai :last-child et
   retiré son filet : un correctif de sémantique n'a pas à changer le dessin.
   Rétablir l'intention d'origine (pas de filet sous la dernière entrée) est un
   point à arbitrer, pas une régression à rattraper. */
.journal-entry-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.journal-entry-date { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 14px; }
.journal-entry-obj { font-size: 13px; margin-bottom: 4px; }
.journal-entry-dur-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.journal-entry-dur { font-size: 11px; color: var(--color-text-muted); }
/* white-space: pre-wrap — la note de séance est le SECOND champ de l'app saisi
   dans un <textarea> (views/eval.js et views/sessionEdit.js, quatre zones pour
   un seul champ), donc le second à pouvoir contenir des retours à la ligne. Le
   premier, la consigne d'exercice, porte ce même pre-wrap sur ses trois sites
   de rendu (.detail-notes, .desktop-notes-text, .teacher-notes-text) ; la note,
   elle, ne l'avait sur aucun des deux siens, si bien que le journal — seul
   écran qui l'affiche — aplatissait en une phrase ce qui avait été écrit en
   plusieurs lignes. Sans danger pour les guillemets qui l'encadrent : les deux
   chemins d'écriture (submitEvaluation, sessionEditChanges — store.js)
   appliquent `.trim()`, donc une note ne commence ni ne finit par un blanc.
   overflow-wrap: break-word — même raison que sur `.detail-notes` : la note
   est l'autre champ que l'utilisateur tape, et un jeton insécable y déborde
   de la même façon. */
.journal-entry-note { font-size: 12px; font-style: italic; opacity: 0.75; margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: break-word; }

.desktop-journal-entry { cursor: pointer; border-radius: var(--radius-sm); transition: background 0.15s; margin: 0 calc(var(--space-2) * -1); padding-left: var(--space-2); padding-right: var(--space-2); }
.desktop-journal-entry:hover { background: color-mix(in srgb, var(--color-accent) 5%, transparent); }

/* Les deux icônes de fin de ligne du Journal, crayon et corbeille. Ce sont
   deux VRAIS <button> : la ligne entière est cliquable à la souris, mais elle
   contient déjà la corbeille, et une ligne marquée role="button" autour d'un
   bouton rendrait celui-ci inatteignable au lecteur d'écran (deux contrôles
   imbriqués). Le crayon, longtemps un <span> décoratif, porte donc désormais
   lui-même l'ouverture de l'édition : c'est le contrôle nommé du clavier, la
   ligne reste la grande cible de la souris. Un clic sur l'une des deux icônes
   ne déclenche jamais l'action de la ligne, closest() les trouvant avant elle
   dans la délégation d'évènements. */
.journal-edit-btn, .journal-delete-btn {
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 2px; margin: 0; cursor: pointer;
  color: var(--color-text-muted);
}
/* Cible tactile. Les deux boutons mesurent 17px de côté (ICON_PENCIL(13) /
   ICON_TRASH(13) plus 2px de rembourrage, box-sizing: border-box) — exactement
   la taille de .cooldown-info-btn, dont le commentaire écrit déjà « très en
   dessous de ce qui s'attrape au doigt » et qui porte pour cette raison son
   propre pseudo-élément d'extension depuis toujours. Ces deux-là n'en avaient
   aucun, et ce sont les SEULS contrôles de l'app sous les 24px de côté que
   demande WCAG 2.5.8 (niveau AA) : `.btn-icon` fait 40, `.tools-close` 32, le
   bouton flottant 52. Celui qui SUPPRIME une séance était donc le plus petit
   contrôle du carnet, à 13,8px de son voisin.

   Même mécanisme que la pastille « i » : un ::before absolument positionné,
   donc hors flux, qui n'ajoute pas un pixel à la mise en page et ne pousse
   aucun voisin (un enfant absolu n'est pas un élément flex).

   La MOITIÉ de l'écart et pas davantage. Les deux coques posent le même
   `gap: var(--space-3)` entre le crayon et la corbeille (voir journalEntry et
   desktopJournalEntry, views/detail.js), et ici les DEUX voisins s'étendent —
   contrairement à la pastille « i », dont le voisin n'a pas d'extension et qui
   peut donc prendre l'écart entier. Les deux zones se touchent sans jamais se
   recouvrir : 17 + 13,8 = 30,8px de côté, au-dessus du plancher de 24. Aller
   plus loin demanderait d'écarter les deux boutons, donc de toucher la
   maquette.

   Contrepartie assumée : verticalement la zone déborde sur la ligne, qui est
   elle-même cliquable (`data-action="open-session-edit"` sur .journal-entry).
   Sur le crayon c'est la même action ; sur la corbeille, un tap de bord ouvre
   la demande de confirmation au lieu de l'édition — un dialogue, jamais une
   suppression. `node tools/audit-clavier.mjs` (contrôle CIBLE) le vérifie. */
.journal-edit-btn, .journal-delete-btn { position: relative; }
.journal-edit-btn::before, .journal-delete-btn::before {
  content: ''; position: absolute; inset: calc(var(--space-3) / -2);
}
/* Couleur d'erreur au survol/focus sur la corbeille plutôt qu'un simple
   changement de forme, cohérent avec .link-btn-danger (fiche exercice,
   suppression de l'exercice entier) ; le crayon va vers le doré, pour que les
   deux gestes ne se ressemblent jamais au moment où on les vise. */
.journal-delete-btn:hover, .journal-delete-btn:focus-visible { color: var(--color-error); }
.journal-edit-btn:hover, .journal-edit-btn:focus-visible { color: var(--color-accent-ink-strong); }

/* Quatre contrôles-icône de « Gérer mes routines » (monter/descendre/
   modifier/supprimer une routine personnelle, views/routines.js) : 32px de
   côté en dur, contrairement au crayon/corbeille du Journal, qui n'ont pas
   cette marge et ont donc besoin d'une extension ::before. Au-dessus du
   plancher de 24px (WCAG 2.5.8) sans extension déclarée dans
   TOUCH_EXTENSIONS (tools/audit-clavier.mjs, contrôle CIBLE). */
.routine-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none;
  background: none; border: none; cursor: pointer; padding: 0;
  color: var(--color-text-muted);
}
.routine-icon-btn:hover, .routine-icon-btn:focus-visible { color: var(--color-accent-ink-strong); }

/* — actions archiver/désarchiver/supprimer (fiche exercice) — */
.detail-danger-zone {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--space-3); margin-top: var(--space-4);
}

/* — « Retirer cet élève » (fiche élève enseignant) — */
/* Même déclaration que .detail-danger-zone, sous un nom propre à cette
   famille : teacherStudentDetail.js n'emploie qu'un seul markup mobile-first
   (reflow CSS seul, aucune double coque), donc .link-btn-danger seul suffit
   ici, sans .desktop-link-danger. */
.teacher-danger-zone {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--space-3); margin-top: var(--space-4);
}

/* — session active — */
/* Vrai <button> et non un <span> : "Annuler" abandonne une séance en cours,
   la seule sortie de cet écran, et rien ne justifiait qu'elle soit le seul
   contrôle de l'app hors d'atteinte du clavier. Réinitialisation d'apparence
   au passage (même idiome que .desktop-signout-link), pour que le texte reste
   identique au pixel près. */
/* .session-pause partage cette réinitialisation : c'est le même genre de
   contrôle (un vrai <button> réduit à son texte, aux deux bouts de l'en-tête
   de séance), et deux règles jumelles finiraient par diverger. Elle est POSÉE
   LÀ plutôt qu'en pleine largeur sous "Terminer la séance" pour une raison
   mécanique : .session-center ne peut pas descendre sous son anneau de 220px
   (min-height:auto sur un enfant flex), .screen fait exactement 100dvh et ne
   défile pas, et le bloc du bas grandit déjà de deux lignes pendant le temps
   supplémentaire — un second bouton pleine largeur poussait donc "Terminer la
   séance" hors de l'écran sur les téléphones les plus courts. L'en-tête, lui,
   ne contient que "Annuler" et ne coûte pas une ligne de plus. */
/* .session-adjust rejoint la même règle, et pour la même raison qu'y a rejoint
   .session-pause : c'est le même genre de contrôle (un vrai <button> réduit à
   son texte, rangé avec les autres commandes de séance), et une troisième règle
   jumelle finirait par diverger des deux premières. Elle ouvre le panneau
   « Ajuster la séance » (views/sessionAdjust.js) depuis l'en-tête mobile — où
   elle ne coûte pas une ligne, contrainte de hauteur ci-dessus — et depuis la
   rangée d'actions de la carte desktop. */
.session-cancel, .session-pause, .session-adjust {
  font-size: 13px; color: var(--color-accent-ink); cursor: pointer;
  background: none; border: none; padding: 0; font-family: inherit;
}
.session-center { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-4); padding: 0 var(--space-4); text-align: center; }
/* Nom de l'exercice travaillé — la MÊME règle pour les deux dispositions
   (en-tête de la carte desktop et haut du bloc central mobile), qui portaient
   jusqu'ici l'une des styles en ligne et l'autre rien du tout. Une seule
   ligne, tronquée à l'ellipse : .screen fait 100dvh sans défilement, et
   .session-center ne peut pas descendre sous son anneau de 220px.
   Aucune marge ici : l'en-tête desktop est un flex align-items:center, et une
   marge basse y décalerait le titre vers le haut par rapport à la mention
   d'état posée en face. L'écart au libellé « Objectif » est donc porté en
   ligne, du seul côté mobile. */
.session-exercise {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 19px; line-height: 1.2; letter-spacing: normal; margin: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.session-objectif-label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 4px; }
.session-objectif { font-size: 15px; max-width: 260px; margin: 0 auto; }
.session-total { font-size: 12px; color: var(--color-text-muted); }
.timer-time { font-family: var(--font-heading); font-variant-numeric: tabular-nums; fill: var(--color-text); }

/* — temps supplémentaire (minuteur écoulé, la séance continue) —
   Seul le grand chiffre passe en doré : c'est le seul élément assez grand
   pour que --color-accent reste lisible sur les deux thèmes (fond crème
   comme fond encre). Les deux mentions sous l'anneau restent sur les gris de
   texte habituels, qui suivent le thème d'eux-mêmes, plutôt que sur
   un doré, qui n'apporterait rien de plus à cette taille-là.
   L'anneau, lui, n'a rien de spécial à faire : il représente le temps qui
   RESTE, il est donc simplement vide (voir timerReadout dans views/active.js). */
.timer-time--overtime { fill: var(--color-accent); }
.session-overtime-note { color: var(--color-text-soft); }
.session-overtime-hint {
  font-size: 12px; font-style: italic;
  color: var(--color-text-muted);
}
/* Uniquement sous la mention (mobile) : sur desktop, la phrase est un enfant
   direct de .desktop-session-body, dont le `gap` gère déjà l'espacement. */
.session-overtime-note + .session-overtime-hint { margin-top: 3px; }
/* Apparition des deux mentions et bascule du chiffre : réutilise lb-soft,
   l'idiome d'apparition douce déjà validé ailleurs (micro-pause terminée,
   voir plus bas), avec la courbe --ease-out du fichier plutôt que le
   `ease-out` natif, trop mou pour un changement d'état qu'on veut voir
   arriver. Une seule passe, jamais en boucle : ce moment n'a lieu qu'une
   fois par séance. */
.timer-time--overtime, .session-overtime-note, .session-overtime-hint,
.desktop-session-time--overtime {
  animation: lb-soft 320ms var(--ease-out) both;
}

/* — eval — */
/* Les trois choix d'évaluation, coque mobile (écran d'évaluation ET édition
   d'une séance passée : même fonction de rendu, ratingChoicesMobileHtml dans
   views/eval.js). Deux lignes centrées — le mot, puis le critère qui le
   départage —, là où la pastille n'en portait qu'une.

   La typographie de titre descend sur .eval-choice-label : posée sur le
   conteneur, elle était héritée par le critère, qui doit rester en police de
   corps comme son jumeau desktop (.desktop-eval-choice-hint).

   Le critère garde --color-text-muted à l'état sélectionné, au lieu de suivre
   le doré hérité comme le fait le desktop par opacity: 0.72. Sur le lavis doré
   d'une pastille sélectionnée, 72 % de --color-accent-ink passerait sous le
   plancher AA pour un corps de 12px ; le jeton muted, lui, est mesuré par
   tools/audit-lisibilite.mjs sur ce fond. */
.eval-choice {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  padding: 12px 14px; text-align: center;
  border-radius: var(--radius-md); cursor: pointer;
  border: 1px solid var(--color-divider); color: var(--color-text);
}
.eval-choice-label {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 15px;
}
.eval-choice-hint {
  font-size: 12px; line-height: 1.5; color: var(--color-text-muted);
}
.eval-choice--selected {
  border-color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 12%, transparent); color: var(--color-accent-ink);
}

/* — édition d'une séance passée (Journal, fiche exercice) — maquettes
   validées : Journal de pratique.dc.html (mobile, écran plein) et
   Bureau - Aujourd'hui.dc.html (desktop, modale à fond estompé). Un seul
   arbre DOM (voir views/sessionEdit.js), deux blocs coprésents comme le
   reste de l'app (.session-edit-mobile / .session-edit-desktop, bascule via
   la media query plus bas) — jamais un rendu conditionnel séparé qui
   dupliquerait la logique. Valeurs de base ci-dessous = mobile. */
.session-edit-mobile {
  position: absolute; inset: 0; z-index: 10; background: var(--color-surface);
  display: flex; flex-direction: column;
}
.session-edit-desktop { display: none; }
.session-edit-desktop-card {
  width: 600px; max-width: 100%; max-height: 100%; overflow-y: auto;
  background: var(--color-neutral-100); border: 1px solid var(--color-divider);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
}
.session-edit-footer-pinned { padding: var(--space-4); border-top: 1px solid var(--color-divider); }

/* Chips de durée (10/20/30/45 min + la valeur d'origine si hors de cet
   ensemble, voir views/sessionEdit.js) — pilule compacte, distincte de
   .desktop-duration-chip (gros chiffre + libellé, écran "Démarrer une
   session") : cette maquette-ci utilise un format à une seule ligne, partagé
   mobile/desktop (la seule différence entre les deux, taille/padding, est
   surchargée dans la media query plus bas). */
.session-edit-durees { display: flex; gap: 6px; }
.session-edit-chip {
  flex: 1; text-align: center; cursor: pointer; padding: 10px 0;
  border-radius: var(--radius-sm); font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 14px; border: 1px solid var(--color-divider); color: var(--color-text-soft);
}
.session-edit-chip--active { border-color: var(--color-accent-600); background: color-mix(in srgb, var(--color-accent) 12%, transparent); color: var(--color-accent-ink-strong); }

/* Ligne discrète "Aucun impact sur votre progression" — réutilise
   l'animation lb-soft déjà définie plus haut (micro-pause), jamais une
   nouvelle animation pour un même effet d'apparition douce. */
.session-edit-noimpact {
  display: flex; align-items: center; gap: 8px; font-size: 12px; font-style: italic;
  color: var(--color-text-muted); animation: lb-soft 400ms ease-out both;
}

/* Encart "Ce qui change dans le cycle" — même traitement doré que
   .desktop-hint-box (voir plus haut), avec ses propres lignes avant/après
   (valeur barrée → valeur dorée) : structure propre à cet encart, jamais
   réutilisée ailleurs, d'où des classes dédiées plutôt que génériques. */
.session-edit-compare {
  border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
  background: color-mix(in srgb, var(--color-accent) 7%, transparent);
  border-radius: var(--radius-md); padding: var(--space-3); display: flex; flex-direction: column; gap: var(--space-3);
  animation: lb-soft 320ms ease-out both;
}
.session-edit-compare-title { font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent-ink); }
.session-edit-compare-row { display: flex; align-items: center; gap: 9px; }
.session-edit-compare-label { font-size: 11px; color: var(--color-text-muted); margin-bottom: 2px; }
.session-edit-compare-before {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 16px;
  color: var(--color-text-muted); text-decoration: line-through; text-decoration-thickness: 1px;
}
.session-edit-compare-after { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 16px; color: var(--color-accent-ink-strong); }
.session-edit-compare-divider { height: 1px; background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
.session-edit-compare-hint { font-size: 12px; line-height: 1.6; color: var(--color-text-soft); }

/* — compte / connexion — */
.link-btn {
  background: none; border: none; padding: 0; margin: 0; font: inherit;
  color: var(--color-accent-ink); cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.link-btn:hover { color: var(--color-accent-ink-strong); }
.link-btn-danger { color: var(--color-error); }
/* Vers la couleur de texte du thème, jamais vers le noir : en thème sombre,
   assombrir un rouge déjà clair l'éloignerait du fond au lieu de l'appuyer. */
.link-btn-danger:hover { color: color-mix(in srgb, var(--color-error) 80%, var(--color-text)); }
.auth-error {
  font-size: 13px; color: var(--color-error);
  background: var(--color-error-bg); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3);
}
.auth-info {
  font-size: 13px; color: var(--color-text); opacity: 0.75;
  background: color-mix(in srgb, var(--color-text) 6%, transparent); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3);
}
/* Confirmation d'envoi du lien de réinitialisation ("Mot de passe oublié") :
   remplace le formulaire au même endroit, d'où l'apparition douce lb-soft
   (idiome déjà en place ailleurs, jamais une nouvelle animation pour un même
   effet) plutôt qu'une substitution sèche. Encadré discret dans l'esprit de
   .auth-info, mais avec l'interligne d'un vrai paragraphe : c'est ici le
   contenu principal de l'écran, pas une note en marge. */
.auth-sent {
  border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
  background: color-mix(in srgb, var(--color-accent) 7%, transparent);
  border-radius: var(--radius-md); padding: var(--space-4);
  animation: lb-soft 320ms var(--ease-out) both;
}

.account-email {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 19px;
}

/* — indicateur de synchronisation — */
.sync-badge {
  position: absolute; top: max(var(--space-3), env(safe-area-inset-top)); right: var(--space-3);
  z-index: 5; font-size: 10px; letter-spacing: 0.02em; padding: 4px 10px;
  border-radius: 999px; background: var(--color-surface); box-shadow: var(--shadow-sm);
  border: 1px solid var(--color-divider); color: var(--color-text-soft);
}
.sync-badge--syncing { color: var(--color-accent-ink); border-color: var(--color-accent); }
.sync-badge--synced { color: var(--color-text-muted); }
.sync-badge--pending { color: var(--color-on-accent-plate); background: var(--color-accent-100); border-color: transparent; }
.sync-badge--error { color: var(--color-error); background: var(--color-error-bg); border-color: transparent; }

/* — régions live (app/index.html : #live-polite et #live-alert) —
   Retirées de l'affichage sans être retirées de l'arbre d'accessibilité :
   display:none et visibility:hidden le seraient tous les deux, et une région
   live cachée de cette façon n'annonce rien. D'où la technique du rognage à
   1px, la seule qui laisse le contenu lisible par un lecteur d'écran. Aucune
   font-size ici (le contenu n'est jamais peint, et le plancher de 10px
   d'audit-lisibilite.mjs porte sur du texte visible) ; white-space:nowrap
   évite qu'un message long ne déborde du rognage sur certains moteurs. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* — toast (confirmations ponctuelles : mêmes couleurs que le badge de sync ci-dessus) —
   Second et dernier élément de l'app posé contre le bord bas, et il ignorait
   les DEUX variations de hauteur du socle que le bouton flottant ci-dessus
   connaît à moitié : ni le rembourrage de sécurité de la barre d'onglets, ni
   la bande « Séance en cours » qui s'intercale. Il est rendu en frère de
   .screen (voir currentViewHtml, main.js), donc le :has() se porte sur .phone
   et non sur .screen — et il est rendu PAR-DESSUS (z-index 6, fond opaque),
   donc il ne débordait pas discrètement : il recouvrait « Reprendre », et sur
   un téléphone à home indicator la bande entière. */
.toast {
  position: absolute; left: 50%; bottom: calc(88px + var(--bottom-bar-grow)); transform: translateX(-50%);
  z-index: 6; max-width: calc(100% - var(--space-4) * 2);
  font-size: 13px; text-align: center; padding: 10px 16px;
  border-radius: 999px; box-shadow: var(--shadow-md); border: 1px solid transparent;
  animation: toast-in 0.15s ease-out;
}
.phone:has(.session-resume) > .toast { bottom: calc(88px + var(--session-resume-h) + var(--bottom-bar-grow)); }
.toast--success { background: var(--color-accent-100); color: var(--color-on-accent-plate); border-color: var(--color-accent); }
.toast--error { background: var(--color-error-bg); color: var(--color-error); }
.toast--info { background: var(--color-surface); color: var(--color-text-soft); border-color: var(--color-divider); }
@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, 6px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}

/* — micro-pause (écran "Session en cours", voir views/active.js) —
   volutes de vapeur douces (lb-steam), filet de progression qui se vide sur
   toute la longueur de la pause (lb-drain, animation CSS pure — jamais de
   mutation JS seconde par seconde pour elle), apparition douce du message de
   fin (lb-soft). Reprises telles quelles de la maquette validée.

   La DURÉE et le décalage de lb-drain sont écrits dans le gabarit et non ici :
   la longueur de la pause est déclarée une seule fois (MICRO_PAUSE_SECONDS,
   store.js) et le filet REPREND au lieu de repartir de 100 % à chaque
   re-rendu, par un animation-delay négatif dérivé du temps qui reste. Cette
   règle-ci ne doit donc jamais reprendre de `animation-duration` : elle
   redeviendrait une seconde autorité sur la longueur de la pause. */
@keyframes lb-steam {
  0% { opacity: 0; transform: translateY(2px) scaleY(0.8); }
  30% { opacity: 0.9; }
  70% { opacity: 0.4; }
  100% { opacity: 0; transform: translateY(-3px) scaleY(1.15); }
}
@keyframes lb-drain {
  from { width: 100%; }
  to { width: 0%; }
}
@keyframes lb-soft {
  from { opacity: 0; transform: translateY(3px); }
  to { opacity: 1; transform: none; }
}

/* Liste sérielle (views/serialList.js, migration 0040).

   Les trois boutons d'une rangée du brouillon : 32×32, donc au-dessus des 24px
   de WCAG 2.5.8 et hors du relevé du contrôle CIBLE (qui ne porte que sur les
   contrôles réduits à une ICÔNE — ceux-ci portent un caractère Unicode, comme
   la flèche « → » de « Voir la fiche → »). Lavis au survol et au pressé, jamais
   un fond plein : c'est la règle du Carnet.

   La remise à zéro est un LAVIS D'ENCRE qui s'éteint, plus une ligne qui dit le
   nombre perdu et disparaît. Pas de rouge : la règle du worksheet n'est pas une
   erreur, et `--color-error` est réservé aux échecs. Aucune des deux n'est une
   keyframe DÉPLAÇANTE (l'une anime un fond, l'autre une opacité), donc ni l'une
   ni l'autre n'entre dans MOVING_KEYFRAMES ni ne demande de surcharge sous
   « mouvement réduit » : ce sont exactement les « fondus qui rendent une
   apparition compréhensible » que le bloc suivant conserve. */
.serial-row-btn {
  width: 32px; height: 32px; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 0;
  border-radius: var(--radius-md); cursor: pointer;
  font-family: var(--font-body); font-size: 14px; line-height: 1;
  color: var(--color-text-muted);
}
.serial-row-btn:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); color: var(--color-text); }
.serial-row-btn:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.serial-spot { min-width: 0; }
.serial-reset-flash { animation: serial-reset-flash 1.1s var(--ease-out) forwards; }
.serial-reset-note { animation: serial-reset-note 2.6s var(--ease-out) forwards; }
@keyframes serial-reset-flash {
  from { background: color-mix(in srgb, var(--color-text) 9%, transparent); }
  to { background: transparent; }
}
@keyframes serial-reset-note {
  0% { opacity: 0; }
  12% { opacity: 1; }
  70% { opacity: 1; }
  100% { opacity: 0; }
}

/* Mouvement réduit (réglage système). "Moins et plus doux", jamais "rien" :
   les fondus qui rendent une apparition compréhensible sont conservés, seuls
   les déplacements le sont pas. Sont explicitement épargnés parce qu'ils
   portent une information et non un effet : lb-drain (le filet qui matérialise
   les 15 s de micro-pause), lb-spin (envoi en cours) et la barre de
   progression desktop. lb-steam, purement décoratif et en boucle, s'arrête. */
@media (prefers-reduced-motion: reduce) {
  .timer-time--overtime, .session-overtime-note, .session-overtime-hint,
  .desktop-session-time--overtime {
    animation-name: lb-fade;
  }
  .toast { animation-name: lb-fade; }
  .auth-sent { animation-name: lb-fade; }
  /* Les deux blocs qui apparaissent dans « Modifier la séance » (voir
     views/sessionEdit.js) : ils RÉUTILISENT lb-soft — leur propre commentaire
     le dit, « jamais une nouvelle animation pour un même effet d'apparition
     douce » — sans que la contrepartie soit reprise avec, alors que les trois
     autres emplois de cette animation le sont juste au-dessus. Ils sont
     déclarés plus HAUT dans ce fichier (voir .session-edit-noimpact /
     .session-edit-compare), donc une surcharge de même spécificité écrite ici
     l'emporte ; pour la célébration et le panneau d'outils, dont les règles
     suivent cette media query, il a fallu au contraire un bloc dédié placé
     APRÈS elles (voir plus bas). */
  .session-edit-noimpact, .session-edit-compare { animation-name: lb-fade; }
  /* Modèles d'exercice : le retour au pressé passe du grossissement au fond,
     comme .tools-chip plus bas. Sûr ici (et pas après la media query desktop
     comme pour le panneau des outils) parce que .tpl-chip n'a aucune
     surcharge desktop : rien d'aussi précis ne lui est déclaré plus loin. */
  .tpl-chip { transition-property: background-color, border-color, color; }
  .tpl-chip:active {
    transform: none; background: color-mix(in srgb, var(--color-text) 14%, transparent);
  }
  /* Pastilles de catégorie : même traitement, même raison, et même absence de
     surcharge desktop (voir le bloc .cat-* près de .tpl-*). */
  .cat-chip { transition-property: background-color, border-color, color; }
  .cat-chip:active {
    transform: none; background: color-mix(in srgb, var(--color-text) 14%, transparent);
  }
  /* Pastilles de priorité : même traitement, même raison, même absence de
     surcharge desktop que les deux rangées ci-dessus. */
  .prio-chip { transition-property: background-color, border-color, color; }
  .prio-chip:active {
    transform: none; background: color-mix(in srgb, var(--color-text) 14%, transparent);
  }
  /* Pastilles de durée de routine : même traitement, même raison, même
     absence de surcharge desktop que les trois rangées ci-dessus. */
  .routine-duration-chip { transition-property: background-color, border-color, color; }
  .routine-duration-chip:active {
    transform: none; background: color-mix(in srgb, var(--color-text) 14%, transparent);
  }
  /* Carte d'exercice : le retour au pressé passe du tassement à la bordure.
     Sélecteur repris au caractère près (même spécificité), et ce bloc est
     écrit après lui : à spécificité égale, seule la déclaration la plus
     tardive l'emporte. Rien de plus précis n'est déclaré ailleurs sur
     .exercise-card (seul .exercise-card--static:hover suit, autre
     propriété). */
  .exercise-card:not(.exercise-card--static) { transition-property: box-shadow, border-color; }
  .exercise-card:not(.exercise-card--static):active {
    transform: none; border-color: color-mix(in srgb, var(--color-text) 34%, transparent);
  }
  /* La célébration de changement de phase a son propre bloc "mouvement
     réduit", placé juste APRÈS ses règles d'animation (plus bas dans ce
     fichier) : les surcharges de cette media query n'ont pas de spécificité
     particulière, elles perdraient donc contre des sélecteurs plus précis
     écrits plus loin. */
  [style*="lb-steam"] { animation: none !important; }
  /* Même idiome que la ligne ci-dessus, et pour la même raison mécanique :
     une déclaration `animation` écrite dans un attribut `style` ne se surcharge
     ni par une classe ni par un sélecteur plus précis — seul un `!important`
     d'auteur bat une déclaration en ligne ordinaire. Deux apparitions sont
     dans ce cas, toutes deux en fin de minuteur de pause : « Vingt minutes
     passées. » (reposDoneHtml, views/today.js) et « Reprenez quand vous
     voulez » (la micro-pause terminée, views/active.js). L'attribut-sélecteur
     était donc déjà là pour lb-steam, jamais pour lb-soft, qui est pourtant
     l'animation la plus employée du fichier.
     `animation-name` seul plutôt que `animation: none` : ces deux pilules
     apparaissent alors qu'aucun geste ne les demande (un minuteur arrive à
     zéro), donc le fondu est ce qui rend leur arrivée compréhensible — c'est
     la règle énoncée en tête de ce bloc. Seule la montée de 3px part, et la
     durée comme le fill-mode du raccourci en ligne sont conservés. */
  [style*="lb-soft"] { animation-name: lb-fade !important; }
}
@keyframes lb-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Recadrage de photo de profil (Compte → Identité, views/account.js) —
   premier spinner de l'app (le reste des actions en cours utilise un simple
   changement de libellé de bouton, voir saveProfileLabel) : la maquette
   validée en demande un explicitement ici, pendant l'envoi réel vers
   Storage. */
@keyframes lb-spin {
  to { transform: rotate(360deg); }
}

/* — dialogue de confirmation (suppression d'exercice) — */
.modal-backdrop {
  position: absolute; inset: 0; z-index: 10;
  background: color-mix(in srgb, #000 45%, transparent);
  display: flex; align-items: center; justify-content: center; padding: var(--space-4);
}
.modal-card {
  background: var(--color-surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  padding: var(--space-4); max-width: 320px; width: 100%;
}
.modal-card h4 { margin: 0 0 var(--space-2); }
.modal-actions { display: flex; gap: var(--space-2); justify-content: flex-end; }

/* — célébration d'un franchissement de phase (voir views/celebration.js) —
   Réutilise .modal-backdrop : c'est le même geste de mise à l'écart du reste
   de l'écran, il n'y a pas de raison qu'il s'obscurcisse différemment ici.
   Une seule superposition à la fois en pratique, mais z-index au-dessus des
   autres : si deux se croisaient, c'est celle-ci qui doit rester devant.
   Fonctionne telle quelle en mobile comme en desktop, la superposition étant
   posée sur .phone (position:relative dans les deux cas, voir main.js). */
.celebrate-backdrop { z-index: 11; }
.celebrate-card {
  width: 100%; max-width: 348px; max-height: 100%; overflow-y: auto; text-align: center;
  background: var(--color-surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  border: 1px solid color-mix(in srgb, var(--color-accent) 34%, transparent);
  padding: var(--space-5) var(--space-4) var(--space-4);
}
/* Cachet de cire : l'auréole est un simple anneau superposé au sceau, laissé
   à opacité 0 au repos — sans ça, il resterait dessiné en permanence sur les
   rendus qui ne rejouent pas l'animation (voir le guard de celebration.js). */
.celebrate-seal { position: relative; display: inline-flex; }
.celebrate-seal svg { display: block; }
.celebrate-seal-numeral { fill: var(--color-accent-ink); }
.celebrate-halo {
  position: absolute; inset: 0; border-radius: 50%; pointer-events: none; opacity: 0;
  border: 1px solid var(--color-accent);
}
.celebrate-eyebrow {
  margin-top: var(--space-3);
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-accent-ink);
}
.celebrate-title { font-size: 27px; margin: 5px 0 0; }
.celebrate-sub {
  margin-top: 6px; font-size: 12.5px; font-style: italic; line-height: 1.5;
  color: var(--color-text-muted);
}
.celebrate-pips { display: flex; justify-content: center; gap: 4px; margin: var(--space-4) 0; }
/* Aligné à gauche, contrairement au reste de la carte : c'est un vrai
   paragraphe à lire, jamais une accroche centrée. */
.celebrate-text {
  margin: 0 0 var(--space-4); text-align: left; font-size: 13.5px; line-height: 1.7;
  color: var(--color-text-soft);
}
.celebrate-btn { margin-top: 0; }

/* Séquence d'apparition, jouée une seule fois par célébration (classe posée
   au premier rendu seulement, voir views/celebration.js) : le fond s'estompe,
   la carte monte, le cachet s'appose avec son auréole, puis le texte se pose
   ligne à ligne. Un moment rare (quatre fois par exercice au maximum), le
   seul de l'app à s'autoriser une vraie séquence plutôt qu'une apparition
   simple. */
.celebrate-backdrop.celebrate--enter { animation: lb-fade 200ms var(--ease-out) both; }
.celebrate-card.celebrate--enter { animation: celebrate-card-in 260ms var(--ease-out) both; }
.celebrate-card.celebrate--enter .celebrate-seal { animation: celebrate-seal-in 420ms var(--ease-out) 120ms both; }
.celebrate-card.celebrate--enter .celebrate-halo { animation: celebrate-halo 900ms var(--ease-out) 260ms both; }
.celebrate-card.celebrate--enter .celebrate-eyebrow { animation: lb-soft 320ms var(--ease-out) 220ms both; }
.celebrate-card.celebrate--enter .celebrate-title { animation: lb-soft 320ms var(--ease-out) 260ms both; }
.celebrate-card.celebrate--enter .celebrate-sub { animation: lb-soft 320ms var(--ease-out) 300ms both; }
.celebrate-card.celebrate--enter .celebrate-pips { animation: lb-soft 320ms var(--ease-out) 340ms both; }
.celebrate-card.celebrate--enter .celebrate-text { animation: lb-soft 320ms var(--ease-out) 380ms both; }
.celebrate-card.celebrate--enter .celebrate-btn { animation: lb-soft 320ms var(--ease-out) 420ms both; }
@keyframes celebrate-card-in {
  from { opacity: 0; transform: translateY(8px) scale(0.97); }
  to { opacity: 1; transform: none; }
}
@keyframes celebrate-seal-in {
  from { opacity: 0; transform: scale(0.86); }
  to { opacity: 1; transform: none; }
}
@keyframes celebrate-halo {
  from { opacity: 0.5; transform: scale(0.72); }
  to { opacity: 0; transform: scale(1.75); }
}
/* Mouvement réduit : la carte et son contenu apparaissent toujours (le fondu
   est ce qui rend l'arrivée compréhensible), mais d'un seul tenant, sans
   montée ni échelonnement. L'auréole du cachet, seul élément purement
   décoratif, disparaît complètement. Bloc distinct de celui du haut de
   fichier et placé ici volontairement. Chaque sélecteur reprend exactement
   celui qu'il neutralise plutôt qu'un `> *` plus court : à spécificité
   inférieure, une media query ne l'emporte pas (elle n'en ajoute aucune),
   l'échelonnement resterait en place et le mouvement réduit ne serait
   respecté qu'à moitié. Seule une spécificité égale, plus loin dans le
   fichier, gagne. */
@media (prefers-reduced-motion: reduce) {
  .celebrate-card.celebrate--enter,
  .celebrate-card.celebrate--enter .celebrate-seal,
  .celebrate-card.celebrate--enter .celebrate-eyebrow,
  .celebrate-card.celebrate--enter .celebrate-title,
  .celebrate-card.celebrate--enter .celebrate-sub,
  .celebrate-card.celebrate--enter .celebrate-pips,
  .celebrate-card.celebrate--enter .celebrate-text,
  .celebrate-card.celebrate--enter .celebrate-btn { animation-name: lb-fade; animation-delay: 0s; }
  .celebrate-card.celebrate--enter .celebrate-halo { animation: none; }
}

/* — outils de pratique : métronome et bourdon (voir views/practiceTools.js) —
   Feuille qui monte du bas sur mobile, carte centrée à partir de 1024px (la
   bascule est dans la media query desktop plus bas). Un panneau plutôt qu'un
   écran : on règle un tempo pendant qu'on a l'instrument en main, on ne
   navigue pas. Réutilise .modal-backdrop pour la mise à l'écart du reste,
   comme la célébration de phase. */
.tools-backdrop { z-index: 10; align-items: flex-end; padding: 0; }
.tools-sheet {
  width: 100%; max-height: 92%; overflow-y: auto;
  background: var(--color-surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  border-top: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
  box-shadow: var(--shadow-lg);
  padding: 0 var(--space-4) calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
}
/* Poignée d'affordance de la feuille mobile : rien à saisir réellement (pas
   de glissé implémenté), mais elle dit d'un coup d'œil que ce bloc vient du
   bas et s'y range. Retirée en desktop, où la carte est centrée. */
.tools-grip {
  width: 34px; height: 4px; border-radius: 999px; margin: 9px auto 0;
  background: color-mix(in srgb, var(--color-text) 20%, transparent);
}
.tools-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  padding: var(--space-3) 0 var(--space-2);
}
.tools-title { margin: 0; font-size: 19px; }
.tools-close {
  flex: none; width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none; cursor: pointer; border-radius: var(--radius-md);
  color: var(--color-text-soft);
}
@media (hover: hover) and (pointer: fine) {
  .tools-close:hover { background: color-mix(in srgb, var(--color-text) 8%, transparent); }
}
.tools-body { display: flex; flex-direction: column; gap: var(--space-4); }
.tools-block { display: flex; flex-direction: column; gap: var(--space-3); }
.tools-rule { height: 1px; background: var(--color-divider); }
.tools-label {
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-accent-ink);
}
/* Aligné à gauche et en italique : c'est la note pédagogique du bloc, la même
   voix que les popovers "i" du reste de l'app, mais toujours visible ici —
   un panneau d'outils n'a pas la place d'un popover sans se faire rogner par
   son propre défilement. */
.tools-note {
  margin: 0; font-size: 12px; line-height: 1.6; font-style: italic;
  color: var(--color-text-soft);
}

.metro-lamps { display: flex; align-items: center; justify-content: center; gap: 9px; min-height: 12px; }
/* Opacité de repos 0.22 : c'est aussi la valeur d'arrivée du flash joué en
   WAAPI (voir setBeatListener dans main.js), pour que la lampe se pose
   exactement sur son état CSS plutôt que de sauter à la fin de l'animation. */
.metro-lamp {
  width: 9px; height: 9px; border-radius: 50%; opacity: 0.22;
  background: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.metro-lamp--accent { background: var(--color-accent); }

.metro-readout { display: flex; align-items: center; justify-content: center; gap: var(--space-4); }
.metro-step {
  flex: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--color-divider); background: transparent; color: var(--color-text);
  font-family: var(--font-body); font-size: 19px; line-height: 1;
  transition: background-color 140ms ease, transform 140ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .metro-step:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
.metro-step:active { transform: scale(0.94); }
/* min-width figée : "Prestissimo" et "Largo" n'ont pas la même largeur, et
   sans plancher les deux boutons − / + se déplaceraient à chaque changement
   d'indication, pendant qu'on est justement en train d'appuyer dessus. */
.metro-dial { text-align: center; min-width: 138px; }
.metro-bpm {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 46px; line-height: 1; font-variant-numeric: tabular-nums;
}
.metro-bpm small {
  font-family: var(--font-body); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  margin-left: 6px; color: var(--color-text-soft);
}
.metro-mark {
  margin-top: 3px; min-height: 19px; font-size: 13.5px; font-style: italic;
  color: var(--color-text-soft);
}

/* flex-wrap : la rangée « Mute aléatoire » porte quatre pastilles ET une
   pastille « i » à côté de son intitulé, donc elle ne tient plus sur une seule
   ligne sous ~340px de large. Sans effet à toute largeur où la rangée entre
   déjà (le `gap` sert alors les deux axes sans qu'aucun retour n'ait lieu) :
   c'est un débordement évité, pas une mise en page changée. */
.tools-inline {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--space-3);
}
.tools-inline-label { font-size: 12px; color: var(--color-text-soft); }
/* L'intitulé porte sa pastille « i » : elle doit rester sur la même ligne que
   lui, et son wrap (.cooldown-info-wrap) est un <div>, d'où un conteneur en
   flex plutôt que le <span> des autres intitulés.
   Le nom décrit un COMPOSANT et non un périmètre (même parti que
   .desktop-hint-box, employée par un bloc mobile) : la règle sert les DEUX
   intitulés du panneau qui portent un « i », celui de la rangée « Mute
   aléatoire » (.tools-inline-label) et le titre du bloc « Bourdon »
   (.tools-label). Elle s'appelait .tools-mute-label tant qu'il n'y en avait
   qu'un. */
.tools-label-info { display: inline-flex; align-items: center; gap: 6px; }
.tools-chips { display: flex; gap: 5px; }
.tools-chip {
  min-width: 36px; padding: 7px 10px; cursor: pointer;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent;
  font-family: var(--font-body); font-size: 12.5px; line-height: 1.1;
  color: var(--color-text-soft);
  transition: background-color 140ms ease, border-color 140ms ease, transform 140ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .tools-chip:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
.tools-chip:active { transform: scale(0.96); }
.tools-chip--active {
  border-color: var(--color-accent); color: var(--color-accent-ink);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}
.tools-actions { display: flex; gap: var(--space-2); }
.tools-actions .btn { flex: 1; padding: 10px var(--space-3); font-size: 14.5px; }

/* Six colonnes : les douze demi-tons tiennent en deux rangées régulières, et
   la seconde commence sur Fa♯, donc chaque colonne garde le même rapport
   d'un rang à l'autre. Une grille auto-fit se serait réarrangée selon la
   largeur et aurait cassé cette lecture. */
.drone-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; }
.drone-note {
  padding: 10px 0; cursor: pointer;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 15.5px; line-height: 1.1;
  color: var(--color-text);
  transition: background-color 140ms ease, border-color 140ms ease, transform 140ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .drone-note:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
}
.drone-note:active { transform: scale(0.96); }
.drone-note--on {
  border-color: var(--color-accent); color: var(--color-accent-ink);
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
}

/* Pilule d'appel du panneau (écran de séance et "Démarrer une session") :
   bordure et couleurs posées en ligne par practiceToolsPill selon que
   quelque chose sonne ou non, le reste ici. */
.tools-pill {
  display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px 8px 11px;
  border-radius: 999px; font-size: 12.5px; cursor: pointer; user-select: none;
  transition: transform 140ms var(--ease-out);
}
.tools-pill:active { transform: scale(0.97); }

/* Apparition, jouée une seule fois par ouverture (classe posée au premier
   rendu seulement, voir le guard de views/practiceTools.js — ce panneau
   provoque un changement d'état à chaque battement de tempo réglé, il
   rejouerait donc son entrée en boucle sans ça).
   Rien pour la fermeture : le DOM est reconstruit d'un coup à chaque
   changement d'état, il n'y a aucun moment où une carte "sortante" existe
   encore (même limite que toutes les autres superpositions de l'app). */
.tools-backdrop.tools--enter { animation: lb-fade 200ms var(--ease-out) both; }
.tools-sheet.tools--enter { animation: tools-sheet-in 280ms var(--ease-drawer) both; }
@keyframes tools-sheet-in {
  from { transform: translateY(100%); }
  to { transform: none; }
}

/* ═══ layout desktop (≥1024px, phase 1/4 — "Aujourd'hui" seulement) ═══
   .desktop-shell coexiste dans le DOM avec .screen (voir renderToday) ;
   c'est la media query en bas de fichier qui bascule laquelle des deux
   s'affiche selon la largeur — jamais de logique JS de largeur d'écran,
   pour rester cohérent avec le reste du projet (voir CLAUDE.md). Les vues
   qui n'ont pas encore leur propre phase desktop n'ont pas de
   .desktop-shell du tout : phone--has-desktop n'est alors jamais ajouté
   (voir main.js) et .phone garde son comportement mobile habituel, même
   au-delà de 1024px — un repli volontaire, pas un oubli. */
.desktop-shell { display: none; }

.desktop-sidebar {
  flex: none; width: 252px; align-self: stretch; background: var(--color-surface);
  position: relative; display: flex; flex-direction: column; padding: var(--space-6) 0 var(--space-4);
}
.desktop-sidebar::before {
  content: ''; position: absolute; top: 0; bottom: 0; right: 0; width: 1px; background: var(--color-divider);
}
.desktop-sidebar::after {
  content: ''; position: absolute; top: 0; bottom: 0; right: 5px; width: 1px;
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
}
.desktop-sidebar-stitch {
  position: absolute; top: var(--space-6); bottom: var(--space-6); right: 14px; width: 1px;
  background: repeating-linear-gradient(to bottom, color-mix(in srgb, var(--color-text) 22%, transparent) 0 9px, transparent 9px 20px);
}
.desktop-sidebar-brand { padding: 0 var(--space-6) var(--space-6); }
.desktop-sidebar-brand-title { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 23px; line-height: 1.1; }
.desktop-sidebar-brand-sub { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-text-muted); margin-top: 5px; }

/* Le `gap: 2px` DESCEND d'un cran, il n'est pas dupliqué : il séparait les
   quatre boutons, seuls enfants du <nav>, et ce sont désormais leurs enveloppes
   d'item qu'il doit séparer (voir renderDesktopSidebar, render.js — le <nav>
   garde son repère de page, donc role="list" va sur une enveloppe intérieure).
   Le laisser sur .desktop-nav en aurait fait une déclaration morte, le <nav>
   n'ayant plus qu'un enfant. L'enveloppe d'item, elle, n'a besoin d'aucune
   règle : elle est étirée par le flex colonne et .desktop-nav-item est déjà
   `width: 100%`. */
.desktop-nav { display: flex; flex-direction: column; padding-right: 22px; }
.desktop-nav-list { display: flex; flex-direction: column; gap: 2px; }
.desktop-nav-item {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  padding: 11px var(--space-4) 11px var(--space-6); cursor: pointer; position: relative;
  background: none; border: none; font-family: var(--font-body);
  color: var(--color-text-soft);
}
.desktop-nav-item span { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 16px; }
.desktop-nav-item:hover { color: var(--color-text); background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.desktop-nav-item--active, .desktop-nav-item--active:hover {
  color: var(--color-accent-ink); background: color-mix(in srgb, var(--color-accent) 9%, transparent);
}
.desktop-nav-item--active::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2.5px; background: var(--color-accent);
}

/* — rappel de séance en cours (reliure desktop) —
   Cinquième entrée de navigation, transitoire, donc bâtie sur
   .desktop-nav-item pour s'aligner exactement sur les quatre autres (voir
   sessionResumeNavItem, render.js). Le filet supérieur la détache du groupe
   permanent ; le titre de l'exercice tient sur une seconde ligne, comme
   .desktop-sync-time sous son état. */
.desktop-nav-resume {
  margin-top: var(--space-4); padding-top: var(--space-4); padding-right: 22px;
  border-top: 1px solid var(--color-divider);
}
.desktop-nav-item--resume, .desktop-nav-item--resume:hover {
  align-items: flex-start; color: var(--color-accent-ink);
  background: color-mix(in srgb, var(--color-accent) 8%, transparent);
}
.desktop-nav-item--resume svg { margin-top: 2px; }
.desktop-nav-item--resume > span { min-width: 0; }
/* Sélecteur à deux classes, jamais `.desktop-nav-resume-sub` seul :
   `.desktop-nav-item span` (16px, police de titre) est plus spécifique qu'une
   classe unique et l'emporterait sur les trois déclarations ci-dessous. */
.desktop-nav-item--resume .desktop-nav-resume-sub {
  display: block; font-family: var(--font-body); font-weight: 400; font-size: 11.5px;
  color: var(--color-text-soft); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.desktop-sync {
  margin-top: auto; padding: var(--space-4) var(--space-6) 0; margin-right: 22px;
  border-top: 1px solid var(--color-divider); display: flex; align-items: center; gap: 8px;
  color: var(--color-text-soft);
}
.desktop-sync--synced { color: var(--color-accent-ink); }
.desktop-sync--syncing { color: var(--color-accent-ink); }
.desktop-sync--pending { color: var(--color-accent-ink-strong); }
.desktop-sync--error { color: var(--color-error); }
.desktop-sync-label { font-size: 12.5px; line-height: 1.3; color: inherit; }
.desktop-sync-time { font-size: 11px; color: var(--color-text-muted); line-height: 1.4; }

.desktop-content { flex: 1; min-width: 0; height: 100vh; overflow-y: auto; padding: var(--space-8); }
/* Barre de défilement masquée (maquette validée) — cohérent avec l'esthétique
   "carnet manuscrit" du reste de l'app, jamais de scrollbar système visible
   ailleurs non plus (.screen-scroll en hérite nativement sur mobile, où le
   défilement tactile ne montre pas de barre). */
.desktop-content.no-bar::-webkit-scrollbar { width: 0; }
.desktop-content-inner { max-width: 1180px; margin: 0 auto; }

/* En-tête partagé par les 3 écrans desktop (Aujourd'hui/Archivés/Compte,
   phases 1 et 2a) — titre + sous-titre à gauche, élément optionnel à droite
   (bouton, note, ou rien) selon l'écran ; c'est justement pour ça que
   .desktop-header ne préjuge de rien sur son contenu, seulement de la ligne
   d'ensemble (voir today.js/archive.js/account.js). */
.desktop-header {
  display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-6);
  padding-bottom: var(--space-4); border-bottom: 1px solid var(--color-divider); margin-bottom: var(--space-6);
}
.desktop-header h1 { font-size: 46px; }
.desktop-header-resume { font-size: 13.5px; color: var(--color-text-muted); margin-top: 8px; }

/* Colonne plus étroite que .desktop-content-inner (1180px) pour le corps
   d'Archivés/Compte — seul l'en-tête garde la pleine largeur (maquette
   validée, phase 2a) ; Aujourd'hui n'en a pas besoin, sa grille de cartes
   occupe déjà toute la largeur du contenu. */
.desktop-narrow { max-width: 940px; display: flex; flex-direction: column; gap: var(--space-6); }

.desktop-today-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(324px, 1fr)); gap: var(--space-4); align-items: start; }

.desktop-empty-state {
  border: 1px dashed color-mix(in srgb, var(--color-text) 22%, transparent); border-radius: var(--radius-lg);
  background:
    linear-gradient(to bottom, color-mix(in srgb, var(--color-text) 4%, transparent) 0 1px, transparent 1px 34px) 0 46px / 100% 34px repeat-y,
    var(--color-neutral-100);
  padding: var(--space-8) var(--space-6); display: flex; flex-direction: column; align-items: center;
  text-align: center; gap: var(--space-3); min-height: 360px; justify-content: center;
}
.desktop-empty-state p { margin: 0; max-width: 420px; font-size: 14px; line-height: 1.65; color: var(--color-text-muted); text-wrap: pretty; }
/* (Il y avait ici .desktop-empty-state--repos, la variante dorée de cette
   carte pour la suggestion de pause de repos 20 min. La suggestion ne
   remplace plus l'état vide : c'est une bande posée au-dessus du contenu,
   entièrement en styles en ligne dans reposStripHtml — voir today.js.) */

/* Lien doré discret générique (pas seulement "vers les archives", malgré le
   nom d'origine) — réutilisé pour toute action secondaire en texte plutôt
   qu'en bouton (Aujourd'hui vide → Archivés, Archivés vide → Aujourd'hui). */
.desktop-text-link {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 14.5px;
  color: var(--color-accent-ink); text-decoration: none; background: none; border: none; cursor: pointer; padding: 0;
}
.desktop-text-link:hover { text-decoration: underline; }

/* — archivés (phase 2a) — */
.desktop-archive-group-label { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); }
.desktop-archive-group-label span {
  font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap;
  color: var(--color-text-muted);
}
.desktop-archive-group-label i { flex: 1; height: 1px; background: var(--color-divider); font-style: normal; }
/* Les trois dernières pistes sont FIXES, et c'est ce qui aligne les colonnes
   d'une rangée à l'autre : chaque .desktop-archive-row est sa PROPRE grille
   (une par ligne), donc une piste `auto` ou `max-content` se dimensionnerait
   ligne par ligne et la liste sortirait en escalier. Une piste fixe ne
   grandit pas non plus sous la pression de son contenu : un contenu plus
   large que sa piste DÉBORDE par-dessus la colonne voisine (overflow visible).
   La deuxième piste doit donc être tenue d'accord avec ce qu'elle accueille.

   Elle ne l'était plus, et c'est un défaut de CALENDRIER : 116px avait été
   mesurée pour le texte seul du chemin d'archivage AUTOMATIQUE
   (« 13 séances · maîtrisé », 112,6px), puis deux contenus sont arrivés
   après elle sans que personne ne la repose — la mise de côté MANUELLE, dont
   le mot est plus long (« 13 séances · mis de côté », 129,2px, donc un
   débordement de 13px à elle seule), et la CATÉGORIE (0017), une pastille de
   77 à 133px posée dans une piste qui n'avait déjà plus un pixel. Mesuré sur
   le HTML réellement rendu : 209px de contenu dans 116px, soit 79px de
   chevauchement par-dessus la colonne de récurrence, sur CHAQUE ligne
   porteuse d'une catégorie. Le même défaut vivait sur « Progression », qui
   partage cette rangée et dont la cellule porte en plus une pastille de
   priorité — l'écran n'était pas nommé au signalement, il est corrigé du
   même geste.

   Deux moitiés indissociables, et aucune ne suffit seule :
   1. 140px est le maximum MESURÉ du texte seul (129,2px en Georgia, le serif
      large qui borne Lora par le haut) plus une marge de 11px. Les 24px
      viennent de la piste du TITRE, jamais de la troisième : celle-ci est
      déjà pleine (170,1px de contenu réel pour 176px), et la rétrécir
      rouvrirait le même défaut une colonne plus loin.
   2. La cellule PASSE en flex qui revient à la ligne : le texte reste
      insécable (il est un élément flex anonyme, qui hérite du `nowrap` de son
      conteneur, donc jamais coupé au milieu d'une phrase) et la pastille
      passe dessous quand elle ne tient pas. C'est la GARANTIE : une catégorie
      est du texte que l'utilisateur TAPE, bridé à 16ch par .cat-tag mais
      jamais à zéro, donc aucune largeur de piste ne peut à elle seule fermer
      le défaut. `align-items: baseline` reproduit le `vertical-align:
      baseline` que la pastille portait en flux inline.

   Contrepartie assumée : sur « Archivés », une ligne porteuse d'une catégorie
   fait désormais deux lignes de haut, la pastille passant sous le texte —
   exactement ce que fait déjà la coque mobile dans .list-row-sub. La tenir
   sur une seule ligne demanderait ~225px de piste, donc de descendre le titre
   sous 140px à 1024px de large (252px de reliure, 36,8px de rembourrage) :
   POINT À ARBITRER PAR LE PROPRIÉTAIRE. */
.desktop-archive-row {
  display: grid; grid-template-columns: 1fr 140px 176px 92px; align-items: center; gap: var(--space-3);
  padding: 13px var(--space-3); border-bottom: 1px solid var(--color-divider); cursor: pointer; border-radius: var(--radius-sm);
}
.desktop-archive-row:hover { background: color-mix(in srgb, var(--color-text) 3.5%, transparent); }
.desktop-archive-row-title { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 18px; line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.desktop-archive-row-meta {
  font-size: 12.5px; color: var(--color-text-muted); white-space: nowrap;
  display: flex; flex-wrap: wrap; align-items: baseline; row-gap: 3px; min-width: 0;
}
/* Sert un <div> sur « Progression » (« Voir → », la rangée entière étant le
   contrôle) et un vrai <button> sur « Archivés » (« Rouvrir », la rangée y
   portant deux gestes distincts). Les quatre déclarations de réinitialisation
   ne servent donc que le second : elles sont NEUTRES pour le premier, un <div>
   n'ayant ni fond, ni filet, ni rembourrage propres, et héritant déjà son
   interligne et son curseur de la rangée. */
.desktop-archive-row-cta {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 13.5px;
  color: var(--color-accent-ink); text-align: right; white-space: nowrap;
  background: none; border: 0; padding: 0; line-height: inherit; cursor: pointer;
}

/* — compte (phase 2a) — */
.desktop-account-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--space-4); align-items: start; }
/* Réutilise .exercise-card tel quel (même fond/bordure/radius/ombre) pour le
   bloc "Identité", jamais interactif contrairement à "Pratique"/"Enseignement"
   — annule juste le curseur et le survol de la carte de base, sans dupliquer
   le reste de sa définition. */
.exercise-card--static { cursor: default; }
.exercise-card--static:hover { box-shadow: var(--shadow-sm); border-color: var(--color-divider); }
.desktop-account-footer {
  border-top: 1px solid var(--color-divider); padding-top: var(--space-4);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
}
.desktop-signout-link {
  font-size: 13.5px; color: var(--color-text-soft);
  background: none; border: none; cursor: pointer; padding: 0; font: inherit;
}
.desktop-signout-link:hover { color: var(--color-text); }
/* Pendant desktop de .link-btn-danger (compte/connexion, plus haut), pour les
   actions IRRÉVERSIBLES rendues dans la famille .desktop-stack (fiche
   exercice desktop) : la suppression d'un exercice réutilisait
   .desktop-signout-link — un style neutre pensé pour « Se déconnecter »,
   « Passer » ou « Annuler », aucun des trois n'étant destructeur — au lieu du
   rouge que sa jumelle mobile porte déjà (.link-btn-danger, data-action=
   "open-delete-confirm", views/detail.js). Desktop ne signalait donc pas
   qu'il s'agit d'une suppression définitive, contrairement au mobile. Même
   forme que .desktop-signout-link (aucune autre déclaration à dupliquer),
   seule la couleur change ; le survol mélange vers --color-text et jamais
   vers le noir, comme .link-btn-danger — assombrir un rouge déjà clair en
   thème sombre l'éloignerait du fond au lieu de l'appuyer. */
.desktop-link-danger {
  font-size: 13.5px; color: var(--color-error);
  background: none; border: none; cursor: pointer; padding: 0; font: inherit;
}
.desktop-link-danger:hover { color: color-mix(in srgb, var(--color-error) 80%, var(--color-text)); }
/* Troisième couleur de la même forme, et le pendant desktop de .link-btn (le
   doré de l'app) : l'archivage d'un exercice réutilisait lui aussi
   .desktop-signout-link, donc le gris neutre pensé pour « Se déconnecter »,
   « Passer » et « Annuler » — trois gestes qui QUITTENT un écran sans rien
   écrire — alors que sa jumelle mobile porte le doré (.link-btn,
   data-action="archive-exercise"/"unarchive-exercise", views/detail.js).
   Desktop rangeait donc dans la même famille visuelle un geste qui ÉCRIT sur
   l'exercice et deux gestes qui ne font que sortir, et la rangée du bas de la
   fiche se lisait « neutre, puis rouge » là où le mobile lit « doré, puis
   rouge ». Le doré et jamais le rouge : archiver est réversible d'un clic
   (« Désarchiver » prend sa place au même endroit), contrairement à la
   suppression juste à côté. Même forme que .desktop-signout-link (aucune
   autre déclaration à dupliquer), seule la couleur change, et le survol suit
   .link-btn:hover vers --color-accent-ink-strong. */
.desktop-link-accent {
  font-size: 13.5px; color: var(--color-accent-ink);
  background: none; border: none; cursor: pointer; padding: 0; font: inherit;
}
.desktop-link-accent:hover { color: var(--color-accent-ink-strong); }

/* — écrans empilés (phase 2b) : créer/éditer, démarrer, session, évaluation — */
.desktop-stack { max-width: 672px; margin: 0 auto; padding-bottom: var(--space-8); }
.desktop-stack-back {
  display: inline-flex; align-items: center; gap: 7px; margin-bottom: var(--space-4);
  font-size: 13.5px; color: var(--color-text-muted);
  background: none; border: none; cursor: pointer; padding: 0; font-family: var(--font-body);
}
.desktop-stack-back:hover { color: var(--color-text); }
.desktop-stack-card { background: var(--color-neutral-100); border: 1px solid var(--color-divider); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; }
/* Filet doré vertical — seuls créer/démarrer/évaluation l'ont (repère "il y a
   quelque chose à faire ici") ; "session en cours" a son propre en-tête en
   ligne (.desktop-session-header), sans ce filet (maquette validée). */
.desktop-stack-header { padding: var(--space-6) var(--space-6) var(--space-4); border-bottom: 1px solid var(--color-divider); position: relative; }
.desktop-stack-header::before { content: ''; position: absolute; left: 0; top: var(--space-6); bottom: var(--space-4); width: 2.5px; background: var(--color-accent); }
.desktop-stack-body { padding: var(--space-6); display: flex; flex-direction: column; gap: var(--space-5); }
.desktop-stack-footer { padding: var(--space-4) var(--space-6); border-top: 1px solid var(--color-divider); background: var(--color-surface); display: flex; align-items: center; gap: var(--space-3); }
.desktop-field-label { font-size: 12.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-muted); }
.desktop-field-optional { text-transform: none; letter-spacing: 0; font-style: italic; color: var(--color-text-muted); }
.desktop-field-hint { font-size: 12px; color: var(--color-text-muted); font-style: italic; }
/* Encart doré — même traitement pour le rappel "le rythme se règle tout seul"
   (créer/devoir, rhythmHintHtml) et la conséquence de l'évaluation choisie
   (évaluation, consequenceHtml), texte seul change (maquette validée).
   Déclaré ICI, hors de la media query desktop, et pas par accident : les deux
   emplois précités sont rendus par les DEUX coques depuis une fonction unique,
   et le bandeau "en repos" de views/detail.js s'en sert aussi côté mobile. Le
   préfixe .desktop- nomme donc un composant, pas un périmètre — même cas que
   .desktop-field-hint juste au-dessus (hint du champ objectif de "Démarrer",
   rendu des deux côtés) et que .desktop-archive-group-label. */
.desktop-hint-box { display: flex; align-items: flex-start; gap: 11px; padding: var(--space-3); background: color-mix(in srgb, var(--color-accent) 7%, transparent); border-radius: var(--radius-md); }
.desktop-hint-box-text { font-size: 12.5px; line-height: 1.65; color: var(--color-text-soft); }

/* Flex plutôt qu'un grid à 4 colonnes fixes : la rangée compte un pavé de
   plus quand la durée en cours est hors barème (voir durationOptions dans
   views/start.js), et repeat(4, 1fr) l'aurait renvoyé seul à la ligne, au
   quart de la largeur. `flex: 1` valant `1 1 0%`, les pavés restent d'égale
   largeur exactement comme avec 1fr : à quatre pavés le rendu est
   inchangé. */
.desktop-duration-grid { display: flex; gap: 9px; }
.desktop-duration-chip { flex: 1; min-width: 0; padding: 15px 6px; text-align: center; cursor: pointer; border-radius: var(--radius-md); border: 1px solid var(--color-divider); color: var(--color-text-soft); }
/* Fond fixe (--color-accent-100), pas color-mix(...transparent) : ce dernier
   se mélange avec --color-neutral-100 sous-jacent, qui change de valeur en
   mode sombre (voir plus haut). Le texte --color-on-accent-plate, lui, reste
   fixe par design, et deviendrait illisible sur un fond qui aurait suivi le
   thème. Même paire déjà éprouvée dans les deux thèmes que .tag-accent. */
.desktop-duration-chip--active { border-color: var(--color-accent); background: var(--color-accent-100); color: var(--color-on-accent-plate); }
.desktop-duration-chip-min { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 26px; line-height: 1; }
.desktop-duration-chip-label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; margin-top: 3px; opacity: 0.7; }

.desktop-session-header { padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--color-divider); display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
.desktop-session-body {
  padding: var(--space-8) var(--space-6); display: flex; flex-direction: column; align-items: center; gap: var(--space-5);
  background: linear-gradient(to bottom, color-mix(in srgb, var(--color-accent) 5%, transparent), transparent 62%);
}
.desktop-session-time { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 104px; line-height: 0.95; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.desktop-session-progress { width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: 7px; }
.desktop-session-progress-track { height: 3px; background: color-mix(in srgb, var(--color-text) 10%, transparent); border-radius: 2px; overflow: hidden; }
.desktop-session-progress-fill { height: 100%; background: var(--color-accent); transition: width 1s linear; }
/* Temps supplémentaire (voir .timer-time--overtime plus haut) : la barre est
   pleine et le reste — elle mesure la durée prévue, qui n'avance plus. On
   l'estompe pour qu'elle cesse de tirer l'œil, le grand chiffre doré prenant
   le relais comme point de repère. */
.desktop-session-time--overtime { color: var(--color-accent); }
.desktop-session-progress-fill--overtime { background: color-mix(in srgb, var(--color-accent) 40%, transparent); }
.desktop-session-progress-labels { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--color-text-muted); }

.desktop-eval-ratings { display: grid; grid-template-columns: repeat(3, 1fr); gap: 11px; }
.desktop-eval-choice { padding: var(--space-4) var(--space-3); cursor: pointer; border-radius: var(--radius-md); border: 1px solid var(--color-divider); color: var(--color-text-soft); }
.desktop-eval-choice--selected { border-color: var(--color-accent); background: var(--color-accent-100); color: var(--color-on-accent-plate); }
.desktop-eval-choice-label { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 21px; line-height: 1.15; }
.desktop-eval-choice-hint { font-size: 12px; line-height: 1.55; margin-top: 6px; opacity: 0.72; }

/* — espace enseignant (phase 3/4) : layout desktop obtenu par surcharge
   @media directement sur le même arbre que le mobile (teacherRoster.js /
   teacherStudentDetail.js), jamais un second rendu dupliqué comme pour
   Aujourd'hui/Archivés/Compte/Créer/Démarrer/Session/Évaluation — cet espace
   n'a pas de reliure desktop, la structure mobile actuelle est déjà la bonne
   base à élargir. Valeurs de base ci-dessous = valeurs mobiles actuelles,
   inchangées ; seules les règles dans la media query plus bas les
   surchargent. */
/* max(--space-4, safe-area-inset-top) et non --space-4 nu : cette plaque est
   la bande supérieure des deux écrans de l'espace enseignant, et son propre
   bloc de droite (.teacher-banner-trailing, plus bas) dégageait l'encoche
   depuis toujours pendant que le titre à sa gauche démarrait à 18,4px du bord
   physique — les deux moitiés d'une même rangée calculées depuis deux
   origines. Voir .screen-topbar plus haut pour le raisonnement complet. */
.teacher-banner { background: var(--color-neutral-900); color: #f8f4f4; padding: max(var(--space-4), env(safe-area-inset-top)) var(--space-4) var(--space-3); flex: none; }
/* align-items:flex-start (pas center) : le titre, plus grand, définit de
   toute façon la hauteur de la ligne et démarre en haut dans les deux cas —
   ça laisse .teacher-banner-trailing seul maître de sa position verticale
   via margin-top, sans que ce choix affecte le titre. */
.teacher-banner-inner { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
.teacher-banner-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 27px; line-height: 1.1; letter-spacing: normal; margin: 0;
}
/* Le badge de synchro flottant (.sync-badge, position:absolute sur .phone,
   ancré au bord réel du viewport) partage le même coin haut-droit que ce
   bloc (nom de l'enseignant) — sans cette marge les deux se chevauchaient.
   Formule alignée sur celle du badge (max(--space-3, safe-area-inset-top))
   + sa hauteur/marge approximatives, pour rester correct même sous une
   encoche. S'applique à toutes les largeurs, pas seulement mobile : à
   partir de 1024px .teacher-banner-inner est centré et plafonné à 1240px
   (voir media query plus bas) alors que le badge reste ancré au bord du
   viewport — jusqu'à ~1540px de large, l'écart entre les deux ne suffit
   toujours pas à éviter le chevauchement sans cette marge. */
.teacher-banner-trailing { margin-top: calc(max(var(--space-3), env(safe-area-inset-top)) + 12px); }
.teacher-sort-row { flex: none; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--color-divider); background: var(--color-neutral-100); }
.teacher-sort-row-inner { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.teacher-roster { flex: 1; overflow-y: auto; padding: var(--space-3) var(--space-4) var(--teacher-floating-clearance); }
.teacher-roster-grid { display: flex; flex-direction: column; gap: var(--space-2); }
.teacher-student-card { padding: var(--space-3); }
/* Même dégagement que .teacher-banner ci-dessus : c'est la bande supérieure de
   la fiche élève, et le nom de l'élève — son <h1> — démarrait à 13,8px du bord
   physique, donc entièrement sous la barre d'état de l'app installée. */
.teacher-detail-header { background: var(--color-neutral-900); color: #f8f4f4; padding: max(var(--space-3), env(safe-area-inset-top)) var(--space-4) var(--space-4); flex: none; }
.teacher-header-avatar { width: 46px; height: 46px; font-size: 20px; }
.teacher-header-name {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 25px; line-height: 1.15; letter-spacing: normal; margin: 0;
}
.teacher-sticky-bar { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--color-divider); background: var(--color-neutral-100); position: sticky; top: 0; z-index: 3; }
.teacher-readonly-note { display: none; }
.teacher-section { padding: var(--space-4) var(--space-4) var(--space-2); }

/* — devoir assigné : seule carte modifiable de la fiche élève — */
/* Même idiome que .exercise-card-provenance côté élève ("Devoir de X") :
   book + italique doré, sur --color-accent-ink. */
.teacher-homework-eyebrow {
  display: flex; align-items: center; gap: 5px; margin-bottom: 2px;
  font-size: 11px; font-style: italic; color: var(--color-accent-ink);
  overflow: hidden; white-space: nowrap;
}

/* Les trois rangées « icône + libellé » ci-dessus (.exercise-card-provenance,
   .list-row-provenance, .teacher-homework-eyebrow) déclaraient chacune
   `text-overflow: ellipsis` sur ELLES-MÊMES, c'est-à-dire sur un conteneur
   flex — or cette propriété ne s'applique qu'à un conteneur de BLOC (CSS
   Overflow), et les enfants d'un conteneur flex ne sont pas du contenu inline
   mais des éléments flex. La déclaration ne pouvait donc jamais rien produire,
   pendant que l'`overflow: hidden` de la rangée, lui, coupait réellement : un
   « Devoir de Jean-Baptiste de la Tour du Pin » se lisait « Devoir de
   Jean-Baptiste de la T », tranché au milieu d'un mot et sans le moindre signe
   qu'il manque quelque chose. Deux des trois portent le nom affiché d'un
   enseignant, donc du texte tapé et de longueur non bornée.
   L'ellipse vit maintenant sur le seul enfant qui porte du TEXTE, où elle
   s'applique (un <span> blockifié par sa qualité d'élément flex est un
   conteneur de bloc). Deux précisions qui expliquent ce qui n'est PAS écrit
   ici :
     — `white-space: nowrap` n'est pas répété : il est HÉRITÉ de la rangée, et
       une déclaration qui ne change rien décrirait faux.
     — `min-width: 0` non plus, alors que c'est le réflexe habituel sur un
       élément flex : `min-width: auto` ne vaut une taille minimale fondée sur
       le contenu que si l'`overflow` de l'élément est `visible` (CSS Flexbox
       §4.5), et l'ellipse EXIGE justement `overflow: hidden`. C'est donc cette
       même déclaration qui rend le libellé rétrécissable.
   `node tools/audit-lisibilite.mjs` (contrôle TRONCATURE) mesure les deux
   faits sur le HTML réellement rendu. */
.exercise-card-provenance span,
.list-row-provenance span,
.teacher-homework-eyebrow span {
  overflow: hidden; text-overflow: ellipsis;
}
.teacher-card-actions {
  display: flex; justify-content: flex-end;
  border-top: 1px solid var(--color-divider); padding-top: 8px;
}
/* Trois pilules depuis le chantier archiver/supprimer un devoir (migration
   0038, écrite mais pas encore jouée), et non plus une seule : la marge
   négative de .teacher-edit-btn (-8px à droite) suppose un unique occupant
   collé au bord droit de la carte. Seul le DERNIER enfant garde ce tirage —
   c'est lui qui doit rester flush avec le bord — les autres retrouvent une
   marge droite normale, et le padding horizontal de 8px déjà porté par
   chaque pilule (des deux côtés) suffit alors à les séparer, sans qu'aucun
   gap n'ait besoin d'être inventé. */
.teacher-card-actions .teacher-edit-btn:not(:last-child) { margin-right: 0; }
/* Consigne de l'élève sur sa fiche enseignant (views/teacherStudentDetail.js).
   Même traitement visuel que l'objectif d'une séance juste à côté (italique
   atténué derrière un filet doré) : c'est déjà, sur cet écran, la marque du
   texte que l'ÉLÈVE a écrit, par opposition à ce que l'app calcule.
   white-space: pre-wrap parce que linkifyText (render.js) laisse les retours à
   la ligne en \n et compte sur le CSS pour les rendre — c'est la même raison
   qui l'a mis sur .detail-notes et .desktop-notes-text, et ce bloc est le
   troisième et dernier site de rendu d'une consigne. Il est donc aussi le
   troisième porteur des liens de linkifyText, d'où son ajout aux deux règles
   `a` de ces deux classes (voir plus haut). */
.teacher-notes-text {
  font-size: 11.5px; line-height: 1.5; font-style: italic;
  color: var(--color-text-muted); white-space: pre-wrap; text-wrap: pretty;
  overflow-wrap: break-word;
  border-left: 1px solid color-mix(in srgb, var(--color-accent) 40%, transparent);
  padding-left: 8px;
}
/* Marges négatives compensant le padding : la cible tactile fait bien 30px
   de haut alors que le libellé reste optiquement aligné sur le bord de la
   carte et sur la ligne de méta juste au-dessus. */
.teacher-edit-btn {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: none; cursor: pointer;
  padding: 6px 8px; margin: -6px -8px -4px 0;
  font-family: var(--font-body); font-size: 12px; line-height: 1;
  color: var(--color-accent-ink); border-radius: var(--radius-sm);
  transition: background-color 140ms ease, transform 140ms var(--ease-out);
}
/* Le survol ne doit pas se déclencher au doigt (un tap laisse sinon la carte
   "allumée" derrière l'écran suivant) : réservé aux vrais pointeurs. */
@media (hover: hover) and (pointer: fine) {
  .teacher-edit-btn:hover { background: color-mix(in srgb, var(--color-accent) 13%, transparent); }
}
.teacher-edit-btn:focus-visible {
  background: color-mix(in srgb, var(--color-accent) 13%, transparent);
  outline: 2px solid var(--color-focus); outline-offset: 1px;
}
/* Retour au pressé : première occurrence de cet idiome dans l'app, à
   généraliser un jour aux .btn (aucun n'a d'état :active aujourd'hui). */
.teacher-edit-btn:active { transform: scale(0.97); }

/* Placé APRÈS la règle qu'il neutralise, et avec exactement le même
   sélecteur : une media query n'ajoute aucune spécificité, la déclarer plus
   haut dans le fichier n'aurait servi à rien. Le retour visuel du pressé
   reste, porté par le fond seul. */
@media (prefers-reduced-motion: reduce) {
  .teacher-edit-btn { transition: background-color 140ms ease; }
  .teacher-edit-btn:active { transform: none; background: color-mix(in srgb, var(--color-accent) 20%, transparent); }
}

/* Variante « Supprimer » de .teacher-edit-btn (chantier archiver/supprimer un
   devoir, migration 0038, écrite mais pas encore jouée) : même dessin, teinte
   danger — le même registre exactement que .link-btn-danger/.desktop-link-
   danger (rouge dès le repos, pas seulement au survol), rejoué ici sur la
   pilule icône+texte plutôt que sur le lien nu, parce que c'est CE dessin que
   porte déjà « Modifier » sur la même carte. Chaque règle est placée APRÈS
   celle qu'elle neutralise, avec exactement la même partie de sélecteur
   (classe + pseudo-classe) : à spécificité égale, seul l'ordre du fichier
   décide, même idiome que la surcharge de mouvement réduit ci-dessus. */
.teacher-edit-btn--danger { color: var(--color-error); }
@media (hover: hover) and (pointer: fine) {
  .teacher-edit-btn--danger:hover { background: color-mix(in srgb, var(--color-error) 13%, transparent); }
}
.teacher-edit-btn--danger:focus-visible { background: color-mix(in srgb, var(--color-error) 13%, transparent); }
@media (prefers-reduced-motion: reduce) {
  .teacher-edit-btn--danger:active { background: color-mix(in srgb, var(--color-error) 20%, transparent); }
}

.teacher-archive-section { padding: var(--space-4) var(--space-4) 0; }
.teacher-archive-grid { display: flex; flex-direction: column; gap: 1px; margin-top: var(--space-2); }
/* display:contents (mobile) : ce conteneur "disparaît" de la mise en page —
   les deux sections qu'il regroupe (actifs/sessions) restent des enfants
   directs de .teacher-sheet-body, empilées exactement comme si ce wrapper
   n'existait pas. Bascule en grille 2 colonnes uniquement à ≥1024px
   (media query plus bas) — même contenu, même sections, jamais dupliqué. */
.teacher-sheet-columns { display: contents; }

/* ═══ retours de pratique — enseignant (Volet B, chantier 8+9) ═══
   Mockup Claude Design validé « Enseignant - retours de pratique » : lecteur
   média natif habillé .teacher-feedback-plate — jamais .teacher-plate, le
   marqueur sans style propre qui redéfinit seulement le jeton d'anneau de
   focus des plaques à couleur fixe (voir plus haut) — et aucun contrôle
   custom à maintenir : le lecteur natif affiche déjà sa propre durée. Le
   filtre et les deux bordures viennent tels quels de la feuille de référence
   du mockup, sur des jetons qui existaient déjà ici à l'identique. */
.teacher-feedback-plate {
  filter: sepia(0.22) saturate(0.82) contrast(1.05);
  box-sizing: border-box; border: 6px solid var(--color-surface);
  outline: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.teacher-feedback-plate audio, .teacher-feedback-plate video { display: block; width: 100%; }

/* Une ligne de la liste (en attente ou répondue) : mêmes cinq déclarations que
   .teacher-student-card — une ligne plutôt qu'une grille de cartes, ces items
   portant plus de texte (exercice, extrait, média, récence) qu'un seul nom
   d'élève. */
.teacher-feedback-item {
  display: flex; align-items: flex-start; gap: var(--space-3); width: 100%;
  padding: var(--space-3); border: 1px solid var(--color-divider);
  border-radius: var(--radius-lg); background: var(--color-surface);
  box-shadow: var(--shadow-sm); cursor: pointer; text-align: left;
}
.teacher-feedback-group { display: flex; flex-direction: column; gap: var(--space-2); }

/* Détail d'une demande : colonne de lecture + rail de contexte élève. Le
   rail n'existe QUE sur ordinateur (maquette validée) — display:none plutôt
   qu'un rendu conditionnel côté JS, même idiome que .teacher-readonly-note. */
.teacher-feedback-detail-grid { display: flex; flex-direction: column; gap: var(--space-6); }
.teacher-feedback-rail { display: none; }

/* ═══ le guide (documentation, views/guide.js) ═══
   Sommaire cliquable. Volontairement de vrais <button> et non des liens
   href="#ancre" : le hash de l'URL est surveillé par le SDK Supabase et par
   cleanAuthHash (auth.js), et le défilement se fait donc en JS (voir
   l'action "guide-jump" dans main.js). Entrée/Espace fonctionnent nativement
   du seul fait que ce sont des <button>, sans le câblage clavier qu'exigent
   les <div role="button"> du reste de l'app. */
.guide-toc { display: flex; flex-direction: column; }
/* Enveloppe de liste du sommaire (role="list", views/guide.js) : elle reprend
   le display du <nav> qui la contient, donc les entrées s'empilent exactement
   comme quand elles en étaient les enfants directs. */
.guide-toc-list { display: flex; flex-direction: column; }
.guide-toc-item {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: 10px var(--space-2); min-height: 40px;
  margin: 0 calc(var(--space-2) * -1);
  background: none; border: 0; border-bottom: 1px solid var(--color-divider);
  border-radius: var(--radius-sm); text-align: left; cursor: pointer;
  font-family: var(--font-body); color: inherit;
  transition: background-color 140ms ease;
}
/* Filet retiré sous la DERNIÈRE entrée du sommaire. La règle s'écrivait
   `.guide-toc-item:last-child` tant que les boutons étaient les enfants
   directs du <nav> ; depuis que chacun vit dans son role="listitem", ce
   sélecteur les désignerait TOUS (chaque bouton est le seul enfant du sien) et
   les dix filets disparaîtraient. Elle est donc reportée d'un cran, sur le
   dernier item de la liste : elle désigne exactement le même bouton qu'avant.
   Contrairement à `.journal-entry:last-child` (supprimée, elle ne matchait
   rien), celle-ci est vivante — mesuré, 1 bouton sur 10 par coque. */
.guide-toc-list > *:last-child > .guide-toc-item { border-bottom: 0; }
.guide-toc-num {
  flex: none; width: 19px;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 11.5px; font-variant-numeric: tabular-nums; letter-spacing: 0.04em;
  color: var(--color-accent-ink);
}
.guide-toc-label {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 13.5px; line-height: 1.35; color: var(--color-text);
}
/* Survol réservé aux vrais pointeurs : au doigt, un tap laisserait sinon la
   ligne allumée jusqu'au tap suivant, ce qui se lit comme une sélection
   (même raisonnement que .teacher-edit-btn et .btn-gold). */
@media (hover: hover) and (pointer: fine) {
  .guide-toc-item:hover { background: color-mix(in srgb, var(--color-accent) 8%, transparent); }
}
.guide-toc-item:active { background: color-mix(in srgb, var(--color-accent) 13%, transparent); }
.guide-toc-item:focus-visible {
  background: color-mix(in srgb, var(--color-accent) 8%, transparent);
  outline: 2px solid var(--color-focus); outline-offset: 1px;
}

@media (min-width: 1024px) {
  /* Sommaire en marge plutôt qu'en tête de document : c'est le seul écran de
     l'app assez long pour qu'on s'y perde, et un sommaire qu'on ne voit plus
     dès la deuxième section ne sert à rien. position:sticky s'ancre sur le
     défilement de la PAGE, cette disposition n'ayant pas de .desktop-content
     (height:100vh + overflow-y:auto) comme security.js/news.js : en
     réintroduire un ici casserait le collant sans autre symptôme. */
  .guide-desktop-grid {
    display: grid; grid-template-columns: 188px minmax(0, 1fr);
    gap: var(--space-6); align-items: start;
  }
  .guide-toc--aside { position: sticky; top: var(--space-6); }
  .guide-toc--aside .guide-toc-item { border-bottom: 0; padding: 6px var(--space-2); min-height: 0; }
}

@media (min-width: 1024px) {
  .phone--has-desktop { max-width: none; min-height: 0; box-shadow: none; background: transparent; display: block; }
  /* Le socle mobile n'existe plus sur cette coque : .screen est masqué juste
     en dessous, donc ni barre d'onglets ni bande « Séance en cours » (le
     rappel de séance y vit dans la reliure, voir sessionResumeNavItem). Les
     deux corrections retombent à zéro sur les jetons plutôt que par une
     surcharge de .toast à la spécificité relevée — le seul élément qui les
     lise ici est justement le toast, rendu en frère de .screen et donc
     toujours visible. Cela couvre du même coup l'iPad en paysage à ≥1024px,
     où env(safe-area-inset-bottom) n'est pas nul alors qu'aucune barre ne
     l'absorbe. */
  .phone--has-desktop { --bottom-bar-grow: 0px; --session-resume-h: 0px; }
  .phone--has-desktop > .screen { display: none; }
  .phone--has-desktop > .sync-badge { display: none; }
  .phone--has-desktop > .desktop-shell { display: flex; min-height: 100vh; background: var(--color-bg); }

  /* Élargit .phone sans basculer vers .desktop-shell (pas de reliure ici,
     voir plus haut) — seul le plafond de largeur/l'ombre changent, .screen
     reste affiché et continue de piloter tout l'espace enseignant. */
  .phone--full-width { max-width: none; box-shadow: none; background: var(--color-bg); }

  /* Le dégagement d'encoche de la règle de base doit être REPRIS ici : cette
     surcharge réécrit le raccourci `padding` en entier, donc l'omettre
     rouvrirait le défaut au-delà de 1024px — c'est-à-dire sur l'iPad en
     paysage, seule largeur où .screen reste affiché (.phone--full-width). */
  .teacher-banner { padding: max(var(--space-5), env(safe-area-inset-top)) var(--space-8); }
  /* align-items reste flex-start et .teacher-banner-trailing garde sa marge
     de dégagement (règles de base) même ici : .sync-badge s'ancre au bord
     réel du viewport tandis que ce bloc est centré et plafonné à 1240px —
     tant que le viewport ne dépasse pas ~1540px, ce plafond ne laisse pas
     assez d'écart pour éviter le badge, donc annuler la marge ici
     réintroduirait le chevauchement sur beaucoup de résolutions desktop
     courantes (1280/1366/1440). */
  .teacher-banner-inner { max-width: 1240px; margin: 0 auto; }
  .teacher-banner-title { font-size: 38px; }

  .teacher-sort-row { padding: var(--space-4) var(--space-8) var(--space-3); background: transparent; }
  .teacher-sort-row-inner { max-width: 1240px; margin: 0 auto; }

  .teacher-roster { padding: var(--space-4) var(--space-8) var(--teacher-floating-clearance); }
  .teacher-roster-inner { max-width: 1240px; margin: 0 auto; }
  .teacher-roster-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(318px, 1fr)); gap: var(--space-4); align-items: start; }
  .teacher-student-card { padding: var(--space-4); }

  /* Même reprise que .teacher-banner ci-dessus, et pour la même raison. */
  .teacher-detail-header { padding: max(var(--space-4), env(safe-area-inset-top)) var(--space-8) var(--space-5); }
  .teacher-detail-header-inner { max-width: 1240px; margin: 0 auto; }
  .teacher-header-avatar { width: 58px; height: 58px; font-size: 25px; }
  .teacher-header-name { font-size: 34px; }

  .teacher-sticky-bar { padding: var(--space-3) var(--space-8); }
  .teacher-sticky-bar-inner { max-width: 1240px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
  .teacher-readonly-note { display: block; font-size: 12.5px; color: var(--color-text-muted); font-style: italic; }
  /* Scopé à cette barre précise : .btn-block reste plein-largeur partout
     ailleurs (formulaires mobiles, etc.), jamais touché globalement. */
  .teacher-sticky-bar .btn-block { width: auto; margin-top: 0; padding: 10px var(--space-5); font-size: 14.5px; }

  .teacher-sheet-body { max-width: 1240px; margin: 0 auto; padding: var(--space-6) var(--space-8) var(--space-8); display: flex; flex-direction: column; gap: var(--space-6); }
  /* display:contents : les deux sections (actifs/sessions) restent des
     enfants directs de .teacher-sheet-body pour la mise en page — c'est ce
     qui permet de les empiler sur mobile (aucun changement) et de les
     transformer en deux colonnes ici sans dupliquer leur contenu. */
  .teacher-sheet-columns { display: grid; grid-template-columns: minmax(0, 55fr) minmax(0, 45fr); gap: var(--space-6); align-items: start; }
  .teacher-sheet-body .teacher-section, .teacher-sheet-body .teacher-archive-section { padding: 0; }

  .teacher-archive-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--space-2); }

  .teacher-feedback-detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) 268px; gap: var(--space-6); align-items: start; }
  .teacher-feedback-rail {
    display: flex; flex-direction: column; gap: var(--space-3);
    padding: var(--space-4); border: 1px solid var(--color-divider); border-radius: var(--radius-md);
  }

  /* — édition d'une séance passée : bascule mobile plein écran → modale
     desktop à fond estompé (voir .session-edit-* en base plus haut). */
  .session-edit-mobile { display: none; }
  .session-edit-desktop {
    display: flex; align-items: center; justify-content: center; padding: var(--space-6);
    position: absolute; inset: 0; z-index: 10; background: color-mix(in srgb, #000 45%, transparent);
  }
  .session-edit-chip { font-size: 15px; padding: 11px 0; }
  .session-edit-compare-before, .session-edit-compare-after { font-size: 17px; }

  /* — outils de pratique : feuille montante mobile → carte centrée desktop.
     Même arbre DOM, jamais un second rendu (voir views/practiceTools.js) :
     seuls l'ancrage, les coins et la courbe d'entrée changent. Une feuille
     collée au bas d'un écran de 27 pouces serait à des centaines de pixels
     du regard, là où la carte se pose au centre comme les autres dialogues
     desktop de l'app. */
  .tools-backdrop { align-items: center; padding: var(--space-6); }
  .tools-sheet {
    max-width: 420px; max-height: 100%;
    border-radius: var(--radius-lg); border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
    padding: 0 var(--space-5) var(--space-5);
  }
  .tools-grip { display: none; }
  .tools-head { padding-top: var(--space-4); }
  /* Reprend l'entrée de la célébration de phase plutôt que la montée par le
     bas, qui n'a plus de sens une fois la carte détachée du bord. */
  .tools-sheet.tools--enter { animation: celebrate-card-in 240ms var(--ease-out) both; }
}

/* Mouvement réduit des outils de pratique. Placé APRÈS la media query desktop
   ci-dessus, et pas dans le bloc du haut de fichier : la surcharge desktop de
   .tools-sheet.tools--enter a exactement la même spécificité (0,2,0), donc
   seule une déclaration plus tardive dans le fichier l'emporte. Écrit plus
   haut, ce bloc n'aurait aucun effet au-delà de 1024px, silencieusement.
   Le panneau apparaît toujours (le fondu est ce qui rend son arrivée
   compréhensible), sans la montée ni le grossissement ; les retours au pressé
   restent, portés par le fond seul. Le flash de la lampe de battement, lui,
   est de l'information et non un effet : il est conservé, débarrassé de son
   seul grossissement (voir setBeatListener dans main.js). */
@media (prefers-reduced-motion: reduce) {
  .tools-sheet.tools--enter { animation-name: lb-fade; }
  .tools-pill, .tools-chip, .drone-note, .metro-step { transition-property: background-color, border-color; }
  .tools-pill:active { transform: none; }
  .metro-step:active { transform: none; background: color-mix(in srgb, var(--color-text) 14%, transparent); }
  .tools-chip:active, .drone-note:active {
    transform: none; background: color-mix(in srgb, var(--color-text) 14%, transparent);
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1b1a19; --color-surface: #242220; --color-text: #ece9e6;
    --color-divider: color-mix(in srgb, #ece9e6 16%, transparent);
    /* Même relation qu'en clair : neutral-100 sert de "carte" légèrement
       distincte de --color-surface (jamais identique), jamais du texte fixe
       sur fond sombre — ces usages-là (bandeaux enseignant, .tag-neutral)
       sont épinglés sur la valeur claire littérale plutôt que sur cette
       variable, précisément pour ne pas suivre ce changement. */
    --color-neutral-100: #322d2b;
    --color-error: #e09393;
    --color-prio-haute: #d97f76; --color-prio-moyenne: #c99a3a; --color-prio-basse: #74b183;
  }
}

/* Aucun thème FORCÉ : le carnet suit le seul réglage du système, donc la
   media query ci-dessus est la seule autorité de thème de ce fichier. Les
   deux blocs `:root[data-theme="dark"]` / `:root[data-theme="light"]` qui
   vivaient ici ont été retirés le 14 août 2026 avec le sélecteur
   Clair/Sombre/Automatique de « Préférences » (changement de décision
   produit, voir app/js/views/account.js) : plus rien ne pose `data-theme` sur
   <html>, ces sélecteurs ne pouvaient donc plus matcher. Leur `color-scheme`
   part avec eux — celui de `:root` (`light dark`, en tête de fichier) suffit,
   c'est exactement ce qu'il décrit. Le contrôle « SYSTÈME » de
   tools/audit-lisibilite.mjs interdit qu'un sélecteur de thème forcé
   réapparaisse dans l'une des deux feuilles. */
