/* ==========================================================================
   OMNIA — Pages
   --------------------------------------------------------------------------
   Styles propres à chaque écran. Tout ce qui est réutilisable vit dans
   components.css ; ici, uniquement ce qu'une page seule justifie.
   ========================================================================== */

/* ==========================================================================
   Coquille : compléments de la barre supérieure, menu, pied de page
   ========================================================================== */

.topbar__level,
.topbar__me {
  display: grid;
  place-items: center;
  border-radius: var(--r-full);
  transition: transform var(--dur-2) var(--ease);
}

.topbar__me { cursor: pointer; }

@media (hover: hover) {
  .topbar__level:hover,
  .topbar__me:hover { transform: scale(1.06); }
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  padding: var(--s3) var(--s4);
  border-radius: var(--r-md);
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--ink);
  text-align: start;
  cursor: pointer;
  transition: background-color var(--dur-2) var(--ease);
}

.menu-item .icon { color: var(--ink-3); flex: none; }
.menu-item > span:first-of-type { flex: 1 1 auto; }
.menu-item__go { color: var(--ink-4) !important; }

@media (hover: hover) {
  .menu-item:hover { background: var(--surface-2); }
  .menu-item:hover .icon { color: var(--ink-2); }
}

.menu-item--danger { color: var(--wrong); }
.menu-item--danger .icon { color: currentColor; }

@media (hover: hover) {
  .menu-item--danger:hover { background: var(--wrong-wash); }
}

.footer {
  margin-block-start: var(--section-gap);
  padding-block: var(--s7) var(--s9);
  border-block-start: 1px solid var(--hairline);
}

.footer__link {
  color: var(--ink-2);
  cursor: pointer;
  transition: color var(--dur-2) var(--ease);
}

.footer__link:hover { color: var(--ink); }

/* L'indicateur hors connexion n'existe que lorsqu'il est vrai. */
.footer__offline {
  display: none;
  align-items: center;
  gap: var(--s2);
  color: var(--accent-ink);
}

html.is-offline .footer__offline { display: inline-flex; }

/* ==========================================================================
   Accueil
   ========================================================================== */

/* `minmax(0, 1fr)` et non la colonne implicite. Une piste de grille se
   dimensionne par défaut sur le contenu le plus large qu'elle abrite, et une
   salutation composée en quarante pixels de display suffit à la faire
   déborder : à 320 px, la colonne mesurait 324 px dans un conteneur de 280,
   et « Bel après-midi, Max. » se faisait couper par l'`overflow-x: hidden` du
   corps. Le zéro comme minimum autorise la piste à devenir plus étroite que
   son contenu, et le texte se replie au lieu de sortir. La déclinaison à deux
   colonnes, plus bas, applique déjà cette précaution. */
.home__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s7);
  align-items: start;
  padding-block: clamp(2rem, 1.5rem + 3vw, 4rem) var(--s7);
}

/* L’en-tête apporte déjà sa respiration : la première section ne la double pas. */
.home__head + .section,
.library__head + .section,
.review__hero + .section,
.profile__hero + .section { padding-block-start: 0; }

@media (min-width: 62rem) {
  .home__head {
    grid-template-columns: minmax(0, 1fr) 22rem;
    gap: var(--s9);
    /* La salutation commence en haut de page : aligner sur le bas de la carte
       creusait un vide au-dessus du titre. */
    align-items: start;
  }
}

.home__goal {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
  border-radius: var(--r-lg);
}

.home__goal-value {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  font-family: var(--font-display);
  font-size: 2rem;
  line-height: 1;
  letter-spacing: var(--track-display);
}

.home__goal-value .faint { font-size: var(--text-md); font-family: var(--font-ui); }

.home__goal-week {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s2);
  height: 4.5rem;
}

.week-bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s2);
  height: 100%;
}

.week-bar__track {
  position: relative;
  flex: 1 1 auto;
  width: 100%;
  max-width: 1.5rem;
  border-radius: var(--r-full);
  background: color-mix(in oklab, var(--ink) 6%, transparent);
  overflow: hidden;
}

.week-bar__fill {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  /* La barre occupe toute la hauteur et se rétracte par `transform` : animer
     `height` remet en page la colonne à chaque image, et il y en a sept. */
  height: 100%;
  transform: scaleY(var(--h-ratio, 0.06));
  transform-origin: bottom center;
  border-radius: var(--r-full);
  background: var(--ink-4);
  transition: transform var(--dur-3) var(--ease);
}

.week-bar__fill.is-goal {
  background: linear-gradient(180deg, var(--accent-bright), var(--accent-deep));
  box-shadow: 0 0 10px -1px color-mix(in oklab, var(--accent) 65%, transparent);
}

.week-bar__label {
  font-size: var(--text-2xs);
  color: var(--ink-4);
  text-transform: uppercase;
}

.home__goal-foot {
  padding-block-start: var(--s4);
  border-block-start: 1px solid var(--hairline);
  font-size: var(--text-sm);
  color: var(--ink-2);
}

.home__flame {
  color: var(--accent);
  filter: drop-shadow(0 0 6px color-mix(in oklab, var(--accent) 60%, transparent));
}

.home__goal-link {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  color: var(--ink-2);
  transition: color var(--dur-2) var(--ease);
}

.home__goal-link:hover { color: var(--ink); }

/* Bloc « Révisions du jour » : la carte la plus lumineuse de la page,
   parce que c'est l'action qui fait revenir. */
.home__review {
  display: grid;
  gap: var(--s7);
  border-radius: var(--r-lg);
}

@media (min-width: 52rem) {
  .home__review {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--s9);
  }
}

.home__review-main { display: flex; flex-direction: column; gap: var(--s2); }
.home__review-unit { font-size: var(--text-lg); color: var(--ink-2); font-family: var(--font-ui); letter-spacing: 0; }

.home__review-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  text-align: center;
}

.home__clear {
  display: flex;
  align-items: center;
  gap: var(--s6);
  border-radius: var(--r-lg);
  flex-wrap: wrap;
}

.home__clear-mark {
  display: grid;
  place-items: center;
  width: 4rem;
  height: 4rem;
  flex: none;
  border-radius: var(--r-full);
  background: var(--right-wash);
  color: var(--right);
}

/* ==========================================================================
   Page d'accueil publique
   ========================================================================== */

.landing__hero {
  position: relative;
  display: grid;
  gap: var(--s8);
  padding-block: clamp(3rem, 2rem + 6vw, 7rem) var(--s9);
  align-items: center;
}

@media (min-width: 62rem) {
  .landing__hero { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: var(--s10); }
}

.landing__art {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--r-2xl);
  overflow: hidden;
  background: var(--cover-ink);
  box-shadow: var(--shadow-4);
}

.landing__art svg { position: absolute; inset: 0; width: 100%; height: 100%; opacity: .9; }

.landing__steps {
  display: grid;
  gap: var(--s5);
  counter-reset: step;
}

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

.landing__step {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  padding: var(--s6);
  border-radius: var(--r-lg);
}

.landing__step::before {
  counter-increment: step;
  content: counter(step, decimal-leading-zero);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--track-label);
  color: var(--accent-ink);
}

.landing__step h3 {
  font-family: var(--font-display);
  font-size: var(--display-4);
  font-weight: 400;
}

/* ==========================================================================
   Bibliothèque
   ========================================================================== */

.library__head {
  display: flex;
  flex-direction: column;
  gap: var(--s6);
  padding-block: clamp(2.5rem, 2rem + 3vw, 4rem) var(--s6);
}

.library__controls {
  position: sticky;
  inset-block-start: 4rem;
  z-index: var(--z-raised);
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  padding-block: var(--s4);
  background: linear-gradient(var(--paper) 62%, transparent);
  backdrop-filter: blur(4px);
}

.library__filters {
  display: flex;
  gap: var(--s2);
  overflow-x: auto;
  scrollbar-width: none;
  padding-block-end: 2px;
  /* Les filtres défilent horizontalement sur mobile sans barre visible. */
  scroll-snap-type: x proximity;
  mask-image: linear-gradient(90deg, transparent, #000 1rem, #000 calc(100% - 1rem), transparent);
}

.library__filters::-webkit-scrollbar { display: none; }
.library__filters > * { scroll-snap-align: start; }

.library__count {
  font-size: var(--text-sm);
  color: var(--ink-3);
}

.theme-card.is-hidden { display: none; }

.theme-card__go {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink-2);
  transition: background-color var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}

@media (hover: hover) {
  .theme-card:hover .theme-card__go { background: var(--ink); color: var(--paper); }
}

/* ==========================================================================
   Thème — Découverte
   ========================================================================== */

.theme__hero {
  /* `minmax(0, 1fr)` : sans minimum nul, la piste implicite se dimensionne
     sur le titre, composé en display. À 320 px, « Capitales du monde »
     donnait une colonne de 325 px dans un conteneur de 284, et le titre
     comme le chapô se faisaient couper par l'`overflow-x: hidden` du
     corps. Même défaut, même correction que pour `.home__head`. */
  grid-template-columns: minmax(0, 1fr);
  position: relative;
  display: grid;
  gap: var(--s7);
  padding-block: var(--s6) var(--s8);
  align-items: center;
}

@media (min-width: 58rem) {
  .theme__hero { grid-template-columns: minmax(0, 1fr) minmax(0, 20rem); gap: var(--s9); }
}

.theme__hero-cover {
  border-radius: var(--r-2xl);
  box-shadow: var(--shadow-3);
}

.theme__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  font-size: var(--text-sm);
  color: var(--ink-2);
}

.theme__meta > span { display: inline-flex; align-items: center; gap: var(--s2); }
.theme__meta .icon { color: var(--ink-4); }

/* Bandeau d'action : l'unique appel principal du thème. */
.theme__action {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s5);
  padding: var(--s5) var(--s6);
  border-radius: var(--r-lg);
}

.theme__action-note { font-size: var(--text-sm); color: var(--ink-2); }

/* Le parcours en cinq étapes. */
.stepper {
  display: grid;
  gap: var(--s3);
}

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

.step {
  position: relative;
  display: flex;
  gap: var(--s3);
  padding: var(--s5);
  border-radius: var(--r-lg);
  transition: transform var(--dur-3) var(--ease), box-shadow var(--dur-3) var(--ease);
}

@media (min-width: 48rem) {
  .step { flex-direction: column; }
}

.step__mark {
  position: relative;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink-3);
  transition: background-color var(--dur-3) var(--ease), color var(--dur-3) var(--ease);
}

.step__label {
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: var(--track-tight);
}

.step__text { font-size: var(--text-sm); color: var(--ink-3); }

.step[data-state="doing"] .step__mark {
  background: var(--accent-wash);
  color: var(--accent-ink);
  box-shadow: 0 0 0 1px var(--accent-line);
}

.step[data-state="done"] .step__mark {
  background: var(--accent);
  color: #1a1409;
  box-shadow: 0 0 14px -3px color-mix(in oklab, var(--accent) 70%, transparent);
}

/* Trait de liaison entre les étapes : le parcours se lit comme un chemin. */
@media (min-width: 48rem) {
  .step:not(:last-child)::after {
    content: "";
    position: absolute;
    inset-block-start: calc(var(--s5) + 1.25rem);
    inset-inline-start: calc(100% - var(--s1));
    width: calc(var(--s3) + var(--s2));
    height: 1px;
    background: var(--hairline-strong);
  }
  .step[data-state="done"]:not(:last-child)::after { background: var(--accent-line); }
}

/* Contenu éditorial. */
.discovery {
  display: flex;
  flex-direction: column;
  gap: var(--s9);
  padding-block: var(--s8);
}

.discovery__intro {
  max-width: 48rem;
  padding-inline-start: var(--s6);
  border-inline-start: 2px solid var(--accent-line);
}

.discovery__intro p { color: var(--ink); text-wrap: pretty; }

.discovery__block { display: flex; flex-direction: column; gap: var(--s5); }

.discovery__title {
  font-family: var(--font-display);
  font-size: var(--display-4);
  font-weight: 400;
  letter-spacing: var(--track-tight);
}

.discovery__quote {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  max-width: 42rem;
  margin-inline: auto;
  padding: var(--s8) 0;
  text-align: center;
}

.discovery__quote-mark { margin-inline: auto; color: var(--accent); opacity: .5; }
.discovery__quote blockquote { font-style: italic; }

.repere-list { display: grid; gap: var(--s4); }

@media (min-width: 48rem) {
  .repere-list { grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); }
}

.repere {
  display: flex;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.repere__dot {
  width: 6px;
  height: 6px;
  margin-block-start: 0.55em;
  flex: none;
  border-radius: var(--r-full);
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent);
}

.repere__term { font-size: var(--text-md); }

.frise { position: relative; display: grid; gap: var(--s6); padding-inline-start: var(--s6); }

.frise::before {
  content: "";
  position: absolute;
  inset-block: 0.5rem;
  inset-inline-start: 0;
  width: 1px;
  background: linear-gradient(var(--accent-line), var(--hairline));
}

.frise__step { position: relative; display: flex; flex-direction: column; gap: var(--s1); }

.frise__step::before {
  content: "";
  position: absolute;
  inset-block-start: 0.55rem;
  inset-inline-start: calc(var(--s6) * -1);
  transform: translateX(-2.5px);
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent);
}

.frise__date {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--accent-ink);
  letter-spacing: var(--track-label);
}

.discovery__tip {
  display: flex;
  gap: var(--s5);
  max-width: 44rem;
  border-radius: var(--r-lg);
}

.discovery__tip-icon { color: var(--accent); flex: none; }

.table-scroll { overflow-x: auto; border-radius: var(--r-md); }

.table { width: 100%; border-collapse: collapse; font-size: var(--text-md); }
.table th,
.table td { padding: var(--s3) var(--s4); text-align: start; border-block-end: 1px solid var(--hairline); }
.table th { font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: var(--track-label); color: var(--ink-3); }
.table tbody tr:last-child td { border-block-end: 0; }

/* Liste des connaissances du thème. */
.knowledge-groups { display: flex; flex-direction: column; gap: var(--s7); }

.knowledge-group__head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin-block-end: var(--s3);
}

.knowledge-group__name {
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-3);
}

.knowledge-list {
  display: grid;
  gap: 1px;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--hairline-soft);
  box-shadow: inset 0 0 0 1px var(--hairline);
}

.knowledge {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s3) var(--s5);
  align-items: center;
  padding: var(--s4) var(--s5);
  background: var(--surface);
  transition: background-color var(--dur-2) var(--ease);
}

@media (min-width: 40rem) {
  .knowledge { grid-template-columns: minmax(0, 14rem) minmax(0, 1fr) auto; }
}

@media (hover: hover) {
  .knowledge:hover { background: var(--surface-2); }
}

.knowledge__front {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  letter-spacing: var(--track-tight);
}

.knowledge__back { color: var(--ink-2); }
.knowledge__note { display: block; font-size: var(--text-sm); color: var(--ink-3); margin-block-start: 2px; }

.knowledge__state {
  display: flex;
  align-items: center;
  gap: var(--s3);
  justify-self: end;
}

/* Pastille de force mémorielle : trois quarts de cercle, remplis selon l'ancrage. */
.strength {
  position: relative;
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
  border-radius: var(--r-full);
  background: conic-gradient(
    var(--accent) calc(var(--progress, 0) * 360deg),
    color-mix(in oklab, var(--ink) 10%, transparent) 0
  );
  mask: radial-gradient(circle, transparent 42%, #000 44%);
  -webkit-mask: radial-gradient(circle, transparent 42%, #000 44%);
}

.favorite {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--r-full);
  color: var(--ink-4);
  cursor: pointer;
  transition: color var(--dur-2) var(--ease), background-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-spring);
}

.favorite:hover { background: var(--surface-2); color: var(--ink-2); }

.favorite[aria-pressed="true"] {
  color: var(--accent);
}

.favorite[aria-pressed="true"] .icon { fill: currentColor; }
.favorite.is-pulsing { transform: scale(1.25); }

/* ==========================================================================
   Révisions — page d'accueil
   ========================================================================== */

.review__hero {
  display: grid;
  gap: var(--s7);
  padding-block: clamp(2.5rem, 2rem + 3vw, 4rem) var(--s7);
  align-items: center;
}

@media (min-width: 58rem) {
  .review__hero { grid-template-columns: minmax(0, 1fr) auto; gap: var(--s9); }
}

/* Prévision de charge : quatorze colonnes, aujourd'hui en ambre. */
.forecast {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s2);
  height: 7rem;
  align-items: end;
}

.forecast__day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s2);
  height: 100%;
  justify-content: flex-end;
}

.forecast__bar {
  width: 100%;
  max-width: 1.75rem;
  min-height: 3px;
  height: 100%;
  transform: scaleY(var(--ratio, 0));
  transform-origin: bottom center;
  border-radius: var(--r-xs);
  background: color-mix(in oklab, var(--ink) 12%, transparent);
  transition: transform var(--dur-3) var(--ease);
}

.forecast__day.is-today .forecast__bar {
  background: linear-gradient(180deg, var(--accent-bright), var(--accent-deep));
  box-shadow: 0 0 12px -2px color-mix(in oklab, var(--accent) 70%, transparent);
}

.forecast__label { font-size: var(--text-2xs); color: var(--ink-4); }

.tough-list { display: grid; gap: 1px; border-radius: var(--r-lg); overflow: hidden; box-shadow: inset 0 0 0 1px var(--hairline); }

.tough {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s4);
  align-items: center;
  padding: var(--s4) var(--s5);
  background: var(--surface);
}

.tough__ratio { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--ink-3); }

/* ==========================================================================
   Profil
   ========================================================================== */

.profile__hero {
  display: grid;
  gap: var(--s6);
  padding-block: clamp(2.5rem, 2rem + 3vw, 4rem) var(--s7);
  align-items: center;
}

@media (min-width: 48rem) {
  .profile__hero { grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--s7); }
}

.profile__identity { display: flex; flex-direction: column; gap: var(--s2); }

.profile__level {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-size: var(--text-md);
  color: var(--ink-2);
}

.profile__xp { display: flex; flex-direction: column; gap: var(--s2); min-width: 14rem; }

/* Calendrier d'activité : la lumière accumulée jour après jour. */
.calendar {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(7, 1fr);
  gap: 3px;
  overflow-x: auto;
  padding-block-end: var(--s2);
  scrollbar-width: none;
}

.calendar::-webkit-scrollbar { display: none; }

.calendar__cell {
  width: 0.875rem;
  height: 0.875rem;
  border-radius: 3px;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
  transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}

.calendar__cell[data-level="1"] { background: color-mix(in oklab, var(--accent) 24%, transparent); }
.calendar__cell[data-level="2"] { background: color-mix(in oklab, var(--accent) 46%, transparent); }
.calendar__cell[data-level="3"] { background: color-mix(in oklab, var(--accent) 70%, transparent); }
.calendar__cell[data-level="4"] {
  background: var(--accent);
  box-shadow: 0 0 8px -1px color-mix(in oklab, var(--accent) 70%, transparent);
}

.calendar__cell:hover { transform: scale(1.35); }

.badge-grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
}

.badge-tile {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: var(--s4);
  border-radius: var(--r-lg);
  transition: transform var(--dur-3) var(--ease);
}

.badge-tile__name { font-size: var(--text-md); font-weight: 600; }
.badge-tile__desc { font-size: var(--text-xs); color: var(--ink-3); }
.badge-tile.is-locked { opacity: .62; }

@media (hover: hover) {
  .badge-tile:hover { transform: translateY(-2px); }
}

.history-list { display: grid; gap: 1px; border-radius: var(--r-lg); overflow: hidden; box-shadow: inset 0 0 0 1px var(--hairline); }

.history {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s4);
  align-items: center;
  padding: var(--s4) var(--s5);
  background: var(--surface);
}

.history__mark {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink-3);
  flex: none;
}

.history__meta { font-size: var(--text-sm); color: var(--ink-3); }
.history__score { font-family: var(--font-mono); font-size: var(--text-md); text-align: end; }

/* Barres horizontales de répartition (par exercice, par catégorie). */
.split { display: grid; gap: var(--s4); }

.split__row { display: grid; grid-template-columns: minmax(6rem, 9rem) minmax(0, 1fr) auto; gap: var(--s4); align-items: center; }

@media (max-width: 47.99rem) {
  /* Sur téléphone, le libellé d'une ligne peut prendre deux lignes plutôt que
     de se couper — c'est un nom de thème, la seule chose qu'on vient y lire.
     L'alignement passe alors en haut : centré, la valeur flotterait au milieu
     d'un libellé de deux lignes, ce qui casse la lecture en colonnes. */
  .split__row { align-items: start; column-gap: var(--s3); }
}
.split__label { font-size: var(--text-sm); color: var(--ink-2); }
.split__value { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--ink-3); }

/* ==========================================================================
   Réglages
   ========================================================================== */

.settings { display: flex; flex-direction: column; gap: var(--s7); padding-block: var(--s7) var(--s9); }

.settings__group {
  display: grid;
  /* Troisième occurrence du même piège, après `.home__head` et
     `.theme__hero` : une piste implicite se dimensionne sur son contenu le
     plus large. Ici c'est « Apprentissage » en display, qui à 320 px poussait
     l'en-tête du groupe hors de la carte. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s6);
  padding: clamp(1.25rem, 1rem + 1.5vw, 2rem);
  border-radius: var(--r-lg);
}

.settings__group-head { display: flex; flex-direction: column; gap: var(--s1); }

.settings__group-title {
  font-family: var(--font-display);
  font-size: var(--display-4);
  font-weight: 400;
}

.settings__fields { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); }

@media (min-width: 44rem) {
  .settings__fields--two { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.settings__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s5);
  padding-block: var(--s3);
}

.settings__row + .settings__row { border-block-start: 1px solid var(--hairline); }

.danger-zone { box-shadow: inset 0 0 0 1px var(--wrong-wash), inset 0 0 0 2px transparent; }

/* ==========================================================================
   Authentification
   ========================================================================== */

body.auth { min-height: 100svh; }

.auth__grid { display: grid; min-height: 100svh; }

@media (min-width: 60rem) {
  .auth__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.auth__form {
  display: grid;
  place-items: center;
  padding: var(--gutter);
}

.auth__inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 24rem;
  padding-block: var(--s9);
}

.auth__art {
  position: relative;
  display: none;
  overflow: hidden;
  background: var(--cover-ink);
}

@media (min-width: 60rem) {
  .auth__art { display: block; }
}

.auth__art-layer { position: absolute; inset: 0; opacity: .8; }
.auth__art-layer svg { width: 100%; height: 100%; }

.auth__art-copy {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  padding: clamp(2rem, 1.5rem + 3vw, 4rem);
  color: #f7f3ea;
  background: linear-gradient(transparent, rgba(10, 9, 8, .72));
}

.auth__art-note {
  max-width: 30rem;
  margin-block-start: var(--s4);
  font-size: var(--text-md);
  line-height: var(--leading-loose);
  opacity: .78;
}

/* ==========================================================================
   Hors connexion et erreurs
   ========================================================================== */

.plainpage {
  display: grid;
  place-items: center;
  min-height: 70svh;
  padding: var(--s9) var(--gutter);
  text-align: center;
}

.plainpage__inner { display: flex; flex-direction: column; align-items: center; gap: var(--s5); max-width: 34rem; }

.plainpage__code {
  font-family: var(--font-display);
  font-size: clamp(4rem, 3rem + 6vw, 7rem);
  line-height: 1;
  letter-spacing: var(--track-display);
  color: var(--ink-4);
}

.plainpage__trace {
  width: 100%;
  max-width: 52rem;
  margin-block-start: var(--s6);
  padding: var(--s5);
  border-radius: var(--r-md);
  background: var(--surface-inset);
  box-shadow: inset 0 0 0 1px var(--hairline);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1.7;
  color: var(--ink-2);
  text-align: start;
  overflow-x: auto;
  white-space: pre;
}

/* ==========================================================================
   RÉVISIONS — les trois durées
   --------------------------------------------------------------------------
   Trois cartes de largeur égale, dans l'ordre croissant. Le format conseillé
   n'est pas plus grand ni plus coloré : il porte un liseré ambré et un mot.
   Grossir la carte du milieu ferait un tarif, pas un conseil.
   ========================================================================== */

.formats {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}

.format {
  position: relative;
  display: grid;
  gap: var(--s2);
  padding: var(--s6);
  align-content: start;
}

.format__minutes {
  font-family: var(--font-display);
  font-size: var(--display-3);
  line-height: 1;
  color: var(--ink);
}
.format__minutes .numeral { font-variant-numeric: tabular-nums; }
.format__label { font-size: var(--text-lg); }
.format__aside { color: var(--ink-2); font-size: var(--text-sm); }
.format__count {
  margin-block-start: var(--s2);
  color: var(--accent-ink);
  font-size: var(--text-sm);
  font-weight: 550;
}

.format--advised { box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 1px 0 0 var(--bevel), var(--shadow-2); }
.format--advised .format__minutes { color: var(--accent-ink); }

.format__flag {
  position: absolute;
  inset-block-start: var(--s4);
  inset-inline-end: var(--s4);
  padding-inline: var(--s2);
  height: 1.25rem;
  display: grid;
  place-items: center;
  border-radius: var(--r-full);
  background: var(--accent-wash);
  color: var(--accent-ink);
  font-size: var(--text-2xs);
  font-weight: 650;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* — Objectifs du jour ————————————————————————————————————————— */

.quest {
  display: grid;
  gap: var(--s3);
  align-content: start;
}
/* L'en-tête d'un objectif : le repère à gauche, la récompense à droite.

   Elle portait `class="row row--between"` plus deux valeurs en attribut
   `style`. Trois décisions de disposition réparties entre deux classes
   génériques et un style en ligne, répétées à l'identique dans trois
   fichiers : c'est ce que le composant supprime. */
.quest__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s3);
}

.quest__compte { font-size: var(--text-sm); color: var(--ink-3); }

.quest__mark { color: var(--ink-3); }
.quest.is-done .quest__mark { color: var(--accent); }
.quest__reward { color: var(--ink-3); font-size: var(--text-xs); text-align: end; }
.quest__label { font-size: var(--text-base); }
.quest.is-done { box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 1px 0 0 var(--bevel); }
.quest.is-done .quest__label { color: var(--accent-ink); }

/* ==========================================================================
   BOUTIQUE
   --------------------------------------------------------------------------
   La bourse d'abord, les objets ensuite : on doit savoir ce qu'on a avant de
   voir ce qu'on n'a pas. La rareté ne se dit pas par une couleur d'arc-en-ciel
   — Omnia n'en a qu'une — mais par l'intensité du filet et un mot.
   ========================================================================== */

.shop__hero {
  display: grid;
  gap: var(--s7);
  padding-block: clamp(2.5rem, 2rem + 3vw, 4rem) var(--s7);
  align-items: center;
}

@media (min-width: 62rem) {
  .shop__hero { grid-template-columns: minmax(0, 1fr) 22rem; gap: var(--s9); align-items: start; }
}

.shop__purse { display: grid; gap: var(--s3); align-content: start; }
.shop__purse-value { line-height: 1.1; }

.shop-item {
  display: grid;
  gap: var(--s3);
  align-content: start;
}
/* Vignette resserrée : l'aperçu passe de cent quatre à quatre-vingts pixels,
   les marges de vingt-quatre à seize, et la description est partie en fiche.
   Trois cent vingt et un pixels de haut deviennent deux cent dix, et la grille
   passe de quatre à six colonnes sur un écran large. */
.shop-grid { gap: var(--s3); }

.shop-item { position: relative; padding: var(--s4); gap: var(--s2); }
.shop-item__name { font-size: var(--text-md); }

/* La zone d'ouverture recouvre l'aperçu et le nom, jamais le pied : le prix et
   les boutons gardent leur propre clic. */
.shop-item__ouvrir {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: calc(100% - 3.25rem);
  border-radius: inherit;
  cursor: pointer;
  background: none;
}

.shop-item__ouvrir:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

/* Le pied passe explicitement au-dessus de la zone d'ouverture. Sans cette
   ligne, il ne recevait ses clics que par l'ordre de peinture, ce qui tient
   tant que personne ne réordonne le gabarit. Un bouton Acquérir qui ouvrirait
   une fiche au lieu d'acheter serait la pire des surprises. */
.shop-item__foot { position: relative; z-index: 1; }

.shop-item__rarity {
  color: var(--ink-3);
  font-size: var(--text-2xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.shop-item__desc { color: var(--ink-2); font-size: var(--text-sm); line-height: var(--leading-body); }
.shop-item__glyph { color: var(--ink-3); flex: none; }
.shop-item__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
  margin-block-start: var(--s2);
}

/* Le pied sait passer à la ligne, la rangée qu'il contient devait l'apprendre.
 *
 * Prix et réserve — « 320 » puis « 0 en réserve » — voyagent ensemble dans une
 * rangée interne, et la pastille de réserve ne se coupe pas. À cent cinquante
 * pixels dans une carte qui en offre cent quatorze, la rangée sortait par la
 * droite alors même que son parent, lui, tenait : le retour à la ligne
 * s'arrêtait à un niveau trop haut pour servir.
 *
 * Sur un écran large la rangée tient sur une ligne et rien ne change. */
.shop-item__foot .row { flex-wrap: wrap; }
.shop-item__price.is-short { opacity: .55; }

/* Un carré de la teinte vendue : le seul aperçu honnête d'une couleur. */
.shop-item[data-rarity="rare"]       { box-shadow: inset 0 0 0 1px var(--hairline-strong), inset 0 1px 0 0 var(--bevel); }
.shop-item[data-rarity="rare"] .shop-item__rarity { color: var(--ink-2); }
.shop-item[data-rarity="precieux"]   { box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 1px 0 0 var(--bevel); }
.shop-item[data-rarity="precieux"] .shop-item__rarity,
.shop-item[data-rarity="legendaire"] .shop-item__rarity { color: var(--accent-ink); }
.shop-item[data-rarity="legendaire"] {
  box-shadow: inset 0 0 0 1.5px var(--accent-line), inset 0 1px 0 0 var(--bevel), 0 0 32px -18px var(--accent);
}
.shop-item[data-rarity="legendaire"] .shop-item__glyph { color: var(--accent); }

.shop-item.is-locked { opacity: .58; }
.shop-item.is-worn   { box-shadow: inset 0 0 0 1.5px var(--accent-line), inset 0 1px 0 0 var(--bevel); }

/* — Inventaire et collections, au profil ————————————————————— */

.collection {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding-block: var(--s3);
  border-block-end: 1px solid var(--hairline-soft);
}
.collection:last-child { border-block-end: 0; }
.collection__date { color: var(--ink-3); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }

.ledger {
  display: grid;
  gap: var(--s2);
}
.ledger__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  font-size: var(--text-sm);
}
.ledger__when { color: var(--ink-3); font-size: var(--text-xs); }

/* ==========================================================================
   APERÇUS DE BOUTIQUE
   --------------------------------------------------------------------------
   Un aperçu montre l'objet, il ne le symbolise pas. Tous portaient le même
   ambre et le même glyphe : on ne pouvait pas distinguer deux titres sans lire
   leur nom, ce qui est exactement l'inverse de ce que doit faire un aperçu.

   Chaque objet déclare sa teinte dans ses données ; elle est posée en variable
   locale sur l'aperçu, et tout ce qui est dessous en hérite, y compris les
   lueurs, qui sont dérivées de l'accent par calc(). Aucune couleur n'est
   écrite ici.
   ========================================================================== */

.apercu {
  display: grid;
  place-items: center;
  align-content: center;
  gap: var(--s2);
  /* Cinq rem suffisent à reconnaître un cadre, une teinte ou un sceau : au-delà
     on n'apprend plus rien de l'objet, on ajoute seulement du défilement. */
  height: 5rem;
  border-radius: var(--r-md);
  background: var(--surface-inset);
  /* Aucun contour : la carte de l'article en a déjà un.
   *
   * L'aperçu portait le sien, ce qui donnait à chacun des trente-sept
   * articles un cadre dans un cadre — la même signature que la bibliothèque,
   * où l'illustration était encadrée à l'intérieur de sa carte. Un fond plus
   * sombre suffit à détacher la scène de l'objet du texte qui la commente :
   * c'est la différence de valeur qui sépare deux surfaces, pas un trait
   * autour de chacune.
   *
   * Le rayon descend d'un cran avec le trait. Un rayon large sans contour
   * flotte ; posé sur un aplat plus sombre, il doit rester serré pour lire
   * comme un renfoncement plutôt que comme une seconde carte. */
  overflow: hidden;
}

.apercu--neutre { --accent-c: 0; }

/* — Titres : le nom, puis le titre, tels qu'ils s'écriront sous l'avatar — */
.apercu__nom {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--ink);
  line-height: 1;
}

/* L'aperçu montre exactement ce que le pseudonyme deviendra : même formule
   que `.pseudo--tinte`, donc même clarté suivant le thème. Il gardait celle
   des aplats — mesuré à 2,1 pour un contre 4,5 requis sur Papier : on
   achetait une couleur qu'on ne pouvait pas lire. */
.apercu__nom--teinte {
  font-size: var(--display-4);
  color: oklch(var(--accent-ink-l) 0.1 var(--pseudo-h, 66deg));
}

.apercu__titre {
  font-size: var(--text-xs);
  font-weight: 650;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--accent-ink);
  text-shadow: 0 0 14px color-mix(in oklab, var(--accent) 35%, transparent);
}

/* — Palettes : les trois tons réellement produits ————————————————
   Un seul carré mentirait sur la nuance : c'est le clair, le plein et le
   profond qui font la personnalité d'une palette, pas sa teinte moyenne. */
.apercu__palette { display: flex; gap: var(--s2); }

.apercu__ton {
  width: 2.25rem;
  height: 2.75rem;
  border-radius: var(--r-sm);
  box-shadow: inset 0 0 0 1px var(--hairline-strong);
}

.apercu__ton--clair   { background: var(--accent-bright); }
.apercu__ton--plein   { background: var(--accent); }
.apercu__ton--profond { background: var(--accent-deep); }

/* — Fonds : le motif à sa vraie teinte, pas une vignette générique ———— */
.apercu__fond { width: 100%; height: 100%; border-radius: 0; }
/* — Sceaux et jokers : le signe seul, en grand ————————————————— */
.apercu__sceau,
.apercu__joker {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--r-full);
  background: var(--accent-wash);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}

.apercu__sceau .icon,
.apercu__joker .icon { width: 1.6rem; height: 1.6rem; }

/* La carte reprend la forme de sa grille : aperçu, nom, description, pied.
 *
 * `minmax(0, 1fr)` sur la colonne, et c'est le point important : une piste de
 * grille se dimensionne par défaut sur le contenu le plus large qu'on ne peut
 * pas couper, et le pied de carte — prix, puis bouton — en fait partie. Sur un
 * téléphone, la carte mesure cent trente-huit pixels et le pied en réclamait
 * cent cinquante : la colonne s'étendait à cent cinquante, et *tous* les
 * enfants avec elle. L'aperçu, le nom, la description et le pied dépassaient
 * ensemble de vingt-quatre pixels par la droite, par-dessus l'arrondi de la
 * carte et la colonne voisine.
 *
 * Un minimum de zéro autorise la piste à rester dans la carte ; le pied, qui
 * sait déjà passer à la ligne, s'y plie de lui-même. */
.shop-item {
  grid-template-rows: auto auto 1fr auto;
  grid-template-columns: minmax(0, 1fr);
}

/* ==========================================================================
   SOCIAL
   --------------------------------------------------------------------------
   Il n'y a ni mur, ni commentaire, ni mention : on vient se comparer et se
   retrouver, pas se raconter. Les blocs sont donc des lignes, pas des cartes
   de publication, et rien n'invite à s'exprimer.
   ========================================================================== */

.social__hero {
  display: grid;
  gap: var(--s7);
  padding-block: clamp(2.5rem, 2rem + 3vw, 4rem) var(--s7);
  align-items: start;
}

@media (min-width: 62rem) {
  .social__hero { grid-template-columns: minmax(0, 1fr) 24rem; gap: var(--s9); }
}

.social__search { display: grid; gap: var(--s3); align-content: start; }

/* — Une personne ————————————————————————————————————————————— */

.personne {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  padding-block: var(--s3);
  border-block-end: 1px solid var(--hairline-soft);
}

.personne:last-child { border-block-end: 0; }

.personne__ident {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-width: 0;
  flex: 1 1 12rem;
  color: var(--ink);
}

.personne__handle { color: var(--ink-3); font-size: var(--text-sm); }

/* — Une demande d'ami en attente ————————————————————————————————————
 *
 * C'est le bloc vers lequel une notification conduit, et il occupait deux
 * cent quarante pixels sur un écran de 375 : une carte de vingt-quatre pixels
 * de rembourrage, un portrait de quarante-quatre, deux puces de métadonnées
 * qui passaient à la ligne, et le bouton en dessous. Pour une décision qui
 * tient en un mot — accepter, ou non.
 *
 * Rien n'est retiré : le visage, le pseudonyme, le niveau et le bouton sont
 * tous là. Ce qui change est la densité. Le portrait descend à trente-six
 * pixels — la taille des listes du produit, pas celle d'un profil —, les
 * écarts se resserrent, et le bouton reprend sa place sur la ligne au lieu
 * d'ouvrir une seconde rangée. Le bloc passe sous cent quarante pixels.
 *
 * La série est la seule chose qui s'efface, et seulement sous 26 rem : c'est
 * une information sur quelqu'un qu'on ne connaît pas encore, et elle prenait
 * la largeur qui manquait au pseudonyme. */
/* Le compactage était allé un cran trop loin.
 *
 * Il fallait le faire — le bloc mesurait deux cent quarante pixels pour une
 * décision qui tient en un mot. Mais serrer et étouffer sont deux choses
 * différentes : à huit pixels de rembourrage vertical, l'avatar touchait
 * presque la bordure lumineuse du cadre, et les deux boutons se collaient au
 * pseudonyme. Un module premium se reconnaît à ce qu'il laisse respirer ce
 * qu'il met en avant.
 *
 * L'équilibre retenu : la ligne garde sa densité — un portrait de trente-six,
 * tout sur un rang — et c'est le vide *autour* qui revient. Le bloc reste
 * sous les cent soixante-dix pixels, contre deux cent quarante à l'origine. */
#demandes .personne {
  gap: var(--s4);
  padding-block: var(--s3);
}

#demandes .personne__ident { gap: var(--s3); }

#demandes .personne .avatar {
  width: 2.25rem;
  height: 2.25rem;
}

#demandes .personne__meta .chip {
  height: 1.625rem;
  padding-inline: var(--s2);
  font-size: var(--text-2xs);
}

/* Les deux réponses se touchent moins que le reste : elles forment une paire,
   et une paire se lit comme un seul objet quand ses éléments sont proches. */
#demandes .personne__actions { gap: var(--s3); }

/* La carte : assez de marge pour que le contenu ne frôle jamais la bordure
   lumineuse, qui est justement ce qu'on veut voir autour de lui. */
#demandes .plate--pad { padding: var(--s4) var(--s5); }

@media (max-width: 30rem) {
  /* Sur téléphone, les deux réponses passent sous l'identité plutôt que de
     comprimer le pseudonyme à trois caractères. Elles gardent leur hauteur
     tactile et se partagent la largeur — deux cibles franches valent mieux
     qu'une ligne où l'on vise entre les deux. */
  #demandes .personne { gap: var(--s3); }
  #demandes .personne__actions {
    flex-basis: 100%;
    margin-inline-start: 0;
    gap: var(--s2);
  }
  #demandes .personne__actions form { flex: 1 1 0; }
  #demandes .personne__actions .btn { width: 100%; min-block-size: 2.5rem; }
  #demandes .plate--pad { padding: var(--s4); }
}

/* La bordure lumineuse a besoin d'un pas de recul.
 *
 * Elle épousait exactement les bords de la carte : le trait ambre passait à
 * un pixel du fond de la carte, si bien que les deux se lisaient comme un
 * seul objet cerclé de jaune — l'« effet rectangle jaune » — au lieu d'un
 * repère *posé autour* de ce qu'il désigne.
 *
 * Un rembourrage sur la section met de l'air entre le trait et le contenu.
 * C'est tout ce qui manquait : le même trait, à seize pixels du bord de la
 * carte, cesse d'être une surbrillance et redevient un cadre.
 *
 * Le rembourrage bas revient aussi, mais mesuré : il valait quatre-vingt-onze
 * pixels — la respiration entre deux sections d'une page — enfermés à
 * l'intérieur du repère. On désignait de l'espace. L'écart entre sections
 * reste rendu par la marge, qui, elle, est dehors. */
#demandes {
  /* `padding` court, pas `padding-block` : la règle doit l'emporter sur le
     rembourrage vertical généreux de `.section`, qui vaut ici une demi-carte
     de vide enfermée dans le cadre. */
  padding: var(--s4);
  margin-block: calc(var(--s4) * -1) var(--s6);
}

#demandes .section__head { margin-block-end: var(--s3); }

@media (max-width: 26rem) {
  #demandes .personne__meta .chip:not(:first-child) { display: none; }
  #demandes .personne { gap: var(--s2); }
}
.personne__meta { display: flex; gap: var(--s2); flex-wrap: wrap; }
.personne__actions { display: flex; gap: var(--s2); flex-wrap: wrap; margin-inline-start: auto; }

/* — Classement ————————————————————————————————————————————— */

.classement { display: grid; gap: var(--s1); }

.classement__ligne {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-md);
  color: var(--ink);
  transition: background-color var(--dur-2) var(--ease);
}

.classement__ligne:hover { background: var(--surface-2); }
.classement__ligne.is-me { background: var(--accent-wash); box-shadow: inset 0 0 0 1px var(--accent-line); }

.classement__rang {
  width: 2rem;
  flex: none;
  text-align: center;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  color: var(--ink-3);
}

/* Le podium se distingue par la graisse et la couleur, pas par une médaille :
   trois pastilles dorées dans une liste feraient concours de fin d'année. */
.classement__rang[data-podium="1"],
.classement__rang[data-podium="2"],
.classement__rang[data-podium="3"] { color: var(--accent-ink); font-weight: 700; }
.classement__rang[data-podium="1"] { font-size: var(--text-lg); }

.classement__points { font-size: var(--text-sm); color: var(--ink-2); white-space: nowrap; }
.classement__points .numeral { color: var(--ink); font-weight: 600; }

/* — Activité ————————————————————————————————————————————————— */

.activite {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding-block: var(--s3);
  border-block-end: 1px solid var(--hairline-soft);
}

.activite:last-child { border-block-end: 0; }

.activite__mark {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  flex: none;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--accent);
}

.activite__when { color: var(--ink-3); font-size: var(--text-xs); white-space: nowrap; }

/* — Fond de profil acheté ————————————————————————————————————— */

.profile__hero--fond { position: relative; isolation: isolate; }

.profile__fond {
  position: absolute;
  inset-block-start: calc(var(--s6) * -1);
  inset-inline: calc(var(--gutter) * -1);
  height: 12rem;
  z-index: -1;
  overflow: hidden;
  border-radius: var(--r-lg);
  opacity: .45;
  mask-image: linear-gradient(to bottom, black, transparent);
}

.profile__fond .cover { width: 100%; height: 100%; }

/* ==========================================================================
   DÉFIS
   ========================================================================== */

.duel-card { display: grid; gap: var(--s4); align-content: space-between; }

.duel-face {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s6);
  flex-wrap: wrap;
}

.duel-face__joueur { display: grid; justify-items: center; gap: var(--s2); color: var(--ink); }

.duel-face__vs {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--ink-3);
}

/* — L'écran de résultat ————————————————————————————————————————————
   La carte portait `plate--pad-lg` et une marge verticale de `--s9` : sur un
   téléphone, cela faisait 160 pixels de vide avant le premier mot, sur un
   écran qui doit dire une chose — qui a gagné — et proposer trois gestes.
   Les deux mesures deviennent fluides : larges quand il y a la place,
   resserrées quand il n'y en a pas. */
.duel-resultat {
  padding: clamp(1.25rem, 0.7rem + 2.4vw, 2.5rem);
  margin-block: clamp(1rem, 0.4rem + 2.4vw, 3rem);
  /* La carte porte `stack--5` dans le gabarit : vingt pixels entre chacun de
     ses cinq blocs, soit cent pixels de respiration sur un écran qui en
     compte six cents. L'écart devient fluide comme le reste — large là où il
     y a la place, resserré là où chaque pixel repousse les boutons d'un pixel
     de plus sous le pli. La règle est plus spécifique que `.stack--5`, qui
     reste juste partout ailleurs. */
  gap: clamp(0.875rem, 0.4rem + 1.9vw, 1.25rem);
}

/* La respiration dit le rang.
 *
 * Les cinq blocs se suivaient au même écart de vingt pixels : le verdict, le
 * score, les gains, la comparaison et les boutons se lisaient comme cinq
 * informations d'égale importance, alors qu'il y en a trois rangs. Le
 * verdict et le score sont la nouvelle, et se tiennent serrés ; les gains en
 * sont la conséquence ; la comparaison est le détail qu'on consulte, et les
 * actions sont la sortie. Deux respirations plus larges suffisent à séparer
 * les trois, sans ajouter une seule carte ni un seul trait. */
.duel-resultat > .duel-tableau { margin-block-start: calc(var(--s5) * -0.4); }
.duel-resultat > .duel-compare { margin-block-start: var(--s3); }
.duel-resultat > .duel-fin__actions { margin-block-start: var(--s3); }

/* L'issue se voit avant d'être lue. Un liseré et un souffle de couleur au
   bord supérieur : assez pour être perçu du coin de l'œil, trop discret
   pour concurrencer les scores, qui restent l'information. */
.duel-resultat.is-win {
  box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 3px 0 0 var(--accent), var(--shadow-2);
}

.duel-resultat.is-loss,
.duel-resultat.is-draw,
.duel-resultat.is-wait {
  box-shadow: inset 0 0 0 1px var(--hairline), inset 0 3px 0 0 var(--hairline-strong), var(--shadow-2);
}

/* Deux colonnes, toujours. `auto-fit` avec un minimum de 12 rem les faisait
   passer l'une sous l'autre dès 390 pixels : deux scores empilés ne se
   comparent plus, or comparer est tout ce qu'on fait sur cet écran. À 350
   pixels utiles, chaque colonne garde 165 pixels — de quoi loger un avatar,
   un nom tronqué et deux chiffres. */
/* ==========================================================================
   La page de résultat — une célébration, pas un relevé
   --------------------------------------------------------------------------
   L'ordre de lecture est imposé par la mise en page, et il est le même que
   celui de la question qu'on se pose en arrivant : ai-je gagné, de combien,
   qu'est-ce que ça rapporte, comment nous sommes-nous comportés, et enfin
   qu'est-ce que je fais maintenant. Les actions sont séparées du résultat
   par un filet : on ne quitte pas l'écran par accident.
   ========================================================================== */

/* ==========================================================================
   La révélation d'un résultat
   --------------------------------------------------------------------------
   Tout arrivait en même temps. Le sceau, le mot, le score, les gains, le
   tableau et les boutons entraient dans les deux cents millisecondes qui
   suivaient l'affichage, chacun avec sa petite animation, aucune ne sachant
   ce que faisaient les autres. Six mouvements simultanés ne forment pas une
   révélation : ils forment un écran qui tremble une fois, puis se fige. Et
   l'ordre de lecture — ai-je gagné, de combien, qu'est-ce que ça rapporte —
   n'était porté que par la mise en page, jamais par le temps.

   Une seule cadence, déclarée ici, et chaque bloc reçoit son rang. Le regard
   est conduit du verdict aux boutons dans l'ordre où l'on veut qu'il aille,
   et chaque étape a le temps d'être vue avant que la suivante ne commence.
   Neuf dixièmes de seconde en tout : c'est long pour une transition, court
   pour une cérémonie, et c'est exactement le registre d'une fin de partie.

   `--pose` est le pas de base. Le multiplier plutôt qu'écrire douze durées
   permet de régler le tempo entier depuis un seul nombre — et surtout de le
   ramener à zéro d'un trait quand le mouvement n'est pas souhaité.
   ========================================================================== */

.duel-resultat,
.duel-fin {
  --pose: 100ms;
}

/* Le mouvement d'entrée commun : on monte de peu, on ne grossit pas.
   Un bloc qui grandit en apparaissant attire l'œil sur lui-même ; un bloc
   qui monte le rend au bloc suivant, ce qui est le propre d'une cascade. */
@keyframes duel-monte {
  from { opacity: 0; transform: translate3d(0, 12px, 0); }
  to   { opacity: 1; transform: none; }
}

/* Le rang de chaque bloc. Les valeurs ne sont pas régulières : le verdict et
   le score se suivent de près — c'est la même nouvelle en deux temps — puis
   une respiration plus large avant les gains, qui en sont la conséquence. */
.duel-verdict__mot        { animation: duel-monte 420ms var(--ease-spring) both; animation-delay: calc(var(--pose) * 1.2); }
.duel-verdict__note       { animation: duel-monte 420ms var(--ease) both;        animation-delay: calc(var(--pose) * 2.2); }
.duel-tableau             { animation: duel-monte 460ms var(--ease-spring) both; animation-delay: calc(var(--pose) * 3); }
.duel-gains,
.duel-fin__gains          { animation: duel-monte 460ms var(--ease-spring) both; animation-delay: calc(var(--pose) * 4.6); }
.duel-compare             { animation: duel-monte 460ms var(--ease) both;        animation-delay: calc(var(--pose) * 6); }
.duel-resultat .duel-fin__actions,
.duel-fin .duel-fin__actions { animation: duel-monte 420ms var(--ease) both;     animation-delay: calc(var(--pose) * 9); }

/* — La lumière qui traverse ————————————————————————————————————————
 *
 * Un seul passage, oblique, une fois. C'est l'effet le plus ancien du
 * vocabulaire des récompenses — le reflet sur une surface qu'on vient de
 * gagner — et il ne tient que s'il ne se répète pas : deux passages en font
 * un chargement, trois en font un bandeau publicitaire.
 *
 * Réservé à la victoire. Une défaite n'a pas à briller ; elle a droit à la
 * même cascade, au même soin, et à aucun éclat — c'est ce qui donne sa valeur
 * à l'éclat quand il vient. */
.duel-resultat.is-win,
.duel-fin.is-win { position: relative; overflow: hidden; }

.duel-resultat.is-win::after,
.duel-fin.is-win::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: -60%;
  width: 55%;
  background: linear-gradient(
    100deg,
    transparent,
    color-mix(in oklab, var(--accent) 22%, transparent) 45%,
    color-mix(in oklab, #fff 30%, transparent) 55%,
    transparent
  );
  transform: skewX(-14deg);
  opacity: 0;
  pointer-events: none;
  animation: duel-lueur 1100ms var(--ease-soft) both;
  animation-delay: calc(var(--pose) * 3.4);
}

@keyframes duel-lueur {
  0%   { opacity: 0;   translate: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { opacity: 0;   translate: 300% 0; }
}

.duel-verdict {
  display: grid;
  justify-items: center;
  gap: var(--s3);
  text-align: center;
}

/* Le sceau : un disque, un trait, une couleur qui dit l'issue. Il est large
   parce qu'il porte à lui seul le verdict pour qui regarde l'écran de loin,
   avant même de lire le mot. */
/* Le sceau suit la largeur, comme tout le reste de cet écran.
 *
 * Quatre-vingt-huit pixels partout, y compris sur un écran de 375 où le
 * verdict entier — sceau, mot, phrase — occupait cent quatre-vingt-quatre
 * pixels et repoussait les boutons de sortie deux pixels sous le pli. Il
 * porte le verdict pour qui regarde de loin ; il n'a pas besoin de la moitié
 * d'un pouce pour cela. À soixante-quatre pixels il reste le premier objet
 * qu'on voit, et les trois boutons repassent au-dessus de la ligne de
 * flottaison — c'est-à-dire qu'on peut sortir sans faire défiler. */
.duel-verdict__sceau {
  position: relative;
  display: grid;
  place-items: center;
  width: clamp(4rem, 2.6rem + 6vw, 5.5rem);
  height: clamp(4rem, 2.6rem + 6vw, 5.5rem);
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink-3);
  box-shadow: inset 0 0 0 1px var(--hairline);
  animation: sceau-pose var(--dur-5) var(--ease-spring) both;
}

/* Deux ondes qui s'échappent du sceau à la victoire. Elles partent une
   fois, à 300 et 600 ms, et ne reviennent pas : une célébration qui boucle
   cesse d'être une célébration. */
.duel-resultat.is-win .duel-verdict__sceau::before,
.duel-resultat.is-win .duel-verdict__sceau::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-full);
  box-shadow: 0 0 0 2px var(--accent);
  opacity: 0;
  animation: sceau-onde 1.4s var(--ease-soft) both;
  pointer-events: none;
}

.duel-resultat.is-win .duel-verdict__sceau::before { animation-delay: 300ms; }
.duel-resultat.is-win .duel-verdict__sceau::after  { animation-delay: 620ms; }

@keyframes sceau-onde {
  0%   { opacity: .55; transform: scale(1); }
  100% { opacity: 0;   transform: scale(1.85); }
}

.duel-resultat.is-win .duel-verdict__sceau {
  background: var(--accent-wash);
  color: var(--accent-ink);
  box-shadow: inset 0 0 0 1px var(--accent-line), 0 0 30px -10px var(--accent);
}

.duel-resultat.is-draw .duel-verdict__sceau { color: var(--ink-2); }

@keyframes sceau-pose {
  0%   { opacity: 0; transform: scale(0.7); }
  60%  { opacity: 1; transform: scale(1.06); }
  100% { opacity: 1; transform: scale(1); }
}

.duel-verdict__mot {
  font-family: var(--font-display);
  font-size: var(--display-1);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: var(--track-tight);
}

.duel-resultat.is-win .duel-verdict__mot { color: var(--accent-ink); }

.duel-verdict__note {
  max-width: 26rem;
  color: var(--ink-3);
  font-size: var(--text-md);
}

/* — Le score, en grand ————————————————————————————————————————————
   Deux nombres et un tiret, à chasse fixe pour que le tiret reste au centre
   quel que soit le nombre de chiffres. */
.duel-tableau {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s4);
}

/* Le score arrive d'un bloc, et c'est son conteneur qui l'amène.
   Ses deux nombres entraient chacun de leur côté : on regardait un score se
   composer sous ses yeux sur l'écran même où l'on vient chercher ce nombre.
   `.duel-tableau` porte désormais l'entrée pour les trois — les deux
   nombres et le tiret arrivent ensemble, et ce sont les chiffres qui
   défilent ensuite, ce qui est le mouvement qu'on attend d'un score. */

.duel-tableau__pts {
  font-family: var(--font-display);
  font-size: clamp(3rem, 2rem + 7vw, 5rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  transition: color var(--dur-3) var(--ease);
}

.duel-tableau__pts.is-winner { color: var(--ink); }
.duel-resultat.is-win .duel-tableau__pts.is-winner { color: var(--accent-ink); }

.duel-tableau__tiret {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 1rem + 3vw, 2.5rem);
  color: var(--ink-4);
}

/* — La comparaison, ligne par ligne ————————————————————————————— */

/* Le tableau comparatif ne porte plus de contour.
 *
 * Il en avait un, à l'intérieur de la carte de résultat qui en a un, et il
 * contient lui-même des lignes séparées par un filet. Trois niveaux de trait
 * pour un tableau de deux lignes — la même signature que la bibliothèque, où
 * l'illustration était encadrée dans sa carte.
 *
 * Le fond plus clair suffit à le détacher : c'est le seul bloc de la carte
 * qui en ait un, donc le seul qu'on lise comme un renfoncement. Un trait
 * par-dessus ne disait rien de plus. Les récompenses, elles, gardent le leur
 * — ce sont les objets qu'on vient chercher, et c'est précisément parce que
 * tout le reste s'est calmé qu'elles ressortent. */
.duel-compare {
  display: grid;
  gap: var(--s4);
  padding: var(--s5);
  border-radius: var(--r-lg);
  background: var(--surface-2);
}

.duel-compare__tetes,
.duel-compare__ligne {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--s3);
}

.duel-compare__qui {
  display: grid;
  justify-items: center;
  gap: var(--s2);
  min-width: 0;
  font-size: var(--text-sm);
  font-weight: 600;
}

.duel-compare__qui > span { max-width: 100%; }

.duel-compare__contre {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: var(--track-label);
  color: var(--ink-4);
}

.duel-compare__lignes {
  display: grid;
  gap: 1px;
  margin: 0;
  background: var(--hairline);
  border-radius: var(--r-md);
  overflow: hidden;
}

.duel-compare__ligne {
  padding: var(--s3) var(--s2);
  background: var(--surface);
  animation: fade var(--dur-3) var(--ease) both;
}

/* Elles entrent derrière leur conteneur, jamais avant : les retards étaient
   écrits en millisecondes absolues, si bien que la première ligne arrivait
   quatre-vingts millisecondes *avant* le bloc qui la contient. */
.duel-compare__ligne:nth-child(1) { animation-delay: calc(var(--pose) * 7); }
.duel-compare__ligne:nth-child(2) { animation-delay: calc(var(--pose) * 7.7); }
.duel-compare__ligne:nth-child(3) { animation-delay: calc(var(--pose) * 8.4); }

/* L'intitulé est au centre, en petit : c'est la seule chose qu'on ne compare
   pas, elle n'a donc pas à peser autant que les deux valeurs qui l'encadrent. */
.duel-compare__cle {
  font-size: var(--text-2xs);
  font-weight: 650;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-4);
  white-space: nowrap;
}

.duel-compare__val {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  text-align: center;
}

/* La meilleure des deux valeurs se marque — jamais les deux, jamais en cas
   d'égalité : un signe posé partout ne signale plus rien. */
.duel-compare__val.is-best { color: var(--ink); font-weight: 600; }
.duel-resultat.is-win .duel-compare__val.is-best:first-child { color: var(--accent-ink); }

/* Le même écran, sans mouvement.
 *
 * La cascade entière est ramenée à un pas nul : chaque bloc garde son
 * animation — un fondu très bref, qui n'est pas un déplacement — et tous
 * partent en même temps. L'ordre de lecture reste porté par la mise en page,
 * qui n'a jamais eu besoin du temps pour le dire ; ce qu'on retire, c'est la
 * mise en scène, pas l'information.
 *
 * Ce qui disparaît complètement : la lumière qui traverse et les ondes du
 * sceau. Toutes deux sont du mouvement pur, sans contenu — les supprimer ne
 * coûte rien à personne. */
@media (prefers-reduced-motion: reduce) {
  .duel-resultat,
  .duel-fin { --pose: 0ms; }

  .duel-verdict__sceau { animation: fade var(--dur-3) linear both; }

  .duel-resultat.is-win .duel-verdict__sceau::before,
  .duel-resultat.is-win .duel-verdict__sceau::after,
  .duel-resultat.is-win::after,
  .duel-fin.is-win::after { animation: none; opacity: 0; }

  .duel-verdict__mot,
  .duel-verdict__note,
  .duel-tableau,
  .duel-tableau__tiret,
  .duel-gains,
  .duel-fin__gains,
  .duel-compare,
  .duel-compare__ligne,
  .duel-resultat .duel-fin__actions,
  .duel-fin .duel-fin__actions { animation: fade var(--dur-2) linear both; animation-delay: 0s; }
}

.duel-score {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s3);
}

.duel-score__joueur {
  display: grid;
  justify-items: center;
  gap: var(--s2);
  padding: clamp(0.75rem, 0.4rem + 1.6vw, 1.5rem);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  text-align: center;
  min-width: 0;
}

/* Le nom d'un adversaire n'a pas de longueur garantie : sans cette borne,
   « Jean-Baptiste de La Rochefoucauld » élargissait sa colonne et poussait
   l'autre hors de l'écran. */
.duel-score__joueur > strong { max-width: 100%; }

.duel-score__joueur.is-winner {
  background: var(--accent-wash);
  box-shadow: inset 0 0 0 1px var(--accent-line), 0 0 32px -18px var(--accent);
}

.duel-score__points { font-family: var(--font-mono); font-size: var(--display-4); }
.duel-score__joueur.is-winner .duel-score__points { color: var(--accent-ink); }

/* ==========================================================================
   TABLEAU DE BORD
   --------------------------------------------------------------------------
   Les graphiques sont en SVG, dessinés côté serveur, sans une ligne de
   JavaScript ni une bibliothèque. Ce n'est pas de l'ascétisme : un graphique
   qui arrive avec la page ne clignote pas, s'imprime, se lit sans script, et
   ne coûte rien à un téléphone ancien.
   ========================================================================== */

.graphe {
  display: block;
  width: 100%;
  height: 8rem;
  overflow: visible;
}

.graphe__aire { fill: var(--accent-wash); }

.graphe__trait {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1;
  /* Sans cela, l'épaisseur du trait suivrait la déformation du viewBox et
     donnerait une courbe épaisse à gauche, fine à droite. */
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.graphe--niveau .graphe__trait { stroke: var(--ink-2); }
.graphe--niveau .graphe__aire { fill: var(--surface-2); }

.graphe__pieds {
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  font-size: var(--text-xs);
  flex-wrap: wrap;
}

/* — Répartition par palier ————————————————————————————————————
   Une seule barre segmentée, plus lisible qu'un camembert : on compare des
   longueurs, ce que l'œil fait bien, plutôt que des angles, ce qu'il fait mal. */

.paliers {
  display: flex;
  height: 1.5rem;
  border-radius: var(--r-full);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
}

.paliers__part {
  flex: var(--part, 0) 1 0;
  min-width: 2px;
  transition: flex-grow var(--dur-5) var(--ease);
}

.paliers__legende { display: grid; gap: var(--s2); }

.palier {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding-block: var(--s2);
  border-block-end: 1px solid var(--hairline-soft);
}

.palier:last-child { border-block-end: 0; }
.palier .numeral { margin-inline-start: auto; font-variant-numeric: tabular-nums; }

.palier__puce {
  width: 0.75rem;
  height: 0.75rem;
  flex: none;
  border-radius: var(--r-xs);
}

/* Les quatre paliers ne prennent pas quatre couleurs : c'est la même lumière,
   du graphite à l'ambre plein. La maîtrise est un dégradé, pas une catégorie. */
[data-palier="maitrise"]      { background: var(--accent); }
[data-palier="consolidation"] { background: color-mix(in oklab, var(--accent) 62%, var(--surface-3)); }
[data-palier="acquisition"]   { background: color-mix(in oklab, var(--accent) 34%, var(--surface-3)); }
[data-palier="decouverte"]    { background: var(--surface-3); }
[data-palier="neuve"]         { background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--hairline); }

/* — Calendrier sur une année ————————————————————————————————— */

.calendar__wrap { overflow-x: auto; }

.calendar--annee {
  grid-auto-flow: column;
  grid-template-rows: repeat(7, auto);
  grid-template-columns: none;
  min-width: max-content;
}

/* ==========================================================================
   AUJOURD'HUI
   --------------------------------------------------------------------------
   Un seul bouton ambre par écran, et c'est ici qu'il compte le plus : Omnia
   sait déjà par où commencer, il n'a qu'à le dire.
   ========================================================================== */

.today {
  display: grid;
  gap: var(--s3);
  padding: var(--s6);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 1px 0 0 var(--bevel), var(--shadow-2);
}

.today__kind {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--text-2xs);
  font-weight: 650;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--accent-ink);
}

.today__aside {
  font-family: var(--font-display);
  font-size: var(--display-4);
  line-height: var(--leading-snug);
  text-wrap: balance;
}

.today__label { align-self: center; color: var(--ink-3); font-size: var(--text-sm); }

.today__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
  gap: var(--s3);
}

.today__fact {
  display: grid;
  /* Le libellé occupe la seconde rangée, et la rangée fait la même hauteur
     partout. Sans cela, « séances aujourd'hui » passait sur deux lignes quand
     ses trois voisins tenaient sur une : la tuile grandissait, le chiffre s'en
     trouvait décalé vers le haut, et quatre tuiles censées être un jeu
     n'étaient plus alignées. C'est le genre d'écart qu'on ne sait pas nommer
     mais qu'on voit tout de suite. */
  grid-template-rows: auto minmax(2.4em, auto);
  align-content: start;
  gap: 2px;
  padding: var(--s3) var(--s4);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--hairline);
  transition: box-shadow var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}

@media (hover: hover) {
  .today__fact:hover { transform: translate3d(0, -1px, 0); box-shadow: inset 0 0 0 1px var(--hairline-strong); }
}

.today__fact-value { font-family: var(--font-mono); font-size: var(--text-xl); font-weight: 600; }
.today__fact-label { font-size: var(--text-2xs); color: var(--ink-3); text-transform: uppercase; letter-spacing: var(--track-label); }

/* — Succès, rangés par catégorie ————————————————————————————————
   Cent soixante-six succès dans une grille unique ne se lisent pas. Chaque
   catégorie s'ouvre à la demande et annonce son décompte : on va voir ce qui
   manque, on ne le subit pas. */

.succes-groupe { padding-block: var(--s4); }

.succes-groupe__tete {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(4rem, 8rem) auto;
  align-items: center;
  gap: var(--s4);
  cursor: pointer;
  list-style: none;
}

.succes-groupe__tete::-webkit-details-marker { display: none; }
.succes-groupe__compte { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--ink-3); }
.succes-groupe__jauge { min-width: 0; }

.succes-groupe__fleche {
  color: var(--ink-3);
  transition: transform var(--dur-2) var(--ease);
}

.succes-groupe[open] .succes-groupe__fleche { transform: rotate(180deg); }

@media (max-width: 40rem) {
  .succes-groupe__tete { grid-template-columns: minmax(0, 1fr) auto auto; }
  .succes-groupe__jauge { display: none; }
}

/* ==========================================================================
   Notifications (réglages)
   --------------------------------------------------------------------------
   Une liste de choix, pas un tableau de bord. Chaque ligne dit ce qui sera
   envoyé et dans quelles circonstances : sans la seconde phrase, personne ne
   peut décider en connaissance de cause, et cocher devient un pari.
   ========================================================================== */

.notifs__list {
  display: grid;
  gap: var(--s2);
}

.notifs__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--s3);
  padding: var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  cursor: pointer;
  /* La couleur seule change au survol : une bordure qui s'épaissit décalerait
     le texte d'un pixel, et huit lignes qui frémissent se remarquent. */
  transition: background-color var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease);
}

.notifs__item:hover { background: var(--surface-2); }

.notifs__item:has(:checked) {
  border-color: color-mix(in oklch, var(--accent) 45%, var(--hairline));
  background: color-mix(in oklch, var(--accent) 6%, transparent);
}

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

.notifs__body { display: grid; gap: 2px; min-width: 0; }
.notifs__label { font-weight: 600; }

.notifs__aside {
  color: var(--ink-3);
  font-size: var(--text-sm);
  line-height: 1.45;
  text-wrap: pretty;
}

.notifs__hours { margin-top: var(--s4); }

.notifs__state {
  margin-top: var(--s3);
  padding: var(--s3);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: var(--text-sm);
  line-height: 1.5;
  text-wrap: pretty;
}

.notifs__state[data-tone='ok']     { color: var(--right); background: color-mix(in oklch, var(--right) 10%, transparent); }
.notifs__state[data-tone='alerte'] { color: var(--wrong); background: color-mix(in oklch, var(--wrong) 10%, transparent); }

/* Sous quarante rem, la case et son libellé restent côte à côte, mais la
   description passe dessous : c'est la seule mise en page où un texte de deux
   lignes ne pousse pas la case hors du pouce. */
@media (max-width: 40rem) {
  .notifs__item { padding: var(--s3) var(--s2); gap: var(--s2); }
  .notifs__aside { font-size: var(--text-xs); }
}

/* ==========================================================================
   Bibliothèque : la barre de filtres sur téléphone
   --------------------------------------------------------------------------
   Huit puces — cinq catégories, trois niveaux — se pressaient dans trois cent
   quatre-vingt-dix pixels. Elles défilaient déjà, mais si serrées qu'on en
   voyait sept à moitié plutôt que trois en entier, et l'œil ne savait pas où
   se poser. On respire : moins de puces visibles, chacune lisible, et le reste
   à un glissement de pouce.

   Les niveaux passent après un intervalle plus large que le pas entre deux
   catégories. Ce n'est pas un ornement : c'est ce qui fait comprendre, sans
   titre ni séparateur, qu'on change de famille de filtres.
   ========================================================================== */

@media (max-width: 47.99rem) {
  .library__filters {
    gap: var(--s3);
    /* L'accroche devient ferme : un glissement s'arrête sur une puce entière,
       jamais entre deux. */
    scroll-snap-type: x mandatory;
    padding-inline: 2px;
  }

  .library__filters .chip {
    padding-inline: var(--s5);
    font-size: var(--text-sm);
  }

  /* Le filet qui sépare les deux familles s'écarte : c'est lui qui fait
     comprendre, sans titre, qu'on passe des catégories aux niveaux. */
  .library__filters .hairline { margin-inline: var(--s4) !important; }

  .library__controls { gap: var(--s3); padding-block: var(--s3); }
}

/* ==========================================================================
   Les quatre façons de travailler un thème
   --------------------------------------------------------------------------
   Une carte doit répondre à quatre questions avant qu'on ne clique : ce que
   fait le mode, où j'en suis, combien de temps, ce que ça rapporte. Elles sont
   empilées dans cet ordre, du plus général au plus précis, et l'œil descend
   naturellement sans avoir à chercher.

   L'état est porté par la couleur du repère, jamais par un mot supplémentaire :
   « à faire » reste sourd, « en cours » s'ambre, « terminé » se coche. Ajouter
   une étiquette « terminé » à côté d'un repère déjà coché reviendrait à dire
   deux fois la même chose, ce que faisait l'ancienne frise.
   ========================================================================== */

.modes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: var(--s4);
}

.mode {
  display: grid;
  grid-template-rows: auto auto auto auto;
  gap: var(--s3);
  padding: var(--s5);
  border-radius: var(--r-lg);
}

.mode__mark {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--ink-3);
  transition: color var(--dur-2) var(--ease), background-color var(--dur-2) var(--ease);
}

.mode[data-state="doing"] .mode__mark { background: var(--accent-wash); color: var(--accent-ink); }
.mode[data-state="done"]  .mode__mark { background: var(--right-wash); color: var(--right); }

.mode__body { display: grid; gap: var(--s1); }

.mode__label {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  letter-spacing: var(--track-tight);
}

.mode__text {
  color: var(--ink-2);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  text-wrap: pretty;
}

.mode__etat {
  color: var(--ink-3);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

/* La jauge est fine et sans chiffre : la valeur est déjà écrite au-dessus. */
.mode__jauge {
  height: 3px;
  border-radius: var(--r-full);
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  overflow: hidden;
}

.mode__jauge-fill {
  display: block;
  height: 100%;
  width: calc(var(--progress, 0) * 100%);
  border-radius: inherit;
  background: var(--accent);
}

.mode[data-state="done"] .mode__jauge-fill { background: var(--right); }

.mode__pied {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s4);
  align-items: center;
  margin-block-start: auto;
  padding-block-start: var(--s2);
  border-block-start: 1px solid var(--hairline);
  color: var(--ink-3);
  font-size: var(--text-2xs);
}

.mode__meta { display: inline-flex; align-items: center; gap: var(--s1); min-width: 0; }
.mode__meta--gain { color: var(--accent-ink); }
.mode__meta .icon { flex: none; }

/* Sur téléphone, la carte s'allonge : l'icône passe à côté du titre plutôt
   qu'au-dessus, ce qui économise une ligne entière par carte, soit quatre
   lignes sur la page. */
@media (max-width: 34rem) {
  .modes { gap: var(--s3); }
  .mode { grid-template-columns: auto 1fr; padding: var(--s4); gap: var(--s2) var(--s3); }
  .mode__mark { grid-row: 1; width: 2.25rem; height: 2.25rem; }
  .mode__body { grid-column: 2; }
  .mode__etat, .mode__jauge, .mode__pied { grid-column: 1 / -1; }
}

/* ==========================================================================
   Groupes de connaissances repliables
   ==========================================================================
   L'en-tête devient la commande. Il conserve son aspect d'étiquette discrète :
   ce n'est pas un bouton qu'on cherche, c'est un titre qu'on découvre
   cliquable en s'en approchant. Le compte à droite dit ce qu'on trouvera
   dedans avant d'ouvrir ; c'est lui qui remplace le fait de tout voir.
   ========================================================================== */

.knowledge-group[open] { margin-block-end: var(--s6); }

.knowledge-group > .knowledge-group__head {
  cursor: pointer;
  list-style: none;
  align-items: center;
  padding-block: var(--s2);
  border-radius: var(--r-sm);
  transition: color var(--dur-2) var(--ease);
}

.knowledge-group > .knowledge-group__head::-webkit-details-marker { display: none; }

@media (hover: hover) {
  .knowledge-group > .knowledge-group__head:hover .knowledge-group__name { color: var(--ink); }
}

.knowledge-group__compte {
  margin-inline-start: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-4);
}

.knowledge-group__fleche {
  flex: none;
  color: var(--ink-4);
  transition: transform var(--dur-2) var(--ease);
}

.knowledge-group[open] > .knowledge-group__head .knowledge-group__fleche { transform: rotate(180deg); }

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

/* ==========================================================================
   Action principale d'un thème, sur écran étroit
   --------------------------------------------------------------------------
   « Réviser 45 connaissances » demande 325 pixels. À 320 comme à 360, la
   colonne n'en offre que 284 : le bouton dépassait et se faisait couper par
   l'`overflow-x: hidden` du corps — on lisait « Réviser 45 connaissa ».

   Il ne pouvait pas se resserrer : un bouton porte `white-space: nowrap`,
   et c'est voulu, un libellé coupé en deux au milieu d'un bouton est pire
   qu'un bouton large. On lui donne donc toute la largeur disponible et le
   droit de passer à la ligne, ce que font les applications natives pour leur
   action principale. Le libellé reste entier, et la cible grandit.
   ========================================================================== */

@media (max-width: 24rem) {
  .theme__hero .row--wrap > .btn,
  .review__hero .row--wrap > .btn {
    width: 100%;
    white-space: normal;
    text-align: center;
  }
}

/* Une rangée de réglage, sur écran très étroit.
   -------------------------------------------------------------------------
   La rangée met le libellé à gauche et la commande à droite. À 390 pixels
   les trois barres segmentées mesurent 123, 143 et 282 pixels et tiennent.
   À 320, la colonne n'en offre plus que deux cent trente : la commande
   dépassait, et la faire défiler la réduisait à quarante-deux pixels — une
   barre plus étroite que l'un de ses propres onglets.

   On empile donc : le libellé au-dessus, la commande en dessous sur toute la
   largeur. C'est la disposition de tous les écrans de réglages natifs à cette
   taille, et elle rend les trois barres entièrement lisibles sans rien faire
   défiler. */

@media (max-width: 24rem) {
  .settings__row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s3);
  }

  /* Le libellé occupe la ligne entière : sans `min-width: 0`, un élément
     flexible refuse de devenir plus étroit que son texte et dépasse. */
  .settings__row > .stack { min-width: 0; }

  .settings__row .segmented {
    width: 100%;
    min-width: 0;
    /* Filet de sécurité : « Système · Complètes · Réduites » demande 282 px
       pour 242 disponibles, même resserré. Le débordement défile plutôt que
       de se faire couper — et sur toute la largeur, l'accroche est utilisable,
       ce qui n'était pas le cas quand la barre ne faisait que 42 px. */
    overflow-x: auto;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
  }

  .settings__row .segmented::-webkit-scrollbar { display: none; }

  .settings__row .segmented__option {
    flex: 1 0 auto;
    justify-content: center;
    padding-inline: var(--s3);
    scroll-snap-align: center;
  }
}

/* ==========================================================================
   Choix du thème d'un défi
   --------------------------------------------------------------------------
   Vingt-quatre thèmes, quatre familles, et le regard doit trouver en deux
   secondes. Des cartes très compactes, rangées par famille, sur une grille
   qui remplit la largeur disponible plutôt que d'imposer une colonne.

   Le champ radio reste dans le flux mais invisible : il porte l'état, le
   focus clavier et l'accessibilité, et c'est `:checked` qui habille la carte.
   Rien de tout cela ne demande une ligne de script.
   ========================================================================== */

.theme-pick { border: 0; padding: 0; margin: 0; min-width: 0; }

.theme-pick__famille {
  display: block;
  margin-block: var(--s4) var(--s2);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Le sélecteur de thème.

   Il n'a plus de cartes à lui : ce sont celles de la bibliothèque, rendues en
   mode radio par le même composant. Ne restent donc ici que les trois choses
   qu'une carte-lien n'a pas — masquer le bouton natif, marquer la sélection,
   et rendre la coche visible. Tout le reste — dimensions, couverture,
   typographie, rembourrage, comportement mobile — est déjà écrit une fois
   pour `.theme-card`, et le sera toujours au même endroit. */

.theme-pick { border: 0; padding: 0; margin: 0; min-width: 0; }

.theme-pick__grille { margin-block-end: var(--s4); }

/* L'entrée des tuiles : une seule fois, courte, et jamais rejouée.

   Elles héritaient de `.reveal`, dont l'observateur pose `is-in` au passage
   dans le champ. Après un retour arrière ou un rafraîchissement de zone, la
   classe repartait de zéro et les vingt-quatre tuiles réapparaissaient en
   cascade — un scintillement là où rien n'avait changé. La cascade est
   désormais portée par la grille elle-même, en `animation ... both` : elle
   garde son état final et ne se redéclenche pas. */
.theme-pick__grille .theme-card--compact {
  animation: tuile-pose 240ms var(--ease) both;
  animation-delay: calc(var(--i, 0) * 18ms);
}

@keyframes tuile-pose {
  from { opacity: 0; transform: translate3d(0, 6px, 0); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .theme-pick__grille .theme-card--compact { animation: none; }
}

/* Le bouton d'envoi reste à portée : sur une grille de vingt-quatre cartes,
   remonter chercher un bouton après avoir choisi tout en bas est un
   aller-retour que rien ne justifie.
 *
 * Il était écrit `position: sticky`, et il ne l'a jamais été : `body` porte
 * `overflow-x: hidden`, ce qui en fait une boîte de défilement au sens du
 * moteur — et une boîte qui ne défile jamais. Tout collant se cale sur elle
 * et reste immobile. Mesuré sur un écran de 375 : le bouton quittait
 * l'écran au bout de six cents pixels et le récapitulatif l'accompagnait ;
 * il fallait redescendre deux mille quatre cents pixels de vignettes pour
 * lancer une partie qu'on avait décidée depuis longtemps.
 *
 * `fixed` ne dépend d'aucun ancêtre, et c'est la seule chose qui tienne ici.
 * Il n'est posé que sur téléphone : sur un grand écran, la grille tient en
 * quelques rangées et une barre en travers du bas coûterait plus qu'elle ne
 * rapporte. */
.defis__envoi {
  position: relative;
  display: flex;
  justify-content: center;
  padding-block: var(--s4);
  z-index: 2;
}

/* Le socle du bouton : un dégradé qui monte du fond de page, pour qu'il ne
   se pose pas à même la dernière rangée de vignettes. */
.defis__envoi::before {
  content: "";
  position: absolute;
  inset-inline: calc(var(--gutter) * -1);
  inset-block: -1rem -1.5rem;
  background: linear-gradient(to top, var(--paper) 42%, transparent);
  pointer-events: none;
  z-index: -1;
}

/* — La barre de lancement, au téléphone ————————————————————————————
 *
 * `fixed`, et non `sticky`. Le `sticky` d'origine n'a jamais collé : `body`
 * porte `overflow-x: hidden`, ce qui en fait une boîte de défilement au sens
 * du moteur — et une boîte qui, elle, ne défile jamais. Tout élément collant
 * se cale sur elle et reste immobile. Mesuré : le bouton quittait l'écran au
 * bout de six cents pixels, et il fallait redescendre toute la grille des
 * thèmes pour lancer une partie décidée depuis longtemps.
 *
 * Elle n'apparaît qu'une fois l'adversaire choisi. C'est la quatrième étape
 * du parcours : elle n'a rien à faire à l'écran pendant qu'on en est à la
 * première, où elle ne serait qu'un bouton gris posé en travers. Le `:has()`
 * la fait entrer au moment exact où elle devient actionnable.
 *
 * Aucune autre page n'est touchée : les deux règles sont conditionnées à la
 * présence de ce bouton, qui n'existe que sur l'écran des défis. */
@media (max-width: 47.99rem) {
  form:has(.ami-pick input:checked) .defis__envoi {
    position: fixed;
    inset-inline: 0;
    /* Juste au-dessus de la barre d'onglets, dont le corps réserve déjà la
       hauteur : la même valeur ici et là, sinon les deux se chevauchent. */
    inset-block-end: calc(5.75rem + env(safe-area-inset-bottom));
    padding: var(--s3) var(--gutter);
    z-index: 3;
  }

  form:has(.ami-pick input:checked) .defis__envoi::before {
    inset-inline: 0;
    inset-block: -1.5rem 0;
    background: linear-gradient(to top, var(--paper) 62%, transparent);
  }

  /* La hauteur que la barre n'occupe plus dans le flux est rendue au bas de
     la page : sans cela, elle recouvrirait la fin de la liste des défis. */
  body:has(.ami-pick input:checked) {
    padding-block-end: calc(10.5rem + env(safe-area-inset-bottom));
  }
}

/* — Et le même confort à la souris ————————————————————————————————
 *
 * Le bouton restait au fil du formulaire sur grand écran, sous le sélecteur
 * de thèmes. Or ce sélecteur fait vingt-quatre tuiles : mesuré à 1440 par
 * 900, le bouton commençait à 1 780 pixels du haut, soit deux écrans pleins
 * sous le pli. On choisissait un adversaire, on choisissait un thème — et il
 * fallait redescendre chercher le bouton, ou remonter s'être trompé de
 * thème puis redescendre à nouveau. Le pouce n'était pas le sujet ; la
 * distance, si, et elle est pire ici que sur téléphone.
 *
 * `fixed` et non `sticky`, pour la raison écrite plus haut : `body` porte
 * `overflow-x: hidden`, ce qui en fait une boîte de défilement qui ne défile
 * jamais, et tout élément collant s'y cale sans jamais coller.
 *
 * La forme diffère de celle du téléphone, et volontairement. Une barre pleine
 * largeur au bas d'un écran de 1440 serait un bandeau de mille quatre cents
 * pixels pour un bouton de deux cents : c'est le geste du pouce, pas celui du
 * curseur. Ici, le bouton flotte seul, centré, posé sur son ombre — il ne
 * masque que ce qu'il couvre, et la réserve en bas de page garantit qu'il ne
 * couvre jamais la dernière rangée. */
@media (min-width: 48rem) {
  form:has(.ami-pick input:checked) .defis__envoi {
    position: fixed;
    inset-inline: 0;
    inset-block-end: var(--s6);
    padding: 0;
    z-index: 3;
    /* Le conteneur est traversant : il occupe toute la largeur pour centrer
       son bouton, et il ne doit voler aucun clic sur ce qu'il survole. */
    pointer-events: none;
  }

  form:has(.ami-pick input:checked) .defis__envoi > .btn {
    pointer-events: auto;
    /* Il flotte : il lui faut l'ombre qui le dit, et une lueur ambre qui le
       détache du contenu qui défile dessous. */
    box-shadow:
      inset 0 1px 0 0 rgba(255, 255, 255, 0.3),
      0 8px 28px -10px color-mix(in oklab, var(--accent) 80%, transparent),
      var(--shadow-4);
  }

  /* Le socle en dégradé n'a plus lieu d'être : il barrait toute la largeur
     de l'écran pour un bouton qui n'en occupe qu'un huitième. */
  form:has(.ami-pick input:checked) .defis__envoi::before { display: none; }

  /* La réserve, pour que le bouton ne recouvre jamais la fin de la page. */
  body:has(.ami-pick input:checked) { padding-block-end: 7.5rem; }
}

/* Les intitulés de section du formulaire : un pas au-dessus d'un simple
   libellé de champ. C'est un parcours en trois temps — qui, quoi, lancer. */
.ami-pick > .field__label,
.theme-pick > .field__label {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-display);
  font-size: var(--display-4);
  font-weight: 400;
  letter-spacing: var(--track-tight);
  text-transform: none;
  color: var(--ink);
  /* Les titres respirent : ils annoncent une section entière, pas un champ.
     `--s2` les collait au contenu au point qu'on les lisait comme une
     étiquette de la première carte. */
  margin-block-end: var(--s5);
}

/* Le numéro de l'étape, à la place du filet.

   Il dit deux choses qu'un trait ne dit pas : où l'on en est, et combien il
   en reste. Il passe en ambre plein dès que l'étape est franchie — un état,
   pas une décoration. */
.ami-pick > .field__label::before,
.theme-pick > .field__label::before {
  content: attr(data-num);
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--r-full);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  color: var(--ink-3);
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  flex: none;
  transition: background-color 160ms var(--ease), color 160ms var(--ease), box-shadow 160ms var(--ease);
}

/* Étape franchie : un choix est fait dans ce groupe. */
.ami-pick:has(input:checked) > .field__label::before,
.theme-pick:has(input:checked) > .field__label::before {
  background: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  color: var(--on-accent);
}

/* — Le récapitulatif porte le troisième temps ————————————————————— */
.defi-resume::before {
  content: "3";
  position: absolute;
  inset-block-start: calc(var(--s4) * -1);
  inset-inline-start: var(--s5);
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--text-2xs);
  font-weight: 700;
  box-shadow: 0 0 0 3px var(--paper);
}

.defi-resume { position: relative; }

/* ==========================================================================
   Le récapitulatif de lancement
   --------------------------------------------------------------------------
   Contre qui, sur quoi, à quel niveau — les trois éléments du défi réunis à
   l'endroit où l'on appuie. Il tient volontairement en une bande basse :
   c'est une confirmation, pas un troisième écran de choix.
   ========================================================================== */

.defi-resume {
  display: grid;
  gap: var(--s4);
  padding: var(--s5);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 1px 0 0 var(--bevel), var(--shadow-2);
}

.defi-resume[hidden] { display: none; }

.defi-resume.is-entre { animation: resume-arrive 200ms var(--ease-spring) both; }

@keyframes resume-arrive {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: none; }
}

/* Une phrase, pas une balance.
 *
 * Le bloc opposait l'adversaire et le thème dans deux colonnes de même poids
 * autour d'un « VS ». Voir le commentaire du gabarit : on n'affronte pas un
 * thème. Ce qui suit se lit dans un seul sens — qui, puis sur quoi — et la
 * hiérarchie n'a plus besoin d'être expliquée.
 *
 * `flex-wrap` plutôt qu'une grille : sur un écran étroit, la phrase se replie
 * en deux lignes au lieu de comprimer ses deux moitiés. Une confirmation qui
 * tronque le nom de l'adversaire ne confirme rien. */
.defi-resume__phrase {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--s2) var(--s3);
  margin: 0;
}

.defi-resume__qui,
.defi-resume__quoi {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-width: 0;
}

/* L'adversaire est le sujet de la phrase : son nom prend le corps du texte
   courant et la graisse qui va avec. Le thème, lui, est le complément — même
   corps, moins de graisse, et son niveau en dessous. */
.defi-resume__nom {
  max-width: 12rem;
  font-size: var(--text-md);
  font-weight: 650;
  line-height: 1.2;
}

.defi-resume__liant {
  font-size: var(--text-sm);
  color: var(--ink-3);
}

.defi-resume__sujet {
  display: grid;
  gap: 1px;
  min-width: 0;
}

.defi-resume__titre {
  max-width: 12rem;
  font-size: var(--text-md);
  font-weight: 550;
  line-height: 1.2;
}

.defi-resume__niveau {
  font-size: var(--text-2xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-4);
}

/* Les deux dessins ont la même hauteur mais pas la même forme : un visage est
   rond, une couverture est un rectangle. Les forcer au même carré, comme le
   faisait la version en balance, déformait la seconde — et les faire peser
   pareil n'a plus lieu d'être, puisqu'ils ne s'opposent plus. */
.defi-resume__visage,
.defi-resume__vignette {
  display: grid;
  place-items: center;
  flex: none;
}

.defi-resume__visage .avatar { width: 2.5rem; height: 2.5rem; }

.defi-resume__vignette .cover {
  width: 3.25rem;
  height: 2.5rem;
  aspect-ratio: auto;
  border-radius: var(--r-sm, 0.5rem);
}

.defi-resume__format {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s2);
  padding-block-start: var(--s3);
  border-block-start: 1px solid var(--hairline);
}

/* — Le bouton, point focal de l'écran ————————————————————————————
   Il est déjà ambre et large ; ce qui lui manquait, c'est de *réagir*. La
   lueur ne s'allume qu'une fois actif : un bouton désactivé qui rayonne
   invite à un geste qui ne marchera pas. */
.defi-lancer {
  min-block-size: 3.5rem;
  padding-inline: var(--s8);
  font-size: var(--text-lg);
  letter-spacing: -0.01em;
  overflow: hidden;
  isolation: isolate;
}

.defi-lancer:not(:disabled) {
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.32),
    0 6px 28px -8px color-mix(in oklab, var(--accent) 80%, transparent),
    var(--shadow-2);
}

/* Un reflet qui traverse le bouton une fois qu'il devient actionnable.
   Il ne tourne pas en boucle : il passe, et c'est ce passage qui dit
   « c'est prêt ». */
.defi-lancer:not(:disabled)::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: -60%;
  width: 45%;
  z-index: -1;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.42), transparent);
  transform: skewX(-18deg);
  animation: lancer-reflet 900ms var(--ease) 120ms both;
}

@keyframes lancer-reflet {
  from { inset-inline-start: -60%; }
  to   { inset-inline-start: 130%; }
}

/* L'ancienne animation de sortie de page a été retirée.

   Elle faisait disparaître l'écran pendant la création du duel — un fondu
   à zéro sur toute la page, c'est-à-dire précisément la sensation de
   rechargement qu'on cherche à supprimer. Seul le bouton signale désormais
   que le geste est parti ; le reste de la page ne bouge pas. */

/* Le bouton pendant la création du duel : il respire, et c'est le seul
   élément de l'écran qui bouge. Deux propriétés composées, une seconde de
   cycle — assez pour dire « en cours », jamais assez pour inquiéter. */
.defi-lancer.is-loading {
  animation: lancer-attend 1s var(--ease-inout) infinite;
}

@keyframes lancer-attend {
  0%, 100% { opacity: 1; }
  50%      { opacity: .78; }
}

.defi-lancer:not(:disabled):active {
  transform: scale(0.97);
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.2),
    0 2px 10px -4px color-mix(in oklab, var(--accent) 60%, transparent);
}

/* L'instant où le bouton devient actionnable mérite d'être vu : c'est le
   signal que la décision est complète. Une seule pulsation, jamais en
   boucle — la classe est posée par le navigateur au changement d'état, et
   l'animation ne se rejoue qu'au prochain passage inactif → actif. */
.defi-lancer:not(:disabled) { animation: lancer-prete 240ms var(--ease-spring) both; }

@keyframes lancer-prete {
  0%   { transform: scale(0.98); }
  55%  { transform: scale(1.02); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .defi-resume.is-entre { animation: fade var(--dur-2) linear both; }
  .defi-lancer:not(:disabled) { animation: none; }
  .defi-lancer:not(:disabled)::after { animation: none; opacity: 0; }
  .defi-lancer.is-loading { animation: none; }
  .defi-lancer:not(:disabled):active { transform: none; }
}

/* ==========================================================================
   Choisir un adversaire
   --------------------------------------------------------------------------
   Le même mécanisme que le sélecteur de thèmes juste en dessous — des
   boutons radio natifs, dont le CSS n'habille que l'état `:checked` — parce
   que c'est la même question posée deux fois sur le même écran : lequel ?

   Ce qui change, c'est ce qu'on regarde. On ne lit pas un nom d'ami, on
   reconnaît un visage : l'avatar occupe donc la pastille, et le nom vient
   sous lui en petit, comme légende. C'est l'inverse de la liste déroulante
   qu'il remplace, où le visage n'existait pas et où le pseudo entre
   parenthèses pesait autant que le nom.
   ========================================================================== */

.ami-pick { border: 0; padding: 0; margin: 0; min-width: 0; }

/* Sans ami : on explique l'absence à l'endroit exact où le choix devrait
   être, plutôt que de faire disparaître la section entière. */
.ami-pick__vide {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s3);
  padding: var(--s4);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  font-size: var(--text-sm);
  color: var(--ink-3);
}

.ami-pick:has(.ami-pick__vide) .ami-pick__piste { display: none; }
.ami-pick + .theme-pick { margin-block-start: var(--s6); }

/* Une piste horizontale, et non une grille qui retombe à la ligne.
 *
 * Avec deux amis, une grille laisse un grand vide à droite ; avec trente,
 * elle fait dix rangées et pousse le choix du thème hors de l'écran. Une
 * piste qui défile occupe toujours la même hauteur — deux lignes — quel que
 * soit le nombre d'amis, et montre d'emblée qu'il y en a d'autres plus loin.
 *
 * `scroll-snap` aligne les pastilles au repos : sans lui, on s'arrête
 * régulièrement sur un visage coupé en deux, ce qui donne l'impression
 * d'avoir manqué quelqu'un.
 *
 * Les marges négatives font mordre la piste sur les gouttières de la page :
 * la première pastille commence au bord du texte, mais le défilement va
 * jusqu'au bord de l'écran — sinon on croit la liste terminée là où la
 * gouttière commence. */
.ami-pick__piste {
  display: flex;
  gap: var(--s3);
  align-items: stretch;
  /* Le défilement horizontal d'une piste d'avatars repeint tout le bandeau
     à chaque image sans cela : la promotion en couche laisse le compositeur
     s'en charger seul. */
  transform: translateZ(0);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  /* Le défilement à la molette et au trackpad devient continu plutôt que
     saccadé, et l'inertie tactile est celle du système. `thin` retire la
     barre grasse de Chrome sous Windows, qui ajoutait quinze pixels de haut
     à une piste qui en fait cent vingt. */
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  padding: var(--s3) var(--gutter) var(--s4);
  margin-inline: calc(var(--gutter) * -1);
  scrollbar-width: thin;
}

.ami {
  position: relative;
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: grid;
  justify-items: center;
  gap: var(--s3);
  width: 6.5rem;
  padding: var(--s4) var(--s2) var(--s3);
  cursor: pointer;
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--hairline), inset 0 1px 0 0 var(--bevel), var(--shadow-1);
  -webkit-tap-highlight-color: transparent;
  /* 190 ms, et non 320 : cocher un adversaire est un changement d'état, pas
     une apparition. Un tiers de seconde entre le doigt et l'anneau ambre se
     ressent comme un temps de réponse, et c'est le geste le plus répété de
     l'écran. */
  transition:
    box-shadow var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease-spring),
    background-color var(--dur-2) var(--ease);
  /* L'entrée s'aligne sur celle des vignettes de thème, juste en dessous :
     240 ms et 30 de décalage. À 520 ms et 45 de décalage, la sixième
     pastille finissait de se poser presque une seconde après l'affichage,
     bien après que le doigt s'y soit posé. */
  animation: ami-pose 240ms var(--ease-spring) both;
  /* L'animation d'entrée est déclarée `both` : elle garde son état final et
     ne repart donc pas à zéro si la carte est repeinte. */
  backface-visibility: hidden;
  animation-delay: calc(var(--i, 0) * 30ms);
}

/* La carte entière se soulève, pas seulement le portrait : c'est ce qui la
   fait passer d'une vignette à un objet qu'on choisit. */
.ami:has(> input:checked) {
  background: var(--accent-wash);
  box-shadow:
    inset 0 0 0 1.5px var(--accent),
    inset 0 1px 0 0 var(--bevel),
    0 10px 26px -12px color-mix(in oklab, var(--accent) 75%, transparent),
    var(--shadow-2);
  transform: translate3d(0, -4px, 0);
}

.ami:active { transform: translate3d(0, -1px, 0) scale(0.97); transition-duration: 90ms; }

@keyframes ami-pose {
  from { opacity: 0; transform: translate3d(0, 8px, 0) scale(.96); }
  to   { opacity: 1; transform: none; }
}

/* Le bouton natif reste dans le formulaire mais sort du dessin. `opacity: 0`
   plutôt que `display: none` : un champ masqué par `display` cesse d'être
   focalisable, et le clavier est le seul moyen de parcourir la piste sans
   souris. */
.ami > input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}

/* L'avatar porte l'anneau de sélection sur lui-même, jamais sur la pastille
   entière : un contour autour du bloc engloberait le nom et ferait un
   rectangle là où tout le reste est rond.

   Trois couches d'ombre, et chacune a un rôle. Le liseré couleur surface
   détache le visage du fond quand l'anneau ambre s'allume — sans lui, les
   deux cercles se touchent et bavent l'un dans l'autre. L'ombre portée donne
   la profondeur que le brief demande : elle est basse et large, jamais dure,
   parce qu'un avatar est posé sur la page, pas décollé d'elle. Le troisième
   emplacement est réservé, transparent, pour que l'anneau ambre puisse
   apparaître sans faire varier le nombre d'ombres — ce qui interromprait la
   transition. */
/* Le visage tient dans sa pastille, et il n'y tenait pas.
 *
 * Soixante-quatre pixels d'avatar, plus trois de filet blanc et six d'anneau
 * ambre une fois choisi : quatre-vingt-deux pixels de disque dans une carte
 * de cent quatre, dont huit de rembourrage de chaque côté. Il restait
 * exactement trois pixels de part et d'autre — l'avatar ne se posait pas dans
 * la carte, il la remplissait. Et l'anneau de sélection, lui, débordait
 * franchement.
 *
 * À cinquante-deux, le disque complet fait soixante-dix : dix-huit pixels de
 * marge, le visage reste parfaitement reconnaissable, et l'anneau ambre a
 * enfin la place de s'allumer à l'intérieur de la carte. */
.ami__avatar {
  position: relative;
  z-index: 1;
  width: 3.25rem;
  height: 3.25rem;
  box-shadow:
    0 0 0 3px var(--surface),
    0 0 0 3px transparent,
    0 4px 12px -4px rgba(0, 0, 0, 0.28);
  transition: box-shadow var(--dur-3) var(--ease), transform var(--dur-3) var(--ease-spring);
}

/* L'anneau de sélection reste une ombre, mais elle n'est plus *animée* :
   la transition ne porte que sur `transform`. Une bordure qui grossit en
   fondu repeint la carte à chaque image, et la piste en compte jusqu'à
   trente côte à côte. */
/* Sélectionné : l'anneau se peint *sur* le bord, et le portrait ne grandit
 * plus.
 *
 * Il gagnait six pixels d'anneau à l'extérieur **et** six pour cent d'échelle.
 * Deux façons d'agrandir le même disque, cumulées, au moment précis où l'œil
 * compare la pastille choisie à ses voisines : le portrait sélectionné
 * débordait de son rembourrage et cognait le bord de sa carte.
 *
 * Le contour passe en `inset`, comme celui du duelliste en main : le cercle
 * ambre a exactement le diamètre du cercle voisin. Ce qui reste à l'extérieur
 * est une lueur — sans bord, donc sans taille — et un soulèvement de trois
 * pixels, qui déplace sans agrandir. C'est la sélection qui se voit, pas une
 * dilatation. */
/* Le choix se dit vers l'intérieur.
 *
 * Il restait une lueur portée de dix-huit pixels de flou sous le disque. Une
 * ombre floue n'a pas de bord net, mais elle a une étendue : l'œil lit le
 * portrait choisi comme plus large que ses voisins, ce qui est exactement la
 * comparaison qu'on ne veut pas installer entre deux adversaires possibles.
 *
 * Deux contours internes remplacent l'ombre : l'ambre franc sur le bord, puis
 * un liseré plus clair juste en dessous, qui donne la profondeur que
 * l'ombre donnait. Tout tient à l'intérieur des cinquante pixels du disque,
 * et la carte, qui prend l'ambre de son côté, dit le reste. */
.ami:has(> input:checked) .ami__avatar {
  box-shadow:
    inset 0 0 0 2px var(--accent),
    inset 0 0 0 4px color-mix(in oklab, var(--accent) 22%, transparent);
  transform: translate3d(0, -3px, 0);
}

.ami:active .ami__avatar { transform: scale(0.96); transition-duration: 90ms; }

.ami:has(> input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Le focus clavier garde son anneau extérieur : c'est un indicateur système,
   il doit être franc et il ne se cumule jamais avec la comparaison de tailles
   — on ne parcourt pas la piste au clavier en jugeant des diamètres. */
.ami:has(> input:focus-visible) .ami__avatar {
  box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 3px var(--surface), 0 0 0 5px var(--accent-line);
}

/* Le halo : la lueur discrète qui dit « c'est celui-ci » d'un coup d'œil,
   même quand la piste défile. Calque séparé plutôt qu'une ombre portée sur
   l'avatar, pour qu'il déborde sans agrandir la cible. */
/* Le halo suit le visage qu'il éclaire : réduit avec lui, il resterait sinon
   une auréole d'un pouce autour d'une pastille de deux centimètres. */
/* Les cadres achetés, dans le sélecteur comme dans le bandeau de duel : leur
 * décor reste, leur anneau *sortant* s'arrête au bord du disque.
 *
 * `avatar--aura` ajoute trois pixels d'anneau et dix-huit de lueur autour de
 * l'image ; `avatar--couronne` un dégradé posé à moins six. Sur un profil,
 * cela n'a aucune conséquence. Ici, où douze pastilles s'alignent et se
 * comparent, un joueur paré portait un disque visiblement plus large que les
 * autres — et l'anneau de sélection venait par-dessus.
 *
 * Même règle qu'ailleurs : on garde le dessin, on supprime ce qui dépasse. */
.ami__avatar.avatar--aura {
  box-shadow: inset 0 0 0 1.5px var(--accent-line), inset 0 0 0 3px var(--accent-wash);
}

.ami__avatar.avatar--couronne { box-shadow: inset 0 0 0 1.5px var(--accent-line); }
.ami__avatar.avatar--couronne::after { inset: 0; }
.ami__avatar.avatar--lueur::after { box-shadow: inset 0 0 0 1px var(--accent-line); }

/* Le halo éclaire, il n'entoure plus.
 *
 * À soixante-douze pixels autour d'un avatar de cinquante, ce n'était plus une
 * lueur mais un second disque — flou, mais un disque, et plus large que celui
 * qu'il était censé mettre en valeur. C'est ce qui donnait encore
 * l'impression que le portrait sélectionné dépassait de sa carte.
 *
 * Ramené à la taille du portrait, il redevient ce qu'une lueur doit être : de
 * la lumière derrière l'objet, sans contour propre et sans dimension lisible.
 * La mise en valeur tient au contour interne et à la carte, qui prend
 * l'ambre. */
.ami__halo {
  position: absolute;
  inset-block-start: -0.125rem;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--r-full);
  background: radial-gradient(closest-side, var(--accent) 0%, transparent 70%);
  opacity: 0;
  transform: scale(.7);
  filter: blur(10px);
  pointer-events: none;
  /* Au pas de la pastille qu'il éclaire : à 520 ms contre 190, le halo
     arrivait après coup, comme un second geste qu'on n'avait pas fait. */
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-spring);
}

.ami:has(> input:checked) .ami__halo { opacity: .42; transform: scale(1.1); }

.ami__coche {
  position: absolute;
  inset-block-start: 2.75rem;
  inset-inline-end: 0.625rem;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: 0 0 0 2px var(--surface);
  opacity: 0;
  transform: scale(.5);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-spring);
}

.ami:has(> input:checked) .ami__coche {
  opacity: 1;
  /* La coche arrive un souffle après l'anneau — quarante millisecondes, le
     temps que l'œil ait enregistré le halo. Simultanées, les deux se
     neutralisent ; décalées, la seconde confirme la première. */
  transform: scale(1);
  transition-delay: 40ms;
}

.ami__nom {
  max-width: 100%;
  font-size: var(--text-xs);
  font-weight: 550;
  color: var(--ink-2);
  text-align: center;
  line-height: 1.25;
  transition: color var(--dur-2) var(--ease);
}

.ami:has(> input:checked) .ami__nom { color: var(--accent-ink); font-weight: 700; }

@media (hover: hover) {
  .ami:hover {
    transform: translate3d(0, -3px, 0);
    box-shadow: inset 0 0 0 1px var(--hairline-strong), inset 0 1px 0 0 var(--bevel), var(--shadow-3, var(--shadow-2));
  }
  .ami:hover:has(> input:checked) { transform: translate3d(0, -5px, 0); }
  /* Le survol soulève, il n'enfle pas.
   *
   * Le portrait déjà choisi gagnait ici six pixels d'anneau extérieur *et* six
   * pour cent d'échelle. C'était la dernière raison — et la plus visible, parce
   * qu'elle se déclenche au moment précis où l'on compare deux adversaires —
   * de voir un disque plus gros que ses voisins et débordant de sa carte : à
   * cinquante pixels, l'anneau seul en ajoutait douze sur la largeur.
   *
   * Reste l'élévation, qui est la bonne réponse au survol : elle dit « celui-ci
   * répond au pointeur » sans rien affirmer sur la taille de personne. Le
   * liseré interne s'éclaire d'un cran, ce qui suffit à marquer l'état. */
  .ami:hover .ami__avatar {
    box-shadow:
      inset 0 0 0 1.5px var(--hairline-strong),
      0 6px 16px -5px rgba(0, 0, 0, 0.32);
    transform: translate3d(0, -2px, 0);
  }
  .ami:hover:has(> input:checked) .ami__avatar {
    box-shadow:
      inset 0 0 0 2px var(--accent),
      inset 0 0 0 5px color-mix(in oklab, var(--accent) 30%, transparent);
    transform: translate3d(0, -3px, 0);
  }
  .ami:hover .ami__nom { color: var(--ink-2); }
}

/* Mouvement réduit : la sélection reste parfaitement lisible — anneau ambre,
   halo, coche, nom en pleine encre — mais plus rien ne se déplace ni ne
   change d'échelle. C'est le déplacement qui gêne, pas le fait d'être
   signalé. */
@media (prefers-reduced-motion: reduce) {
  .ami { animation: fade var(--dur-2) linear both; animation-delay: 0s; }
  .ami__avatar,
  .ami:active .ami__avatar,
  .ami:hover .ami__avatar,
  .ami:hover:has(> input:checked) .ami__avatar,
  .ami:has(> input:checked) .ami__avatar { transform: none; }
  .ami__halo,
  .ami:has(> input:checked) .ami__halo { transform: none; }
  .ami__coche,
  .ami:has(> input:checked) .ami__coche { transform: none; transition: opacity var(--dur-2) linear; }
}

.theme-card--choix { cursor: pointer; position: relative; }

/* Le bouton natif reste dans le formulaire mais sort du dessin. `opacity: 0`
   plutôt que `display: none` : un champ masqué par `display` cesse d'être
   focalisable au clavier, et la navigation au clavier est le seul moyen de
   parcourir vingt-quatre cartes sans souris. */
.theme-card--choix > input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}

.theme-card--choix:has(> input:checked) {
  box-shadow:
    inset 0 0 0 2.5px var(--accent),
    0 12px 28px -14px color-mix(in oklab, var(--accent) 80%, transparent),
    var(--shadow-2);
}

.theme-card--compact.theme-card--choix:has(> input:checked) { transform: translate3d(0, -3px, 0); }
.theme-card--compact.theme-card--choix:has(> input:checked) .theme-card__cover.cover { transform: scale(1.05); }

.theme-card--choix:has(> input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Les cartes de défi de la liste : profondeur au survol, comme les cartes
   d'amis et les tuiles de thème. Tout ce qui se clique dans cette section
   réagit maintenant de la même façon. */
.duel-card {
  transition: transform var(--dur-3) var(--ease-spring), box-shadow var(--dur-3) var(--ease);
}

@media (hover: hover) {
  .duel-card:hover {
    transform: translate3d(0, -3px, 0);
    box-shadow: inset 0 0 0 1px var(--hairline-strong), inset 0 1px 0 0 var(--bevel), var(--shadow-3, var(--shadow-2));
  }
}

@media (prefers-reduced-motion: reduce) {
  .duel-card, .duel-card:hover { transform: none; }
}

/* La coche se pose sur la couverture, en dehors du flux : l'ajouter dans la
   colonne décalerait toutes les cartes voisines d'une ligne au moment du
   clic, ce qui est exactement le mouvement qu'on ne veut pas voir. */
.theme-card--compact .theme-card__coche { inset-block-start: var(--s2); inset-inline-end: var(--s2); }

.theme-card__coche {
  position: absolute;
  inset-block-start: var(--s4);
  inset-inline-end: var(--s4);
  z-index: 1;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--on-accent, #fff);
  opacity: 0;
  transform: scale(.6);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}

.theme-card--choix:has(> input:checked) .theme-card__coche { opacity: 1; transform: scale(1); }

/* Ce qu'une carte de choix n'a pas à porter.

   La flèche « → » promet une destination : sur un bouton radio elle promet
   une navigation qui n'aura pas lieu. La durée d'étude estimée renseigne sur
   un parcours d'apprentissage, pas sur un défi de dix questions chronométrées
   à vingt secondes — elle répond à une question que personne ne pose ici.

   Les deux sont masqués plutôt que retirés du gabarit : la carte reste un
   seul fichier, et ce qui la distingue tient dans une règle qu'on peut lire
   d'un bloc. */
/* La vignette de thème répond au même pas que la pastille d'ami : 190 ms.
   Elle héritait des 320 de `.plate`, qui sont ceux d'un survol de carte dans
   la bibliothèque — on y effleure, ici on choisit. Deux gestes voisins qui
   ne répondaient pas à la même vitesse dans le même formulaire. */
.theme-card--choix {
  transition:
    box-shadow var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease-spring),
    background-color var(--dur-2) var(--ease);
}

.theme-card--choix .theme-card__go { display: none; }
.theme-card--choix .theme-card__meta > *:nth-child(2) { display: none; }
.theme-card--choix .theme-card__meta > *:nth-child(2)::before { content: none; }

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

/* « Général » ouvre la marche, mais à la taille des autres.

   Il occupait toute la largeur pour se distinguer. Sur un écran large, cela
   donnait une carte de mille trois cents pixels — une bannière, pas une
   carte — et cela contredisait la règle même qu'on venait d'établir : les
   cartes du sélecteur sont celles de la bibliothèque, aux mêmes dimensions.

   Il se distingue autrement, et mieux : il est seul sur sa rangée, au-dessus
   des familles, et coché par défaut. Trois signaux qui ne demandent aucune
   exception de gabarit. */
.theme-pick__grille--vedette { justify-content: start; }

/* — « Général », le choix par défaut ————————————————————————————————
 *
 * Il ouvre le sélecteur et n'appartient à aucune famille : il pioche dans
 * toute la bibliothèque, ce qui en fait le thème qu'on prend quand on n'a pas
 * d'idée — le plus souvent, et surtout la première fois. Il se lisait pourtant
 * comme une vignette parmi vingt-quatre.
 *
 * Sa mise en avant tient en trois choses, et aucune n'ajoute d'élément : un
 * libellé de section qui le nomme, une arête ambre à la place du filet neutre,
 * et une lueur portée. Pas de badge collé sur la vignette — il couvrirait
 * l'illustration, qui est justement ce que la tuile existe pour montrer.
 *
 * L'arête est ambre en permanence, pas seulement au survol : c'est une
 * recommandation, pas un état. */
/* Sélecteur doublé — `.theme-pick__famille.theme-pick__famille--vedette` —
   et ce n'est pas un tic : `.theme-pick__famille` est déclaré deux fois dans
   cette feuille, dont une fois plus bas que ce bloc. À spécificité égale,
   c'est la dernière déclaration qui gagne : son `display: block` reprenait la
   main et renvoyait l'étincelle à la ligne, seule au-dessus du libellé. Un
   cran de spécificité règle la question sans avoir à déplacer des règles dont
   d'autres pages dépendent. */
.theme-pick__famille.theme-pick__famille--vedette {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  color: var(--accent-ink);
}

.theme-pick__famille--vedette .icon { color: var(--accent); }

.theme-card--vedette {
  box-shadow:
    inset 0 0 0 1px var(--accent-line),
    0 0 18px -8px color-mix(in oklab, var(--accent) 80%, transparent),
    var(--shadow-2);
}

/* Sélectionné, il prend l'ambre pleine comme les autres — la mise en avant
   ne doit pas concurrencer la sélection, qui est la seule information que
   l'on cherche une fois le choix fait. */
.theme-card--vedette:has(> input:checked) {
  box-shadow:
    inset 0 0 0 2px var(--accent),
    0 0 22px -8px color-mix(in oklab, var(--accent) 90%, transparent),
    var(--shadow-3, var(--shadow-2));
}

@media (hover: hover) {
  .theme-card--vedette:hover {
    box-shadow:
      inset 0 0 0 1px var(--accent),
      0 0 24px -8px color-mix(in oklab, var(--accent) 90%, transparent),
      var(--shadow-3, var(--shadow-2));
  }
}

.theme-pick__famille {
  display: block;
  margin-block: var(--s6) var(--s3);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* L'anneau du décompte : un filet qui se vide, posé sur le bord haut du
   panneau. Il n'anime que `transform`, donc rien ne se remet en page. */
.defi-recu__anneau {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  border-radius: var(--r-full);
  background: color-mix(in oklab, var(--ink) 10%, transparent);
  overflow: hidden;
}

.defi-recu__anneau > span {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent-deep), var(--accent-bright));
  transform: scaleX(var(--progress, 1));
  transform-origin: left center;
}

/* — La fenêtre d'un défi reçu ————————————————————————————————————
   Elle s'ouvre sans prévenir, au milieu d'autre chose, et se referme en
   soixante secondes. Elle doit se lire d'un coup d'œil : un titre court,
   une ligne de détail, un décompte, deux boutons. Tout y est un cran plus
   petit qu'ailleurs — ce n'est pas une page, c'est une interruption. */

/* La respiration de la fenêtre.
 *
 * Les six blocs se suivaient à quatre pixels d'intervalle : le nom touchait
 * « vous défie sur », qui touchait la carte du thème, qui touchait le
 * décompte. Tout était lisible et rien n'était séparé — on lisait un
 * paragraphe là où il y a une personne, une phrase et un thème.
 *
 * L'écart de base passe à huit pixels, et les trois articulations qui
 * comptent reçoivent le leur : autour du portrait, sous la phrase de
 * liaison, et avant les boutons. La fenêtre grandit d'une trentaine de
 * pixels — elle reste très en dessous de la moitié d'un écran d'iPhone SE. */
.defi-recu__panneau {
  /* `display: grid`, et c'est là que tout se jouait.
   *
   * Le bloc portait `gap` et `align-items` depuis toujours, sur un élément
   * resté en `display: block` : les deux déclarations n'ont jamais rien
   * fait. L'espacement venait donc uniquement des marges des enfants, dont
   * la plupart n'en ont pas, et le centrage du seul `text-align`. D'où six
   * blocs collés les uns aux autres qu'aucun réglage de `gap` ne pouvait
   * desserrer — on réglait une valeur que personne ne lisait. */
  display: grid;
  justify-items: center;
  /* Le repère de l'anneau du décompte, qui se pose sur le bord haut en
     `position: absolute`. Il tombait juste par accident — sans ancêtre
     positionné déclaré, il se serait calé sur le voile plein écran au
     premier changement de mise en page. */
  position: relative;
  /* La lumière du contour. Une invitation surgit au-dessus de tout le reste
     et doit se détacher du voile quel que soit ce qu'il y a derrière : un
     filet net, un biseau haut, et l'ombre longue de la feuille en dessous.
     Aucun flou supplémentaire, le voile en porte déjà un. */
  box-shadow:
    inset 0 0 0 1px var(--hairline-strong),
    inset 0 1px 0 0 var(--bevel),
    var(--shadow-4);
  max-width: 19rem;
  padding: var(--s5) var(--s4) var(--s4);
  gap: var(--s2);
  text-align: center;
}

/* Le visage occupe la place d'honneur : c'est ce qu'on reconnaît avant de
   lire quoi que ce soit. L'anneau ambre le rattache au défi plutôt qu'au
   carnet d'adresses. */
.defi-recu__portrait {
  display: grid;
  place-items: center;
  margin-block: var(--s3) var(--s2);
}

.defi-recu__visage {
  box-shadow: 0 0 0 3px var(--surface), 0 0 0 5px var(--accent-line), var(--shadow-2);
}

.defi-recu__kind {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-size: var(--text-2xs);
  font-weight: 650;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--accent-ink);
}

/* Le nom, à sa taille de nom.
   Il était écrit en `display-4` avec « vous défie » collé derrière : deux
   corps de texte dans un même bloc, un titre de deux lignes sur les noms un
   peu longs, et l'impression d'un gros titre là où il n'y a qu'une personne
   à reconnaître. Le nom se lit maintenant comme un nom, et la phrase qui le
   relie au thème passe en dessous, en petit — c'est un liant, pas un titre. */
.defi-recu__titre {
  max-width: 100%;
  font-size: var(--text-lg);
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: var(--track-normal);
}

.defi-recu__liant {
  font-size: var(--text-xs);
  line-height: 1.3;
  color: var(--ink-3);
  /* La phrase appartient au nom qu'elle suit, pas au thème qu'elle annonce.
     À égale distance des deux — douze pixels de chaque côté — elle flottait
     entre les blocs sans se rattacher à aucun. Elle se colle au nom et laisse
     une vraie respiration avant la carte : c'est là que le regard doit
     changer d'objet. */
  margin-block: calc(var(--s1) * -1) var(--s3);
}

/* Le thème, en carte compacte : la même vignette que la bibliothèque, le
   titre, et le format sur une ligne. Une pastille avec un glyphe disait le
   nom du thème sans le montrer — or c'est le dessin qu'on reconnaît. */
.defi-recu__carte {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  min-width: 0;
  padding: var(--s2) var(--s3) var(--s2) var(--s2);
  border-radius: var(--r-md);
  background: var(--surface-2);
  /* Un objet posé sur le panneau, pas une zone teintée dedans.
     Le filet seul faisait une découpe ; la lumière rasante en haut et
     l'ombre courte en bas lui donnent une épaisseur, et c'est ce qui la fait
     lire comme la carte du thème plutôt que comme un encadré. */
  box-shadow:
    inset 0 0 0 1px var(--hairline),
    inset 0 1px 0 0 var(--bevel),
    0 1px 3px -2px rgba(0, 0, 0, .28);
  text-align: start;
}

.defi-recu__illu { display: block; flex: none; }

.defi-recu__vignette {
  width: 3rem;
  height: 3rem;
  border-radius: var(--r-sm);
  overflow: hidden;
}

/* Huit pixels entre le titre du thème et sa ligne de format, pas quatre.
   Collées, les deux lignes se lisaient comme un seul paragraphe et le format
   semblait faire partie du nom du thème. */
.defi-recu__carte-corps {
  display: grid;
  gap: var(--s2);
  min-width: 0;
}

.defi-recu__carte-titre {
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink);
}

/* Le format est une seconde information, pas la suite du titre.
   Huit pixels de vide les séparaient déjà ; un filet court au-dessus dit que
   l'on change de registre, du nom du thème à ses règles. */
.defi-recu__carte-detail {
  padding-block-start: var(--s1);
  border-block-start: 1px solid var(--hairline);
  font-size: var(--text-2xs);
  color: var(--ink-3);
  line-height: 1.3;
}

.defi-recu__compte {
  display: inline-flex;
  align-items: baseline;
  gap: 2px;
  margin-block-start: var(--s2);
  padding: 0.125rem var(--s3);
  border-radius: var(--r-full);
  background: var(--accent-wash);
  color: var(--accent-ink);
  font-size: var(--text-2xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.defi-recu__compte .numeral { font-size: var(--text-sm); line-height: 1; }

/* Accepter d'abord, pleine largeur : c'est ce pour quoi la fenêtre s'ouvre.
   Refuser suit, en retrait — la sortie reste évidente sans être proposée.
   Les deux étaient côte à côte, ce qui donnait au refus la moitié de la
   surface d'un geste qu'on ne fait presque jamais. */
.defi-recu__actions {
  display: grid;
  gap: var(--s2);
  width: 100%;
  margin-block-start: var(--s4);
}

.defi-recu__actions .btn { width: 100%; min-block-size: 2.75rem; }

/* La demande est partie, la scène de duel arrive : la fenêtre s'estompe
   sans bouger et n'accepte plus rien. Pas de fermeture, pas de retour à la
   page du dessous — la page suivante prend sa place directement. */
.defi-recu.is-busy .sheet__panel { opacity: .72; transition: opacity var(--dur-2) var(--ease); }
.defi-recu.is-busy { pointer-events: none; }

/* ==========================================================================
   Densité mobile
   --------------------------------------------------------------------------
   Sur téléphone, aucune de ces grilles n'a deux colonnes : à 375 pixels, une
   fois la gouttière de la coquille retirée, il reste vingt et un rem, et le
   plus étroit de nos `--min` en demande onze. Tout est donc empilé, et la
   seule dimension qui compte est la hauteur : ce qu'on gagne ici, on le gagne
   en défilement.

   Le réglage ne touche donc ni les colonnes ni la structure — les deux sont
   déjà justes — mais les trois choses qui font la hauteur d'une carte :
   le rembourrage, l'interligne des titres et la taille des vignettes. Une
   carte de thème passe ainsi d'environ 420 à 330 pixels sans qu'aucun de ses
   quatre renseignements ne disparaisse.

   Ce qui n'est délibérément pas réduit :

   — les surfaces tactiles. Les 44 pixels du bloc « Surfaces tactiles » sont
     un minimum mesuré, pas une marge de confort ; les rogner rendrait les
     cartes plus courtes et les boutons moins sûrs, ce qui est le mauvais
     échange. Les zones invisibles en `::after` continuent de déborder d'une
     carte plus dense, et c'est sans conséquence : elles ne se voient pas.

   — les tailles de texte de contenu. Seuls les titres d'affichage
     descendent d'un cran ; le corps et les libellés restent lisibles à bout
     de bras.
   ========================================================================== */

@media (max-width: 47.99rem) {
  /* Le sélecteur suit la même règle que la bibliothèque, et pour la même
     raison : `auto-fill` retombait à une colonne dès 320 pixels, où deux
     pistes de 10,5 rem plus la gouttière dépassent la place disponible de
     soixante pixels. Deux colonnes explicites tiennent partout, parce
     qu'elles se partagent ce qu'il y a au lieu d'exiger un minimum. */
  /* Trois tuiles par rangée, et non deux.
   *
   * Trois colonnes tenaient tant que la vignette était carrée : le titre
   * disposait alors des 136 pixels de haut de la tuile. Depuis qu'elle suit
   * le cadrage 3/2 de la bibliothèque — c'est ce qui a supprimé le zoom —
   * la tuile fait 72 pixels, et à 107 de large un nom comme « Œuvres et
   * auteurs » passe sur trois lignes et recouvre l'illustration entière.
   * On choisissait dans une liste de titres blancs.
   *
   * Deux colonnes rendent 163 pixels de large et 109 de haut : le nom tient
   * sur une ligne dans la plupart des cas, et il reste les deux tiers de la
   * tuile pour le dessin, qui est la seule chose qu'on lui demande de
   * montrer. */
  .theme-pick__grille {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s2);
  }

  /* Sur téléphone, la rangée de « Général » n'a qu'une carte : sans cette
     règle, elle s'étirerait sur les deux colonnes et retrouverait la
     dimension particulière qu'on vient de lui retirer. */
  .theme-pick__grille--vedette > * { grid-column: span 1; }

  /* — Cartes de thème : deux par rangée ————————————————————————————
     Une colonne de cartes pleine largeur donnait un thème et demi par écran.
     Sur une bibliothèque de vingt-trois thèmes, explorer revenait à faire
     défiler neuf mille pixels, et l'on renonçait avant d'avoir vu l'offre.

     Deux colonnes fixes, et non un `auto-fill` : à 390 pixels il ne reste que
     344 pixels utiles, soit 168 par carte une fois la gouttière retirée.
     Aucun `minmax` raisonnable ne tombe juste sur cette valeur — on l'écrit
     donc, plutôt que de la faire deviner à un algorithme qui n'a pas la même
     idée que nous de ce qui est lisible.

     Ce que la carte perd à 168 pixels, et pourquoi elle peut le perdre :

       le sous-titre — deux lignes de prose en corps 14 sous un titre déjà
         sur deux lignes, c'est un paragraphe dans une vignette ; il reste sur
         la fiche du thème, à un doigt de là ;

       la durée et les pastilles de niveau — trois informations séparées par
         des points médians ne tiennent pas sur 168 pixels sans se couper, et
         un texte coupé ne renseigne pas, il inquiète. Le nombre de
         connaissances reste : c'est celui qu'on compare d'une carte à l'autre ;

       le libellé de l'étape — « Nouveau pour vous » devenait « Nouveau pou… ».
         L'anneau de maîtrise dit déjà où l'on en est, et son absence dit le
         reste.

     Ce que la carte garde : la couverture, la catégorie, le titre, le compte,
     l'anneau. Cinq choses, c'est ce qu'on lit d'un coup d'œil en parcourant.

     L'accueil porte le même attribut sur ses deux grilles de thèmes. Elles
     s'empilaient sur une seule colonne : quatre cartes pleine largeur par
     section, deux sections, trois mille pixels — la moitié de la page
     d'accueil pour deux catalogues que la bibliothèque présente déjà mieux.
     Les mêmes cartes, à la même taille, aux deux endroits. */
  [data-library-grid] {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s3);
  }

  .theme-card { padding: var(--s2); }

  /* La rangée horizontale de « Continuer » garde sa forme : elle n'est pas
     dans la grille et n'a jamais eu de problème de largeur. */
  .theme-card--row .cover { aspect-ratio: 1; }
  .theme-card__body { padding: var(--s3) var(--s2) var(--s2); gap: var(--s1); }
  .theme-card__foot { padding-block-start: var(--s2); gap: var(--s2); }

  /* — Boutique ——————————————————————————————————————————————————————
     Trente-sept articles en une colonne faisaient dix mille cinq cents
     pixels de défilement : la boutique la plus longue page du produit, pour
     un catalogue qu'on vient parcourir du regard.

     `--min: 11,5 rem` ne tient pas deux fois dans les trois cent cinquante
     pixels utiles d'un téléphone — il en faudrait trois cent quatre-vingts.
     Deux colonnes explicites se partagent ce qu'il y a au lieu d'exiger un
     minimum, exactement comme la bibliothèque et le sélecteur de défi. */
  .shop-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s2);
  }

  .shop-item { padding: var(--s3); gap: var(--s1); }
  .shop-item__name { font-size: var(--text-sm); line-height: 1.25; }

  /* La description passe à deux lignes au lieu de disparaître : c'est elle
     qui dit ce que fait un joker, et un joker qu'on n'a pas compris ne
     s'achète pas. */
  .shop-item__desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: var(--text-xs);
  }

  /* — Statistiques ————————————————————————————————————————————————
     La valeur reste la plus grosse chose de la carte : c'est elle qu'on
     vient lire. Seul le blanc autour d'elle se resserre. */
  .stat { padding: var(--s3) var(--s4); }

  /* — Succès ——————————————————————————————————————————————————————— */
  .badge-tile { padding: var(--s3); gap: var(--s2); }

  /* — Défis ———————————————————————————————————————————————————————— */
  .duel-card { gap: var(--s3); }

  /* — Les quatre façons de travailler un thème ————————————————————— */
  .mode { padding: var(--s4); gap: var(--s2); }
  .mode__label { font-size: var(--text-lg); }
  .mode__mark { width: 2.25rem; height: 2.25rem; }
}

/* Les très petits écrans — moins de 22,5 rem, soit un iPhone SE en portrait —
   perdent un cran de plus. Au-delà, c'est le texte qui commencerait à souffrir,
   et un écran dense qu'on ne lit pas n'a rien gagné. */
@media (max-width: 22.5rem) {
  .theme-card__title { font-size: var(--text-sm); }
  .stat { padding: var(--s2) var(--s3); }
}

/* — Ce qu'un défi a rapporté ————————————————————————————————————————
   Une ligne, deux montants, au centre. Elle se lit d'un coup d'œil et ne
   cherche pas à concurrencer le score juste au-dessus : c'est lui la nouvelle,
   le gain n'en est que la conséquence. */

/* Aucun retard : le bloc arrive avec le panneau.
   Il entrait 420 ms après le verdict, et les boutons 760 ms après lui. Sur
   un écran qu'on ouvre pour savoir ce qu'on a gagné et décider de la suite,
   trois quarts de seconde à regarder un panneau incomplet se lisent comme un
   chargement — et l'on tend la main vers un bouton qui n'est pas encore là. */
/* Le butin d'un défi : deux colonnes de largeur égale, comme au bilan.
 *
 * C'était une ligne de texte centrée entre deux filets. Les filets
 * disparaissent avec elle : ils séparaient une mention discrète de ce qui
 * l'entourait, alors que deux tuiles se séparent toutes seules. Un trait de
 * moins sur un écran qui en comptait quatre.
 *
 * L'entrée est déclarée avec le reste de la cascade, plus haut. Elle était
 * réécrite ici en `fade 200ms` sans retard — et comme cette règle est plus
 * bas dans la feuille, c'est elle qui gagnait : le bloc des gains arrivait
 * donc en premier, avant le verdict qu'il commente. */
.duel-gains,
.duel-fin__gains {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--s3);
  width: 100%;
  max-width: 26rem;
  margin-inline: auto;
  padding-block: 0;
  border-block: 0;
}

/* Une seule tuile ne s'étale pas sur toute la largeur : elle serait un
   bandeau, pas une récompense. */
.duel-gains:has(.gain-tuile:only-child),
.duel-fin__gains:has(.gain-tuile:only-child) { max-width: 14rem; }

/* Le sans-faute reprend sa ligne sous les deux colonnes. */
.duel-gains:has(.duel-gains__parfait),
.duel-fin__gains:has(.duel-gains__parfait) {
  grid-auto-flow: row;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* — Les deux tuiles de gain ————————————————————————————————————————
 *
 * Le même objet que `.outro__loot-tile` du bilan de séance, et il n'y a pas
 * de raison qu'il y en ait deux : c'est la même monnaie, gagnée par le même
 * effort, annoncée au même moment du parcours. Les classes sont distinctes
 * parce que les deux écrans vivent dans des feuilles différentes — mais le
 * dessin, lui, ne diverge pas.
 *
 * Un mot sur la taille du nombre. Il est en `clamp` et non en valeur fixe :
 * ces tuiles apparaissent dans un panneau de scène large de 46 rem comme dans
 * une carte de résultat de 44, sur un téléphone comme sur un écran de bureau.
 * Un nombre figé à deux rem est énorme dans l'un et timide dans l'autre. */
.gain-tuile {
  display: grid;
  justify-items: center;
  gap: var(--s1);
  padding: clamp(0.625rem, 0.3rem + 1.4vw, 1rem) var(--s4);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline), inset 0 1px 0 0 var(--bevel);
}

.gain-tuile__mark { color: var(--ink-3); }

.gain-tuile__valeur {
  display: block;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: clamp(1.375rem, 1rem + 1.5vw, 1.875rem);
  font-weight: 600;
  line-height: 1;
}

.gain-tuile__label {
  font-size: var(--text-2xs);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  text-align: center;
}

/* La tuile d'Éclats porte l'ambre, comme au bilan : c'est là que la monnaie
   atterrit, et il ne doit y avoir aucun doute sur l'endroit à regarder. */
.gain-tuile--eclat {
  background: var(--accent-wash);
  box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 1px 0 0 var(--bevel);
}

.gain-tuile--eclat .gain-tuile__mark { color: var(--accent); }
.gain-tuile--eclat .gain-tuile__valeur { color: var(--accent-ink); }

/* Le sans-faute : une puce ambre sur sa propre ligne, sous les deux montants
   qu'elle explique. `flex-basis: 100%` la fait passer à la ligne dans un
   conteneur qui enveloppe — c'est une légende, pas un troisième gain, et la
   mettre à côté des deux autres l'aurait fait compter pour un. */
.duel-gains__parfait {
  /* `grid-column`, et non `flex-basis` : le conteneur des gains est passé de
     la rangée flex à la grille à deux colonnes, et une base de flex n'y veut
     plus rien dire. La ligne se serrait donc dans une seule colonne et
     passait à la ligne sous une des deux tuiles au lieu de les légender
     toutes les deux. */
  grid-column: 1 / -1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  margin-block-start: var(--s1);
  color: var(--accent-ink);
  font-size: var(--text-xs);
  font-weight: 600;
}

.duel-gains__parfait .icon { color: var(--accent); flex: none; }

.duel-gains__item {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--text-sm);
  color: var(--ink-2);
}

.duel-gains__item .numeral {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  /* `--ink-1` n'existe pas : les encres sont `--ink`, `--ink-2`, `--ink-3`,
     `--ink-4`. `color` étant héritée, la déclaration invalide faisait
     simplement hériter du parent — mesuré `rgb(106,100,89)`, c'est-à-dire
     `--ink-2`, la couleur du libellé lui-même. Le montant du gain rendait
     donc exactement comme le mot qu'il qualifie.

     `--ink` est ce que le rôle demande : c'est le chiffre qu'on vient
     gagner, il doit ressortir de son unité. */
  color: var(--ink);
  line-height: 1;
}

/* ==========================================================================
   La scène de duel
   --------------------------------------------------------------------------
   Deux joueurs en vis-à-vis, un signe entre eux, une ligne d'information
   dessous. Le dessin emprunte tout au reste d'Omnia — mêmes plaques, mêmes
   rayons, mêmes filets — parce qu'un duel n'est pas un autre produit : c'est
   le même, dans un moment particulier.

   Ce qui distingue la scène du reste, c'est uniquement ce qui bouge : le
   score qui pulse quand il monte, le contour qui s'allume sur celui dont
   c'est le tour. Rien d'autre n'a le droit d'attirer l'œil, parce que tout le
   reste est fixe pendant deux minutes.
   ========================================================================== */

.duel-hud {
  position: relative;
  display: grid;
  gap: var(--s2);
  /* La taille du portrait, déclarée une seule fois pour les deux camps.
   *
   * Elle vivait sur `.duelliste__avatar`, donc sur l'image elle-même : la
   * boîte qui la contient prenait alors la taille de son contenu, et tout ce
   * qui s'ajoutait à ce contenu — un cadre acheté, un anneau de tour — la
   * faisait grandir. Deux joueurs n'ayant pas les mêmes pièces n'avaient donc
   * pas le même portrait, et le disque changeait de diamètre à chaque
   * changement de tour.
   *
   * Portée par le bandeau, la mesure devient une donnée de la scène : la
   * boîte la réserve, l'image la remplit, et plus rien de ce qu'on y pose ne
   * peut la modifier. Le grand écran ne change que ce nombre. */
  --portrait: clamp(1.875rem, 1.55rem + 1.6vw, 2.125rem);
  /* La même largeur que la carte de question, qui est juste en dessous.
     Sans elle, le bandeau se rétrécit à son contenu — la scène est centrée —
     et l'on obtient sur grand écran une bannière de quatre cents pixels
     au-dessus de propositions qui en font sept cents. Deux blocs empilés qui
     ne s'alignent pas se lisent comme deux morceaux d'écrans différents. */
  width: 100%;
  max-width: 46rem;
  margin-inline: auto;
  padding: var(--s3);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--hairline), inset 0 1px 0 0 var(--bevel), var(--shadow-1);
  margin-block-end: var(--s3);
  /* Pas de `overflow: hidden`, et c'est un choix qui se paie ailleurs.
   *
   * Il n'était là que pour une chose : empêcher le filet ambre du haut — un
   * bandeau de trois pixels sur toute la largeur — de dépasser des coins
   * arrondis. Mais il rognait au passage tout ce qui sort d'un portrait, et
   * le halo du tour sort d'un portrait : à douze pixels de marge intérieure,
   * l'anneau tenait et sa lueur était coupée net contre le bord de la carte.
   * On voyait un demi-halo du côté extérieur de chaque camp.
   *
   * Le filet, lui, se retient tout seul : il suffit de le rentrer de la
   * valeur du rayon. Il s'éteint en dégradé à ses deux extrémités, donc le
   * raccourcir ne se voit pas — alors que rogner un halo, si. */
  animation: scene-pose var(--dur-2) var(--ease) both;
}

/* L'entrée ne descend plus de dix pixels : le bandeau est en haut de la page,
   il pousse la question sous lui en s'installant, et l'œil part chercher la
   question au moment précis où elle bouge. Une simple apparition. */
@keyframes scene-pose {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Une lueur ambre en haut du bandeau quand c'est à nous : le signal le plus
   large possible sans occuper un pixel de mise en page. Elle est portée par
   le bandeau et pilotée par la pastille du pied, donc elle change au même
   instant que le mot qui l'annonce. */
.duel-hud::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  /* Rentré de la valeur du rayon : c'est ce qui le retient dans les coins
     arrondis sans avoir à couper la carte entière. Voir `.duel-hud`. */
  inset-inline: var(--r-lg);
  height: 3px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease);
  pointer-events: none;
}

.duel-hud:has(.duel-hud__tour.is-mine)::before { opacity: 1; }

/* Trois colonnes : un camp, l'écart, l'autre camp.
   `1fr auto 1fr` et non `auto` partout — les deux camps doivent faire
   exactement la même largeur, sinon le nom le plus long décale le tiret et
   la symétrie, qui est tout le propos d'un vis-à-vis, se perd. */
.duel-hud__duo {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  /* Serré au centre : chaque pixel pris ici est un pixel rendu au nom, et
     c'est le nom qui manque de place sur un écran de 375. */
  gap: var(--s1);
  /* La rangée garde sa hauteur quoi qu'il arrive : score à un ou deux
     chiffres, nom présent ou tronqué, jauge pleine ou vide. C'est elle qui
     porte la stabilité du bandeau — tout le reste est posé dessus. */
  min-block-size: 2.75rem;
}

/* Le tiret qui sépare les deux scores. Discret : ce sont les nombres qu'on
   lit, il ne fait que dire qu'ils vont ensemble. */
.duel-hud__ecart {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: 1;
  color: var(--ink-4);
}

/* — Un camp ————————————————————————————————————————————————————— */

.duelliste {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}

/* L'adversaire est le même bloc, retourné : son visage part vers l'extérieur
   et son score vient contre le centre. C'est ce qui met les deux nombres
   côte à côte, à un tiret l'un de l'autre. */
.duelliste.is-rival {
  flex-direction: row-reverse;
  text-align: end;
}

/* La jauge adverse se remplit vers le centre, comme son score y est ramené.
   Deux jauges qui partent toutes les deux de la gauche ne se comparent pas :
   on lit deux barres parallèles. Deux jauges qui se font face montrent
   l'écart entre elles sans qu'on ait à compter. */
.duelliste.is-rival .duelliste__jalons { direction: rtl; }

/* Le portrait porte l'anneau du tour. Un calque à part plutôt qu'une ombre
   sur l'avatar : il peut alors s'épaissir et respirer sans déplacer quoi que
   ce soit autour, ce qui compte quand il s'allume et s'éteint à chaque tour. */
.duelliste__portrait {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  /* La boîte est réservée, elle ne se déduit plus de ce qu'elle contient.
     C'est ce qui garantit que les deux camps occupent exactement la même
     surface, quels que soient les cosmétiques portés de part et d'autre. */
  inline-size: var(--portrait);
  block-size: var(--portrait);
}

/* Le visage identifie, il ne domine pas.
 *
 * À 32 pixels il faisait 45 % de plus que le score, qui en mesurait 22 : le
 * plus gros objet du bandeau était celui qui ne change jamais de la partie.
 * On regarde un bandeau de duel pour deux choses, l'écart et le tour ; le
 * portrait sert à savoir contre qui, et une fois suffit.
 *
 * Il reste plus petit que le score, mais plus si petit qu'avant : à
 * vingt-six pixels fixes, le bandeau du téléphone était une miniature de
 * celui du grand écran — mesuré, 80 pixels de haut contre 110. Le `clamp`
 * lui rend une taille tactile sans jamais dépasser celle du bureau. */
.duelliste__avatar {
  /* `width`/`height`, et non `--size` : le helper `avatar()` écrit
     `style="--size:…"` en ligne, qu'aucune feuille ne peut reprendre. On
     surcharge donc la propriété d'arrivée ; `.avatar__art` étant en 100 %,
     le dessin suit. */
  width: var(--portrait);
  height: var(--portrait);
}

/* — Les cadres achetés, dans le bandeau seulement ————————————————
 *
 * Ils restent : c'est le seul écran où l'adversaire voit ce qu'on a gagné, et
 * les retirer reviendrait à confisquer la récompense au moment précis où elle
 * sert. Mais trois d'entre eux — l'aura, la couronne, la lueur — portent une
 * ombre *sortante* : un anneau de trois pixels, un halo de dix-huit. Sur un
 * profil, cela n'a aucune conséquence ; ici, mis côte à côte avec un joueur
 * qui n'a rien acheté, cela donnait deux disques de diamètres différents à
 * l'endroit exact où l'on compare deux personnes.
 *
 * On garde donc le décor et l'on supprime ce qui déborde : chaque cadre est
 * ramené à l'intérieur du disque, la teinte et le dessin sont intacts, et les
 * deux portraits mesurent la même chose au pixel près. Le halo du tour est
 * alors le seul objet du bandeau qui ait le droit de sortir de son cercle —
 * ce qui est précisément ce qui le rend lisible. */
.duel-hud .avatar--aura {
  box-shadow: inset 0 0 0 1.5px var(--accent-line), inset 0 0 0 3px var(--accent-wash);
}

.duel-hud .avatar--couronne {
  box-shadow: inset 0 0 0 1.5px var(--accent-line);
}

.duel-hud .avatar--couronne::after { inset: 0; }

.duel-hud .avatar--lueur::after {
  box-shadow: inset 0 0 0 1px var(--accent-line);
}

/* Le nom et la jauge, empilés dans la largeur qui reste. `min-width: 0` sur
   un enfant de flex n'est pas une précaution : sans lui, un pseudonyme long
   pousse le score hors du bandeau au lieu de se laisser tronquer. */
.duelliste__ident {
  display: grid;
  /* Le nom et sa jauge forment une seule information. Voir `.duelliste__nom`. */
  gap: 0.2rem;
  flex: 1 1 auto;
  min-width: 0;
  /* Le bloc s'aligne sur l'axe du portrait et du score, pas sur le sien.
     Il était centré dans une rangée de quarante-quatre pixels alors qu'il
     n'en occupe que vingt-cinq : le nom flottait deux pixels au-dessus de la
     ligne optique du chiffre, ce qui se voit d'autant plus que les deux se
     lisent d'un seul regard. */
  align-content: center;
}

/* L'anneau du tour, et la règle qui a changé : il ne dessine plus de cercle
   plus grand que l'avatar.
 *
 * Il était posé à moins cinq pixels, avec un trait de deux, une bande
 * d'accent à cinq et une lueur à vingt-deux ; son second calque, à moins
 * trois de plus, grandissait encore de six pour cent en respirant. La boîte
 * ne bougeait pas — c'était vérifié, trente-trois pixels des deux côtés — mais
 * ce que l'œil compare n'est pas la boîte : c'est le disque. Le joueur en
 * main portait un disque d'une cinquantaine de pixels contre trente-trois à
 * l'autre. Mesuré identique, vu plus gros, et c'est la vue qui a raison.
 *
 * Le contour revient donc *sur* le bord de l'avatar, à l'intérieur : le
 * cercle éclairé a exactement le diamètre du cercle voisin. Il ne reste, à
 * l'extérieur, qu'une lueur — c'est-à-dire une lumière sans bord. Une lueur
 * ne se mesure pas : elle n'a pas de contour à comparer, donc elle ne peut
 * pas faire paraître l'objet plus grand. C'est toute la différence entre
 * éclairer une chose et l'agrandir.
 *
 * Il reste absolu dans une boîte de taille fixe : rien ne bouge autour, ni le
 * nom, ni la jauge, ni la hauteur du bandeau. */
.duelliste__anneau {
  position: absolute;
  inset: 0;
  border-radius: var(--r-full);
  box-shadow: none;
  opacity: 0;
  transition:
    opacity var(--dur-2) var(--ease),
    box-shadow var(--dur-2) var(--ease);
  pointer-events: none;
}

/* Le souffle : un second cercle, plus large, qui s'ouvre et s'éteint.
 *
 * Une remarque honnête sur ce mouvement. Il avait été retiré, et pour une
 * bonne raison — une animation permanente n'informe plus après son premier
 * cycle, et celle-ci vit à côté de la question qu'on est en train de lire.
 * Elle revient parce qu'elle est demandée, et elle revient tenue : deux
 * secondes six par cycle, une opacité qui plafonne à 0,55, un cercle d'un
 * seul pixel. Assez pour qu'on sache d'un coup d'œil de qui c'est le tour,
 * pas assez pour qu'on la regarde.
 *
 * `opacity` et `scale` seulement : le compositeur s'en charge sans jamais
 * rappeler le peintre, et l'anneau ne redessine pas l'avatar sous lui. */
/* Le souffle : la lueur seule, qui va et vient. Aucune échelle, aucun
   déplacement — seule l'opacité varie, et l'opacité n'a pas de dimension.
   C'est ce qui permet à ce mouvement d'exister sans jamais rien agrandir. */
.duelliste__anneau::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 16px 2px color-mix(in oklab, var(--accent) 55%, transparent);
  opacity: 0;
  pointer-events: none;
}

.duelliste.is-turn .duelliste__anneau::after {
  animation: duel-tour-souffle 2.6s var(--ease-inout) infinite;
}

/* Le nom est qualifié : `duel-souffle` existe déjà, plus bas, pour la salle
   d'attente. Deux images-clés homonymes ne cohabitent pas — la dernière
   déclarée gagne partout —, et celle de l'attente aurait donné à l'anneau du
   tour une amplitude de scène d'attente. */
@keyframes duel-tour-souffle {
  0%, 100% { opacity: .18; }
  50%      { opacity: .85; }
}

/* C'est à ce joueur de jouer : l'anneau s'allume. Il s'allume, et c'est
   tout.
 *
 * Il respirait aussi, en boucle, deux secondes et demie par cycle, du début
 * à la fin de la partie. Trois autres choses disaient déjà la même chose au
 * même instant — le filet ambre en haut du bandeau, la pastille du pied
 * passée en ambre plein, le visage adverse estompé. Une quatrième, la seule
 * qui bouge, et qui bouge sans arrêt, à côté de la question qu'on est en
 * train de lire. Une animation permanente n'informe plus après le premier
 * cycle : elle ne fait qu'occuper une couche de composition et le coin de
 * l'œil. */
/* Deux couches, et pas une de plus.
 *
 *   — le contour, `inset` : peint *sur* le bord de l'avatar, donc le cercle
 *     allumé a très exactement le diamètre du cercle voisin ;
 *   — la lueur, sortante mais floue : quatorze pixels de diffusion sans
 *     trait, la seule chose autorisée à dépasser parce qu'elle n'a pas de
 *     bord à comparer.
 *
 * Un liseré clair les séparait, pour donner au contour son épaisseur de
 * matière. Il faisait exactement le défaut inverse de celui qu'on corrigeait :
 * trois pixels mangés sur le pourtour, et le visage du joueur en main
 * paraissait *plus petit* que celui de l'autre. Deux pixels d'ambre suffisent
 * — et ils ne peuvent, eux, tromper personne sur la taille. */
.duelliste.is-turn .duelliste__anneau {
  opacity: 1;
  box-shadow:
    inset 0 0 0 2px var(--accent),
    0 0 14px -2px color-mix(in oklab, var(--accent) 70%, transparent);
}

/* Le visage de celui qui ne joue pas ne s'efface plus.
 *
 * Il tombait à 68 % d'opacité, ce qui répondait à la question « qui joue »
 * une seconde fois — après l'anneau, après le filet ambre du bandeau, après
 * la pastille du pied. Et cela répondait mal : un visage estompé se lit comme
 * un joueur absent ou déconnecté, ce qu'il n'est pas. Il attend son tour, ce
 * qui est l'état normal d'un duel au tour par tour.
 *
 * Les deux portraits sont donc identiques, et un seul objet dit le tour :
 * l'anneau. C'est aussi ce qui garantit que rien ne distingue les deux camps
 * quand personne ne joue — pendant un chargement, entre deux blocs. */
.duelliste__avatar { transition: opacity var(--dur-2) var(--ease); }

/* Le nom du joueur.
 *
 * Il porte déjà `.truncate` dans le gabarit — `overflow: hidden`,
 * `text-overflow: ellipsis`, `white-space: nowrap`. C'est la bonne réponse à
 * un pseudonyme long : la colonne a une largeur imposée par la grille, et un
 * nom qui dépasse doit céder, pas repousser le score.
 *
 * Une remarque sur la « réduction progressive selon la longueur » : le CSS
 * dimensionne d'après le conteneur, jamais d'après le nombre de caractères.
 * Le `clamp` ci-dessous fait donc varier le corps avec la largeur de l'écran,
 * et c'est l'ellipse qui absorbe la longueur du texte. Mesurer la chaîne en
 * JavaScript pour rapetisser la police reviendrait à recalculer une taille
 * pendant la partie — exactement le genre de variation que la stabilité du
 * bandeau interdit.
 *
 * La hauteur, elle, est fixe : `min-block-size` en `em` réserve la ligne
 * quelle que soit la police finale, si bien que la jauge en dessous ne monte
 * ni ne descend. */
/* Douze pixels, c'était la taille d'une mention légale.
 *
 * Le nom de l'adversaire est l'une des trois choses que ce bandeau existe
 * pour dire — on affronte quelqu'un, pas un compteur — et il était composé
 * plus petit que la moindre étiquette du produit. À quatorze, il se lit du
 * coin de l'œil pendant qu'on répond, ce qui est exactement l'usage.
 *
 * La hauteur du bandeau ne bouge pas d'un pixel, et c'est vérifiable :
 * la rangée est tenue à 2,75 rem par `min-block-size`, et la colonne du nom
 * mesure le nom plus l'écart plus les jalons — dix-sept, quatre et quatre,
 * soit vingt-cinq pixels sur quarante-quatre. Il reste dix-neuf pixels de
 * marge ; on en prend quatre.
 *
 * L'écart avec les jalons se resserre en même temps. Le nom et sa jauge sont
 * une seule information — qui, et où il en est — et cinq pixels entre les
 * deux les faisaient lire comme deux lignes empilées. */
.duelliste__nom {
  max-width: 100%;
  font-size: clamp(0.8125rem, 0.72rem + 0.45vw, 0.875rem);
  font-weight: 600;
  line-height: 1.2;
  min-block-size: 1.2em;
  color: var(--ink-2);
  transition: color var(--dur-2) var(--ease);
}

.duelliste.is-turn .duelliste__nom { color: var(--ink); }

/* Le nom de l'adversaire, quand il ne tient pas.
 *
 * `.duelliste__nom` porte `.truncate` depuis toujours, et pourtant le nom
 * était coupé net : vu en partie à 320 pixels, « Testeu », le r avalé par le
 * disque du portrait. Rien n'annonçait qu'il manquait quelque chose — à une
 * lettre près, on lit un autre pseudonyme.
 *
 * La raison n'est pas dans la règle de troncature, elle est dans ce qu'elle
 * doit rogner. `pseudo()` enveloppe le nom dans `.pseudo`, qui est
 * `inline-flex` pour aligner le sceau acheté sur la ligne de base. Un
 * `inline-flex` est un inline *atomique* : le navigateur ne peut pas le
 * couper, donc `text-overflow` n'a aucune coupure à faire et se contente de
 * masquer ce qui dépasse. La troncature était là, elle n'avait pas prise.
 *
 * Rendu inline, le nom redevient du texte et l'ellipse opère. Le sceau perd
 * l'alignement de base du flex — c'est une icône de la hauteur d'une
 * capitale, posée après un nom : l'écart se refait à la marge, et son
 * alignement naturel d'inline suffit à cette taille. Ailleurs dans le
 * produit, sur un profil ou un classement, `.pseudo` garde son flex : c'est
 * ici, et seulement ici, qu'il doit tenir dans soixante pixels. */
.duelliste__nom .pseudo {
  display: inline;
  min-width: 0;
}

.duelliste__nom .pseudo__seal { margin-inline-start: 0.35em; }

/* Le score est le chiffre qu'on cherche : le plus gros du bandeau, en
   chiffres à chasse fixe pour qu'il ne décale pas le tiret en passant de
   9 à 10. */
.duelliste__score {
  flex: none;
  /* Deux chiffres de large dès le premier point, alors qu'un seul s'affiche.
     Sans cette réserve, le passage de 9 à 10 élargit le score, rétrécit la
     colonne du nom et pousse le tiret : trois choses bougent à l'instant
     précis où l'on regarde le score.

     1,2em et non 1,35 : en chiffres tabulaires, un caractère occupe très
     exactement 0,6em, donc deux en font 1,2. Le quart de cadratin
     supplémentaire ne réservait rien — il prenait dix pixels par camp sur la
     colonne du nom, qui à 320 pixels n'en avait plus que vingt-sept et
     coupait « Vous » en « Vou… ». */
  min-inline-size: 1.2em;
  text-align: center;
  font-family: var(--font-display);
  /* Le score domine, il ne se contente pas d'égaler.
     À vingt-six pixels il faisait exactement la taille du portrait : deux
     objets au même rang, dont un seul porte une information qui change. Sur
     grand écran le rapport est de 1,4 en faveur du score ; le téléphone
     l'obtient aussi, sans que le bandeau grandisse d'un pixel — le chiffre
     tient dans la hauteur du portrait, il ne la dépasse pas.

     Le plancher est passé de 1,875 à 2,25rem : c'est le chiffre qu'on vient
     lire, et à trente pixels sur un écran de 320 il était plus petit que le
     titre de la question qu'il surplombe. */
  font-size: clamp(2.25rem, 1.62rem + 2.5vw, 3rem);
  line-height: 1;
  /* La hauteur du chiffre ne dépend plus de la police retenue : elle est
     réservée. Sans cela, le passage d'un palier de `clamp` à l'autre au
     redimensionnement changeait la hauteur de toute la rangée. */
  min-block-size: 1em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  transition: color var(--dur-2) var(--ease);
}

/* Qui mène. C'est l'information que le bandeau existe pour donner et elle
   n'était nulle part : deux nombres de la même couleur demandent une
   soustraction. Un seul des deux est ambre, jamais les deux — à égalité,
   personne ne mène. La classe est posée par `duel.js`. */
.duelliste.is-lead .duelliste__score { color: var(--accent-ink); }

.duelliste__score .is-bump {
  animation: duel-bump var(--dur-5) var(--ease-spring);
  display: inline-block;
  color: var(--accent-ink);
}

@keyframes duel-bump {
  0%   { transform: scale(1) translateY(0); }
  35%  { transform: scale(1.34) translateY(-4px); }
  100% { transform: scale(1) translateY(0); }
}

/* — L'avancement, en segments ————————————————————————————————————
   Dix segments qui remplissent la largeur de la colonne. Ils font office de
   barre — on lit d'un coup la proportion — tout en gardant le découpage par
   question, qui est ce qui permet de voir où en est le bloc de deux. */
.duelliste__jalons {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 2px;
  width: 100%;
}

.duelliste__jalon {
  height: 4px;
  border-radius: var(--r-full);
  background: var(--surface-3, var(--surface-2));
  transform-origin: left center;
  /* Seuls `background-color` et `transform` sont animés.
     `box-shadow` a été retiré : vingt segments changeaient d'état ensemble à
     chaque réponse, et une ombre animée repeint son élément à chaque image.
     Vingt repaints simultanés au moment précis où la carte suivante entre en
     scène, c'est exactement le pic qu'on cherche à supprimer. La lueur est
     rendue par la couleur pleine, qui suffit à distinguer un segment acquis. */
  transition:
    background-color var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease-spring);
}

/* Répondu, sans qu'on sache comment. C'est le cas des défis ouverts avant que
   la trace n'existe : le jalon dit « fait », il ne prétend rien de plus. */
.duelliste__jalon.is-done {
  background: var(--accent);
  transform: scaleY(1.25);
}

/* — Juste et faux ————————————————————————————————————————————————
 *
 * Les deux teintes sont celles du produit — `--right` et `--wrong`, les mêmes
 * qui colorent une proposition corrigée dans un quiz. C'est le point : on
 * apprend la grammaire des couleurs une fois, et un segment vert dans une
 * jauge de duel doit vouloir dire exactement ce qu'il veut dire ailleurs.
 *
 * La lueur est rendue par un `box-shadow` **statique**. La remarque de la
 * règle du dessus reste vraie — une ombre *animée* repeint son élément à
 * chaque image, et vingt segments qui s'animent ensemble font un pic au pire
 * moment — mais une ombre posée une fois et jamais retouchée est peinte une
 * fois. Ce qui bouge, c'est l'éclosion du seul jalon qui vient d'arriver, et
 * elle ne touche qu'à `transform` et `opacity`.
 *
 * Quatre pixels de haut ne laissent pas la couleur porter seule : le segment
 * s'épaissit d'un quart, comme le faisait déjà l'ambre, et la lueur lui donne
 * le reste. */
.duelliste__jalon.is-right {
  background: var(--right);
  box-shadow: 0 0 6px -1px color-mix(in oklab, var(--right) 85%, transparent);
  transform: scaleY(1.5);
}

.duelliste__jalon.is-wrong {
  background: var(--wrong);
  box-shadow: 0 0 6px -1px color-mix(in oklab, var(--wrong) 85%, transparent);
  transform: scaleY(1.5);
}

/* L'éclosion d'un jalon qui vient de tomber : il arrive en s'écrasant sur sa
   ligne, une fois, et ne rejoue jamais. `transform-origin` est déjà au bord
   gauche du segment — hérité de la règle de base — ce qui le fait pousser
   depuis l'intérieur de la jauge plutôt que se gonfler sur place. */
.duelliste__jalon.is-neuf {
  animation: jalon-eclot 420ms var(--ease-spring) both;
}

/* La jauge adverse se remplit vers le centre — `direction: rtl` sur son
   conteneur. Son origine de transformation doit suivre : à gauche, chaque
   segment poussait vers l'extérieur du bandeau, c'est-à-dire à contresens du
   remplissage qu'il est censé prolonger. Deux jauges qui se font face doivent
   grandir l'une vers l'autre. */
.duelliste.is-rival .duelliste__jalon { transform-origin: right center; }

@keyframes jalon-eclot {
  0%   { transform: scaleY(1) scaleX(.4); opacity: .3; }
  55%  { transform: scaleY(2.1) scaleX(1); opacity: 1; }
  100% { transform: scaleY(1.5) scaleX(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .duelliste__jalon.is-neuf { animation: none; }
}

/* — Le pied : à qui le tour ——————————————————————————————————————— */

/* Le pied réserve sa hauteur, pleine ou vide.
 *
 * C'était le décalage le plus visible de tout l'écran, et le plus facile à
 * manquer parce qu'il ne dure qu'une image : le serveur envoie la pastille du
 * tour **vide** — c'est `duel.js` qui écrit « À vous de jouer » au premier
 * sondage. Entre les deux, la pastille mesurait 8 pixels de haut au lieu de
 * 25, et le bandeau entier passait de 80 à 117 pixels en poussant la question
 * vers le bas au moment précis où l'on commençait à la lire.
 *
 * Réserver la hauteur ici la rend indépendante du texte : vide, remplie,
 * « À vous de jouer » ou « Au tour de votre adversaire », le pied occupe la
 * même place. */
.duel-hud__pied {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-width: 0;
  min-block-size: 1.75rem;
}

.duel-hud__tour {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  /* La hauteur ne suit pas le texte : elle est posée. */
  min-block-size: 1.75rem;
  padding: 0.25rem var(--s3);
  border-radius: var(--r-full);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  font-size: clamp(0.75rem, 0.66rem + 0.42vw, 0.8125rem);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: background-color var(--dur-2) var(--ease), color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}

/* C'est à nous : la mention passe en ambre plein. C'est le seul élément du
   bandeau qui change de couleur de fond, et il ne le fait qu'à ce
   moment-là — d'où sa force. */
.duel-hud__tour.is-mine {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: inset 0 0 0 1px var(--accent-line), 0 2px 12px -4px var(--accent);
}

/* `--ink-3` et non `--ink-4` : à onze pixels, le quatrième niveau d'encre
   tombait à 3,2:1 sur le fond du bandeau, sous le minimum lisible. C'est un
   décompte qu'on lit en jouant, pas une mention légale. */
/* Le décompte. `--ink-3` et non `--ink-4` : à onze pixels, le quatrième
   niveau d'encre tombait à 3,2:1 sur le fond du bandeau, sous le minimum
   lisible. C'est un décompte qu'on lit en jouant, pas une mention légale. */
.duel-hud__reste {
  font-size: clamp(0.6875rem, 0.63rem + 0.28vw, 0.75rem);
  color: var(--ink-3);
  white-space: nowrap;
  /* « 10 restantes » puis « 9 restantes » : le mot perd une lettre et le
     bloc se recentrerait. La largeur est réservée pour la plus longue des
     deux formes. */
  min-inline-size: 5.5em;
  text-align: start;
}

/* ==========================================================================
   Le défi ne bouge pas
   --------------------------------------------------------------------------
   `.study__stage` centre son contenu verticalement — `place-content: safe
   center`. C'est le bon réglage pour une séance, où il n'y a qu'une carte à
   l'écran : elle se pose au milieu quelle que soit sa taille.

   En duel, il y a deux blocs empilés, et le centrage devient un défaut. La
   position du bandeau dépend alors de la hauteur de ce qu'il y a *en
   dessous* : à chaque question d'une longueur différente, à chaque bascule
   vers la salle d'attente, le total change et le bloc entier se recentre.
   Mesuré sur cet écran : un énoncé long faisait passer le bandeau de 158 à
   86 pixels du haut — soixante-douze pixels de saut, au moment précis où
   l'on cherche des yeux le score qu'on vient de marquer.

   La correction ne touche pas au centrage, qui fait le calme de l'écran :
   elle donne au pont une hauteur réservée, si bien que le total ne varie
   plus et que le centrage aboutit toujours au même endroit. Toutes les
   phases — question, correction, salle d'attente, verdict — occupent
   désormais la même boîte, et seul leur contenu change.

   La valeur couvre la plus haute des phases réelles. Un énoncé
   exceptionnellement long dépassera encore, et le bloc grandira : mieux vaut
   un décalage rarissime qu'un texte tronqué.
   ========================================================================== */
.duel-hud + .study__deck {
  --pont: 26rem;
  min-block-size: var(--pont);
  /* `start`, et non `center`.
   *
   * Le centrage se justifiait quand les cartes avaient des hauteurs libres :
   * une carte courte posée au sommet d'une grande boîte vide se lit comme une
   * page mal remplie. Depuis que chacune réserve la hauteur du pont (voir
   * plus bas), il n'y a plus de carte courte — donc plus rien à centrer, et
   * le centrage ne faisait plus que du mal : une carte qui *dépasse* la
   * réserve de seize pixels, ce qui arrive dès qu'un énoncé tient sur deux
   * lignes, débordait de huit pixels vers le haut. Les huit derniers pixels
   * de saut de cet écran venaient de là.
   *
   * Aligné au début, ce qui dépasse va vers le bas — où il y a la place — et
   * le haut de la carte ne bouge plus jamais. */
  align-content: start;
}

/* La salle d'attente occupe la boîte entière, et c'est ce qui supprime le
   dernier saut de l'écran.
 *
 * La hauteur réservée empêchait le *bandeau* de bouger : le total ne variait
 * plus, donc le bloc ne se recentrait plus. Mais le centrage, lui, agit
 * toujours sur la carte — et la carte d'attente est bien plus courte qu'une
 * carte de question : treize rem de tête et trois points, contre treize rem
 * de tête et une grille de propositions. Centrée dans vingt-six rem, elle
 * descendait d'une soixantaine de pixels. On répondait à sa deuxième
 * question, et « Son tour » s'affichait plus bas que l'énoncé qu'il
 * remplaçait — le seul mouvement qu'il restait, et il tombait au moment du
 * changement de joueur.
 *
 * En lui donnant la même hauteur qu'à la boîte, la carte n'a plus de marge à
 * se répartir : sa tête se pose exactement là où se posait celle de la
 * question. Les trois points, eux, prennent le vide restant par `auto` —
 * ils flottent au milieu de ce qui reste, ce qui est leur place. */
.duel-hud + .study__deck > .qcard {
  min-block-size: var(--pont);
  justify-content: flex-start;
}

.duel-attente .duel-attente__souffle { margin-block: auto; }

/* Et la tête de carte cesse de centrer son contenu — en duel seulement.
 *
 * `.qcard__head` réserve treize rem et centre ce qu'elle contient. C'est le
 * bon réglage pour une séance d'étude, où la carte est seule à l'écran et où
 * un énoncé d'une ligne doit se poser au milieu comme un énoncé de trois.
 *
 * En duel, c'est ce centrage qui produisait le dernier saut. Le contenu de la
 * tête n'a pas la même hauteur d'une phase à l'autre — un énoncé court, un
 * énoncé long, ou « Son tour » suivi de deux phrases — et centrer trois
 * contenus de hauteurs différentes dans une même boîte place leur première
 * ligne à trois hauteurs différentes. Mesuré : vingt-quatre pixels d'écart
 * entre la première question et l'écran d'attente, et déjà dix-sept entre
 * deux questions successives.
 *
 * Aligné en haut, le libellé de phase se pose toujours au même pixel, et
 * l'énoncé juste dessous. Ce qui varie — la longueur du texte — pousse vers
 * le bas, là où il y a de la place réservée pour cela, au lieu de déplacer ce
 * qui précède. C'est la règle générale d'un écran qui ne doit pas bouger :
 * ce qui change se répand vers le bas, jamais vers le haut. */
.duel-hud + .study__deck .qcard__head { justify-content: flex-start; }

@media (max-width: 30rem) {
  /* Un peu moins haut sur téléphone : la carte y est plus étroite, donc plus
     haute à contenu égal, mais l'écran est court et une réserve trop
     généreuse pousserait les propositions sous la ligne de flottaison. */
  /* Une seule valeur à changer : la carte d'attente la lit par `--pont`. */
  .duel-hud + .study__deck { --pont: 22rem; }
}

/* Les tout petits écrans — 320 et en dessous.
 *
 * Rien ne rétrécit ici : c'est la marge du bandeau qui cède, et elle seule.
 * Le portrait, le score et la pastille gardent la taille gagnée sur mobile ;
 * les pixels récupérés vont à la colonne du nom, qui est la seule à en
 * manquer à cette largeur. La hauteur du bandeau ne change pas. */
@media (max-width: 20rem) {
  .duel-hud { padding-inline: var(--s2); }
  .duel-hud__duo { gap: 0.125rem; }
}

@media (prefers-reduced-motion: reduce) {
  .duelliste__score .is-bump { animation: none; }
  .duel-hud { animation: none; }
  .duelliste__jalon.is-done { transform: none; }
  /* L'anneau du tour reste — c'est une information —, son souffle s'arrête.
     La lueur se pose à mi-course, visible et immobile : on sait toujours de
     qui c'est le tour, sans que rien ne bouge. */
  .duelliste.is-turn .duelliste__anneau::after { animation: none; opacity: .55; }
}

/* — La salle d'attente ————————————————————————————————————————————— */

.duel-attente__souffle {
  display: flex;
  justify-content: center;
  gap: var(--s2);
  padding-block: var(--s6);
}

.duel-attente__souffle span {
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--ink-4);
  animation: duel-souffle 1.4s var(--ease) infinite;
}

.duel-attente__souffle span:nth-child(2) { animation-delay: 180ms; }
.duel-attente__souffle span:nth-child(3) { animation-delay: 360ms; }

/* Opacité et échelle seulement : rien qui provoque un recalcul de mise en
   page, puisque cette animation tourne pendant toute l'attente. */
@keyframes duel-souffle {
  0%, 100% { opacity: .25; transform: scale(.85); }
  50%      { opacity: 1;   transform: scale(1.1); }
}

@media (prefers-reduced-motion: reduce) {
  .duel-attente__souffle span { animation: none; opacity: .6; }
  .duelliste__score .is-bump { animation: none; }
}

/* — Le verdict, rendu dans la scène ————————————————————————————————— */

/* Le verdict, rendu dans la scène du duel, réutilise `.duel-verdict` et
   `.duel-tableau` de la page de résultat — mêmes classes, même dessin. Ne
   restent ici que les deux écarts propres au fait d'être dans une carte de
   jeu plutôt que sur une page : pas de marge extérieure, et un score un peu
   moins haut, parce qu'il partage la scène avec la barre de progression.

   Les anciennes règles `duel-fin__scores`, `__col`, `__qui` et `__pts` ont
   disparu avec le balisage qu'elles habillaient. */
.duel-fin.duel-resultat {
  margin-block: 0;
  padding: var(--s6) var(--s5);
  gap: var(--s5);
  /* La même surface que la page de résultat.
     Le verdict rendu dans la scène héritait de `.qcard`, qui n'a ni fond ni
     rayon : il flottait à même l'écran d'exercice pendant que le même
     verdict, rechargé à la même adresse, s'affichait sur une carte. Deux
     dessins pour un seul moment, à une seconde d'intervalle. */
  border-radius: var(--r-lg);
  background: var(--surface);
}

/* Le score reste second, ici comme sur la page : c'est le verdict qui est la
   nouvelle. Il montait à quatre rem quand le mot en faisait deux et demie. */
.duel-fin .duel-tableau__pts { font-size: clamp(2rem, 1.55rem + 3.2vw, 3rem); }
.duel-fin .duel-verdict__mot { font-size: clamp(2.5rem, 1.75rem + 3.6vw, 3.25rem); }


.duel-fin__actions {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--s3);
  padding-block-start: var(--s5);
}

/* Sur la page de résultat, les actions sont détachées du résultat par un
   filet et une respiration. On ne quitte pas cet écran par inadvertance :
   ce qui précède se lit, ce qui suit s'actionne, et la frontière entre les
   deux doit se voir.
 *
 * `:not(.duel-fin)` écarte le panneau rendu dans la scène, qui porte les
 * deux classes. Là-bas, rien ne s'intercale entre les gains et les boutons —
 * il n'y a pas de tableau comparatif — si bien que le filet du bas des gains
 * et celui du haut des actions se retrouvaient à quelques pixels l'un de
 * l'autre, avec du vide entre les deux. On lisait une rangée vide au milieu
 * de l'écran de victoire. Sur la page, le tableau les sépare et les deux
 * filets font leur travail. */
.duel-resultat:not(.duel-fin) .duel-fin__actions {
  margin-block-start: var(--s2);
  padding-block-start: var(--s5);
  border-block-start: 1px solid var(--hairline);
  /* Même remarque que pour les gains : l'entrée vit dans la cascade. */
}

/* La revanche est l'action qu'on veut voir en premier après une partie :
   elle passe en tête sur téléphone, où l'ordre de la pile est l'ordre de
   lecture, et prend la lueur ambre du bouton principal. */
.duel-resultat .duel-fin__actions .defi-revanche {
  order: -1;
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.3),
    0 6px 24px -8px color-mix(in oklab, var(--accent) 75%, transparent),
    var(--shadow-2);
}

/* Sur téléphone, les actions s'empilent au lieu de se serrer.
 *
 * Se replier ne suffisait pas : trois boutons dont les libellés font de deux
 * à dix-neuf caractères donnent, une fois repliés, une pile de largeurs
 * différentes et centrées — trois bords gauches distincts, qu'on relit un
 * par un. Empilés à pleine largeur, ils forment une colonne unique, et
 * chacun devient une cible de la largeur du pouce.
 *
 * L'ordre du balisage reste le bon : le plus engageant en dernier, donc le
 * plus près du pouce.
 */
@media (max-width: 30rem) {
  .duel-fin__actions { flex-direction: column; align-items: stretch; }

  /* Les boutons du verdict sont posés en `btn--sm`, ce qui les fait à
     trente-quatre pixels de haut — mesuré. C'est une taille de barre
     d'outils, pas une taille de doigt : quarante-quatre est le minimum
     tactile, et c'est ce que font déjà tous les autres boutons du produit
     sur téléphone. On relève la hauteur sans toucher au composant `btn--sm`
     lui-même, qui reste juste partout où il sert vraiment de bouton
     secondaire à la souris. */
  .duel-fin__actions .btn {
    width: 100%;
    min-block-size: 2.75rem;
  }
}

/* Sur téléphone, le reste de l'écran se resserre.
 *
 * Le bandeau de duel n'y figure plus : il est désormais dessiné à sa taille
 * de téléphone d'emblée, et c'est le grand écran qui l'élargit. Un composant
 * dont la contrainte est la hauteur se dessine à partir du cas le plus
 * serré, sinon chaque valeur existe en deux exemplaires et les deux
 * divergent.
 */
@media (max-width: 47.99rem) {
  /* Les cartes d'amis restent confortables au pouce : une carte pleine et
     un tiers de la suivante à l'écran, ce qui montre qu'il y en a d'autres
     sans qu'on ait à le deviner. */
  /* Le rapport du grand écran, reporté ici : cinquante-six pixels d'avatar
     dans une carte de quatre-vingt-douze, c'était le même trop-plein qu'au
     bureau — dix-sept pixels de marge autour d'un disque qui, une fois
     l'anneau ambre allumé, en occupait soixante-huit. À quarante-six, la
     carte respire et l'anneau tient à l'intérieur. */
  .ami { width: 5.75rem; padding: var(--s3) var(--s1) var(--s2); }
  .ami__avatar { width: 2.875rem; height: 2.875rem; }
  .ami__halo { width: 2.875rem; height: 2.875rem; inset-block-start: -0.125rem; }
  .ami__coche { inset-block-start: 2.125rem; inset-inline-end: 0.625rem; width: 1.25rem; height: 1.25rem; }
  .ami-pick__piste { gap: var(--s2); }

  /* Le récapitulatif se resserre : il précède un bouton qui doit rester
     atteignable sans défiler. */
  /* Les trois règles précédentes visaient la version en balance — une grille
     à trois colonnes, un « VS » et deux vignettes carrées de même taille. Ce
     balisage n'existe plus ; ce qui reste est une phrase qui se replie toute
     seule, et il suffit de resserrer la carte autour d'elle. */
  .defi-resume { padding: var(--s4); gap: var(--s3); }
  .defi-resume__visage .avatar { width: 2.25rem; height: 2.25rem; }
  .defi-resume__vignette .cover { width: 2.875rem; height: 2.25rem; }
  .defi-resume__nom,
  .defi-resume__titre { max-width: 9rem; font-size: var(--text-sm); }

  /* Le CTA prend toute la largeur : c'est la cible la plus importante de
     l'écran, elle mérite la largeur du pouce. */
  .defis__envoi .defi-lancer { width: 100%; min-block-size: 3.25rem; }

  /* Fin de partie : le verdict se resserre pour que le score et les gains
     tiennent dans le même écran que lui. */
  .duel-verdict__sceau { width: 4.5rem; height: 4.5rem; }
  /* Le verdict reste le titre de l'écran.
     Il passait en `display-2` sur téléphone — trente pixels — pendant que le
     score gardait ses cinquante-neuf : le nombre criait deux fois plus fort
     que le mot qui dit ce qui vient d'arriver. Le mot reprend la tête, le
     score se resserre juste assez pour lui laisser la place. */
  .duel-verdict__mot { font-size: clamp(2.5rem, 1.75rem + 3.6vw, 3.25rem); }
  .duel-resultat .duel-tableau__pts { font-size: clamp(2rem, 1.55rem + 3.2vw, 3rem); }
  .duel-verdict__sceau { width: 4rem; height: 4rem; }
  .duel-compare { padding: var(--s4); gap: var(--s3); }
  .duel-compare__qui .avatar { width: 2.5rem; height: 2.5rem; }
}

/* Le bandeau de duel sur grand écran.
 *
 * La place ne manque pas et le pouce n'est plus le sujet : le portrait et le
 * score gagnent quelques pixels, la respiration s'ouvre. Rien de plus — la
 * structure ne change pas, sans quoi on entretiendrait deux bandeaux. */
@media (min-width: 48rem) {
  /* Une seule mesure à changer : la boîte du portrait, l'image et l'anneau
     la lisent tous les trois. La règle qui posait `width`/`height` sur
     l'avatar seul a disparu — elle faisait grandir l'image sans faire
     grandir la boîte, ce qui remettait les deux camps en désaccord sur
     grand écran précisément. */
  .duel-hud { padding: var(--s4); gap: var(--s3); --portrait: 2.375rem; }
  .duelliste { gap: var(--s3); }
  /* Le grand écran n'a plus rien à dire sur le corps du nom : le `clamp` de
     la règle de base atteint déjà son maximum bien avant 48 rem. Cette ligne
     figeait treize pixels et annulait l'agrandissement là où il y avait le
     plus de place pour lui. */
  .duelliste__nom { font-size: 0.875rem; }
  .duelliste__score { font-size: var(--display-2); }
  .duel-hud__ecart { font-size: var(--display-3); }
  .duel-hud__tour { font-size: var(--text-sm); padding: var(--s1) var(--s4); }
}


/* ==========================================================================
   La carte de thème s'adapte à sa propre largeur
   --------------------------------------------------------------------------
   Le contenu compact — vignette plus basse, titre plus petit, sous-titre et
   métadonnées secondaires retirés — était déclenché par la largeur de
   *l'écran*. C'était confondre deux questions distinctes :

     combien de cartes tiennent par rangée ?   → dépend de l'écran ;
     que tient-il dans une carte ?              → dépend de la carte.

   La confusion se voyait à la rupture. À 767 pixels, une carte large de 349
   affichait le contenu dessiné pour 170 : titre de quatorze pixels, pas de
   sous-titre, pas de durée — une carte à moitié vide. À 768 pixels, à trois
   pixels près de la même largeur, elle retrouvait tout. Et entre 520 et 767
   pixels — grands téléphones en paysage, petites tablettes — cette bande
   entière était mal servie.

   La requête de conteneur pose la question à l'endroit où elle se pose
   vraiment. Une carte de moins de quinze rem se compacte, qu'elle soit sur un
   téléphone, dans la colonne étroite d'une tablette ou dans un futur panneau
   latéral dont personne n'a encore parlé. Le seuil est mesuré : à 15 rem, le
   sous-titre tient sur deux lignes sans se couper ; en dessous, il coupe.
   ========================================================================== */

.theme-card { container-type: inline-size; container-name: fiche; }

/* La taille de réserve reste pilotée par l'écran, et non par le conteneur.

   Ce n'est pas une inconséquence, c'est une contrainte du langage :
   `@container` interroge le conteneur *ancêtre*. Une règle visant
   `.theme-card` à l'intérieur d'une requête sur `.theme-card` désignerait une
   carte imbriquée dans une carte, ce qui n'existe pas — elle ne s'applique
   jamais. Seuls les descendants (titre, vignette, sous-titre) peuvent être
   pilotés ainsi, et ils le sont.

   `contain-intrinsic-size` échappe donc à la règle. Ce n'est pas un choix de
   dessin mais une estimation : ce que le navigateur suppose d'une carte qu'il
   n'a pas encore tracée. Le mot-clé `auto` lui fait retenir la vraie taille
   dès qu'il l'a vue ; la valeur écrite ne compte qu'au premier passage. Une
   approximation liée à l'écran suffit largement, et elle est mesurée. */
@media (max-width: 47.99rem) {
  .theme-card { contain-intrinsic-size: auto 321px; }
}

@container fiche (max-width: 15rem) {
  .theme-card__body { gap: var(--s1); }
  .theme-card__title { font-size: var(--text-md); line-height: 1.25; }
  .theme-card .cover { aspect-ratio: 3 / 2; }

  .theme-card__body > p.faint { display: none; }
  .theme-card__meta > *:not(:first-child) { display: none; }
  .theme-card__meta > *::before { content: none; }
  .theme-card__foot .chip { display: none; }
  .theme-card__body > .tag { font-size: 10px; }
}

/* La rangée horizontale de « Continuer » n'est jamais compactée : elle porte
   déjà sa propre mise en page, et sa vignette est carrée par nature. */
.theme-card--row { container-type: normal; }

/* ==========================================================================
   La salle d'attente d'un défi
   --------------------------------------------------------------------------
   Un écran qui ne dure qu'une minute et pendant lequel on ne peut rien faire
   d'autre qu'attendre. Il n'a donc qu'une obligation : être lisible d'un coup
   d'œil, et le rester quand le décompte descend.

   Tout y est centré et empilé, y compris sur grand écran. Une mise en page
   qui s'étale demanderait de regarder à deux endroits ; ici il n'y a qu'une
   chose à savoir, et elle change toutes les secondes.
   ========================================================================== */

.attente {
  display: grid;
  justify-items: center;
  gap: var(--s5);
  max-width: 26rem;
  margin: var(--s9) auto;
  padding: var(--s7) var(--s5);
  border-radius: var(--r-2xl);
  background: var(--surface);
  box-shadow: inset 0 1px 0 0 var(--bevel), var(--shadow-3);
  text-align: center;
}

.attente__faces {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--s3);
  width: 100%;
}

.attente__versus {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink-3);
}

.attente__face { display: grid; justify-items: center; gap: var(--s1); min-width: 0; }
.attente__nom { font-weight: 600; max-width: 100%; }
.attente__niveau { font-size: var(--text-xs); color: var(--ink-3); }

.attente__theme { display: grid; gap: var(--s2); justify-items: center; }

.attente__titre {
  font-family: var(--font-display);
  font-size: var(--display-3);
  font-weight: 400;
  letter-spacing: var(--track-tight);
  line-height: 1.1;
}
.attente__regle { font-size: var(--text-sm); color: var(--ink-3); max-width: 22rem; }

/* — Le décompte —————————————————————————————————————————————————
   L'anneau se vide par `transform`, jamais par une géométrie recalculée :
   il tourne pendant toute la minute, et un recalcul de mise en page par
   quart de seconde se sentirait sur un téléphone de quelques années. */

.attente__compte { display: grid; justify-items: center; gap: var(--s3); }
.attente__unite { font-size: var(--text-sm); color: var(--ink-3); }

/* — Le cercle lumineux du décompte ————————————————————————————————
 *
 * Un seul objet, et il est rond. La lueur de l'arc, portée par le
 * `drop-shadow` du SVG, est nécessairement mince et discontinue : elle suit
 * le trait, donc elle disparaît là où l'arc s'est vidé. Ce qui manquait est
 * une lumière *autour* du compteur — continue, douce, et qui ne dépend pas
 * de l'avancement.
 *
 * Elle est portée par l'anneau lui-même, en `box-shadow` sur un élément
 * arrondi à 50 % : un `box-shadow` épouse le rayon de sa boîte, donc il ne
 * peut pas être carré, et il n'est rogné par rien puisqu'il ne vit pas dans
 * le SVG. Deux couches : une auréole large et faible qui pose la lumière,
 * une arête très fine qui lui donne un bord — sans quoi un halo seul se lit
 * comme un flou d'affichage.
 *
 * Elle respire lentement, en opacité seulement. Aucune échelle : le cercle
 * ne doit pas grandir, il doit briller. Six secondes par cycle — c'est un
 * écran d'attente, le mouvement y accompagne le temps qui passe, il ne le
 * commente pas. */
.attente__compte .ring {
  position: relative;
  border-radius: var(--r-full);
}

.attente__compte .ring::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: var(--r-full);
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--accent) 22%, transparent),
    0 0 26px 2px color-mix(in oklab, var(--accent) 30%, transparent);
  pointer-events: none;
  animation: attente-lueur 6s var(--ease-inout) infinite;
}

@keyframes attente-lueur {
  0%, 100% { opacity: .35; }
  50%      { opacity: 1; }
}

/* Les dix dernières secondes : la lumière passe au rouge avec le trait, sans
   changer de rythme. C'est la couleur qui alerte, pas la vitesse — accélérer
   ajouterait de l'urgence à une attente sur laquelle on ne peut rien. */
.attente.is-urgent .attente__compte .ring::after {
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--wrong) 26%, transparent),
    0 0 26px 2px color-mix(in oklab, var(--wrong) 32%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .attente__compte .ring::after { animation: none; opacity: .6; }
}

/* Les dix dernières secondes se signalent par la couleur seule. Faire battre
   le nombre attirerait l'œil sur ce qu'on ne peut pas changer. */
.attente.is-urgent .ring__value { stroke: var(--wrong, var(--accent)); }
.attente.is-urgent .ring__label { color: var(--wrong, var(--accent-ink)); }

.attente__etat { font-size: var(--text-sm); color: var(--ink-2); min-height: 1.4em; }

.attente__actions { display: grid; gap: var(--s2); width: 100%; max-width: 20rem; }
.attente__actions .btn { width: 100%; }

.attente__note {
  font-size: var(--text-xs);
  color: var(--ink-4);
  max-width: 22rem;
  line-height: 1.5;
}

.attente.is-clos { opacity: .72; }
.attente--clos .attente__theme { gap: var(--s3); }

@media (max-width: 47.99rem) {
  .attente { margin: var(--s5) auto; padding: var(--s5) var(--s4); gap: var(--s4); }
  .attente__anneau { width: 3.25rem; height: 3.25rem; }
}

/* ==========================================================================
   Typographie mobile : ce qui doit se lire plutôt que tenir sur une ligne
   --------------------------------------------------------------------------
   `.truncate` coupe à l'ellipse. C'est le bon choix sur écran large, où une
   ligne unique aligne proprement une liste et où il reste assez de place pour
   que la coupe soit rare. Sur téléphone, la même règle produit « Dates clés
   de l'histoire de Fr… » et « Présidents de la République fr… » : le nom du
   thème, qui est la seule chose qu'on vient lire, disparaît avant sa fin.

   Deux lignes valent mieux qu'une ligne fausse. La règle ne s'applique qu'aux
   endroits où le texte est un *nom* — un titre de thème, un intitulé de
   connaissance — et jamais aux endroits où la hauteur d'une ligne est
   structurante : les visages d'un duel, les puces, les métadonnées, qui
   restent coupés parce qu'ils sont accessoires et que leur boîte est calibrée.
   ========================================================================== */

@media (max-width: 47.99rem) {
  .plate--interactive .truncate,
  .tough .truncate,
  .duel-card .truncate,
  .split__row .truncate {
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* La coupure de mot reste interdite : on préfère une ligne courte à un
       mot brisé. `overflow-wrap` ne sert que de filet pour les mots plus
       longs que la colonne, qui existent en géographie. */
    overflow-wrap: anywhere;
    hyphens: manual;
  }
}

/* — La carte d'un défi dans une liste ————————————————————————————————
   Elle portait sa disposition en attributs `style` répartis sur deux
   fichiers : `gap`, `min-width:0`, `align-items`. Nommées, ces trois règles
   se retrouvent, s'ajustent en média-requête, et ne peuvent plus diverger
   d'un écran à l'autre. */

.duel-card__tete { display: flex; align-items: center; gap: var(--s3); }

/* `min-width: 0` est indispensable : sans lui, un enfant de flex refuse de
   se réduire sous sa largeur de contenu, et le nom du thème pousse la puce
   « Gagné » hors de la carte au lieu de se tronquer. */
.duel-card__qui { display: grid; gap: 2px; flex: 1 1 auto; min-width: 0; }
.duel-card__theme { font-size: var(--text-sm); color: var(--ink-3); }

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

.duel-card__format { font-size: var(--text-sm); color: var(--ink-3); }

/* — Les cibles tactiles de la partie Défis ————————————————————————
 *
 * Les actions des cartes — « Relever », « Décliner », « Revanche », « Voir »
 * — sont posées en `btn--sm`, qui vaut 34 pixels de haut. Relevé sur les
 * quatre écrans : soixante-dix-neuf boutons sous le seuil tactile, presque
 * tous ici. Trente-quatre pixels est une taille de barre d'outils à la
 * souris ; au pouce, c'est en dessous de ce qu'on vise sans regarder.
 *
 * La règle est volontairement bornée à la partie Défis. `btn--sm` reste
 * juste ailleurs, où il sert de bouton secondaire sur des écrans qu'on
 * parcourt plutôt qu'on n'actionne ; l'élargir partout dépasserait cette
 * passe et changerait la densité de la boutique et du profil.
 */
@media (max-width: 47.99rem) {
  .duel-card .btn,
  .social__hero .plate .btn,
  .empty .btn {
    min-block-size: 2.75rem;
    padding-inline: var(--s4);
  }

  /* Les deux actions d'une invitation se partagent la largeur plutôt que de
     se serrer à droite : à 44 pixels de haut, elles méritent la largeur qui
     va avec. */
  .duel-card__pied .row--tight { flex: 1 1 auto; justify-content: stretch; }
  .duel-card__pied .row--tight > form { flex: 1 1 auto; }
  .duel-card__pied .row--tight > form > .btn,
  .duel-card__pied .row--tight > .btn { width: 100%; }
}

/* ==========================================================================
   Le résultat d'un défi tient dans un écran de bureau
   --------------------------------------------------------------------------
   Il mesurait 1 143 pixels pour une fenêtre de 900 : le verdict, le score,
   les gains, le tableau comparatif et les boutons, empilés en une seule
   colonne dans une carte large de 704. Deux cent quarante pixels sous le
   pli, sur l'écran où l'on vient chercher un résultat — et l'essentiel de
   ce qui manquait était le détail, pas le verdict.

   La colonne unique venait du téléphone, où elle est la seule mise en page
   possible. Sur un écran de bureau elle laisse la moitié de la carte vide et
   paie ce vide en hauteur. Les deux blocs de même rang — ce que le défi a
   rapporté, et comment les deux joueurs se sont comportés — passent donc
   côte à côte. Ils sont de même nature : deux compléments qu'on consulte
   après avoir lu le verdict, ni l'un ni l'autre prioritaire.

   Ce qui ne bouge pas : l'ordre de lecture (verdict, score, puis le reste,
   puis la sortie), le contenu, et la colonne unique dès qu'on repasse sous
   48 rem. La grille ne s'applique qu'à la *page* de résultat — le panneau
   rendu dans la scène du duel garde sa colonne, il vit dans un écran
   d'exercice qui n'a pas la même largeur utile.
   ========================================================================== */
@media (min-width: 48rem) {
  .duel-resultat:not(.duel-fin) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "verdict verdict"
      "score   score"
      "gains   compare"
      "actions actions";
    align-items: start;
    /* La marge extérieure valait trois rem en haut et autant en bas : la
       carte flottait au milieu de rien pendant que son contenu débordait. */
    margin-block: var(--s5);
  }

  .duel-resultat:not(.duel-fin) > .duel-verdict { grid-area: verdict; }
  .duel-resultat:not(.duel-fin) > .duel-tableau { grid-area: score; }
  /* Les tuiles prennent toute leur colonne : la largeur maximale de 26 rem
     est là pour empêcher un bandeau sur téléphone, pas pour laisser un vide
     à côté d'un tableau qui, lui, s'étale. Sans cela les libellés passaient
     à la ligne dans une colonne pourtant deux fois trop large. */
  .duel-resultat:not(.duel-fin) > .duel-gains {
    grid-area: gains;
    align-self: center;
    max-width: none;
  }
  .duel-resultat:not(.duel-fin) > .duel-compare { grid-area: compare; }
  .duel-resultat:not(.duel-fin) > .duel-fin__actions { grid-area: actions; }

  /* Les marges de rang, posées pour une pile, n'ont plus de sens dans une
     grille dont les gouttières font le même travail. */
  .duel-resultat:not(.duel-fin) > .duel-tableau,
  .duel-resultat:not(.duel-fin) > .duel-compare,
  .duel-resultat:not(.duel-fin) > .duel-fin__actions { margin-block-start: 0; }

  /* Le verdict se resserre : le sceau porte le résultat pour qui regarde de
     loin, il n'a pas besoin de quatre-vingt-huit pixels pour cela quand la
     hauteur manque. */
  .duel-resultat:not(.duel-fin) .duel-verdict { gap: var(--s2); }
  .duel-resultat:not(.duel-fin) .duel-verdict__sceau { width: 4.5rem; height: 4.5rem; }
  .duel-resultat:not(.duel-fin) .duel-compare { padding: var(--s4); gap: var(--s3); }
  .duel-resultat:not(.duel-fin) .duel-compare__qui .avatar { width: 2.25rem; height: 2.25rem; }
}
