.anima-combat-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px;
  color: var(--ink);
  margin: 16px 0;
}

/* Troisième et dernière gouttière emboîtée avant la liste (cf. le bloc jumeau sur
   `.anima-encounter-focus` dans adventures.css). 14 → 6 px sous le seuil mobile rend
   16 px à la rangée d'initiative ; avec les 30,6 px du bloc parent, la rangée passe de
   256,4 à 303,0 px à 360 px de viewport, et de 318,4 à 365,0 à 422 px.
   6 px et non 0 : la bordure du panneau reste la séparation visible entre la liste et
   la carte, mais une rangée collée à cette bordure se lit mal. Descendre plus bas ne
   rendrait d'ailleurs rien de plus : le plafond absolu à 360 px (toutes gouttières à
   zéro) est de 326 px, et le premier seuil qui débloquerait une assertion — huit
   pastilles avec un nom lisible — en demande 327,8. Mesuré, pas estimé. */
@media (max-width: 767.98px) {
  .anima-combat-panel {
    padding-left: 6px;
    padding-right: 6px;
  }
}

.anima-combat-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

/* Ce que voit un joueur NON-MJ à la place des deux boutons ci-dessus. Il n'avait aucune
   règle : du texte brut dans un conteneur flex, sans hauteur ni alignement, donc une
   barre qui changeait de taille selon qui regardait.
   Aligné sur ce que rend la branche MJ plutôt que sur une valeur inventée : mesuré sur
   la page-témoin, un `.btn-sm` d'une seule ligne fait 32,6 px de haut ; 1,8 rem en vaut
   32,4. Même famille et même graisse de caractères que les boutons, en couleur atténuée
   parce que ce n'est pas une commande. */
.anima-turn-readout {
  display: inline-flex;
  align-items: center;
  min-height: 1.8rem;
  font-family: 'Cinzel', Georgia, serif;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

/* ── GM « Passage du temps » control ── */
.anima-time-passage {
  margin-bottom: 12px;
}

.anima-time-passage-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  margin-top: 8px;
  padding: 10px;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.15));
  border-radius: 6px;
}

/* `flex: 1 1 6rem` et non une largeur en dur : les deux champs partagent la ligne du
   formulaire quand elle est large, et passent à la ligne quand elle ne l'est plus. Le
   `6rem` reste la taille SOUHAITÉE (celle qu'imposait l'ancien `width`), pas un plafond.
   Le `min-width` est ce qui rend le rétrécissement sûr : sans lui, un champ flex ne
   descend pas sous sa largeur de contenu et déborde au lieu de céder. */
.anima-time-passage-field {
  display: flex;
  flex-direction: column;
  flex: 1 1 6rem;
  min-width: 4rem;
  gap: 4px;
  font-size: 0.8rem;
}

.anima-time-passage-field input,
.anima-time-passage-field select {
  width: 100%;
  min-width: 0;
  font-size: 0.85rem;
  padding: 4px 6px;
}

.anima-combat-section {
  margin-bottom: 14px;
}

.anima-combat-section:last-child {
  margin-bottom: 0;
}

.anima-section-label {
  font-family: 'Cinzel', serif;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin-bottom: 6px;
}

/* ── Roster: two-column layout (Initiative ∣ Historique) ──
   Single full-width column pre-combat; grid on desktop once combat is active. */
.anima-combat-cols {
  display: block;
}

/* 768 px et non 721 : c'est le seuil de tout le reste du projet (les compressions mobiles
   s'arrêtent à 767,98), et les deux doivent tomber ensemble. Entre 721 et 767, l'ancien
   seuil donnait la grille de bureau AVEC les compressions de téléphone encore actives —
   gouttière de panneau à 6 px, avatar de rangée à 24 px — c'est-à-dire une mise en page de
   téléphone tassée dans deux colonnes.
   La conséquence était mesurable et à l'envers : ÉLARGIR la fenêtre de 767 à 768 px
   faisait RÉTRÉCIR la liste (425,8 → 387,9 px) et la piste du nom (200,8 → 146,9), parce
   que les compressions cessaient d'un coup dans une grille inchangée.
   Ce que le nouveau seuil coûte, mesuré à 740 px — la seule largeur où le changement se
   voit : une colonne au lieu de deux, page 1909 → 2149 px (+240, soit +12 %). Ce qu'il
   rend : une liste de 683 px au lieu de 409, une piste de nom de 457,9 px au lieu de
   184,2, et le journal en pleine largeur sous elle. À cette largeur-là, l'échange est
   franchement favorable. */
@media (min-width: 768px) {
  /* Two columns only when the log is expanded; otherwise the roster spans the
     full width and the collapsed log sits as a slim bar below it. */
  .anima-combat-cols.is-combat.is-log-open {
    display: grid;
    grid-template-columns: 1.6fr 1fr;
    gap: var(--space-4);
    align-items: start;
  }
}

/* ── Motif de repli : en-tête cliquable + pastille de compte ──
   Deux jeux de noms, UNE seule déclaration : `.anima-log-*` est le tiroir
   « Historique » du panneau de combat, `.anima-fold-*` les cinq sections repliables de
   la modale d'attaque. Le projet a déjà deux tiroirs (celui-ci et « Fond » du tableau
   blanc) ; en écrire un troisième jeu de valeurs, c'est trois motifs qui divergeront.
   Le corps replié est masqué par `.is-folded` (display: none) et NON retiré du DOM :
   `Encounter::CombatSpecialFields` porte son propre état d'affichage, le détruire au
   repli viderait ses champs à l'écran tout en laissant la valeur dans le payload. */
.anima-log-toggle,
.anima-fold-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.4rem 0.6rem;
  color: var(--muted);
  cursor: pointer;
}
.anima-log-toggle:hover,
.anima-log-toggle.is-open,
.anima-fold-toggle:hover,
.anima-fold-toggle.is-open {
  border-color: var(--accent);
  color: var(--link);
}
.anima-log-toggle .anima-section-label,
.anima-fold-toggle .anima-section-label {
  margin: 0;
}
.anima-log-toggle-chevron,
.anima-fold-chevron {
  font-size: 0.8rem;
  opacity: 0.7;
}
.anima-log-count,
.anima-fold-count {
  margin-left: auto;
  font-size: 0.8rem;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 0.5rem;
  line-height: 1.4;
}
/* La pastille n'est pas décorative : elle est la seule trace d'une saisie qu'on vient
   de replier. Elle porte donc la couleur d'accent, pas celle du texte discret. */
.anima-fold-count {
  color: var(--accent);
  border-color: var(--accent);
}
.anima-fold-body {
  margin-top: 8px;
}
.anima-fold-body.is-folded {
  display: none;
}
/* Pastille de bandeau qui porte un NOM et non un nombre : la modale de magie replie par
   voie, et le seul endroit où le nom du sort choisi est lisible est le bouton de sa
   liste — que replier masque. Sœur de `.anima-fold-count`, même place (poussée à droite
   par `margin-left: auto`), mais elle doit pouvoir se tronquer : un nom de sort fait
   jusqu'à une quarantaine de caractères et la ligne du bandeau n'est pas plus large que
   le corps de la modale sur un téléphone. */
.anima-fold-selection {
  margin-left: auto;
  max-width: 55%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.8rem;
  color: var(--link);
}

.anima-combat-col-roster,
.anima-combat-col-log {
  min-width: 0;
}

/* ── Roster container ── */
.anima-combat-roster {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* ── Spectators group (hors combat) ── */
.anima-spectators-label {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px dashed var(--border);
}

.anima-roster-spectators {
  opacity: 0.65;
}

/* ── Roster toolbar (Ajouter / Générer / Pool / Galerie) ── */
.anima-roster-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* ── Participant dropdown menu (⋯) ── */
.anima-pmenu {
  position: relative;
  display: inline-flex;
}

.anima-pmenu-trigger {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  border-radius: var(--radius-md);
  width: 1.9rem;
  height: 1.9rem;
  line-height: 1;
  font-size: 1.1rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.anima-pmenu-trigger:hover,
.anima-pmenu-trigger.is-open {
  border-color: var(--accent);
  color: var(--link);
}

.anima-pmenu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1450;
}

.anima-pmenu-pop {
  /* position/left/top|bottom/max-height posés inline (viewport-fixed) pour
     échapper au rognage du roster / de la fenêtre flottante de combat. */
  position: fixed;
  z-index: 1451;
  min-width: 13rem;
  max-width: calc(100vw - 0.5rem);
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-2);
  padding: 0.35rem;
}

.anima-pmenu-item {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 0.5rem;
  text-align: left;
  background: transparent;
  border: 0;
  color: var(--ink);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.6rem;
  font-size: 0.85rem;
  cursor: pointer;
}

.anima-pmenu-item:hover {
  background: var(--surface-2);
}

.anima-pmenu-item.is-danger {
  color: var(--hp);
}

.anima-pmenu-sep {
  height: 1px;
  background: var(--border);
  margin: 0.3rem 0.4rem;
}

/* ── HP wrapper on the init row (view + inline edit) ── */
.anima-participant-hp {
  display: inline-flex;
  align-items: center;
}

.anima-participant-hp-form {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.anima-participant-hp-input {
  width: 4rem;
  font-size: 0.8rem;
  padding: 1px 4px;
}

.anima-participant-hp-max {
  font-size: 0.8rem;
  color: var(--muted);
}

.anima-participant-hp-btn {
  font-size: 0.8rem;
  padding: 1px 6px;
  line-height: 1;
}

.anima-participant-hp-error {
  font-size: 0.8rem;
  color: var(--hp);
}

.anima-participant-hp-toggle {
  cursor: pointer;
}

.anima-combat-error {
  background: var(--hp-soft);
  border: 1px solid var(--hp);
  border-radius: 4px;
  color: var(--hp);
  padding: 6px 10px;
  font-size: 0.8rem;
  margin-bottom: 10px;
}

.anima-combat-hint {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.anima-action-pick-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.8rem;
}


.anima-init-row {
  display: grid;
  /* Cinq zones nommées et non plus neuf : [init] [avatar] [nom] [pastilles] [PV].
     L'ordre des colonnes EST l'ordre de sacrifice — le nom prend `1fr` et ne se
     tronque qu'après que les pastilles ont été plafonnées à trois, l'inverse de la
     version précédente où le nom (2e colonne, `1fr`) cédait avant les sept `auto`. */
  /* La piste d'initiative est un MINIMUM, pas une largeur : un score négatif à
     quatre caractères (« -105 ») déborde 2rem, passe à la ligne et fait bondir
     la rangée de 42 à 66,7 px — mesuré. `minmax` la laisse s'élargir pour ce
     cas-là seulement, sans rien changer aux scores à un ou deux chiffres. */
  grid-template-columns: minmax(2rem, auto) auto minmax(0, 1fr) auto auto;
  /* La ligne porte encore, APRÈS les PV, des contrôles qui ne font pas partie des
     cinq zones : jauge de bouclier, étiquette PJ/PNJ, compteur d'actions, bouton
     d'init, menu de gestion. En flux `row` (le défaut), ils repartent à la ligne
     suivante — mesuré dans Chromium à 360 px : `grid-template-rows` calculé passe
     à « 32px 34.1875px » et la hauteur de ligne à 82,2 px au lieu de 44,2. Le flux
     `column` les place dans des colonnes implicites `auto`, donc sur la même
     rangée, sans toucher au dimensionnement des cinq premières. */
  grid-auto-flow: column;
  align-items: center;
  gap: 6px;
  padding: 5px 8px;
  border-radius: 4px;
  /* Bloc de référence du voile de dépliage (`.anima-init-row-hit`). C'est la SEULE
     chose que `position: relative` change ici : la rangée ne se décale pas, et la
     seule position absolue qu'elle contenait déjà — le remplissage de la jauge de
     PV — est ancrée à sa propre piste, qui est relative elle aussi. */
  position: relative;
  /* Largeur du liseré de la rangée active, nommée pour que le voile puisse la
     reprendre au lieu de la redeviner. Unité obligatoire : `calc(-1 * 0)` n'est pas
     une longueur, et `left` la refuserait. */
  --init-row-edge: 0px;
}

/* Hors combat, la piste d'initiative disparaît. Elle ne peut pas simplement être
   retirée du flux : les cinq pistes ci-dessus sont NOMMÉES par leur position et
   `grid-auto-flow: column` les distribue dans l'ordre du document, donc un enfant en
   moins décale tous les suivants d'un cran.
   Mesuré dans Chromium sur une recopie de la règle ci-dessus, rangée de 360 px, le
   score simplement retiré et rien d'autre : les pistes se résolvent à
   « 36px 214px 0px 70px 0px ». L'avatar (24 px de contenu) hérite du `minmax(2rem, auto)`
   de l'initiative et traîne 12 px de vide, le nom perd son `1fr` pour un `auto`, et —
   le pire, que ni le lint ni le build ne voient — LES PASTILLES D'ÉTAT TOMBENT SUR LE
   `1fr` QUI NE RESTE PLUS RIEN et se rendent sur 0 px : elles disparaissent de l'écran.
   Les quatre pistes restantes sont donc redéclarées ici, dans le même ordre de
   sacrifice : [avatar] [nom] [pastilles] [PV]. Même relevé, variante appliquée :
   « 24px 178.562px 53.4375px 70px » — les pastilles retrouvent leurs 53,44 px et le nom
   récupère les 42 px (piste + gouttière) que l'initiative lui rendait.
   Le HUD flottant n'a pas besoin de cette variante et ne s'en trouve pas gêné : il
   repasse la rangée en `display: flex` (cf. `.combat-panel--compact .anima-init-row`
   plus bas), où `grid-template-columns` est inerte et un enfant de moins ne décale rien. */
.anima-init-row.no-init {
  grid-template-columns: auto minmax(0, 1fr) auto auto;
}

/* Le voile qui déplie la feuille du participant : un vrai `<button>`, étalé sur la
   rangée entière, transparent, sans contenu. Le motif vient de `.anima-modal-scrim`
   (components.css) et il est ici pour la même raison — le conteneur ne peut pas être
   à la fois présentationnel et cliquable.
   `position: absolute` le sort du flux, et c'est ce qui rend le motif possible à la
   largeur de référence : la grille de la rangée ne le voit pas, aucune de ses cinq
   pistes ne perd un pixel.
   `left` mord dans le liseré de la rangée active, qui est HORS de `inset: 0` (celui-ci
   se cale sur la boîte de remplissage) : sans ce décalage la cible tactile serait
   amputée du liseré sur son bord gauche, exactement là où le pouce vise en premier.
   `touch-action: manipulation` et non `none` : ce voile ne traîne rien, il reçoit une
   tape. `none` interdirait de faire défiler la liste avec un doigt posé sur une
   rangée, c'est-à-dire partout. */
.anima-init-row-hit {
  position: absolute;
  inset: 0;
  left: calc(-1 * var(--init-row-edge));
  z-index: 0;
  width: auto;
  cursor: pointer;
  touch-action: manipulation;
}

/* Les deux seuls contrôles qui ont leur propre geste repassent AU-DESSUS du voile.
   C'est le bouton du nom qu'on remonte et non sa piste : la piste est large, et la
   remonter ferait de tout l'espace vide à droite d'un nom court une zone morte. Même
   raison pour le bouton de PV plutôt que sa zone. Une jauge de PV non modifiable ne
   rend pas ce bouton et reste donc sous le voile, comme le reste de ce qui se lit.
   Les DEUX propriétés comptent, et pas de la même façon selon le contrôle. Le bouton
   du nom a besoin de `position` : sans elle, `z-index` ne le remonte pas et le voile
   reçoit son clic (relevé). Le bouton de PV, lui, est un ÉLÉMENT FLEX de sa zone, et
   un `z-index` autre qu'`auto` sur un élément flex crée un contexte d'empilement même
   en `position: static` — relevé aussi : la position seule retirée, il gardait le
   clic ; les deux retirées, il le perdait. `position` est gardée pour lui malgré tout,
   pour que la remontée ne dépende pas de ce que sa zone reste un conteneur flex. */
.anima-init-name-link,
.anima-init-row .anima-participant-hp-toggle {
  position: relative;
  z-index: 1;
}

/* Dernier levier pris, et seulement parce qu'il manquait 4,8 px au pire cas une
   fois tous les autres appliqués : la gouttière passe de 6 à 4 px sous le seuil
   mobile. Cinq gouttières entre six zones, donc 10 px rendus — la piste du nom
   du cumul le plus chargé passe de 59,2 à 69,2 px, pour un plancher de 64.
   C'est la gouttière qui a été choisie plutôt que la piste d'initiative : à
   1,05rem en monospace, une initiative à trois chiffres occupe déjà ~34 px des
   36 px de sa piste, la rogner la ferait passer à la ligne. */
@media (max-width: 767.98px) {
  .anima-init-row {
    gap: 4px;
    /* La rangée entière est le bouton qui ouvre la feuille du participant : sa
       hauteur EST la cible tactile. Les 42 px sont ceux qu'elle faisait avant que
       l'avatar ne passe à 24 px — repris ici plutôt que laissés tomber à 38,36,
       puisqu'ils ne coûtent rien horizontalement. La hauteur reste constante d'une
       rangée à l'autre, ce que l'assertion 1 du harnais vérifie. */
    min-height: 42px;
  }
}

.anima-init-row.sheet-open {
  background: var(--surface-2);
}

/* Le liseré est déclaré à partir du jeton, et le jeton sert aussi au voile : les deux
   ne peuvent pas diverger. */
.anima-init-row.active {
  background: var(--zeon-soft);
  --init-row-edge: 3px;
  border-left: var(--init-row-edge) solid var(--accent);
}

.anima-init-score {
  /* Sans ça, un score négatif peut se couper sur son propre trait d'union quand
     la piste est comprimée. */
  white-space: nowrap;
  font-family: monospace;
  font-size: 1.05rem;
  color: var(--accent);
  text-align: right;
  font-weight: 700;
}

.anima-init-score.missing {
  color: var(--muted);
}

.anima-init-avatar {
  display: inline-flex;
  align-items: center;
}

/* Sur téléphone, l'avatar de la rangée passe de 32 à 24 px : 8 px rendus à la
   piste du nom, mesurés. Il ne sert ici qu'à reconnaître un participant d'un
   coup d'œil, pas à détailler un portrait — la feuille et la fiche le montrent
   en grand. Hors mobile il garde ses 32 px, la place n'y manque pas.
   Conséquence relevée APRÈS coup : c'est l'avatar qui commandait la hauteur de
   la rangée, qui tombait à 38,36 px. La rangée est la cible tactile qui ouvre la
   feuille du participant, et on refait cet écran pour le doigt : le
   `min-height` ci-dessous lui rend ses 42 px, ce qui ne coûte pas un pixel
   horizontal.

   UNE SEULE INITIALE ICI, et c'est une réparation, pas une concession. La boîte de 24 px
   n'offre que 22 px de contenu (1 px de bordure de chaque côté), et deux capitales y
   demandent — relevé sur la page-témoin, polices de repli, la plus large paire étant
   « OM » — 23,1 px à 0,65 rem : elles DÉBORDAIENT déjà de 1,1 px, rognées en silence par
   l'`overflow: hidden` de `.anima-character-avatar`. À 0,8 rem, le plancher du projet, il
   en faudrait 28,5 et le manque passerait à 6,5 px.
   Les deux autres sorties ont été chiffrées avant de choisir celle-ci : élargir la boîte
   à ~30 px coûte 6,5 px à la piste du nom, qui tombe de 65,3 à 58,8 px pour un plancher
   de 64 et fait virer combat-roster-test.mjs au ROUGE ; garder 0,65 rem laissait huit
   textes sous le plancher pour un avatar déjà rogné.
   Une lettre à 0,8 rem tient dans les 22 px avec de la marge, la police respecte le
   plancher, et la piste du nom ne perd rien : l'avatar garde ses 24 px. Le découpage en
   spans d'une lettre est fait par components/character/avatar.hbs ; le `nth-child` ne
   masque QUE l'affichage — `aria-label` porte toujours le nom complet. */
@media (max-width: 767.98px) {
  .anima-init-row .anima-character-avatar--mini {
    width: 24px;
    height: 24px;
    font-size: 0.8rem;
  }

  .anima-init-row .anima-character-avatar--mini .anima-character-avatar-initial:nth-child(n + 2) {
    display: none;
  }
}

/* Le nom est seul dans sa zone (l'avatar et les pastilles sont sortis en
   colonnes voisines). `display: block` et non `flex` : l'ellipsis ne s'applique
   qu'au contenu en ligne d'un conteneur de bloc — en flex, les enfants sont des
   boîtes de bloc et `text-overflow` n'a rien à tronquer. */
.anima-init-name {
  display: block;
  min-width: 0;
  font-size: 0.95rem;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Le nom est un `<button>` depuis que la rangée a cessé d'en être un, et il porte SA
   PROPRE troncature parce qu'un bouton est une boîte ATOMIQUE : la piste, en
   `overflow: hidden` + `text-overflow: ellipsis`, ne sait pas couper dedans — elle le
   laisse tomber en entier et n'affiche que les points de suspension. Mesuré à 360 px
   en retirant ces quatre déclarations : le bouton d'un nom long réclamait 248,1 px
   dans une piste de 80,9, la piste le lâchait, et c'est elle qui affichait « … » à sa
   place. `max-width: 100%` — résolu contre la piste, qui est son bloc conteneur —
   l'empêche de la déborder et donc d'être lâché. */
.anima-init-name-link {
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* `.anima-hp-bar` porte un `min-width: 80px` (characters.css) qui réserve à la
   jauge de quoi rester visible. Dans la rangée d'initiative, cette réserve ne
   sert personne : la piste y est dimensionnée sur son contenu, et la barre elle-
   même y est mesurée à 0 px de large — seul le libellé s'affiche. Le plancher
   annulait donc l'économie du libellé compact (66,8 px de contenu, remontés à
   80). Le lever rend 13,2 px de plus à la piste du nom. */
.anima-init-row .anima-hp-bar {
  min-width: 0;
}

/* La piste de la jauge est en `flex: 1` (donc `flex: 1 1 0%`) et VIDE : son unique
   enfant, le remplissage, est en position absolue, sa contribution naturelle est donc
   nulle. Dans la rangée d'initiative, la colonne du nom en `1fr` absorbe tout l'espace
   libre et la piste est mesurée à 0 px de large — à toutes les largeurs d'écran, de 360
   à 3840 px. Ce n'est pas un problème de résolution : hors de cette rangée le même
   composant fait 431 px sur la fiche de personnage.
   On lui rend une largeur fixe, mais SEULEMENT à partir de 768 px. En dessous, la piste
   du nom est mesurée à 65,3 px pour un plancher de 64 px gardé par
   combat-roster-test.mjs : il n'y a pas 40 px à y prendre, et le libellé « 141/220 »
   porte déjà le chiffre — perdre la barre n'y coûte rien.
   L'ÉPAISSEUR de la piste, elle, est traitée dans characters.css (6 px) : elle ne change
   aucune des largeurs mesurées ici, ni la hauteur de la rangée. */
@media (min-width: 768px) {
  .anima-init-row .anima-hp-bar-track {
    flex: 0 0 40px;
  }
}

/* Le HUD de combat flottant (`.combat-panel--compact`, posée par `@compact` et par lui
   seul) rend les mêmes rangées dans une fenêtre étroite sur un écran de bureau. Le
   seuil ci-dessus porte sur le VIEWPORT : il le classait du côté « il y a la place »
   alors qu'il n'y est pas, et il a donc fallu l'en exclure — la jauge y était invisible
   à toute largeur. Ce qui suit lui rend la barre SANS lui demander de largeur.

   La zone de PV est déjà un conteneur flex en `flex-wrap: wrap` (characters.css) : c'est
   ce qui permet à la pastille d'état de prendre sa propre ligne. La piste fait pareil.
   `flex-basis: 100%` ne coûte RIEN en largeur — la contribution intrinsèque d'un
   pourcentage indéfini est nulle, et la zone de PV reste dimensionnée par son libellé.

   Mesuré dans la vraie chaîne du HUD (fenêtre flottante → corps → panneau compact →
   liste), viewport 1920 px, sur les rangées de la page-témoin du dépôt plus une rangée
   mourante (le cas où la zone de PV se replie déjà, absent de cette page-témoin) :

     fenêtre   liste   jauge avant → après   nom avant → après   rangée avant → après
       240      190          0 → 62,7…70,7      inchangé            42,8 → 39,2
       300      250          0 → 65…70,7        inchangé            42,8 → 39,2
       360      310          0 → 70,7           inchangé            39,2 → 39,2
       420      370          0 → 70,7           inchangé            39,2 → 39,2

   « inchangé » est exact au dixième de pixel, rangée par rangée, aux quatre largeurs :
   la piste du nom la plus étroite vaut 54,3 px à 360 px de fenêtre avant comme après.
   La rangée mourante est la seule à payer quelque chose : 62,4 → 64,4 px de haut (la
   gouttière de 8 px entre libellé et pastille tombe à 2, la ligne de jauge en reprend 8).
   La hauteur de rangée est commandée par l'avatar (32 px) et la zone de PV repliée
   mesure 29,6 px : c'est `row-gap: 2px` qui la maintient sous les 32, et c'est pour ça
   qu'il est ici. À 8 px de gouttière la zone monte à 35,6 px et la rangée à 42,8.
   Au passage, les rangées chargées à 240 et 300 px cessent de se replier de travers :
   la hauteur redevient constante là où elle ne l'était pas.

   L'AUTRE piste a été mesurée puis écartée, pas devinée : un seuil porté sur le
   CONTENEUR (`container-type: inline-size` sur `.anima-combat-roster` + `@container`)
   fonctionne — la crainte qu'il piège les descendants `position: fixed` du roster
   (agrandissement de portrait, menu ⋯) et leur empilement est infondée en Chromium 1228,
   vérifié boîte par boîte et par `elementFromPoint`, avec et sans, résultats identiques.
   Mais il n'achète pas grand-chose : la jauge de 40 px n'est payable qu'à partir de
   400 px de liste (au-delà, le nom garde 104,3 px pour un plancher de 64), soit une
   fenêtre de 450 px — rien à 360, à 300 ni à 240, qui sont justement les largeurs du
   défaut. Sur la page, il ne changerait rien du tout : la liste y mesure 303 px à
   360 px de viewport et 365 px à 422, puis saute à 648,4 px à 768 — n'importe quel
   seuil de conteneur entre les deux reproduit exactement le rendu d'aujourd'hui.

   Ce que ça NE règle pas, et qui reste entier : sous 320 px de fenêtre, la piste du nom
   des rangées chargées vaut 0 px (mesuré à 300 comme à 240) contre un plancher de 64.
   C'est un défaut antérieur et distinct, qu'aucune garde ne couvre ; il n'est ni
   aggravé ni corrigé ici. */
.combat-panel--compact .anima-init-row .anima-hp-bar {
  row-gap: 2px;
}

.combat-panel--compact .anima-init-row .anima-hp-bar-track {
  flex: 0 0 100%;
}

/* ── La piste du nom dans le HUD : le défaut laissé ouvert par le bloc précédent ──

   La rangée est une grille SANS repli (`grid-auto-flow: column`) dont une seule colonne
   est élastique, celle du nom (`minmax(0, 1fr)`). Quand la somme des colonnes fixes
   dépasse la liste, c'est donc le nom, et lui seul, qui encaisse — jusqu'à ZÉRO. Et une
   boîte de zéro pixel en `overflow: hidden` ne rend AUCUN pixel : ni glyphe, ni points de
   suspension, qui coûtent à eux seuls 11,44 px à la taille du nom (mesuré). Le nom n'y
   est donc pas tronqué, il est ABSENT, et la rangée cesse de dire qui joue.

   Ce n'est pas une largeur que l'utilisateur a choisie : `DEFAULT.w` de
   components/ui/floating-window.js vaut 360, et à 360 la rangée de l'acteur du tour, une
   fois chargée, est déjà sous le plancher — 63,75 px pour 64, et 54,3 px si les polices
   web ne sont pas servies (c'est ce dernier chiffre qu'a relevé le bloc précédent). Pire,
   le tiroir « Historique » suffit à tout effacer sans toucher à la fenêtre :
   `.anima-combat-cols.is-combat.is-log-open` passe en deux colonnes sous un seuil de
   VIEWPORT — la même erreur de seuil que celle traitée ci-dessus — et un clic fait tomber
   la liste de 310 à 179,7 px, où plus aucune des huit rangées ne rend un caractère de nom.

   Ce qui suit ne pose AUCUN seuil, ni de viewport ni de conteneur : une fenêtre qu'on
   redimensionne au pixel n'offre aucune largeur contre laquelle calibrer un seuil. La
   rangée devient une ligne flex qui REPLIE, et le plancher de lisibilité est porté par le
   nom lui-même. Tout le reste est en `flex: 0 0 auto` : quand la ligne ne peut plus tenir
   la zone d'état (pastilles · PV · bouclier) sans pousser le nom sous son plancher, c'est
   la zone d'état qui descend, morceau par morceau. Et quand la ligne ne peut même plus
   tenir le nom derrière l'avatar, le nom descend à son tour et reprend son plancher plus
   bas — mesuré à 300 px de fenêtre, tiroir ouvert, où la liste ne fait plus que 142,8 px :
   la rangée s'étale sur trois lignes et le nom garde 65,58 px. La place disponible décide
   seule, à la largeur réelle du conteneur.

   Les 64 px sont le plancher de lisibilité que combat-roster-test.mjs surveille sur la
   page ; combat-hud-test.mjs relit cette valeur-ci dans la feuille et refuse de tourner si
   les deux ont divergé.

   Mesuré dans la même chaîne (fenêtre flottante → corps → panneau compact → liste),
   viewport 1920 px, polices RÉELLES chargées (Roboto 400), sur les huit rangées de la
   page-témoin. « muettes » compte celles dont la piste est plus étroite que les points de
   suspension eux-mêmes, donc sans un seul caractère lisible :

     fenêtre   liste    nom le plus étroit    muettes/8    hauteur de rangée
       240      190       0,00 →  79,36        8 → 0       39,19 → 70,80
       280      230       0,00 →  72,81        4 → 0       39,19 → 70,80
       300      250       3,75 →  71,33        3 → 0       39,19 → 39,19…70,80
       360      310      63,75 →  66,75        0 → 0       39,19 → 39,19…62,80
       420      370     123,75 → 123,75        0 → 0       39,19 → 39,19
     tiroir « Historique » ouvert, la liste tombe à :
       300      142,8     0,00 →  65,58        8 → 0       39,19 → 98,44
       360      179,7     0,00 →  69,05        8 → 0       39,19 → 70,80
       420      216,6     0,00 →  82,28        5 → 0       39,19 → 70,80

   À 420 px la fenêtre a la place et RIEN ne bouge : les huit noms et les huit hauteurs
   sont identiques au centième près. À 360, une seule rangée sur huit se replie.

   Ce que les autres zones y perdent : rien, et l'une d'elles y gagne. Les pistes des
   pastilles (0 · 8,64 · 32,33 · 52,19) et du bouclier (6,39) sont inchangées au centième
   près dans les huit états. L'initiative garde ses 36 px et l'avatar ses 32. La zone de
   PV, elle, CESSE de céder : la grille la comprimait de 70,67 à 62,67 px sur les rangées
   chargées — elle avait donc déjà, dans le HUD, le défaut d'ordre que l'assertion 4 de
   combat-roster-test.mjs traque sur la page — et elle vaut désormais 70,67 partout.
   Le débordement silencieux disparaît au passage : à 240 px la liste portait 230 px de
   contenu pour 190 px visibles, coupés sans barre de défilement par l'`overflow: hidden`
   de la fenêtre flottante. Contenu et conteneur coïncident maintenant dans les huit états.

   Ce que ça coûte : la hauteur de rangée, là où la rangée se replie. `row-gap: 2px` et
   non les 6 px de `gap` pour la même raison que sur la zone de PV juste au-dessus. */
.combat-panel--compact .anima-init-row {
  display: flex;
  flex-wrap: wrap;
  row-gap: 2px;
}

/* Personne ne cède, sauf le nom — et le nom ne cède que jusqu'à son plancher. C'est cette
   règle-ci qui transforme le manque de place en REPLI plutôt qu'en compression : sans
   elle, les zones voisines rétrécissent et la rangée reste sur une ligne illisible.
   Elle porte sur TOUS les enfants et non sur une liste de classes, parce qu'un enfant
   ajouté plus tard serait rétrécissable par défaut : la liste nommée serait juste, puis
   fausse en silence, et c'est le nom qui repaierait. */
.combat-panel--compact .anima-init-row > * {
  flex: 0 0 auto;
}

/* Reprend le minimum que la grille donnait à la piste d'initiative, pour la même raison :
   un score négatif à quatre caractères doit pouvoir l'élargir, jamais la rogner. */
.combat-panel--compact .anima-init-row > .anima-init-score {
  min-width: 2rem;
}

.combat-panel--compact .anima-init-row > .anima-init-name {
  flex: 1 1 64px;
  min-width: 64px;
}

.anima-init-badges {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
}

/* `flex: 0 0 auto` : les pastilles ne cèdent jamais de place. C'est le nom, et
   lui seul, qui absorbe le manque — cf. l'ordre de sacrifice de la grille. */
.anima-init-state-badge,
.anima-init-state-badge-more {
  font-size: 0.8rem;
  flex: 0 0 auto;
  line-height: 1.2;
}

/* Le compteur de surplus est PLAT : ni pastille, ni bordure, ni marge intérieure.
   Mesuré à 360 px, son cadre lui faisait coûter 34,0 px de piste — quatre fois
   une icône de pastille (8,6 px) pour afficher deux caractères. Il reste
   distinct des icônes par sa couleur (--muted) et par son « + », ce qui suffit :
   il ne se lit pas comme un état de plus, il annonce qu'il y en a d'autres. */
.anima-init-state-badge-more {
  color: var(--muted);
}

/* Adventure::ShieldGauge rend une pastille PAR bouclier, sans conteneur : en
   flux `column`, chacune prendrait sa propre colonne implicite. L'enveloppe les
   regroupe dans une seule zone.
   La pastille est ici rendue en forme `@iconOnly` (un 🛡 nu) : la forme longue
   (« 🛡 Bouclier 12/20 PV ») prenait 162,4 px à 360 px et la forme encadrée
   « 🛡 12/20 » encore 71,8 — dans les deux cas le nom tombait à 0. L'icône seule
   coûte 12,1 px. Les points restent dans le `title` et dans la feuille.
   Le plafond de largeur est TOMBÉ avec le chiffre, et c'est délibéré : `max-width`
   + `overflow: hidden` faisaient céder le bouclier APRÈS le nom — mesuré, la jauge
   se coupait à 33,2 px alors que le nom était déjà à 0. Un garde-fou qui ne
   protège que le second servi n'en est pas un ; c'est la taille de la zone qu'il
   fallait corriger, pas son débordement. Le harnais surveille désormais que la
   piste du bouclier est la même d'une rangée à l'autre, ce que ce plafond
   empêchait précisément de garantir. */
.anima-init-shields {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
}

/* ── Feuille du participant (Encounter::CombatParticipantSheet) ──
   Ce qui a quitté la rangée : modes, ajout d'effet, détail créature, bouton
   d'initiative, menu de gestion, compteur d'actions, étiquette PJ/PNJ. */

.anima-participant-sheet {
  border: 1px solid var(--accent);
  border-radius: 4px;
  background: var(--surface-2);
  padding: 8px 10px;
  margin: 2px 0 8px;
}

/* ── Fusion : un seul encadré quand la feuille et la barre d'action désignent le même
   participant ──
   combat-roster.hbs cède les deux sous des conditions indépendantes ; quand elles
   tombent ensemble, les deux boîtes se suivent. On les recoud ici en une seule, à deux
   sections : la feuille perd son bord bas et la barre son bord haut.

   LE `-2px` N'EST PAS UNE RETOUCHE OPTIQUE, il annule le `gap` du roster, et il faut une
   MARGE NÉGATIVE parce qu'un `gap` ne se surcharge pas depuis l'enfant. `.anima-combat-
   roster` est un flex colonne à `gap: 2px` (plus haut dans ce fichier) : mettre les marges
   à zéro ne ferme rien, le gap n'est pas une marge et reste posé entre les deux boîtes.
   Mesuré avant correctif, à 360 comme à 422 px : `barre.top − feuille.bottom = 2,00 px`,
   soit un liseré de fond de roster qui traverse la couture et INTERROMPT les bordures
   gauche et droite — l'encadré fusionné était plus ouvert qu'avant la fusion. Avec la
   marge négative : écart 0,00 px, bords latéraux alignés au centième (mesuré aussi).
   Donc : ne pas « nettoyer » ce `-2px` en le remettant à `0`. Le seul autre correctif
   serait de retirer le `gap` du roster, ce qui recollerait toutes les rangées entre elles.

   Piloté par l'ADJACENCE RÉELLE (`:has(+ …)`) et non par un drapeau de template, et
   c'est le point important : le contenu de `:actionBar` est encore sous condition côté
   panneau (`allHaveInitiative`, `activeAttacker`), il peut donc être VIDE alors que la
   roster a bien cédé le bloc. Un drapeau ouvrirait alors la feuille par le bas sans rien
   dessous ; `:has()` ne se déclenche que si la barre existe pour de bon.
   La barre garde son bord gauche épais en temps normal ; fusionnée, elle prend celui de
   la feuille pour que le cadre reste d'un seul tenant. */
.anima-participant-sheet:has(+ .anima-inline-action-bar) {
  border-bottom: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  margin-bottom: -2px; /* = -1 × le `gap` du roster ; cf. le bloc ci-dessus */
}

.anima-participant-sheet + .anima-inline-action-bar {
  margin-top: 0;
  margin-bottom: 8px;
  border: 1px solid var(--accent);
  border-top: 1px dashed var(--border);
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.anima-participant-sheet-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.anima-participant-sheet-name {
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.anima-participant-sheet-close {
  margin-left: auto;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.anima-participant-sheet-hp {
  margin-bottom: 6px;
}

.anima-participant-sheet-badges {
  list-style: none;
  margin: 0 0 6px;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-size: 0.85rem;
  color: var(--ink);
}

.anima-participant-sheet-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  align-items: center;
}

/* Le bouton d'ajout d'effet portait une icône seule dans la rangée ; dans la
   feuille il porte son libellé, donc il lui faut une bordure pour se lire
   comme un bouton. */
.anima-participant-sheet .anima-effect-add-btn {
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface-2);
  color: var(--ink);
  padding: 3px 10px;
  font-size: 0.8rem;
}

/* Encounter::CombatModeToggle n'avait AUCUNE règle : dans la rangée, ses deux
   boutons s'affichaient avec le style natif du navigateur. Vérifié par grep sur
   app/styles/ — `anima-mode-btn`, `anima-mode-toggle-group` et `anima-mode-error`
   n'apparaissaient nulle part. Le déménagement dans la feuille est l'occasion de
   les aligner sur `.anima-init-btn`. */
.anima-mode-toggle-group {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
}

.anima-mode-btn {
  font-size: 0.8rem;
  padding: 3px 10px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink);
  border-radius: 4px;
  cursor: pointer;
}

.anima-mode-btn.active {
  border-color: var(--accent);
  color: var(--accent);
}

.anima-mode-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.anima-mode-error {
  font-size: 0.8rem;
  color: var(--hp);
}

.anima-participant-sheet-shields {
  margin-top: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

/* ── Déclaration d'art martial du tour (Encounter::MartialArtDeclaration) ──
   Encadré à part dans la feuille : ce n'est pas un bouton d'action de plus,
   c'est un choix qui vaut jusqu'à ce qu'on le change. Les boutons d'option
   s'enroulent (`flex-wrap`) — le Kung Fu en propose cinq, et la feuille doit
   rester lisible dans le HUD flottant comme sur téléphone. */
.anima-ma-decl {
  margin-top: 6px;
  padding: 6px 8px;
  border: 1px dashed var(--border);
  border-radius: 4px;
}

.anima-ma-decl-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 4px;
}

.anima-ma-decl-title {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
}

.anima-ma-decl-clear {
  margin-left: auto;
  font-size: 0.8rem;
  padding: 2px 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink);
  border-radius: 4px;
  cursor: pointer;
}

.anima-ma-decl-clear.active {
  border-color: var(--accent);
  color: var(--accent);
}

.anima-ma-decl-group {
  margin-top: 4px;
}

.anima-ma-decl-art {
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 3px;
}

.anima-ma-decl-options {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

/* Colonne : le libellé de l'option au-dessus de son résumé chiffré. Mettre les
   deux côte à côte allongeait chaque bouton au point qu'un seul tenait par
   ligne pour le Kung Fu. */
.anima-ma-decl-opt {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--ink);
  border-radius: 4px;
  cursor: pointer;
  text-align: left;
}

.anima-ma-decl-opt.active {
  border-color: var(--accent);
  color: var(--accent);
}

.anima-ma-decl-opt-label {
  font-size: 0.8rem;
  font-weight: 600;
}

.anima-ma-decl-opt-effect {
  font-size: 0.8rem;
  color: var(--muted);
}

.anima-ma-decl-opt.active .anima-ma-decl-opt-effect {
  color: inherit;
}

.anima-ma-decl-hint {
  margin-top: 5px;
  font-size: 0.8rem;
  color: var(--muted);
}

.anima-participant-sheet-pools,
.anima-participant-sheet-immunities {
  margin-top: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  font-size: 0.85rem;
  color: var(--muted);
}

.anima-active-actor-label {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-right: 0.35rem;
}

/* Marqueur « le MJ agit hors de l'ordre d'initiative », rendu dans l'encadré FUSIONNÉ à
   la place du bloc d'identité. Il ne porte pas le nom : la tête de la feuille, juste
   au-dessus, l'affiche déjà. Ce qu'il rétablit, c'est la seule chose que « Action de X »
   disait de plus que « ▶ Tour de X » — le « ▶ » de la rangée signale le premier cas, rien
   ne signalait le second.
   Mêmes place et marge que le bloc qu'il remplace, pour que la ligne de navigation
   (« ← Acteur précédent », « ⏭ Joueur suivant ») ne se déplace pas d'un cas à l'autre. */
.anima-out-of-turn-marker {
  display: inline-flex;
  align-items: center;
  margin-right: 0.35rem;
  color: var(--accent);
}

.anima-init-badge {
  font-size: 0.8rem;
  padding: 1px 6px;
  background: var(--zeon-soft);
  border-radius: 3px;
  color: var(--zeon);
}

.anima-init-badge.npc {
  background: var(--hp-soft);
  color: var(--hp);
}

.anima-creature-pools {
  display: inline-flex;
  gap: 4px;
  align-items: center;
}

.anima-creature-pool {
  font-size: 0.8rem;
  padding: 1px 6px;
  border-radius: 3px;
  white-space: nowrap;
}

.anima-creature-pool--zeon {
  background: var(--zeon-soft);
  color: var(--zeon);
}

.anima-creature-pool--fatigue {
  background: var(--surface-2);
  color: var(--muted);
}

.anima-creature-detail-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.15));
  background: transparent;
  color: var(--ink);
  border-radius: 0.5rem;
  padding: 0.05rem 0.4rem;
  font-size: 0.8rem;
  line-height: 1.4;
  cursor: pointer;
}
.anima-creature-detail-toggle.is-open {
  background: var(--surface-alt, rgba(0, 0, 0, 0.06));
}
.anima-creature-detail-count {
  font-size: 0.8rem;
  white-space: nowrap;
}
.anima-creature-detail {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.25rem 0.5rem 0.4rem 2.2rem;
  font-size: 0.8rem;
  color: var(--ink);
}

.anima-init-btn {
  font-size: 0.8rem;
  padding: 3px 10px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--zeon);
  border-radius: 4px;
  cursor: pointer;
}

.anima-pj-init-form {
  display: flex;
  gap: 6px;
  margin-top: 6px;
  align-items: center;
}

.anima-pj-init-form input {
  max-width: 100px;
}

/* PAS DE `max-height` SUR `.anima-combat-log` HORS MODE COMPACT, et c'est une décision
   prise à la mesure, pas un oubli — le mode compact, lui, en a un (8rem, dans
   pages/characters-combat.css) et il ne contredit rien : là-bas le panneau est une petite
   fenêtre flottante au-dessus du tableau blanc, plafonner EST sa raison d'être.
   Ce qui a été mesuré à 360 px, journal ouvert à cinq entrées dont une dépliée :
   — le journal est le DERNIER bloc du panneau. Rien ne le suit — ni dans
     `.anima-combat-cols`, ni après. « Cinq entrées poussent tout ce qui suit » est faux :
     il n'y a rien à repousser, seulement une page plus longue ;
   — plafonner à 60 vh ferait passer la page de 2936 à 2677 px (4,3 → 3,9 écrans), à
     18 rem de 2936 à 2587 (3,7 écrans). On gagne donc entre 0,4 et 0,6 écran ;
   — le prix est un défilement DANS un conteneur qui défile déjà (673 px de contenu dans
     414 ou 324), au bas d'une page de quatre écrans — c'est-à-dire exactement là où le
     pouce arrive après avoir fait défiler. C'est le piège que l'incrément B a retiré de
     `.anima-modifier-list` et que garde l'assertion 2 du harnais des modales ;
   — et le journal est REPLIÉ PAR DÉFAUT (`combat-panel.js`, `logOpen = false`) : dans
     l'état par défaut, un plafond ne changerait rigoureusement rien. Il ne jouerait que
     dans l'état que l'utilisateur vient d'ouvrir POUR LIRE.
   Un demi-écran de défilement en moins ne paie pas un piège au doigt sur le seul écran
   qu'on ouvre pour lire. Si le journal devait un jour cesser d'être le dernier bloc, la
   première ligne de ce raisonnement tombe et la question se repose. */
.anima-combat-log-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  border-bottom: 1px dashed var(--border);
  font-size: 0.8rem;
}

.anima-combat-log-row .icon {
  font-size: 0.8rem;
}

.anima-combat-log-row .text {
  flex: 1;
  color: var(--muted);
}

.anima-combat-log-row .turn {
  font-family: monospace;
  font-size: 0.8rem;
  color: var(--muted);
}

.anima-combat-log-empty {
  color: var(--muted);
  font-size: 0.8rem;
  font-style: italic;
  padding: 4px 8px;
}

/* `.anima-modal-backdrop` et la boîte `.anima-combat-modal, .anima-modal` vivaient ici.
   Elles n'avaient rien de propre au combat — 37 templates emploient le fond, et la boîte
   s'appliquait aux quelque cinquante modales du projet. Ce fichier est chargé 16e sur les
   23 feuilles de index.html : sept le suivent, mais aucune ne redéclarait la boîte, si
   bien qu'il gagnait quand même contre la définition de components.css (6e), celle de
   pages/characters.css (10e) et une dizaine de largeurs de variantes. Tout est remonté dans
   components.css, le fichier des primitives, avec la media query mobile.
   `.anima-combat-modal` n'a plus de déclaration propre : la classe reste dans le markup
   (combat-action-modal.hbs) comme point d'accroche, mais rien de la boîte n'était
   spécifique au combat. */

.anima-combat-modal h4,
.anima-modal h4 {
  font-size: 0.95rem;
  margin-bottom: 14px;
  color: var(--accent);
}

.anima-modal-section {
  margin-bottom: 14px;
}

/* ── Aide critique : en-tête avec bouton « ? » + panneau rétractable (Tables 47/48) ── */
.anima-critical-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.anima-critical-header h4 {
  margin-bottom: 0;
}

.anima-critical-help {
  margin: 12px 0 16px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface-2);
  font-size: 0.8rem;
  line-height: 1.5;
}
.anima-critical-help-section {
  margin-bottom: 12px;
}
.anima-critical-help-section:last-child {
  margin-bottom: 0;
}
.anima-critical-help-heading {
  font-family: 'Cinzel', serif;
  font-size: 0.82rem;
  color: var(--accent);
  margin-bottom: 6px;
}
.anima-critical-help ol,
.anima-critical-help ul {
  margin: 0;
  padding-left: 1.15rem;
}
.anima-critical-help li {
  margin-bottom: 5px;
}
.anima-critical-help li:last-child {
  margin-bottom: 0;
}

.anima-critical-location-table {
  width: 100%;
  border-collapse: collapse;
}
.anima-critical-location-table td {
  padding: 3px 8px;
  border-bottom: 1px solid var(--border);
}
.anima-critical-location-table td:last-child {
  text-align: right;
  white-space: nowrap;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.anima-critical-loc-group td {
  font-weight: 600;
  color: var(--ink);
}
.anima-critical-loc-group td:last-child {
  color: var(--ink);
}
.anima-critical-loc-part td:first-child {
  padding-left: 1.6rem;
  color: var(--muted);
}

/* ── Résolution live : niveau/résistance/marge + Tables 47 & 48 surlignées ── */
.anima-critical-result {
  margin: 12px 0;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface-2);
}
.anima-critical-result-line {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.8rem;
  padding: 2px 0;
}
.anima-critical-result-line span {
  color: var(--muted);
}
.anima-critical-result-margin {
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
}
.anima-critical-result-margin strong {
  color: var(--accent);
  font-size: 0.95rem;
}
/* Effet et localisation : le libellé reste court, mais l'effet est une phrase entière
   (« Localisation requise. Malus = marge… »). Sur une rangée en `space-between` elle
   serait écrasée à droite ; ces deux-là s'empilent. */
.anima-critical-result-effect {
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
}
.anima-critical-result-effect strong {
  font-weight: 500;
  line-height: 1.45;
}

/* `.anima-critical-flow-section` a disparu du markup : les Tables 47 et 48 ne sont plus
   affichées en permanence dans le flux, elles sont rangées dans le panneau d'aide (leur
   rangée active, elle, est remontée en ligne de résultat ci-dessus). La mise en forme
   des deux tables suit dans le panneau. */
.anima-critical-effects-table,
.anima-critical-help .anima-critical-location-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8rem;
}
.anima-critical-effect-row td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
  color: var(--muted);
}
.anima-critical-effect-range {
  white-space: nowrap;
  font-weight: 600;
  color: var(--ink);
}

/* Ligne active = celle désignée par la marge (Table 47) ou le jet (Table 48). */
.anima-critical-effect-row.is-active td,
.anima-critical-loc-group.is-active td,
.anima-critical-loc-part.is-active td {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--ink);
  font-weight: 600;
}

/* `display: block` est ici parce que la balise porteuse varie : ce libellé s'écrit en
   `<label for>` dès qu'il désigne un champ, et en `<div>` quand il n'est qu'un titre de
   bloc. Un `<label>` est inline par défaut — sans cette déclaration il se coucherait à
   côté de son `<select>` et la marge basse ci-dessous ne produirait aucun espace. */
.anima-modal-label {
  display: block;
  font-size: 0.8rem;
  text-transform: uppercase;
  color: var(--muted);
  letter-spacing: 0.06em;
  margin-bottom: 6px;
  font-weight: 600;
}

.anima-weapon-slots {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.anima-weapon-slot {
  padding: 7px 14px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--zeon);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.8rem;
}

.anima-weapon-slot.active {
  border-color: var(--zeon);
  color: var(--zeon);
  background: var(--zeon-soft);
}

.anima-weapon-slot.disabled,
.anima-weapon-slot:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  filter: grayscale(1);
}

.anima-modifier-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 200px;
  overflow-y: auto;
}

.anima-modifier-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.8rem;
  color: var(--ink);
  cursor: pointer;
}

/* Sous le seuil mobile, la liste des douze modificateurs perd son plafond et son
   défilement propre : mesuré, elle demandait 265 px pour 200 visibles, soit une zone
   défilante DANS la zone défilante de la modale. Sur un écran tactile le geste est
   capté par le cadre intérieur et la modale ne bouge pas — c'est un piège au doigt, et
   c'est l'assertion 2 du harnais des modales.
   CETTE RÈGLE NE TRAITE QUE CETTE LISTE-LÀ. Le projet garde au moins un autre
   défilement imbriqué, `.anima-picker-list` (characters.css : `overflow-y: auto`,
   `max-height: 240px`) à l'intérieur du sélecteur d'objets, qui est lui-même en
   `overflow: hidden` avec sa propre hauteur maximale — c'est un motif différent,
   antérieur, et hors du périmètre de cette refonte. L'assertion 2 du harnais ne le voit
   pas : la page-témoin ne monte pas ce sélecteur. Vert ne veut donc pas dire « plus
   aucun défilement imbriqué dans le projet », mais « aucun dans les quatre modales
   mesurées ».
   La liste passe en rangée qui enveloppe : à 332 px de large, les douze libellés
   tiennent en six lignes au lieu de douze, ce qui rembourse la hauteur qu'on vient de
   déplafonner. La section est repliée par défaut, donc ces lignes ne coûtent rien tant
   qu'on ne les ouvre pas. */
@media (max-width: 767.98px) {
  .anima-modifier-list {
    flex-direction: row;
    flex-wrap: wrap;
    max-height: none;
    overflow-y: visible;
    gap: 4px 12px;
  }

  .anima-modifier-row {
    /* Une case par « colonne » d'environ la moitié de la largeur : deux par ligne quand
       les libellés sont courts, une seule quand ils sont longs. Sans cette base, un
       flex-wrap laisserait chaque case prendre sa largeur naturelle et la colonne de
       cases à cocher serait en dents de scie. */
    flex: 1 1 8.5rem;
    min-width: 0;
  }
}

/* `.anima-modal-actions` est remontée dans components.css (primitive : vingt modales
   l'emploient, dont les modales d'aide en jeu), avec son ancrage collant sous le seuil
   mobile. */

.anima-equip-buttons {
  display: flex;
  gap: 4px;
  margin-top: 4px;
  flex-wrap: wrap;
}

.anima-equip-btn {
  font-size: 0.8rem;
  padding: 1px 6px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--zeon);
  border-radius: 3px;
  cursor: pointer;
  transition: background 0.1s, border-color 0.1s;
}

.anima-equip-btn:hover {
  background: color-mix(in srgb, var(--zeon) 15%, var(--surface-2));
  border-color: var(--zeon);
}

.anima-equip-btn.active {
  background: var(--zeon);
  border-color: var(--zeon);
  color: var(--on-accent);
  font-weight: 600;
}

/* ── Unarmed Art Picker ─────────────────────────────────────────────── */
.anima-unarmed-art-picker { margin-top: 0.5rem; }
/* `display: block` pour la même raison que `.anima-modal-label` : la balise porteuse varie
   entre `<div>` (simple titre) et `<label for>` (libellé d'un champ), et un `<label>` est
   inline par défaut — il se coucherait à côté de son `<select>` en perdant sa marge basse. */
.anima-uap-section-label  { display: block; font-size: 0.8rem; color: var(--bs-secondary); margin-bottom: 0.25rem; }
.anima-uap-arts           { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.5rem; }
.anima-uap-art-btn        { padding: 0.2rem 0.6rem; border: 1px solid var(--bs-border-color); border-radius: 4px; background: transparent; font-size: 0.82rem; cursor: pointer; }
.anima-uap-art-btn.active { background: var(--bs-warning); border-color: var(--bs-warning); color: #000; } /* contrast pair for bs-warning, not our token system */
.anima-uap-degree-badge   { font-size: 0.8rem; opacity: 0.75; margin-left: 0.2rem; }
.anima-uap-damage-ref     { font-size: 0.8rem; color: var(--bs-info); margin: 0.25rem 0; }
.anima-uap-adv-ref        { font-size: 0.8rem; color: var(--bs-secondary); margin-bottom: 0.25rem; }
.anima-uap-options        { margin-top: 0.4rem; }
.anima-uap-option-row     { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; cursor: pointer; }
/* Rappel en lecture seule de la déclaration du tour : elle se CHOISIT dans la
   feuille du combattant (Encounter::MartialArtDeclaration), pas ici. */
.anima-uap-turn-decl      { font-size: 0.8rem; color: var(--muted); margin-top: 0.4rem; }
.anima-uap-godhand-badge  { font-size: 0.8rem; color: var(--bs-warning); font-weight: 600; }
.anima-uap-godhand-warn   { font-size: 0.8rem; color: var(--bs-warning); margin-top: 0.25rem; }

/* ── Impact calculator ──────────────────────────────────────────────── */
.anima-result-impact-section {
  border-top: 1px solid var(--bs-border-color);
  margin-top: 0.75rem;
  padding-top: 0.75rem;
}

.anima-result-impact-row {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-bottom: 0.5rem;
}

.anima-impact-result {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
  font-size: 0.9rem;
  font-weight: 600;
}

.anima-impact-gravity { padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.8rem; }
.anima-impact-gravity--G1 { background: var(--bs-secondary-bg); }
.anima-impact-gravity--G2 { background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); }
.anima-impact-gravity--G3 { background: var(--bs-orange-bg-subtle, #fdf0e0); color: #a0522d; } /* contrast pair for bs-orange-bg-subtle fallback, not our token system */
.anima-impact-gravity--G4 { background: var(--bs-danger-bg-subtle); color: var(--bs-danger-text-emphasis); }
.anima-impact-gravity--G5 { background: var(--bs-danger); color: #fff; } /* contrast pair for bs-danger, not our token system */

.anima-impact-note {
  font-size: 0.8rem;
  color: var(--bs-warning-text-emphasis);
  margin-top: 0.25rem;
}

.anima-impact-at-reduction {
  font-size: 0.8rem;
  color: var(--bs-secondary);
}

/* ── Modale d'auras Nemesis (en-tête froid « vide », cf. onglet Nemesis) ── */
.anima-nemesis-modal,
.anima-creature-aura-modal {
  max-width: 640px;
  max-height: 85vh;
  overflow-y: auto;
  padding: 1.25rem 1.5rem 1.5rem;
}

.nemesis-aura-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.nemesis-modal-title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  margin: 0 0 1rem;
  font-family: 'Cinzel', serif;
  font-size: 1.05rem;
  color: #6ea8c7;
}

.nemesis-section {
  margin-bottom: 1.25rem;
}
.nemesis-section:last-child {
  margin-bottom: 0;
}

.nemesis-section-label {
  font-family: 'Cinzel', serif;
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #6ea8c7;
  border-bottom: 1px solid rgba(110, 168, 199, 0.3);
  padding-bottom: 0.3rem;
  margin-bottom: 0.6rem;
}

/* Auras actives maintenues */
.nemesis-active-aura {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid rgba(110, 168, 199, 0.5);
  border-radius: 6px;
  background: rgba(110, 168, 199, 0.08);
  margin-bottom: 0.5rem;
}
.nemesis-active-name {
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.nemesis-active-targets {
  font-size: 0.8rem;
  color: var(--bs-secondary, #8a8a8a);
  margin-left: auto;
}
.nemesis-tag {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.05rem 0.35rem;
  border-radius: 4px;
  background: rgba(110, 168, 199, 0.2);
  color: #6ea8c7;
}

/* Lignes d'aura activables */
.nemesis-aura-row {
  border: 1px solid rgba(110, 168, 199, 0.3);
  border-radius: 6px;
  padding: 0.6rem 0.75rem;
  margin-bottom: 0.6rem;
  background: rgba(110, 168, 199, 0.04);
}
.nemesis-aura-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.nemesis-aura-name {
  font-weight: 600;
}
.nemesis-aura-toggles {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.nemesis-aura-desc {
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--bs-secondary, #8a8a8a);
  margin: 0.4rem 0 0.6rem;
}

/* Toggles (Greater / Concentrée / self-buffs) */
.nemesis-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.82rem;
  cursor: pointer;
}
.nemesis-selfbuff-toggle {
  display: flex;
  margin-bottom: 0.4rem;
}

/* Cibles */
.nemesis-aura-targets {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.75rem;
  margin-bottom: 0.6rem;
}
.nemesis-targets-label {
  font-size: 0.8rem;
  color: var(--bs-secondary, #8a8a8a);
}
.nemesis-target {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.82rem;
  padding: 0.15rem 0.45rem;
  border: 1px solid var(--border, #3a3a3a);
  border-radius: 4px;
  cursor: pointer;
}
.nemesis-hint {
  font-size: 0.8rem;
  font-style: italic;
  color: var(--bs-secondary, #8a8a8a);
}
.nemesis-empty {
  font-size: 0.85rem;
  color: var(--bs-secondary, #8a8a8a);
  font-style: italic;
}

/* Modale picker de capacités (catalogue magie / ki / nemesis) */
.capability-source-tabs {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-bottom: 0.6rem;
}
.capability-search {
  margin-bottom: 0.75rem;
}
.capability-list {
  max-height: 55vh;
  overflow-y: auto;
}
.capability-group {
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--accent, #6ea8c7);
  border-bottom: 1px solid var(--border, #3a3a3a);
  padding-bottom: 0.25rem;
  margin: 0.75rem 0 0.4rem;
}
.capability-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 0.6rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border, #3a3a3a);
  border-radius: 6px;
  background: var(--surface, transparent);
  color: var(--ink);
  margin-bottom: 0.35rem;
}
.capability-name {
  font-weight: 600;
}
.capability-cost {
  font-size: 0.8rem;
  color: var(--bs-secondary, #8a8a8a);
}
.capability-notes {
  font-size: 0.8rem;
  font-style: italic;
  color: var(--bs-secondary, #8a8a8a);
}
.capability-row .btn {
  margin-left: auto;
}

/* Auto-buffs défensifs */
.nemesis-armor-group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  margin-bottom: 0.6rem;
}
.nemesis-armor-label {
  font-size: 0.82rem;
  color: var(--bs-secondary, #8a8a8a);
}
.nemesis-radio {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.82rem;
  cursor: pointer;
}
.nemesis-save-selfbuffs {
  margin-top: 0.5rem;
}

/* ── Les surfaces d'action sur téléphone (1/2 — ce que ce fichier possède) ──
   Aucune surface d'action n'avait de règle mobile. Relevé à 360 px sur la page-témoin du
   cadre AVANT toute retouche, parce que resserrer au jugé aurait coûté de la lisibilité là
   où il n'y avait rien à gagner :
     sélecteur d'acteur   9 boutons sur 9 LIGNES, 435,8 px de haut
     barre d'action      12 boutons sur 8 lignes, 353,5 px
     barre MJ du roster   4 boutons sur 4 lignes, 165,4 px
     feuille              20 px de gouttière intérieure, 281 px utiles sur 303
     barre d'action       24 px de gouttière intérieure, 277 px utiles
     tableau du journal   28 px + 3 de bordure, 256 px utiles sur 287
   Le coût dominant est VERTICAL et il vient du sélecteur : une pastille par ligne. Le
   panneau est déjà à 6 px de gouttière ; ce qui s'ajoute à l'intérieur se paie deux fois,
   d'où les gouttières intérieures ramenées au même ordre.

   EN FIN DE FICHIER, et c'est nécessaire : une media query n'ajoute PAS de spécificité.
   Placé plus haut, ce bloc perdait contre la déclaration de `.anima-participant-sheet`
   qui le suit dans ce même fichier — vérifié à la mesure, la gouttière restait à 20 px.
   La moitié qui vise des règles de encounter-combat-phase2.css est au bout de CE
   fichier-là, pour la même raison : il est chargé après celui-ci. */
@media (max-width: 767.98px) {
  .anima-participant-sheet {
    padding: 8px 6px;
  }

  /* Le sélecteur d'acteur passe en PASTILLES. C'est un choix — « qui joue maintenant ? »
     est une question, pas une action — et c'est ce qui le sort de son unique colonne :
     `max-width` garantit deux pastilles par ligne (la moitié, moins la demi-gouttière),
     et le nom cède en s'abrégeant au lieu de réclamer la ligne entière.
     Le nom est le SEUL élément qu'on abrège, et seulement ici : la rangée d'initiative
     garde son plancher de lisibilité, et l'avatar reste à côté pour lever l'ambiguïté. */
  .anima-combat-hint .anima-action-pick-btn {
    max-width: calc(50% - 3px);
    min-width: 0;
    padding: 3px 8px 3px 4px;
  }

  .anima-combat-hint .anima-action-pick-btn > span:last-child {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Le catalogue de capacités cesse d'être une boîte défilante dans une modale qui
     défile. Mesuré : 4 541 px de contenu dans 380 px de boîte — c'est le deuxième pire
     défilement imbriqué du corpus, après le corps de modale traité dans
     components.css. Deux modales concernées, le sélecteur d'auras et celui de
     capacités, et sur les deux la liste EST le contenu : rien ne se perd à laisser la
     modale la porter en entier. */
  .capability-list {
    max-height: none;
    overflow-y: visible;
  }
}
