/* ===========================================================================
   Mathsprep — feuille de style unique.

   Identité visuelle : encre et papier. Un indigo profond pour la structure, un
   vert forêt pour l'accent de la plateforme, l'ocre chaud RÉSERVÉ aux blocs
   d'attention du contenu pédagogique, un fond légèrement bleuté qui repose
   l'œil sur de longues séances de lecture. Le ton est universitaire : pas de
   dégradé décoratif, pas d'ombre colorée, pas d'emoji dans l'interface — les
   icônes sont des SVG en trait, en currentColor.

   Contrastes vérifiés sur fond blanc (WCAG AA exigé, AAA quand possible) :
     texte #1b2233        15,9:1      indigo #2a4a86      8,7:1
     texte discret #4a5468 7,6:1      vert   #1e6b4f      6,4:1
     blanc sur indigo      8,7:1      ocre   #a35d0a      5,1:1

   Échelle générale à 93,75 % : 1 rem vaut 15 px et non 16. Toute la
   typographie étant en rem, titres, corps, KaTeX et champs suivent d'un bloc.
   Ne pas descendre plus bas : le corps de texte deviendrait trop petit pour un
   visiteur dont l'écran n'est pas mis à l'échelle.
   =========================================================================== */

/* ------------------------------------------------------------------ tokens */

:root {
  --primary: #2a4a86;
  --primary-dark: #1f3a6e;
  --primary-soft: #eef2fa;
  --primary-line: #c9d5ec;

  --accent: #1e6b4f;
  --accent-soft: #ebf3ef;
  --accent-line: #c3d9cd;

  /* L'ocre est réservé au CONTENU pédagogique ([[attention]], [[methode]],
     indices d'exercices) : côté plateforme, l'accent est le vert forêt. Servir
     à la fois de signal d'alerte et d'accent neutre dilue le signal. */
  --warn: #a35d0a;
  --warn-soft: #fdf4e6;
  --warn-line: #e8cfa6;

  --ink: #1b2233;
  --ink-soft: #4a5468;
  --ink-faint: #6b7488;

  --bg: #f7f8fb;
  --surface: #ffffff;
  --surface-alt: #f2f4f9;
  /* Surface « en creux » : la correction d'un exercice. Gris neutre, SANS
     dominante bleue — un panneau indigo tenu sur toute la hauteur d'un
     corrigé de huit étapes est agressif, et c'est ce qui a été reproché à la
     première version. La distinction est portée par le liseré et l'étiquette,
     la teinte ne fait que soutenir. */
  --surface-sunk: #f8f9fa;
  --border: #dfe3ec;
  --border-strong: #c6ccdb;

  --danger: #a3271f;
  --danger-soft: #fbeeed;
  --success: #1c6446;
  --success-soft: #eaf3ee;

  /* LONGUEUR DE LIGNE. 48 rem = 720 px, soit environ 90 caractères en Inter
     15 px une fois le rembourrage retiré.

     Valeur ARBITRÉE, pas canonique. La typographie classique vise 65 à 75
     caractères ; un premier réglage à 40 rem les respectait mais a été jugé
     trop étroit — une colonne de 600 px dans une zone de plus de 1000 px se
     lit comme un gaspillage, et les formules mathématiques en ligne allongent
     les mots au point qu'une mesure de prose pure est trop sévère ici.
     90 caractères est le compromis retenu. Régler par cette seule ligne.

     La zone de contenu fait plus de 1000 px sur un écran large,
     ce qui donnait près de 130 caractères par ligne : l'œil perd sa ligne à
     chaque retour à la marge, et la fatigue est mise sur le compte du contenu
     alors qu'elle vient de la mise en page.
     On borne la COLONNE DE LECTURE (la carte d'exercice, la colonne des
     sections), pas la prose à l'intérieur : borner la prose laissait la carte
     pleine largeur avec 350 px de vide à droite, ce qui se lit comme un
     défaut d'affichage. Grilles de cartes et en-têtes ne sont pas concernés. */
  --measure: 48rem;

  /* LARGEUR DE PAGE, calée sur la colonne de lecture et non l'inverse.

     La coquille faisait 1420 px : la carte blanche du contenu en mesurait
     alors 1094, dont 1018 utiles, pour une colonne de lecture de 720 — soit
     près de 300 px de blanc À L'INTÉRIEUR de la carte, de part et d'autre du
     texte. Ce vide se lit comme un défaut d'affichage, et c'est exactement ce
     qu'on reproche à la page.

     Le calcul, à refaire si l'un des termes change :
       largeur de page = mesure + rembourrage de la carte (2 x 38)
                         + menu latéral (262) + gouttière (20)
                         + rembourrage de la coquille (2 x 22)
                       = 720 + 76 + 262 + 20 + 44 = 1122, arrondi à 1140.
     Il reste ainsi une dizaine de pixels de jeu de chaque côté du texte, et
     plus de vide inutile. Le blanc qui subsiste est HORS de la carte, sur le
     fond de page — là, il se lit comme une marge, pas comme un trou.

     En dessous de 1140 px de fenêtre, la coquille se réduit d'elle-même et la
     mesure cesse de mordre : le contenu remplit alors toute la carte. */
  --page-max: 1140px;

  --radius-lg: 20px;
  --radius: 12px;
  --radius-md: 8px;
  --radius-sm: 4px;

  /* Rythme vertical. Une refonte « premium » tient davantage à l'air entre les
     sections qu'à la couleur : une seule idée par section, largement respirée. */
  --space-section: clamp(52px, 7vw, 92px);
  --space-block: clamp(24px, 3.2vw, 40px);

  --shadow-sm: 0 1px 2px rgba(27, 34, 51, 0.06);
  --shadow: 0 6px 20px rgba(27, 34, 51, 0.08);
  --shadow-lg: 0 18px 48px rgba(27, 34, 51, 0.10);

  --font-heading: 'Source Serif 4', Georgia, 'Times New Roman', serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;

  --sidebar-width: 262px;
  --header-height: 62px;
}

html {
  font-size: 93.75%;
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  /* Réserve la gouttière de la barre de défilement, qu'elle soit présente ou
     non. Sans elle, un onglet dont le contenu tient dans la fenêtre et un
     onglet qui déborde n'ont pas la même largeur utile : toute la colonne
     centrée SAUTE de 8 px au changement d'onglet. Le CSS est correct, et
     pourtant la page se lit comme mal alignée. */
  scrollbar-gutter: stable;
}

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

/* L'attribut `hidden` doit toujours l'emporter.
   Le navigateur l'applique via `display: none` dans sa feuille par défaut — donc
   la moindre règle d'auteur qui pose un `display` la neutralise. C'est ce qui
   arrivait au bouton « Déconnexion » : `.btn { display: inline-flex }` gagnait
   sur le `hidden`, et le bouton restait affiché pour un visiteur non connecté,
   alors que la propriété `element.hidden` valait bien `true`. Un contrôle par
   script ne voyait donc rien ; seule l'image le montrait. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* --------------------------------------------------------------- typographie
   Chaque niveau vaut environ 1,4 fois le suivant. Le hero de l'accueil est plus
   grand que le titre de page, mais par sa PROPRE règle : ne jamais remonter les
   valeurs par défaut de h1/h2 pour agrandir une bannière — c'est ainsi qu'on
   finit avec des intertitres lus comme des bandeaux. */

h1, h2, h3, h4, h5 {
  font-family: var(--font-heading);
  font-weight: 600;
  line-height: 1.22;
  color: var(--ink);
  margin: 0 0 0.5em;
  letter-spacing: -0.01em;
}

h1 { font-size: 2.75rem; }
h2 { font-size: 1.95rem; }
h3 { font-size: 1.35rem; }
h4 { font-size: 1.15rem; }
h5 { font-size: 1.02rem; }

p { margin: 0 0 1em; }
a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }

.eyebrow {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.6em;
}

.lead {
  font-size: 1.12rem;
  color: var(--ink-soft);
  max-width: 62ch;
}

.muted { color: var(--ink-soft); }

/* ------------------------------------------------------------------ boutons
   Rien ne doit ressembler à un bouton sans être cliquable. Les mentions
   informatives (voir .hero-mentions) sont du texte, sans bordure ni fond. */

/* Les dimensions d'un bouton se règlent en HAUTEUR FIXE plus rembourrage
   horizontal, jamais en rembourrage vertical relatif : « 0.62em » se multiplie
   par la taille du texte, si bien que deux boutons de libellés différents
   n'avaient pas la même hauteur. Une hauteur minimale donne une ligne de base
   commune à tous les boutons de la page, et garantit une cible tactile
   suffisante en mobile. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  min-height: 40px;
  padding: 0 1.15em;
  border: 1px solid var(--primary);
  border-radius: var(--radius-md);
  background: var(--primary);
  color: #fff;
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  letter-spacing: 0.005em;
  line-height: 1.2;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.btn:hover { background: var(--primary-dark); border-color: var(--primary-dark); text-decoration: none; }
.btn:disabled { opacity: 0.55; cursor: not-allowed; }

.btn.secondary { background: transparent; color: var(--primary-dark); }
.btn.secondary:hover { background: var(--primary-soft); color: var(--primary-dark); border-color: var(--primary); }

.btn.ghost { background: transparent; color: var(--ink-soft); border-color: var(--border-strong); }
.btn.ghost:hover { background: var(--surface-alt); color: var(--ink); }

.btn.small { min-height: 34px; padding: 0 0.9em; font-size: 0.85rem; }

.actions { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: 1.4rem; }

:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ------------------------------------------------------------------- coquille */

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  gap: 20px;
  max-width: var(--page-max);
  margin: 22px auto;
  padding: 0 22px;
  align-items: start;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: rgba(255, 255, 255, 0.94);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}

/* En-tête et pied de page partagent la largeur de la coquille : sans cela les
   liens de navigation démarreraient 140 px à gauche du titre du module. */
.header-inner {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 9px 22px;
  display: flex;
  align-items: center;
  gap: 1.2rem;
  min-height: var(--header-height);
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--font-heading);
  font-size: 1.32rem;
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.015em;
}
.brand:hover { text-decoration: none; }
.brand-mark { color: var(--primary); display: inline-flex; }
.brand-sub {
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  border-left: 1px solid var(--border-strong);
  padding-left: 0.6rem;
  margin-left: 0.1rem;
}

/* BUDGET DE LARGEUR DE LA BARRE, à respecter si l'on ajoute un lien.
   Connecté en administrateur, la barre porte NEUF éléments : six pages
   publiques, « Espace étudiant », « Administration » et « Déconnexion ».
   Avec l'ancien rembourrage elle réclamait 956 px pour 900 disponibles une
   fois la page ramenée à 1140 px — d'où un retour à la ligne qui laissait le
   bouton « Déconnexion » SEUL sur une seconde ligne, et un en-tête à 92 px.

   Le resserrement ci-dessous (gouttière, rembourrage, corps) rend environ
   100 px et laisse une marge d'une quarantaine. Ajouter un lien, ou rallonger
   un libellé de plus de ~40 px, fera resurgir le défaut : le plus long est
   « Règlement et conditions » (179 px), c'est lui qu'il faudrait raccourcir. */
.top-nav {
  display: flex;
  align-items: center;
  gap: 0.2rem;
  margin-left: auto;
  flex-wrap: wrap;
}
.top-nav a {
  padding: 0.42em 0.62em;
  border-radius: var(--radius-md);
  color: var(--ink-soft);
  font-size: 0.89rem;
  font-weight: 500;
  /* Un libellé de deux mots ne se coupe jamais en deux lignes dans une barre
     horizontale : il pousse toute la barre à la ligne, ou il la déforme. */
  white-space: nowrap;
}
.top-nav a:hover { background: var(--surface-alt); color: var(--ink); text-decoration: none; }
.top-nav a.is-current { color: var(--primary); background: var(--primary-soft); }

/* `.top-nav a` (spécificité 0-1-1) l'emporte sur `.btn` (0-1-0) : sans ces
   règles, le lien « Se connecter » héritait de la couleur de texte discrète du
   menu et s'affichait en gris ardoise sur fond indigo — quasi illisible, et
   invisible à tout contrôle qui ne regarde pas l'image. */
.top-nav a.btn,
.top-nav button.btn { margin-left: 0.4rem; }
.top-nav a.btn { color: #fff; }
/* Le bouton de la barre suit le même corps que les liens qui l'entourent :
   sans cela il reste plus gros qu'eux et consomme la marge qu'on vient de
   récupérer. `.btn.small` garde sa hauteur minimale, donc sa cible tactile. */
.top-nav .btn.small { font-size: 0.84rem; padding: 0 0.7em; }
.top-nav a.btn:hover { background: var(--primary-dark); color: #fff; }
/* Sur la page de connexion, le lien du menu reçoit `.is-current`, qui pose un
   fond très pâle — combiné au texte blanc du bouton, il devenait illisible :
   un rectangle vide en haut à droite. Un bouton reste un bouton, même quand il
   désigne la page courante. */
.top-nav a.btn.is-current { background: var(--primary); color: #fff; }
.top-nav a.btn.secondary,
.top-nav a.btn.ghost { color: var(--primary); }

.mobile-toggle {
  display: none;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  border-radius: var(--radius-md);
  padding: 0.4em 0.55em;
  cursor: pointer;
  color: var(--ink);
}

/* ------------------------------------------------------------ menu latéral

   Ne pas y réintroduire le chevron de dépliage, la pastille de compteur ni le
   filet d'arbre en `::before` : ils ont été essayés, et écartés.

   Ce qui fait le caractère du menu :
     · le semestre est un BOUTON ENCADRÉ, en capitales, avec son compteur
       aligné à droite — pas une ligne de liste ;
     · le semestre ouvert est un APLAT PLEIN, blanc sur indigo ;
     · chaque module porte un FILET À GAUCHE qui devient indigo au survol,
       et la ligne glisse de 3 px — c'est le filet qui fait l'arborescence,
       d'où le retrait du trait vertical qui la doublait ;
     · l'ouverture est ANIMÉE en hauteur ;
     · la pastille pleine signale un module dont le contenu est publié, et les
       groupes de parcours ou de filière portent leur étiquette.

   Voir `renderSemesterMenu` : le surlignage se lit SUR LA ROUTE, jamais sur
   l'état. */

/* UN MENU COLLANT DOIT ÊTRE PLUS COURT QUE SA COLONNE, SINON IL NE COLLE PAS.

   `position: sticky` n'a d'effet que dans l'intervalle qui sépare la hauteur de
   l'élément de celle de son bloc conteneur. Ici le bloc conteneur est la case de
   grille, dont la hauteur vaut celle de la LIGNE — donc du plus grand des deux
   enfants de `.app-shell`. Un menu déployé mesure jusqu'à 976 px ; la fiche d'un
   module, elle, n'en fait que 702 : le menu devenait lui-même le plus grand, sa
   case de grille épousait exactement sa hauteur, l'intervalle tombait à zéro et
   il remontait avec la page — bord supérieur à 85 px, puis −408 px après 493 px
   de défilement, soit un suivi au pixel près du contenu.

   Deux bornes règlent les deux défauts d'un coup, et elles vont ENSEMBLE :

   - `max-height` : le menu ne dépasse jamais la fenêtre, donc il ne peut plus
     dicter la hauteur de la ligne dès qu'il y a de quoi défiler. Sans elle, même
     un collage qui fonctionne épingle le HAUT du menu et laisse sa fin — les
     derniers modules d'un semestre ouvert, et la carte d'accès — sous le bord
     inférieur de la fenêtre, hors d'atteinte pour toujours ;
   - `overflow-y: auto` : ce qui dépasse se rejoint en faisant défiler le menu
     lui-même. `overscroll-behavior: contain` empêche ce défilement, arrivé en
     butée, d'entraîner la page derrière lui.

   Ne pas retirer l'une en gardant l'autre : `max-height` seule couperait le
   menu, `overflow` seul ne servirait jamais. */

.sidebar {
  position: sticky;
  top: calc(var(--header-height) + 16px);
  max-height: calc(100vh - var(--header-height) - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
  box-shadow: var(--shadow-sm);
}

/* La barre de défilement du menu est fine et discrète : dans une colonne de
   262 px, les 15 px d'une barre ordinaire repoussent les intitulés de module et
   en font passer plusieurs sur deux lignes. */
.sidebar::-webkit-scrollbar { width: 8px; }
.sidebar::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}
.sidebar::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }

.sidebar-title {
  margin: 0 0 10px;
  font-size: 0.86rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* RYTHME DE LA COLONNE : un seul intervalle, 8 px, entre deux semestres.

   Chaque bouton étant enfermé dans son `.semester-item`, des marges posées sur
   le bouton ne se fusionnent pas : elles s'empilent en 12 px, et en 18 px sous
   le titre qui porte les siennes — la colonne se lit alors comme six blocs
   flottants au lieu d'une liste. La marge est donc portée par l'INTERVALLE
   (`item + item`), ce qui garantit qu'il n'y en a ni avant le premier ni après
   le dernier. */
.semester-item + .semester-item { margin-top: 8px; }

.semester-toggle {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 10px 12px;
  border: 1px solid var(--primary-line);
  border-radius: 6px;
  background: var(--primary-soft);
  font-family: var(--font-body);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--primary-dark);
  cursor: pointer;
  text-align: left;
  transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}
.semester-toggle:hover { transform: translateX(3px); border-color: var(--primary); }
.semester-toggle .st-title { flex: 1; }
.semester-toggle .st-count { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------- semestre en préparation

   LE MENU LATÉRAL NE PORTE AUCUNE MARQUE — décision du propriétaire, alignée
   sur la plateforme sœur, où aucun semestre n'en porte : tous affichent leur
   compteur. La colonne reste une liste régulière, où chaque ligne dit la même
   chose que sa voisine. L'état se découvre sur la page du semestre, qui a la
   place de l'expliquer.

   La marque ci-dessous ne sert donc qu'en TITRE de page ou de section. Elle est
   grise, et c'est délibéré : l'ocre est l'alerte, l'indigo l'information
   d'état, le vert l'identité — « en préparation » n'est aucun des trois, c'est
   une absence, et une absence ne demande pas d'encre. Elle ne prend pas la
   casse du titre qu'elle accompagne : un titre en serif, une marque en Inter. */
.sem-preparation {
  display: inline-block;
  vertical-align: middle;
  margin-left: 0.5em;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 0.62em;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Le bandeau de bas de page d'un semestre fermé : même gabarit que l'appel à
   l'achat qu'il remplace, sans son liseré indigo — il n'appelle à rien. */
.locked-notice.is-preparation {
  border-color: var(--line);
  background: var(--surface-alt);
}
.locked-notice.is-preparation svg { color: var(--muted); }

/* Le semestre est DÉJÀ lisible : on confirme, on ne vend pas. Le vert est ici
   à sa place — il est l'accent d'identité, et cette ligne ne signale ni une
   alerte ni un état en attente, mais un fait acquis. */
.locked-notice.is-ouvert {
  border-color: var(--accent-line, var(--line));
  background: var(--surface-alt);
}
.locked-notice.is-ouvert svg { color: var(--accent); }

/* Un semestre du cursus réel que la plateforme ne porte pas : cité, non lié. */
.os-sem-muet { color: var(--muted); }

/* ------------------------------------ la page d'un semestre en préparation

   Aucune couleur ajoutée : filets gris, numéros en serif clair. « En
   préparation » est une absence, et une absence ne demande pas d'encre — la
   même raison qui garde la marque du menu latéral en gris. */
.preparation-status {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  padding: 20px 22px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-alt);
}
.preparation-status-icon { color: var(--muted); flex: none; line-height: 0; margin-top: 2px; }
.preparation-status h3 { margin: 0 0 4px; font-size: 1.05rem; }
.preparation-status p { margin: 0; color: var(--muted); }

.preparation-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 14px;
  margin-top: 1.1em;
}
.preparation-item {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
}
/* Le numéro est un repère de progression, pas un compteur : il reste discret,
   en serif clair, et ne prend jamais le pas sur le titre du module. */
.preparation-num {
  flex: none;
  font-family: var(--font-title);
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1;
  color: var(--line-strong, var(--line));
  font-variant-numeric: tabular-nums;
}
.preparation-item h3 { margin: 0 0 3px; font-size: 1rem; }
.preparation-item p { margin: 0; font-size: 0.88rem; color: var(--muted); }

/* Les cartes portent du texte : sans `min-width: 0`, un titre long refuse de
   se replier et fait déborder la grille — le piège déjà payé sur le cours. */
.preparation-item > div { min-width: 0; }

/* Un module déjà accordé dans un semestre par ailleurs fermé. Le numéro cède
   la place à une coche verte — le vert est l'accent d'identité, et il dit ici
   un fait acquis, pas une alerte ni une attente. */
.preparation-item.est-ouvert { border-color: var(--accent-line); }
.preparation-item.est-ouvert .preparation-num { color: var(--accent); line-height: 0; margin-top: 3px; }
.preparation-item.est-ouvert .btn { margin-top: 10px; }

.semester-item.open .semester-toggle {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

/* Ouverture animée en hauteur.

   ATTENTION AU PLAFOND : le S6 aligne DOUZE modules, répartis en cinq groupes
   de parcours étiquetés — près de 800 px. Un plafond trop bas couperait la
   moitié du semestre sans rien signaler. Refaire la mesure avant d'ajouter un
   module au cursus. */
.semester-body {
  display: block;
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  padding: 0 0 0 9px;
  transform: translateY(-4px);
  /* `visibility` en plus de la hauteur : replié, le panneau reste dans le
     document et ses liens resteraient atteignables au clavier, invisibles.
     Elle est transitionnée pour ne masquer qu'À LA FIN de la fermeture. */
  visibility: hidden;
  transition: max-height 0.28s ease, opacity 0.22s ease, transform 0.22s ease,
              visibility 0s linear 0.28s;
}
.semester-item.open .semester-body {
  max-height: 900px;
  opacity: 1;
  padding: 2px 0 9px 9px;
  transform: translateY(0);
  visibility: visible;
  transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
  .semester-toggle, .semester-body { transition: none; }
}

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

.module-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 3px 0;
  padding: 8px 10px;
  border-left: 2px solid var(--primary-line);
  border-radius: 0 8px 8px 0;
  background: transparent;
  font-size: 0.92rem;
  font-weight: 600;
  line-height: 1.35;
  color: var(--ink-soft);
  transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}
.module-link:hover,
.module-link.active {
  border-left-color: var(--primary);
  background: var(--primary-soft);
  color: var(--primary-dark);
  transform: translateX(3px);
  text-decoration: none;
}
.module-link.active { font-weight: 700; }
.module-link .ml-title { flex: 1; min-width: 0; }
@media (prefers-reduced-motion: reduce) {
  .module-link { transition: none; }
  .module-link:hover, .module-link.active { transform: none; }
}

/* LE SIGLE DE FILIÈRE — « SMA », « SMI », « SMA·SMI ».

   IL EST GRIS, ET IL LE RESTE. Le codage par la couleur est réservé à
   l'information d'état sur cette plateforme : une pastille verte « SMI » se
   lirait comme « validé », une ocre comme une alerte, et l'indigo est déjà pris
   par la pastille de contenu publié, à trois pixels de là. Ce que le sigle dit
   n'est pas un état — c'est une appartenance —, il se distingue donc par la
   CASSE ET LA CHASSE, comme les étiquettes de groupe juste au-dessus.

   Il ne rétrécit pas (`flex-shrink: 0`) : un titre long passe à la ligne, le
   sigle reste entier. Le tabulaire (`font-variant-numeric` n'y suffirait pas)
   n'est pas nécessaire, les trois valeurs étant lues en colonne le long du bord
   droit, alignées par le `flex: 1` du titre. */
.module-link .ml-filiere {
  flex-shrink: 0;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  /* LE SIGLE EST VERT, ET C'EST LA SEULE TEINTE POSSIBLE ICI.

     « SMA » et « SMI » doivent être remarquables à l'œil, et les deux autres
     encres de la charte sont exclues d'avance — l'ocre est l'alerte, et
     l'indigo est l'information d'état, or la
     pastille de contenu publié est indigo et se pose à huit pixels du sigle :
     les deux marques auraient fusionné en une seule tache.

     Le vert est l'accent d'identité, et c'est déjà celui des étiquettes de
     groupe posées juste au-dessus (`.parcours-label`). La colonne de sigles
     prolonge donc ces étiquettes au lieu d'ajouter un signal de plus : même
     information — l'appartenance —, même encre.

     LES DEUX SIGLES PORTENT LA MÊME COULEUR. Donner une teinte à SMA et une
     autre à SMI aurait demandé une quatrième encre, et l'aurait fait entrer par
     la porte que la charte ferme : ce qui sépare SMA de SMI se lit dans les
     trois lettres, la couleur ne fait que les rendre visibles. */
  color: var(--accent);
}
/* Le sigle ne change pas sur la ligne active ni au survol : seule la couleur du
   titre bouge, et un sigle qui virerait en même temps ferait deux mouvements là
   où l'œil n'en attend qu'un. */

/* Pastille pleine = le module a du contenu publié. Un point vaut mieux qu'un
   mot dans une liste déjà dense.

   LA PLACE DE LA PASTILLE EST RÉSERVÉE SUR TOUTES LES LIGNES, remplie ou non.
   Sans cela, un module publié et un module en préparation n'ont pas la même
   largeur utile, et le sigle de filière qui les précède saute de 13 px de l'un
   à l'autre — mesuré : bord droit à 309 px sur une ligne portant la pastille,
   322 px sur une ligne sans. Une colonne qui
   n'est pas droite ne se lit plus en colonne. Le seul décalage qui subsiste est
   celui de la ligne active, dont les 3 px de glissement sont voulus. */
.module-link::after {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: transparent;
  flex-shrink: 0;
}
.module-link.is-ready::after {
  background: var(--primary);
}
.module-link.active.is-ready::after { background: var(--primary-dark); }
.module-link.is-hors-perimetre { color: var(--ink-faint); font-style: italic; font-weight: 500; }

.parcours-group { margin-top: 10px; }
.parcours-label {
  display: block;
  padding: 0 0.5em 0.3em;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
}


/* La carte d'accès ferme la colonne : c'est un PANNEAU PLEIN, et non un
   encadré pâle de plus. Posée en gris clair sous six
   boutons déjà pâles, elle se lisait comme un septième semestre en attente
   plutôt que comme l'appel à l'action qu'elle est. L'aplat indigo la sort de
   la liste sans lui donner plus de place. */
.access-card {
  margin-top: 16px;
  padding: 14px;
  border: 0;
  border-radius: 8px;
  background: var(--primary-dark);
  color: #fff;
  font-size: 0.86rem;
}
.access-card h4 { font-size: 0.95rem; color: #fff; margin-bottom: 0.35em; }
/* Blanc à 78 % sur l'indigo foncé : 7,4:1, au-delà de AA. Ne pas descendre
   plus bas pour « adoucir » — c'est le corps de texte, pas une légende. */
.access-card p { margin: 0 0 0.8em; color: rgba(255, 255, 255, 0.78); }
/* Le bouton indigo de la charte disparaîtrait sur un fond indigo : il
   s'inverse. La variante `ghost` reste creuse, mais en blanc. */
.access-card .btn {
  background: #fff;
  border-color: #fff;
  color: var(--primary-dark);
}
.access-card .btn:hover { background: var(--primary-soft); border-color: var(--primary-soft); color: var(--primary-dark); }
.access-card .btn.ghost {
  background: transparent;
  border-color: rgba(255, 255, 255, 0.55);
  color: #fff;
}
.access-card .btn.ghost:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }

/* ------------------------------------------------------- zone de contenu */

.content-area {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: clamp(17px, 3.4vw, 38px);
  box-shadow: var(--shadow-sm);
}

.section { margin-top: 36px; }
.section:first-child { margin-top: 0; }

/* =========================================================== page d'accueil

   L'accueil garde le MENU LATÉRAL, comme toutes les autres pages. La barre du
   haut n'ayant pas d'onglet « Programme », le menu est le SEUL explorateur du
   cursus : le retirer de la page d'entrée obligerait le visiteur à ouvrir un
   module au hasard pour découvrir ce que la plateforme couvre.

   `.is-landing` ne retire donc plus que la CARTE BLANCHE : le hero et les
   bandes de la présentation se posent à même le fond de page.

   Conséquence à ne pas perdre de vue : la colonne de l'accueil ne fait plus
   1096 px mais 814 (1140 − 44 de rembourrage − 262 de menu − 20 de gouttière).
   Les grilles du hero, des frictions et de la méthode se replient donc sur des
   REQUÊTES DE CONTENEUR et non sur la largeur de fenêtre — voir plus bas :
   sur un écran de 1920 px, la fenêtre est large pendant que la colonne, elle,
   est étroite, et une `@media` ne se déclencherait jamais.
   =========================================================================== */

.app-shell.is-landing .content-area {
  background: transparent;
  border: 0;
  box-shadow: none;
  padding: 0;
}

/* Le conteneur de référence des replis de l'accueil. Sans `container-type`,
   les `@container` ci-dessous ne s'appliquent à rien — en silence. */
.landing {
  container-type: inline-size;
  container-name: landing;
}

.landing > section + section { margin-top: var(--space-section); }

/* ------------------------------------------------------------------- hero */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
  gap: clamp(28px, 4.5vw, 60px);
  align-items: center;
  padding: clamp(18px, 4vw, 46px) 0 clamp(8px, 2vw, 20px);
}

.hero h1 {
  font-size: clamp(2.05rem, 3.6vw, 3.1rem);
  line-height: 1.14;
  letter-spacing: -0.022em;
  margin-bottom: 0.42em;
  /* 15ch cassait le titre en cinq lignes très courtes, avec du vide à droite.
     Une accroche de trois lignes se lit d'un bloc ; cinq se déchiffrent. */
  max-width: 21ch;
}
.hero .lead {
  font-size: clamp(1rem, 1.25vw, 1.14rem);
  max-width: 46ch;
}
.hero .actions { margin-top: 1.7rem; gap: 0.8rem; }
.hero .btn { padding: 0.78em 1.5em; font-size: 1rem; }

/* Mentions du hero : ce sont des informations, pas des commandes. Texte
   capitales espacées séparé par des points médians, aucune bordure ni fond —
   une pastille bordée se lit comme un bouton et déçoit au clic. */
.hero-mentions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.55rem;
  margin: 2rem 0 0;
  font-size: 0.73rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.hero-mentions span:not(:last-child)::after {
  content: '·';
  margin-left: 0.55rem;
  color: var(--border-strong);
}

/* Ce qui est ouvert aujourd'hui. En casse normale, juste sous les mentions en
   capitales : c'est une phrase à lire, pas une quatrième étiquette. Aucun fond
   ni bordure — rien ici ne doit ressembler à un bouton, la règle vaut pour tout
   le hero. */
.hero-ouverture {
  margin: 0.7rem 0 0;
  font-size: 0.86rem;
  color: var(--muted);
}
.hero-ouverture strong { color: var(--ink); font-weight: 600; }

/* --------------------------------------------------------- scène du hero

   « Le moteur » : le code en haut, la ligne en cours surlignée, et deux cadrans
   en dessous que la boucle pilote. Voir `startHeroEngine` dans app.js pour la
   mécanique et les valeurs, qui sont exactes.

   Le mouvement doit rester CALME. Toute la retenue de cette scène tient dans
   trois choix : des transitions longues (0,45 s) là où le navigateur interpole,
   aucun effet lumineux, et une seule chose qui change à la fois. */

.hero-art { position: relative; }

.hero-engine {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 16px;
  overflow: hidden;
}
.hero-engine::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--primary-soft) 1px, transparent 1px),
    linear-gradient(90deg, var(--primary-soft) 1px, transparent 1px);
  background-size: 26px 26px;
  opacity: 0.6;
  pointer-events: none;
}

/* ---- le code, moteur de la scène ---- */

.engine-code {
  position: relative;
  display: grid;
  gap: 1px;
  background: var(--ink);
  border-radius: var(--radius-md);
  padding: 12px 6px;
}
.engine-code .code-line {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1.55;
  color: #99a3ba;
  white-space: pre;
  padding: 0 10px;
  border-left: 2px solid transparent;
  transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
/* La ligne en cours d'exécution. Le liseré ocre suffit à la désigner : un fond
   vif à cet endroit ferait clignoter la scène à chaque pas. */
.engine-code .code-line.is-current {
  color: #ffffff;
  background: rgba(255, 255, 255, 0.07);
  border-left-color: #e8a33d;
}

/* ---- l'état courant du calcul ---- */

.engine-readout {
  position: relative;
  display: flex;
  gap: 1.4rem;
  margin: 12px 2px 10px;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: var(--ink-soft);
}
.engine-readout em { font-style: italic; color: var(--ink-faint); }
.engine-readout strong {
  color: var(--primary);
  font-weight: 600;
  /* Largeur réservée : sans elle, « 0,5000 » puis « ≈ 0,9922 » font sauter la
     ligne à chaque itération. */
  display: inline-block;
  min-width: 8ch;
}

/* ---- les deux cadrans ---- */

.engine-charts {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.engine-charts figure { margin: 0; }
.engine-charts svg { width: 100%; height: auto; display: block; }
.engine-charts figcaption {
  margin-top: 4px;
  text-align: center;
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  color: var(--ink-faint);
}

.engine-charts .axis { stroke: var(--border-strong); stroke-width: 1.2; }
.engine-charts .tick { font-family: var(--font-body); font-size: 10px; fill: var(--ink-faint); }
.engine-charts .limit { stroke: var(--accent); stroke-width: 1.2; stroke-dasharray: 4 3; }

.engine-charts .bar {
  fill: var(--primary);
  opacity: 0.85;
  /* `fill-box` place l'origine sur la boîte du rectangle et non sur le SVG
     entier : sans cela les barres se déforment depuis le coin du dessin. */
  transform-box: fill-box;
  transform-origin: bottom;
  transform: scaleY(0);
  transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.3, 1);
}

.engine-charts .wave {
  fill: none;
  stroke: var(--primary);
  stroke-width: 2;
  stroke-linecap: round;
  transition: stroke-dashoffset 0.45s ease;
}
.engine-charts .envelope {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.1;
  stroke-dasharray: 3 3;
  opacity: 0.65;
}
.engine-charts .peak {
  fill: var(--accent);
  transition: cx 0.45s ease, cy 0.45s ease;
}

.hero-board-note {
  margin: 12px 0 0;
  text-align: center;
  font-size: 0.82rem;
  color: var(--ink-faint);
}


/* ------------------------------------------------------- bande « problème »

   Le texte sur les polycopiés incomplets est un texte de PROBLÈME : il n'a pas
   sa place dans le hero, qui doit annoncer la solution.
   Il ouvre donc sa propre section, et la bascule vers la réponse se fait à la
   fin, une fois les frictions nommées. */

.problem-band {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: clamp(26px, 4vw, 46px);
  text-align: center;
}
.problem-band .problem-lead {
  font-family: var(--font-heading);
  font-size: clamp(1.25rem, 2.1vw, 1.72rem);
  line-height: 1.42;
  color: var(--ink);
  max-width: 34ch;
  margin: 0 auto 0.2em;
}
.friction-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  margin: var(--space-block) 0;
  text-align: left;
}
.friction {
  display: flex;
  gap: 0.7rem;
  align-items: flex-start;
  padding: 15px 17px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.friction svg { color: var(--accent); flex-shrink: 0; margin-top: 2px; }
.friction strong { display: block; font-size: 0.95rem; margin-bottom: 0.15em; }
.friction span { color: var(--ink-soft); font-size: 0.88rem; line-height: 1.55; }

.problem-answer {
  font-size: 1.06rem;
  color: var(--ink);
  max-width: 48ch;
  margin: 0 auto;
}
.problem-answer strong { color: var(--primary); }

/* ------------------------------------------------------- en-tête de section */

.section-head { max-width: 60ch; margin-bottom: var(--space-block); }
.section-head.centered { margin-left: auto; margin-right: auto; text-align: center; }
.section-head h2 {
  font-size: clamp(1.6rem, 2.7vw, 2.15rem);
  letter-spacing: -0.015em;
  margin-bottom: 0.35em;
}
.section-head p { color: var(--ink-soft); margin: 0; }

/* --------------------------------------------------- méthode en trois temps

   Comprendre → S'exercer → Réussir. La numérotation 01 / 02 / 03 suffit à dire
   la progression : un filet de liaison avait été tenté entre les cartes, mais
   il passait derrière leurs fonds opaques et ne se voyait que dans les 18 px
   d'interstice — de l'encre pour rien. Vérifié à l'image, puis retiré. */

.method-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 18px;
}

.method-step {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 24px 22px 22px;
  text-align: center;
}
.method-index {
  display: block;
  font-family: var(--font-heading);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--ink-faint);
  margin-bottom: 14px;
}
.method-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--primary-soft);
  border: 1px solid var(--primary-line);
  color: var(--primary);
  margin-bottom: 14px;
}
.method-step h3 { font-size: 1.22rem; margin-bottom: 0.4em; }
.method-step p {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.93rem;
  line-height: 1.6;
}
.method-step:nth-child(3) .method-icon {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
}

/* ---------------------------------------------------------- cartes communes */


.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px;
  box-shadow: var(--shadow-sm);
}
.card h3 { margin-bottom: 0.4em; }
.card p { color: var(--ink-soft); margin-bottom: 0; font-size: 0.94rem; }
.card-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-md);
  background: var(--primary-soft);
  color: var(--primary);
  margin-bottom: 12px;
}

/* Carte de semestre sur l'accueil */
.semester-card {
  display: block;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.semester-card:hover { border-color: var(--primary-line); box-shadow: var(--shadow); text-decoration: none; }
.semester-card h3 { font-size: 1.2rem; margin-bottom: 0.25em; color: var(--ink); }
.semester-card .sc-count { font-size: 0.82rem; color: var(--ink-faint); }
/* L'ÉTIQUETTE DE FILIÈRE EST DU TEXTE, PAS UNE COULEUR. Le codage par la
   couleur est réservé à l'information d'état sur cette plateforme : une
   pastille verte « SMI » se lirait comme « validé », une ocre comme une
   alerte. La filière se distingue donc par la casse et la chasse, comme les
   étiquettes de parcours du menu latéral, et se pose au-dessus de la ligne de
   pied pour ne pas allonger celle-ci d'un cran. */
.semester-card .sc-filiere {
  display: block;
  margin-bottom: 0.25em;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* Même encre que le sigle du menu latéral — voir `.ml-filiere` pour le
     raisonnement complet. La même information ne se dit pas de deux couleurs
     selon l'endroit où on la lit. */
  color: var(--accent);
}
.semester-card ul {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  font-size: 0.88rem;
  color: var(--ink-soft);
}
.semester-card li { padding: 2px 0; }

/* ---------------------------------------------------------- pages sobres */

.page-head { max-width: 74ch; margin-bottom: 28px; }
.institution-heading { font-size: 1.6rem; margin-top: 1.6em; }

.content-list { padding-left: 1.2em; color: var(--ink-soft); }
.content-list li { margin-bottom: 0.5em; }

.check-list { list-style: none; padding: 0; margin: 0; }
.check-list li {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  margin-bottom: 0.7em;
  color: var(--ink-soft);
}
.check-list svg { color: var(--primary); flex-shrink: 0; margin-top: 3px; }

/* ------------------------------------------------------------- tarification */

/* `max-width` sans marges automatiques = bloc collé à gauche. La grille faisait
   780 px dans un conteneur de 861 px et laissait 81 px de vide à droite, sous un
   en-tête centré : le décalage se voyait immédiatement. Toute largeur bornée au
   milieu d'une page centrée doit porter `margin-inline: auto`. */
.pricing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
  max-width: 780px;
  margin-inline: auto;
}

.pricing-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px;
  display: flex;
  flex-direction: column;
}
.pricing-card.featured {
  border-color: var(--primary);
  box-shadow: var(--shadow);
  position: relative;
}
.pricing-badge {
  position: absolute;
  top: -11px;
  left: 24px;
  background: var(--primary);
  color: #fff;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.28em 0.7em;
  border-radius: 999px;
}
.price-line { display: flex; align-items: baseline; gap: 0.35rem; margin: 0.6em 0 0.2em; }
.price-amount { font-family: var(--font-heading); font-size: 2.6rem; font-weight: 700; color: var(--ink); line-height: 1; }
.price-unit { color: var(--ink-faint); font-size: 0.94rem; }
.price-context { font-size: 0.88rem; color: var(--ink-soft); }
.price-context s { color: var(--ink-faint); }
.pricing-card .actions { margin-top: auto; padding-top: 1.2rem; }

.anchor-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.8rem;
  padding: 12px 16px;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-md);
  font-size: 0.92rem;
  color: var(--ink-soft);
  margin-bottom: 22px;
}
.anchor-strip strong { color: var(--accent); }

.payment-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}
.payment-box {
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
}
.detail-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.55em 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.92rem;
}
.detail-row:last-child { border-bottom: 0; }
.detail-row span:first-child { color: var(--ink-faint); }
.detail-row span:last-child { font-weight: 600; text-align: right; }
.detail-row .not-configured { color: var(--danger); font-weight: 500; font-style: italic; }

.numbered-flow { list-style: none; counter-reset: step; padding: 0; margin: 0; }
.numbered-flow li {
  counter-increment: step;
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: 0.8rem;
  padding: 0.6em 0;
}
.numbered-flow li::before {
  content: counter(step);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  font-size: 0.82rem;
  font-weight: 700;
}
.numbered-flow strong { display: block; grid-column: 2; }
/* Le `li` est une grille « 28px | 1fr ». La pastille numérotée occupe la
   première colonne, le titre la seconde ; sans cette règle, la description
   retombait dans la PREMIÈRE colonne de la ligne suivante — large de 28 px,
   soit un mot par ligne. Défaut invisible à toute mesure de débordement,
   puisque rien ne dépassait : c'est la capture d'écran qui l'a montré. */
.numbered-flow li > span { grid-column: 2; color: var(--ink-soft); font-size: 0.9rem; }

/* ------------------------------------------------------- formulaires / auth */

/* --------------------------------------------------- connexion / inscription

   Accroche, puis une carte à deux onglets qui porte les deux formulaires. Les animations sont volontairement courtes — la
   carte entre, le panneau glisse — et disparaissent sous
   `prefers-reduced-motion`, où la bascule redevient instantanée. */

.auth-page { max-width: 520px; margin: 0 auto; }
.auth-head { text-align: center; margin-bottom: 24px; }
.auth-head h1 { font-size: clamp(1.6rem, 2.6vw, 2.1rem); margin-bottom: 0.35em; }
.auth-head .lead { font-size: 1rem; margin: 0 auto; max-width: 46ch; }

.auth-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 24px;
  overflow: hidden;
  animation: auth-enter 0.34s ease both;
}
.auth-card.is-leaving { animation: auth-leave 0.2s ease both; }

.auth-tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  padding: 4px;
  margin-bottom: 22px;
  background: var(--surface-alt);
  border-radius: var(--radius-md);
}
.auth-tabs button {
  padding: 0.6em 0.8em;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  font: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.auth-tabs button:hover { color: var(--ink); }
.auth-tabs button.active {
  background: var(--surface);
  color: var(--primary);
  box-shadow: var(--shadow-sm);
}

.auth-form-panel {
  display: grid;
  gap: 14px;
  animation: panel-slide 0.34s ease both;
}
.auth-form-panel .btn { justify-content: center; margin-top: 4px; }

.auth-forgot { margin: 14px 0 0; text-align: center; font-size: 0.92rem; }
.auth-forgot a { font-weight: 600; }

.auth-aside {
  margin: 18px 0 0;
  text-align: center;
  font-size: 0.9rem;
  color: var(--ink-soft);
}

@keyframes auth-enter {
  from { opacity: 0; transform: translateY(14px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes auth-leave {
  to { opacity: 0; transform: translateY(10px) scale(0.985); }
}
@keyframes panel-slide {
  from { opacity: 0; transform: translateX(18px); }
  to   { opacity: 1; transform: none; }
}

.form-grid { display: grid; gap: 14px; }
.form-grid.two { grid-template-columns: 1fr 1fr; }

label { display: block; font-size: 0.88rem; font-weight: 600; color: var(--ink); }
input, select, textarea {
  width: 100%;
  margin-top: 6px;
  padding: 0.62em 0.75em;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  font: inherit;
  font-size: 0.95rem;
  font-weight: 400;
  color: var(--ink);
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-soft);
}
textarea { min-height: 130px; resize: vertical; }

/* La ligne de message ne réserve pas de place tant qu'elle est vide. */
.form-message { margin: 0.8em 0 0; font-size: 0.9rem; color: var(--danger); }
.form-message:empty { display: none; }
.form-message.success { color: var(--success); }

.form-switch { margin-top: 1.2em; font-size: 0.9rem; color: var(--ink-soft); }

/* --------------------------------------------------- espace étudiant / admin */

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.25em 0.65em;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
}
.status-pill.approved { background: var(--success-soft); color: var(--success); }
/* « En attente » en indigo pâle, pas en vert : depuis que l'accent est vert,
   une pastille verte se lirait comme « approuvé ». */
.status-pill.pending { background: var(--primary-soft); color: var(--primary); }
.status-pill.rejected { background: var(--danger-soft); color: var(--danger); }

.verify-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.8rem;
  padding: 12px 16px;
  background: var(--primary-soft);
  border: 1px solid var(--primary-line);
  border-radius: var(--radius-md);
  margin-bottom: 22px;
  font-size: 0.92rem;
}

.admin-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.admin-table th {
  text-align: left;
  font-size: 0.76rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 0.6em 0.7em;
  border-bottom: 1px solid var(--border);
}
.admin-table td { padding: 0.7em; border-bottom: 1px solid var(--border); vertical-align: top; }
.admin-table tr:hover td { background: var(--surface-alt); }
.admin-filters { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; margin-bottom: 16px; }
.admin-filters input, .admin-filters select { margin-top: 0; width: auto; min-width: 180px; }
.admin-editor { background: var(--surface-alt); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 16px; }
.access-check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 6px 14px;
  margin-top: 8px;
}
.access-check-grid label {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-weight: 400;
  font-size: 0.86rem;
  color: var(--ink-soft);
}
.access-check-grid input { width: auto; margin: 0; }

/* ------------------------------------------------------------------ module */

.module-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 22px;
}
.module-head .breadcrumb { font-size: 0.84rem; color: var(--ink-faint); margin-bottom: 0.3em; }
.module-head h1 { font-size: 2.1rem; margin: 0; }
.module-tag {
  display: inline-block;
  padding: 0.24em 0.6em;
  border-radius: var(--radius-sm);
  background: var(--primary-soft);
  color: var(--primary);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.tab-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 24px;
}
.tab-bar button {
  padding: 0.6em 1em;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  font: inherit;
  font-size: 0.94rem;
  font-weight: 600;
  color: var(--ink-soft);
  cursor: pointer;
  margin-bottom: -1px;
}
.tab-bar button:hover { color: var(--ink); }
.tab-bar button.active { color: var(--primary); border-bottom-color: var(--primary); }

.chapter-picker { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px; }
.chapter-picker button {
  padding: 0.42em 0.85em;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  font: inherit;
  font-size: 0.87rem;
  color: var(--ink-soft);
  cursor: pointer;
}
.chapter-picker button:hover { border-color: var(--primary-line); color: var(--ink); }
.chapter-picker button.active { background: var(--primary); border-color: var(--primary); color: #fff; font-weight: 600; }

/* Lecteur de cours : sommaire cliquable + sections repliables */
/* LE SOMMAIRE PASSE AU-DESSUS, il n'est plus une colonne latérale.

   Il occupait 230 px à gauche. Dans la colonne de lecture commune à toute la
   page de module (720 px), il ne restait que 462 px pour le texte du cours,
   contre 678 px pour un exercice — et surtout le texte commençait 257 px plus
   à droite que le titre du module. L'onglet Cours était donc à la fois plus
   étroit et désaligné par rapport à tous les autres, ce qui se voyait
   immédiatement en passant d'un onglet à l'autre.

   Empilé, le sommaire tient sur une ou deux lignes, et les sections récupèrent
   toute la largeur : les quatre onglets présentent enfin la MÊME colonne. */
.course-reader { display: block; }
.course-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.3rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 12px;
  margin-bottom: 20px;
  min-width: 0;
}
.course-toc a {
  padding: 0.2em 0;
  font-size: 0.86rem;
  color: var(--ink-soft);
  line-height: 1.4;
}
.course-toc a:hover { color: var(--primary); text-decoration: none; }

.course-sections { min-width: 0; }
.course-section { border: 1px solid var(--border); border-radius: var(--radius-md); margin-bottom: 12px; overflow: hidden; }
.course-section-header {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.85em 1em;
  border: 0;
  background: var(--surface-alt);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* Le titre de section doit dominer les intertitres qu'il contient. Il valait
   1,18 rem quand un « ## » du contenu produisait un .md-h3 de 1,22 rem : la
   hiérarchie était INVERSÉE, le sous-titre était plus gros que son conteneur.
   Aucun contrôle par script ne voit ça, et c'est ce qui rendait la lecture
   d'un chapitre désorientante. Voir aussi la règle .md-h3 scopée plus bas. */
/* `min-width: 0` est INDISPENSABLE : un élément flex vaut `min-width: auto` par
   défaut, donc il refuse de descendre sous la largeur de son contenu. Un titre
   long comme « 1.2 Les deux caractérisations de la borne supérieure » faisait
   alors déborder toute la page en 375 px — défaut apparu en portant la taille
   de 1,18 à 1,3 rem, et invisible sur écran large. */
.course-section-header h2 { font-size: 1.3rem; margin: 0; flex: 1; min-width: 0; }
.course-section-header .chevron { color: var(--ink-faint); transition: transform 0.18s ease; display: inline-flex; }
.course-section.open .course-section-header { background: var(--primary-soft); }
.course-section.open .chevron { transform: rotate(90deg); }
.course-section-body { padding: 18px 20px 22px; }

/* ------------------------------------------------- blocs pédagogiques
   Définition, théorème, démonstration… Le liseré de gauche porte la couleur :
   indigo pour ce qui est énoncé, ocre pour ce qui alerte, gris pour l'exemple.
   La démonstration est repliée par défaut — voir content-engine.js. */

/* LE TYPE SE LIT DANS LA TYPOGRAPHIE, PAS DANS LA COULEUR.

   Deux passes ont été nécessaires pour arriver là, et l'erreur de la première
   mérite d'être notée : les onze types de blocs se distinguaient par leur
   TEINTE. Il a donc suffi de réduire la couleur — demande légitime, la page
   était saturée — pour qu'on ne fasse plus la différence entre un théorème et
   une définition. Couleur et distinction étaient le même levier.

   La typographie mathématique règle ce problème depuis des siècles, et sans
   couleur : un ÉNONCÉ se compose en ITALIQUE, une DÉFINITION en romain, une
   démonstration en romain sous une étiquette en italique. C'est la convention
   des recueils imprimés, elle est immédiatement lisible, et elle ne coûte pas
   un pixel d'encre colorée.

   Aucun bloc ne porte donc plus de fond, à une exception : « attention »,
   seul bloc d'alerte, garde une teinte ocre très pâle. Le reste se distingue
   par le style du corps, la graisse de l'étiquette et l'épaisseur du liseré. */

.content-block {
  border: 0;
  border-left: 2px solid var(--border-strong);
  border-radius: 0;
  background: transparent;
  padding: 2px 0 2px 18px;
  margin: 20px 0;
}
.blk-label {
  display: block;
  font-family: var(--font-heading);
  font-size: 0.98rem;
  font-weight: 700;
  color: var(--primary);
  margin-bottom: 0.4em;
}
.blk-body > *:last-child { margin-bottom: 0; }

/* DEUX NIVEAUX, ET DEUX SEULEMENT. Quand tout est mis en exergue, plus rien ne
   l'est, et la page devient illisible sans qu'aucun contrôle par script ne
   signale quoi que ce soit. La part de blocs à fond coloré doit rester autour
   de 13 % ; elle se compte à l'œil, sur le rendu.

   Ce qui porte un FOND est ce qu'on revient chercher ou ce qui alerte :
     · définition et théorème — la matière de référence, en indigo ;
     · attention — le seul bloc chaud, donc le seul qui reste voyant.
   Tout le reste ne porte qu'un LISERÉ : proposition, lemme, corollaire,
   exemple, remarque, note, méthode. Ce sont des commentaires, ils accompagnent
   la lecture au lieu de l'interrompre.

   MÉTHODE A ÉTÉ SORTI DU FOND OCRE. Il partageait la teinte d'« attention »
   alors qu'il n'alerte de rien : c'est une indication de marche à suivre. Deux
   sens sous une même couleur, c'est exactement la dilution du signal que le
   design system reproche déjà à l'ocre côté interface. Il garde son liseré
   ocre — même famille, poids différent. */

/* ── LES ÉNONCÉS ──────────────────────────────────────────────────────────
   Théorème, proposition, lemme, corollaire : CORPS EN ITALIQUE. C'est le
   signal, et il se voit d'un coup d'œil en parcourant une page. La définition
   reste en ROMAIN : c'est ce qui la sépare du théorème sans une goutte de
   couleur, et c'est aussi la convention imprimée — on énonce en italique, on
   définit en romain. */
.blk-def, .blk-thm { border-left-width: 3px; border-left-color: var(--primary); }
.blk-prop, .blk-lemme, .blk-cor { border-left-color: var(--primary-line); }

.blk-thm .blk-body,
.blk-prop .blk-body,
.blk-lemme .blk-body,
.blk-cor .blk-body { font-style: italic; }

/* Les mathématiques restent droites : KaTeX gère lui-même ses italiques, et
   une formule penchée deux fois devient illisible. */
.blk-thm .katex, .blk-prop .katex, .blk-lemme .katex, .blk-cor .katex,
.blk-thm .blk-body strong, .blk-prop .blk-body strong,
.blk-lemme .blk-body strong, .blk-cor .blk-body strong { font-style: normal; }

.blk-thm .blk-label { color: var(--primary-dark); }
.blk-prop .blk-label, .blk-lemme .blk-label, .blk-cor .blk-label { color: var(--primary); }

/* ── LE COMMENTAIRE ───────────────────────────────────────────────────────
   Exemple, remarque, note : l'étiquette passe en capitales espacées et en
   gris. Elle cesse d'être un titre pour devenir une mention — le lecteur
   la voit s'il la cherche, et l'enjambe sinon. */
.blk-exemple .blk-label,
.blk-remarque .blk-label,
.blk-note .blk-label {
  font-family: var(--font-body);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ── LE PÉDAGOGIQUE ───────────────────────────────────────────────────────
   Méthode ne fait qu'indiquer une marche à suivre : liseré ocre, pas de fond.
   Attention est le SEUL bloc coloré de la plateforme, et c'est ce qui lui rend
   son sens : une seule chose chaude sur la page, on sait qu'il faut la lire. */
.blk-methode { border-left-width: 3px; border-left-color: var(--warn); }
.blk-methode .blk-label { color: var(--warn); }
.blk-attention {
  border-left-width: 3px;
  border-left-color: var(--warn);
  background: var(--warn-soft);
  padding: 12px 16px;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.blk-attention .blk-label { color: var(--warn); }

/* La démonstration est le SEUL bloc cliquable : elle garde son cadre, parce
   qu'un contrôle doit se voir comme un contrôle. C'est une affordance, pas une
   décoration — ne pas l'aplatir « pour uniformiser » avec les autres blocs. */
.blk-preuve {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 2px solid var(--border-strong);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 0;
}
.blk-preuve .blk-label { font-style: italic; font-weight: 600; }
.blk-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.7em 1em;
  border: 0;
  background: transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.blk-toggle:hover { background: var(--surface-alt); }
.blk-preuve .blk-label { margin: 0; color: var(--ink-soft); }
.blk-chevron {
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(-45deg);
  transition: transform 0.18s ease;
}
.blk-preuve:not(.is-collapsed) .blk-chevron { transform: rotate(45deg); }
.blk-preuve .blk-body { padding: 0 18px 16px; }
/* Le carré blanc de fin de démonstration, comme dans un ouvrage. */
.blk-preuve .blk-body::after {
  content: '';
  display: inline-block;
  width: 8px;
  height: 8px;
  border: 1.5px solid var(--ink-faint);
  margin-left: 0.4em;
  vertical-align: -1px;
}

/* -------------------------------------------- exercice à étapes (cœur SMA)
   Le format qui distingue Mathsprep d'un polycopié : énoncé, indications
   progressives, étapes révélées une à une, puis correction rédigée. On ne
   propose l'étape suivante qu'après la précédente — l'étudiant doit avoir la
   possibilité de s'arrêter dès qu'il a compris et de repartir seul. */

.exercise {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 28px;
  overflow: hidden;
  background: var(--surface);
}
.exercise-head {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 11px 20px;
  background: var(--surface-alt);
  border-bottom: 1px solid var(--border);
}
/* « Exercice N » et rien d'autre : ni pastille numérotée, ni titre. Le titre
   existe dans les sources pour l'édition, il n'est pas affiché. */
.exercise-num {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.02rem;
  color: var(--primary-dark);
}
.exercise-body { padding: 20px; }
.exercise-statement > *:last-child { margin-bottom: 0; }

.exercise-tools { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 18px; }

/* ------------------------------------------------- LONGUEUR DE LIGNE
   Le réglage le plus rentable de toute la feuille, et celui qui manquait.

   La zone de contenu fait plus de 1000 px sur un écran large : à 15 px, cela
   donne près de 130 caractères par ligne, soit le double de ce qui se lit
   confortablement. L'œil perd sa ligne à chaque retour à la marge, et la
   fatigue est attribuée au contenu alors qu'elle vient de la mise en page.
   Rien ne le signale — aucun débordement, aucune erreur.

   On borne donc la PROSE, pas le conteneur : les grilles de cartes, les
   en-têtes et les tableaux gardent toute la largeur disponible. Les blocs
   pédagogiques héritent de la contrainte de leur parent, il ne faut donc PAS
   la répéter sur .blk-body, sous peine de la cumuler. */
/* `margin-inline: auto` n'est pas un détail : une largeur bornée posée dans un
   conteneur plus large reste collée à gauche, et se lit comme un défaut
   d'affichage plutôt que comme une colonne de lecture. C'est la même faute que
   celle déjà corrigée sur .pricing-grid.

   MAIS ON NE MET JAMAIS DE MARGE AUTOMATIQUE SUR UN ÉLÉMENT DE GRILLE. Dans
   une grille, `justify-self: stretch` est la valeur par défaut et fait remplir
   la piste ; des marges automatiques sur l'axe en ligne ABSORBENT l'espace
   libre et annulent cet étirement — l'élément est alors dimensionné à son
   contenu maximal. Posé sur `.course-sections`, enfant de la grille
   `.course-reader`, cela lui donnait 642 px dans une piste de 335 px et
   faisait déborder toute la page en mobile. Sur écran large le défaut était
   invisible, la mesure de 720 px écrêtant le débordement.
   La contrainte porte donc sur `.course-section` — une carte, enfant de bloc —
   et non sur la colonne qui la contient. */
.exercise,
.serie-note,
.course-section,
.course-section-body {
  max-width: var(--measure);
  margin-inline: auto;
}
.course-sections { max-width: none; margin-inline: 0; }

/* TOUTE LA PAGE DE MODULE PARTAGE LA MÊME COLONNE, et c'est le point qui
   manquait. Borner les cartes sans borner le titre ni les onglets laissait
   l'en-tête courir sur 1040 px au-dessus de cartes de 720 px centrées : les
   bords gauches étaient décalés de 160 px, et la page se lisait comme mal
   alignée — ce qu'elle était.

   Le défaut ne vient donc pas du centrage des exercices mais de l'absence de
   colonne commune. Ne rien borner alignerait tout, au prix de lignes de
   130 caractères ; on garde la mesure ET l'alignement en bornant l'ensemble. */
.module-head,
.tab-bar,
#moduleContent {
  max-width: var(--measure);
  margin-inline: auto;
}

/* Une formule d'affichage ou un tableau peut légitimement dépasser la mesure :
   ils défilent dans leur propre cadre plutôt que d'être comprimés. */
.exercise-statement .table-scroll,
.exercise-correction .table-scroll,
.course-section-body .table-scroll { max-width: none; }

/* La correction est un PANNEAU TEINTÉ, pas un texte séparé par un filet.
   Un filet d'un pixel ne survit pas au défilement : passé le
   bouton, plus rien ne disait à l'étudiant s'il lisait l'énoncé ou le corrigé,
   et c'est ce qui rendait la concentration difficile sur les exercices longs.
   La teinte, elle, accompagne toute la hauteur du bloc.

   Indigo (--primary-soft) et non --surface-alt : ce dernier habille déjà
   l'en-tête « Exercice N », les deux se seraient confondus. */
.exercise-correction {
  margin-top: 20px;
  padding: 14px 20px 16px;
  background: var(--surface-sunk);
  border-left: 2px solid var(--primary-line);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
/* La région est NOMMÉE, ce qui la rend plus sûre qu'une teinte : un étudiant
   qui arrive au milieu du corrigé en défilant lit « Correction » en remontant
   d'une ligne, au lieu de devoir interpréter une nuance de fond. C'est aussi
   ce qui permet à la teinte de rester quasi invisible. */
.exercise-correction::before {
  content: "Correction";
  display: block;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 1.1em;
}
.exercise-correction[hidden] { display: none; }
.exercise-correction > *:last-child { margin-bottom: 0; }

/* Seule la démonstration porte encore un fond : posée sur la correction
   teintée, elle doit rester blanche pour se lire comme un bloc à ouvrir. */
.exercise-correction .blk-preuve { background: var(--surface); }

/* Les amorces sont les titres d'étapes de la source : ce sont elles qui
   rendent une correction de huit étapes navigable. En gras au corps du texte,
   elles se noyaient dans la prose. Police de titre et indigo les font lire
   comme des paliers, sans en faire des intertitres de section. */
.correction-lead {
  font-family: var(--font-heading);
  font-size: 1.04rem;
  font-weight: 600;
  color: var(--primary-dark);
  margin: 1.7em 0 0.5em;
}
.correction-lead:first-child { margin-top: 0; }

/* ------------------------------------------------------------------- QCM
   Le moteur visuel existe : on ne recrée jamais une liste à puces pour des
   propositions à cocher. Garde-fou : sans réponses
   structurées, aucune auto-notation — afficher une fausse bonne réponse est
   pire que ne rien afficher. */

.qcm-exercise { border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; margin-bottom: 16px; }
.qcm-question { margin-bottom: 18px; }
.qcm-question:last-of-type { margin-bottom: 0; }
.qcm-prompt { font-weight: 600; margin-bottom: 0.6em; }
.qcm-options { display: grid; gap: 6px; }
.qcm-option {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  width: 100%;
  padding: 0.6em 0.8em;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  font: inherit;
  font-size: 0.94rem;
  text-align: left;
  color: var(--ink);
  cursor: pointer;
}
.qcm-option:hover { border-color: var(--primary-line); background: var(--surface-alt); }
.qcm-option.selected { border-color: var(--primary); background: var(--primary-soft); }
.qcm-option.correct { border-color: var(--success); background: var(--success-soft); }
.qcm-option.wrong { border-color: var(--danger); background: var(--danger-soft); }
.qcm-letter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid currentColor;
  font-size: 0.8rem;
  font-weight: 700;
}
.qcm-verdict { margin-top: 12px; font-size: 0.92rem; font-weight: 600; }
.qcm-verdict.ok { color: var(--success); }
.qcm-verdict.ko { color: var(--danger); }
.qcm-hint { font-size: 0.86rem; color: var(--ink-faint); margin-bottom: 12px; }

/* ------------------------------------------------------------------ résumé

   Le résumé est le seul contenu où un titre de premier niveau sépare deux
   DOCUMENTS entiers, et non deux sections d'un même texte. Sans respiration,
   les trois documents se lisent comme un seul bloc de prose.

   Trois réglages, et pas un de plus : un filet au-dessus des titres de
   document, un peu d'air sous les sous-titres, et des puces qui respirent.
   Aucune couleur ajoutée, aucun encadré — la hiérarchie se lit dans
   l'espacement et la graisse, comme partout ailleurs sur la plateforme. */
.resume-body .md-h3 {
  margin-top: 2.4em;
  padding-top: 1.1em;
  border-top: 1px solid var(--border);
}
.resume-body > .md-h3:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.resume-body .md-h4 { margin-top: 1.8em; margin-bottom: 0.5em; }

/* Les puces portent l'essentiel du résumé : un geste par ligne, une erreur par
   ligne. Elles ont donc besoin d'être lisibles une à une, pas serrées. */
.resume-body .md-list { margin: 0.6em 0 1em; padding-left: 1.35em; }
.resume-body .md-list li { margin-bottom: 0.55em; }
.resume-body .md-list li:last-child { margin-bottom: 0; }

/* Numérotation portée par l'interface, jamais par l'énoncé : un sujet paginé
   doit pouvoir afficher « 23 » sur une page qui commence à 21. Discret — un
   chiffre gris devant la question, pas une pastille de couleur. */
.qcm-num {
  display: inline-block;
  min-width: 1.6em;
  margin-right: 0.35em;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* Pagination d'un sujet long. Même dessin que le sélecteur de chapitre du
   cours : la plateforme n'a pas besoin d'un second vocabulaire pour dire
   « choisissez une partie ». */
.qcm-pager { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 18px; }
.qcm-pager button {
  padding: 0.34em 0.8em;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  font: inherit;
  font-size: 0.84rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  cursor: pointer;
}
.qcm-pager button:hover { border-color: var(--primary-line); color: var(--ink); }
.qcm-pager button.active { background: var(--primary); border-color: var(--primary); color: #fff; font-weight: 600; }

/* Où l'on en est. Pas de note chiffrée en cours de route : un score sur
   quarante décourage plus qu'il n'informe tant que le sujet n'est pas fini. */
.qcm-progress {
  font-size: 0.86rem;
  color: var(--ink-faint);
  margin-bottom: 4px;
  font-variant-numeric: tabular-nums;
}

/* Le bouton de vérification d'une seule question. Il disparaît une fois la
   question corrigée — elle ne se rejoue pas. */
.qcm-actions { margin-top: 10px; }

/* Une question corrigée cesse d'inviter au clic : le curseur le dit avant que
   l'étudiant ne l'essaie. */
.qcm-question[data-locked="1"] .qcm-option { cursor: default; }
.qcm-question[data-locked="1"] .qcm-option:hover { border-color: var(--border); background: var(--surface); }
.qcm-question[data-locked="1"] .qcm-option.correct:hover { border-color: var(--success); background: var(--success-soft); }
.qcm-question[data-locked="1"] .qcm-option.wrong:hover { border-color: var(--danger); background: var(--danger-soft); }

/* Un sujet paginé sépare ses questions plus franchement : sur dix questions
   qui se suivent, un simple espacement ne suffit plus à les distinguer. */
.qcm-page .qcm-question + .qcm-question {
  border-top: 1px solid var(--border);
  padding-top: 18px;
}

/* ------------------------------------------------------- contenu markdown */

.md-h { font-family: var(--font-heading); margin: 1.4em 0 0.5em; }
.md-h2 { font-size: 1.55rem; }
.md-h3 { font-size: 1.22rem; }
/* DANS UN CHAPITRE DE COURS SEULEMENT, l'intertitre passe sous le titre de la
   section qui le contient (1,3 rem) : un « ## » du contenu produit un .md-h3,
   qui à 1,22 rem était PLUS GROS que son conteneur à 1,18 rem.
   La règle est scopée à .course-reader exprès. L'onglet Résumé rend lui aussi
   dans un .course-section-body, mais SANS .course-reader autour : ses « ## »
   y sont le niveau de titre le plus haut et doivent le rester. */
.course-reader .course-section-body .md-h3 {
  font-size: 1.12rem;
  margin-top: 2em;
  padding-top: 1em;
  border-top: 1px solid var(--border);
}
/* Le filet marque une rupture ENTRE deux groupes : il n'a pas lieu d'être
   en tête de section, où il doublerait la bordure du conteneur. */
.course-reader .course-section-body > .md-h3:first-child {
  margin-top: 0; padding-top: 0; border-top: 0;
}
.course-reader .course-section-body .md-h4 { font-size: 0.98rem; color: var(--ink-soft); }
.md-h4 { font-size: 1.06rem; }
.md-h5 { font-size: 0.98rem; color: var(--ink-soft); }

.md-break { display: block; height: 0.7em; }
.md-hr { border: 0; border-top: 1px solid var(--border); margin: 1.6em 0; }
.md-list { padding-left: 1.35em; margin: 0.6em 0; }
.md-list li { margin-bottom: 0.35em; }

.md-img { max-width: 100%; height: auto; border-radius: var(--radius-md); margin: 0.6em 0; }

.math-table {
  border-collapse: collapse;
  width: 100%;
  margin: 1em 0;
  font-size: 0.92rem;
}
.math-table th, .math-table td {
  border: 1px solid var(--border);
  padding: 0.5em 0.7em;
  text-align: left;
  vertical-align: middle;
}
.math-table th { background: var(--surface-alt); font-weight: 600; }

/* Un tableau large doit défiler DANS son conteneur. Sans cela sa largeur
   minimale remonte toute la chaîne des parents et fait déborder la page entière
   en mobile. */
.table-scroll { overflow-x: auto; margin: 1em 0; }
.table-scroll .math-table { margin: 0; }

.code-block {
  position: relative;
  background: #1b2233;
  border-radius: var(--radius-md);
  margin: 1em 0;
  overflow: hidden;
}
.code-block pre { margin: 0; padding: 14px 16px; overflow-x: auto; }
.code-block code {
  font-family: var(--font-mono);
  font-size: 0.86rem;
  line-height: 1.6;
  color: #e6e9f2;
  white-space: pre;
}
.code-lang {
  position: absolute;
  top: 8px;
  right: 12px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #8a93ab;
}
.code-inline {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.35em;
}

/* --------------------------------------------- aperçu live (html-rendu)
   Propre à Programmation Web 1 : le code EST la réponse visuelle, donc le
   bloc porte, sous le code, un bouton qui révèle son rendu — jamais chargé
   avant, même logique de recherche-puis-vérification que .blk-preuve. Le
   bouton reprend .code-block (fond sombre) plutôt que .btn : il appartient
   au bloc de code, pas à la mise en page de l'exercice qui l'entoure. */
.code-live-toggle {
  display: flex;
  align-items: center;
  gap: 0.5em;
  width: 100%;
  padding: 0.6em 16px;
  border: 0;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  background: transparent;
  color: #c7cfe2;
  font: inherit;
  font-size: 0.86rem;
  text-align: left;
  cursor: pointer;
}
.code-live-toggle:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
.code-live-toggle::before {
  content: '';
  width: 8px;
  height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.18s ease;
  flex-shrink: 0;
}
.code-live-toggle[aria-expanded="true"]::before { transform: rotate(45deg); }
.code-live-frame {
  border-top: 1px solid var(--border);
}
.code-live-frame iframe {
  display: block;
  width: 100%;
  height: 320px;
  border: 0;
  background: #fff;
}

/* KaTeX : le display ne doit jamais faire déborder la page. */
.katex-display { overflow-x: auto; overflow-y: hidden; padding: 0.3em 0; }
.katex { font-size: 1.04em; }

/* ------------------------------------------------------- états et placeholders */

.placeholder {
  text-align: center;
  padding: 48px 24px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-alt);
}
.placeholder .placeholder-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--primary);
  margin-bottom: 14px;
}
.placeholder h3 { margin-bottom: 0.35em; }
.placeholder p { color: var(--ink-soft); max-width: 52ch; margin: 0 auto 0.5em; }

/* Un cadenas ne se colore pas en vert : le vert se lit comme « acquis ».
   Les avis de verrouillage et le bandeau de démonstration sont de
   l'information de plateforme, donc en indigo. */
.locked-notice {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  padding: 18px 20px;
  border: 1px solid var(--primary-line);
  background: var(--primary-soft);
  border-radius: var(--radius);
  margin-bottom: 22px;
}
.locked-notice svg { color: var(--primary); flex-shrink: 0; }
.locked-notice p { margin: 0; color: var(--ink-soft); flex: 1; min-width: 220px; font-size: 0.94rem; }

.loading-note { color: var(--ink-faint); font-size: 0.92rem; }

.demo-banner {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 10px 16px;
  background: var(--primary-soft);
  border-bottom: 1px solid var(--primary-line);
  font-size: 0.86rem;
  color: var(--ink-soft);
}
.demo-banner strong { color: var(--primary); }

/* ------------------------ séries et annales : cartes puis vue de détail

   L'onglet montre une grille de cartes — une par série ou par sujet — et le
   clic ouvre le détail, avec un bouton de retour. */

/* QUATRE CARTES PAR RANGÉE : à deux par rangée, six séries occupent trois
   écrans et l'onglet ne montre plus un catalogue mais une file d'attente. Un
   catalogue se survole d'un coup d'œil, c'est tout son intérêt.

   La largeur disponible est celle de la colonne commune — 720 px, `--measure`
   sur `#moduleContent` — et non celle de l'écran : la page de module aligne
   TOUT sur cette colonne, titre et onglets compris. D'où 158 px de largeur
   minimale et une gouttière de 14, qui donnent exactement quatre colonnes de
   169 px. NE PAS remonter cette valeur sans retomber à trois.

   Un titre long — « Points fixes, bornes et caractérisation séquentielle du
   sup » — se casse en quatre lignes dans une carte étroite. C'est le prix
   accepté : `height: 100%` et le `flex: 1` du sous-titre alignent les pieds de
   toutes les cartes d'une rangée, si bien qu'un titre long allonge la rangée
   sans la dépareiller. La typographie de la carte est resserrée d'autant.

   `auto-fill` fait le reste : la grille descend d'elle-même à 3, 2 puis 1
   colonne à mesure que la colonne se rétrécit — aucune règle de repli à
   maintenir en mobile. */
.series-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
  gap: 14px;
  margin-top: 20px;
}

.series-card {
  height: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 15px 12px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 5px;
  text-align: left;
  font-family: var(--font-body);
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.series-card:hover {
  border-color: var(--primary);
  box-shadow: 0 2px 10px rgba(42, 74, 134, 0.09);
}
@media (prefers-reduced-motion: reduce) {
  .series-card { transition: none; }
}

.series-num {
  font-family: var(--font-heading);
  font-size: 1rem;
  font-weight: 700;
  color: var(--primary-dark);
  line-height: 1.25;
}
/* `flex: 1` pousse la ligne de métadonnées au BAS de la carte : quel que soit
   le nombre de lignes du sous-titre, toutes les cartes d'une rangée alignent
   leur pied. C'est ce qui rend la grille tranquille malgré des titres de
   longueurs très différentes. */
.series-subtitle {
  flex: 1;
  font-size: 0.84rem;
  color: var(--ink-soft);
  line-height: 1.45;
}
/* Ligne de pied : un filet la sépare de la description, et elle reste en
   casse normale. Une première version la passait en capitales espacées, ce
   qui convient à une étiquette courte mais pas à une phrase — « 15 EXERCICES
   CORRIGÉS • 1 H 30 » criait plus fort que le titre de la carte. */
.series-meta {
  margin-top: 5px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 0.74rem;
  line-height: 1.4;
  color: var(--ink-faint);
}
/* Chaque segment du pied est INSÉCABLE. Le pied d'une annale porte en plus la
   durée et tient donc sur deux lignes ; sans cette règle, la coupure tombait
   au milieu de « 1 h 30 » et laissait un « h » seul en dernière ligne. Aucun
   contrôle par script ne le voit : rien ne déborde, rien n'est en erreur — il
   faut le regarder. La ligne se casse désormais sur un « • », qui est hors des
   spans exprès. */
.series-meta > span { white-space: nowrap; }
/* La provenance se lit AVANT le clic. Une série rédigée pour la plateforme le
   dit sur sa carte, au lieu de le révéler une fois ouverte — c'est la même
   exigence que la mention affichée en tête de document.

   AUCUN codage par la couleur ici : « Ain Chock » et « Mathsprep » se
   distinguent par leur texte. Teinter l'un en vert l'aurait fait lire comme
   un état (« validé »), ce que l'accent de la plateforme ne doit jamais dire. */
.series-origin { color: var(--ink-soft); font-weight: 600; }

/* Les cartes d'annales ont le même GABARIT que les cartes de séries. Les
   teinter de l'accent vert ne voudrait rien dire sur une annale, et se lirait
   comme un état « validé » : ce sont l'année et la session qui distinguent une
   annale, pas une bordure de couleur.

   LA SESSION SE LIT AU REMPLISSAGE, PAS À LA TEINTE. Une annale se distingue
   de sa voisine par son année ET par sa session : sur une promotion donnée,
   c'est même la seule différence. La pastille le dit avant le clic.

   Une seule famille de couleur, l'indigo, parce que c'est une information
   d'état (le vert est l'identité de la plateforme, l'ocre le contenu
   pédagogique). Ce qui sépare les deux genres est l'APLAT : le rattrapage,
   qui est l'exception, le porte ; la session normale, qui est le cas
   ordinaire, reste un contour. Un étudiant repère donc les rattrapages d'un
   coup d'œil, sans qu'aucune couleur ne prétende juger un sujet. C'est la
   même mécanique que les blocs pédagogiques, où `[[attention]]` est le seul
   bloc à fond coloré — et pour la même raison. */
.session-tag {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.055em;
  text-transform: uppercase;
  line-height: 1.5;
  padding: 1px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--ink-soft);
}
.session-tag.is-rattrapage {
  border-color: var(--primary-line);
  background: var(--primary-soft);
  color: var(--primary-dark);
}
/* Un sujet blanc n'est pas une session : contour interrompu, pour qu'il ne se
   confonde avec aucune des deux même sorti de son groupe. */
.session-tag.is-blanc {
  border-style: dashed;
  color: var(--ink-faint);
}

/* Date exacte, quand la source la porte (« 5 janvier 2018 ») : sous la
   pastille, discrète — c'est une précision, pas l'identité du sujet. */
.exam-precision {
  display: block;
  margin-top: 6px;
  font-size: 0.78rem;
  color: var(--ink-faint);
}

/* Titre du second groupe de l'onglet Annales. Plus petit que le titre de
   l'onglet, et séparé par un filet : les sujets blancs sont une annexe des
   annales, pas une seconde section de même rang. */
.exam-group-title {
  font-family: var(--font-heading);
  font-size: 1.08rem;
  color: var(--primary-dark);
  margin: 34px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

.btn-back {
  background: none;
  border: none;
  color: var(--primary-dark);
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  padding: 0;
  margin-bottom: 18px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.btn-back:hover { text-decoration: underline; }

/* Provenance d'une série. Une série rédigée pour la plateforme le dit à
   l'étudiant : les autres sont des feuilles de la faculté, et laisser croire
   qu'elles le sont toutes serait mentir sur ce qu'il achète. En indigo comme
   toute information d'état — l'ocre est réservé au contenu pédagogique. */
/* Provenance d'un exercice isolé — discrète, elle ne concurrence pas l'énoncé. */
.exercise-source {
  color: var(--muted);
  font-size: 0.84rem;
  font-style: italic;
  margin: 10px 0 0;
}

.serie-note {
  border-left: 3px solid var(--primary);
  background: var(--primary-soft, #eef2fa);
  color: var(--ink);
  font-size: 0.9rem;
  line-height: 1.5;
  padding: 10px 14px;
  border-radius: 0 6px 6px 0;
  margin: 0 0 22px;
}

.exam-header-info { margin-bottom: 22px; }
.exam-meta-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.exam-tag {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--primary);
  background: var(--primary-soft);
  border-radius: 999px;
  padding: 4px 12px;
}

/* Pas de repli de `.series-grid` en mobile : `auto-fill` descend seul à deux
   puis une colonne. Une règle fixe à `1fr` sautait de quatre à une. */

/* ------------------------------------------------------------------- footer */

.site-footer {
  border-top: 1px solid var(--border);
  margin-top: 48px;
  /* Le rembourrage latéral est porté par .footer-inner, PAS par le bandeau :
     appliqué à l'extérieur de la largeur bornée, il décalait le contenu du
     pied de 22 px par rapport à celui de la coquille, qui l'applique dedans. */
  padding: 26px 0 34px;
  background: var(--surface);
}
.footer-inner {
  padding-inline: 22px;
  max-width: var(--page-max);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 1.4rem;
  justify-content: space-between;
  font-size: 0.88rem;
  color: var(--ink-soft);
}
.footer-inner a { color: var(--ink-soft); }
.footer-links { display: flex; flex-wrap: wrap; gap: 1rem; }

/* ================================================== pages institutionnelles

   À propos, Contact, Paiement, Règlement — mêmes dispositions, même palette
   que le reste de la plateforme.
   =========================================================================== */

.institution-page { display: grid; gap: 22px; }

.grid { display: grid; gap: 16px; }
.grid.two { grid-template-columns: repeat(auto-fit, minmax(268px, 1fr)); }
.grid.three { grid-template-columns: repeat(auto-fit, minmax(232px, 1fr)); }

.page-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-md);
  background: var(--primary-soft);
  color: var(--primary);
  margin-bottom: 12px;
}
.page-icon.soft { width: 36px; height: 36px; margin: 0; }

.card-kicker {
  display: block;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.35em;
}

.value-card { padding: 22px; }
.value-card h3 { font-size: 1.12rem; }

.feature-row {
  display: flex;
  gap: 0.9rem;
  align-items: flex-start;
  padding: 16px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.feature-row h3 { font-size: 1.04rem; margin-bottom: 0.2em; }
.feature-row p { margin: 0; color: var(--ink-soft); font-size: 0.92rem; }

/* Citation institutionnelle : un aplat sobre, sans guillemets décoratifs. */
.quote-panel {
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--radius-sm);
  padding: 20px 24px;
}
.quote-panel p {
  margin: 0;
  font-family: var(--font-heading);
  font-size: 1.12rem;
  line-height: 1.55;
  color: var(--ink);
}

.reciprocal-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; }
.reciprocal-col {
  padding: 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

/* ------------------------------------------------------------------ contact

   Deux colonnes : accroche et rubriques à gauche, formulaire à droite, une
   seule colonne sous 1080 px. Empilé d'emblée, le formulaire démarrerait à
   860 px du haut — une page entière de défilement avant le premier champ. La
   disposition à deux colonnes vient de là, elle n'est pas gratuite. */

.contact-layout {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(340px, 0.95fr);
  gap: clamp(22px, 3vw, 40px);
  align-items: start;
}
.contact-intro { display: grid; gap: 14px; align-content: start; }
.contact-intro h1 { font-size: clamp(1.7rem, 2.6vw, 2.2rem); margin: 0; }
.contact-intro .lead { margin: 0; }

.contact-highlight {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 15px 17px;
  background: var(--surface);
}
.contact-highlight h3 { font-size: 1.02rem; margin-bottom: 0.2em; color: var(--primary); }
.contact-highlight p { margin: 0; color: var(--ink-soft); font-size: 0.91rem; line-height: 1.6; }
.contact-highlight strong { color: var(--ink); }

.contact-form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 22px;
}
.contact-form-card h2 { font-size: 1.25rem; margin-bottom: 0.6em; }

/* ---------------------------------------------------------------- paiement */

.checkout-hero { text-align: center; padding: 8px 0 4px; }
.checkout-hero h1 { margin: 6px auto 12px; max-width: 20ch; }
.checkout-hero .lead { margin: 0 auto; max-width: 62ch; }

.trust-badges {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 24px;
}
.trust-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.5em 1em;
  background: var(--surface);
  font-weight: 600;
  font-size: 0.86rem;
  color: var(--ink-soft);
}
.trust-badge svg { color: var(--primary); }

.pricing-ribbon {
  position: absolute;
  top: 16px;
  right: 16px;
  border-radius: var(--radius-sm);
  padding: 0.3em 0.7em;
  background: var(--primary-soft);
  color: var(--primary);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}
.pricing-benefits {
  list-style: none;
  padding: 0;
  margin: 18px 0 20px;
  display: grid;
  gap: 10px;
  flex: 1;
}
.pricing-benefits li {
  position: relative;
  padding-left: 28px;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--ink-soft);
}
.pricing-benefits li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 3px;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  background: var(--primary-soft);
  /* Coche dessinée en CSS : aucun caractère typographique, donc aucun risque
     de police manquante. */
  box-shadow: inset 0 0 0 1px var(--primary-line);
}
.pricing-benefits li::after {
  content: '';
  position: absolute;
  left: 5.5px;
  top: 7.5px;
  width: 7px;
  height: 3.5px;
  border-left: 1.8px solid var(--primary);
  border-bottom: 1.8px solid var(--primary);
  transform: rotate(-45deg);
}
.pricing-saving {
  margin: 12px 0 0;
  text-align: center;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--primary);
}

/* Réassurance : QUATRE rubriques sur une ligne, en colonnes fixes.
   `auto-fit, minmax(200px, …)` n'en place que trois dans cette largeur et
   laisse une carte orpheline. Ajouter une cinquième rubrique casse la ligne. */
.trust-section { margin-top: var(--space-section); text-align: center; }
.trust-section h2 { margin-bottom: 26px; }
.trust-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
.trust-grid .card { text-align: left; }

.payment-process-head { text-align: center; margin-bottom: 24px; }
.payment-process-head h2 { margin-bottom: 8px; }
.payment-process-head p { margin: 0 auto; max-width: 60ch; color: var(--ink-soft); }

/* =========================================================================
   ORIENTATION

   Page de repérage, pas de catalogue. Elle doit se lire comme une page de
   brochure universitaire — sobre, aérée, sans un gramme d'ornement — et
   surtout PAS comme une affiche d'orientation pour lycéens : aucun aplat de
   couleur, aucune icône décorative, aucun pictogramme de matière.

   Ce qui porte la hiérarchie est la TYPOGRAPHIE, comme pour les blocs
   pédagogiques : le numéro d'étape en serif, l'année en capitales espacées,
   le titre en serif, le corps en Inter gris. C'est la règle générale de la
   charte, appliquée ici.

   PAS DE FILET DE LIAISON entre les étapes. L'ornement a déjà été essayé sur
   la méthode de l'accueil : il passait derrière les fonds opaques des cartes,
   donc invisible, et a été retiré. La progression se lit dans les numéros. */

.orient-stages {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}

/* Chaque étape est une bande, séparée par un filet — pas une carte. Trois
   cartes posées côte à côte auraient fait « trois offres à choisir », alors
   qu'il s'agit d'une progression que l'étudiant subit dans l'ordre. */
.orient-stage {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr);
  gap: clamp(18px, 3vw, 38px);
  padding: clamp(22px, 3vw, 32px) 0;
  border-top: 1px solid var(--border);
  align-items: start;
}
.orient-stage:last-child { border-bottom: 1px solid var(--border); }

.os-mark { display: grid; gap: 10px; justify-items: start; }

.os-num {
  font-family: var(--font-heading);
  font-size: 2.4rem;
  font-weight: 600;
  line-height: 1;
  color: var(--primary-line);
}

.os-sems {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.86rem;
  font-weight: 600;
}
.os-sems a {
  color: var(--primary);
  text-decoration: none;
  border-bottom: 1px solid var(--primary-line);
  padding-bottom: 1px;
}
.os-sems a:hover { border-bottom-color: var(--primary); text-decoration: none; }
.os-dot { color: var(--ink-faint); }

/* `min-width: 0` : sans lui, un titre long refuse de descendre sous sa
   largeur de contenu et fait déborder la page en 375 px. Piège déjà payé sur
   l'en-tête des sections de cours. */
.os-body { min-width: 0; }

.os-annee {
  margin: 0 0 4px;
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent);
}
.orient-stage h3 {
  font-size: 1.28rem;
  margin: 0 0 0.4em;
  color: var(--ink);
}
.os-text {
  margin: 0;
  color: var(--ink-soft);
  line-height: 1.72;
  max-width: 62ch;
}

/* ------------------------------------------------------------- les deux voies */

.orient-voies {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(18px, 2.4vw, 28px);
  align-items: start;
}

.orient-voie {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: clamp(20px, 2.4vw, 26px);
  height: 100%;
}
/* La voie couverte par la plateforme se distingue par son LISERÉ, pas par un
   fond : un aplat en ferait une réclame, et l'autre voie se lirait comme
   dévaluée. On informe, on ne vend pas. */
.orient-voie.is-covered { border-left: 3px solid var(--primary); }

.ov-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}
.ov-icon { display: inline-flex; color: var(--primary); flex: none; }
.ov-head h3 { font-size: 1.16rem; margin: 0; min-width: 0; color: var(--ink); }

.ov-resume {
  margin: 0 0 18px;
  color: var(--ink-soft);
  font-size: 0.93rem;
  line-height: 1.66;
}

.ov-filieres { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
.ov-filieres li {
  display: grid;
  gap: 3px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.ov-filieres li:first-child { padding-top: 0; border-top: 0; }
.ov-filieres strong {
  font-family: var(--font-heading);
  font-size: 1rem;
  font-weight: 600;
  color: var(--primary-dark);
}
.ov-filieres span {
  color: var(--ink-soft);
  font-size: 0.89rem;
  line-height: 1.62;
}

/* ------------------------------------------------------------------- la note */

.orient-note {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  margin: var(--space-block) 0 0;
  padding: 16px 18px;
  background: var(--surface-alt);
  border-radius: var(--radius);
  color: var(--ink-soft);
  font-size: 0.91rem;
  line-height: 1.66;
}
.on-icon { display: inline-flex; color: var(--primary); flex: none; margin-top: 2px; }

/* --------------------------------------------------------------- responsive */

/* REPLIS DE L'ACCUEIL, MESURÉS SUR LA COLONNE ET NON SUR LA FENÊTRE.

   Depuis que le menu latéral est présent sur l'accueil, une fenêtre de 1920 px
   ne laisse que 814 px à la colonne de contenu : une `@media` ne se
   déclencherait jamais alors que la mise en page est déjà à l'étroit. D'où des
   requêtes de conteneur, sur `.landing` (voir « page d'accueil »).

   ELLES DOIVENT VENIR APRÈS LES RÈGLES QU'ELLES REMPLACENT. Une requête de
   conteneur n'ajoute AUCUNE spécificité : placé plus haut dans le fichier,
   `@container … { .hero { grid-template-columns: 1fr } }` était simplement
   écrasé par le `.hero` qui suivait, en silence — le hero restait sur deux
   colonnes de 380 px.

  1000 px : le hero RESTE SUR DEUX COLONNES, mais tout y est recalibré sur la
            colonne — proportions, gouttière, taille du titre, taille du code
            de la scène. Empiler à 814 px laisserait le grand titre seul avec
            du vide à sa droite et repousserait la scène sous la ligne de
            flottaison, alors que la place existe. Un hero, c'est une accroche
            ET son image, côte à côte : c'est ce qui se lit comme un produit
            plutôt que comme un article.

            Les tailles sont exprimées en `cqi` (1 % de la colonne) et non en
            `vw` : c'est la colonne qui compte, pas la fenêtre.

   640 px : là seulement le hero s'empile, et les frictions et la méthode
            passent en une colonne. */
@container landing (max-width: 1000px) {
  .hero {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
    gap: 32px;
    padding-top: 8px;
  }
  /* Aucune borne en `ch` ici : la colonne EST la borne. Un `max-width: 20ch`
     laissait le titre à 356 px dans une colonne de 469 — quatre lignes très
     courtes et 113 px de vide à droite, exactement le défaut qu'on corrigeait
     en sortant de l'empilement. */
  .hero h1 { font-size: clamp(1.8rem, 4.3cqi, 2.5rem); max-width: none; }
  .hero .lead { font-size: 0.98rem; max-width: none; }
  .hero .actions { margin-top: 1.3rem; }
  .hero .btn { font-size: 0.94rem; padding: 0 1em; }
  /* La scène se resserre avec sa colonne, sans que le CODE rapetisse : ses
     lignes sont courtes (la plus longue, « for n in range(1, 8): », fait
     132 px pour 301 disponibles). Les réduire « au cas où » revenait à rendre
     illisible ce qui est le sujet même de la scène. Ce qui cède, c'est le
     rembourrage, et la ligne d'état, qui est de la légende. */
  .hero-engine { padding: 12px; }
  .engine-readout { font-size: 0.74rem; gap: 0.9rem; margin: 10px 2px 8px; }
  .engine-readout strong { min-width: 7ch; }
  .engine-charts { gap: 8px; }
  .engine-charts figcaption { font-size: 0.68rem; }
}
@container landing (max-width: 640px) {
  .hero { grid-template-columns: 1fr; gap: 28px; }
  .hero-art { max-width: 560px; }
  .hero h1 { max-width: 24ch; }
  .friction-row { grid-template-columns: 1fr; }
  .method-row { grid-template-columns: 1fr; }
}

@media (max-width: 1080px) {
  /* Le repli du hero se décide sur la largeur de la COLONNE (voir les
     `@container` de la section « page d'accueil ») et non sur celle de la
     fenêtre : depuis que le menu latéral est présent sur l'accueil, une
     fenêtre de 1920 px n'y laisse que 814 px de colonne. En colonne unique,
     le texte passe EN PREMIER — placer la scène avant le titre repoussait
     l'accroche sous la ligne de flottaison et faisait accueillir le visiteur
     par un bloc de code. */
  .course-reader { grid-template-columns: 1fr; }
  .course-toc { position: static; border-left: 0; border-top: 1px solid var(--border); padding: 12px 0 0; }
  .contact-layout { grid-template-columns: 1fr; }
  .trust-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 860px) {
  .app-shell { grid-template-columns: 1fr; margin: 14px auto; padding: 0 14px; }
  /* L'en-tête et le pied gardaient 22 px quand la coquille passe à 14 : le
     logo et les liens démarraient 8 px à gauche du contenu. Sur écran étroit
     ce décalage est proportionnellement le plus visible de tous. */
  .header-inner { padding-inline: 14px; }
  .footer-inner { padding-inline: 14px; }
  /* En mobile le menu n'est plus une colonne mais un panneau déplié au-dessus
     du contenu : il n'a rien à quoi coller, et le borner en hauteur y ajouterait
     un second défilement imbriqué dans celui de la page. */
  .sidebar { position: static; display: none; max-height: none; overflow: visible; }
  .sidebar.open { display: block; }
  .mobile-toggle { display: inline-flex; }
  .top-nav { display: none; }
  .top-nav.open {
    display: flex;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 10px 14px 16px;
    gap: 2px;
  }
  .site-header { position: relative; }
  .form-grid.two { grid-template-columns: 1fr; }
  h1 { font-size: 2.1rem; }
  h2 { font-size: 1.6rem; }

  /* ORIENTATION en écran étroit. La colonne du numéro passe au-dessus du
     texte au lieu de le comprimer : à 375 px, 132 px de gouttière ne
     laissaient que 180 px au corps, soit trois mots par ligne. */
  .orient-stage { grid-template-columns: 1fr; gap: 14px; }
  .os-mark {
    grid-auto-flow: column;
    justify-content: start;
    align-items: baseline;
    gap: 14px;
  }
  .os-num { font-size: 1.7rem; }
  .orient-voies { grid-template-columns: 1fr; }
}

/* Une animation ne doit jamais être un obstacle. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* Les scènes du hero doivent rester lisibles sans mouvement : on pose leur
     état final au lieu de les laisser à leur état de départ (invisible). */
  /* La scène du hero doit rester lisible sans mouvement. `startHeroEngine`
     détecte le même réglage et pose l'état final au lieu de lancer la boucle ;
     ces règles neutralisent en plus les transitions. */
  .hero-engine .bar,
  .hero-engine .wave,
  .hero-engine .peak { transition: none !important; }
}

/* ==========================================================================
   PROTECTION DU CONTENU PAYANT — sélection, copie, impression

   CE QUE CE DISPOSITIF EST, ET CE QU'IL N'EST PAS. C'est une DISSUASION, pas
   une protection : le contenu d'un module ouvert est dans le navigateur de
   l'étudiant, et qui sait ouvrir les outils de développement l'en sortira. La
   seule chose qu'on puisse viser est de rendre la copie de masse — tout
   sélectionner, copier, coller ailleurs, ou imprimer en PDF — assez pénible
   pour qu'elle cesse d'être le geste par défaut. Ne pas surestimer ce que ces
   règles achètent, et ne pas les payer trop cher côté lecture.

   ELLE NE PORTE QUE SUR LE CONTENU PAYANT. Le reste du site — accueil, tarifs,
   conditions, contact, coordonnées bancaires — reste sélectionnable : un
   visiteur doit pouvoir copier un RIB ou un numéro, et bloquer la page
   d'accueil n'aurait rien protégé du tout.
   ========================================================================== */
#moduleContent,
#moduleContent * {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Les champs de saisie d'un QCM restent utilisables : `user-select: none` sur
   un ancêtre les rendrait impossibles à corriger au clavier. */
#moduleContent input,
#moduleContent textarea {
  -webkit-user-select: text;
  user-select: text;
}

/* Une image glissée hors de la page est une copie comme une autre. */
#moduleContent img { -webkit-user-drag: none; user-drag: none; pointer-events: none; }

@media print {
  .site-header, .sidebar, .site-footer, .tab-bar, .exercise-tools { display: none; }
  .app-shell { grid-template-columns: 1fr; margin: 0; padding: 0; }
  .content-area { border: 0; box-shadow: none; padding: 0; }

  /* L'IMPRESSION DU CONTENU PAYANT EST NEUTRALISÉE.
     « Imprimer en PDF » est le moyen le plus simple d'emporter un module
     entier, et il contournait tout le reste. La page imprimée porte donc un
     avis à la place du contenu.

     C'EST UN RENVERSEMENT ASSUMÉ : la feuille dépliait auparavant les
     démonstrations à l'impression, pour que l'étudiant puisse travailler sur
     papier. Ce confort et la protection du catalogue ne peuvent pas coexister.
     Retirer ce bloc rétablit l'impression, et rouvre la porte. */
  #moduleContent { display: none !important; }
  #moduleContent + .print-avis,
  .print-avis { display: block !important; }
}

/* L'avis n'existe qu'à l'impression : à l'écran il ne doit rien occuper. */
.print-avis { display: none; }
@media print {
  .print-avis {
    padding: 2em 0;
    font-family: var(--font-body);
    font-size: 12pt;
    color: #000;
  }
}
