:root {
  --header-font: "Cinzel Decorative", serif;
  --body-font: "Spectral", serif;
  --radius-sm: 12px;
  --radius-md: 18px;
  --radius-lg: 26px;

  /* Palette unique. Le theme clair a ete retire : il empechait d'avoir un
     decor coherent et doublait la surface a verifier a chaque ecran.
     Fond souille, tres desature, pour que l'or ressorte sans crier. */
  --bg: #0c0b09;
  --bg-accent: #14120e;
  --surface: rgba(24, 21, 16, 0.94);
  --surface-strong: #1b1712;
  --surface-muted: #211c15;
  --text: #e8dcc0;
  --text-soft: #b8a888;
  --text-faint: #7f7259;
  --border: rgba(198, 172, 116, 0.14);
  --border-strong: rgba(198, 172, 116, 0.3);
  /* Or terni : le seul accent chaud, reserve a ce qui compte. */
  --accent: #c6ac74;
  --accent-soft: rgba(198, 172, 116, 0.12);
  --accent-strong: #8f9a5c;
  --danger: #b8543a;
  --success: #7f9b61;
  --info: #6d90b4;
  --active-btn: #2a2318;
  --hover-btn: #3a3122;
  --pressed-btn: #4a3c26;
  --disabled-btn: #423a2f;
  --card-bg: #1a1611;
  --shadow-soft: 0 18px 40px rgba(0, 0, 0, 0.45);
  --shadow-card: 0 22px 54px rgba(0, 0, 0, 0.55);
  --glow-accent: 0 0 24px rgba(198, 172, 116, 0.18);
  /* Fonds translucides : le decor du camp doit teinter les panneaux sans
     jamais passer devant le texte. */
  --panel-veil: rgba(24, 20, 15, 0.78);
  --shell-veil: rgba(12, 11, 8, 0.28);
  /* Voile et flou volontairement tres faibles : un voile blanc a 32% lavait
     completement le decor du camp derriere la coque, et un flou de 12px le
     reduisait en bouillie. */
  --shell-blur: blur(1px);
  --shell-sheen: linear-gradient(180deg, rgba(198, 172, 116, 0.05), transparent);
  --panel-sheen: linear-gradient(180deg, rgba(198, 172, 116, 0.06), transparent);
  /* Etait utilise 7 fois dans la feuille sans avoir jamais ete defini :
     toutes ces declarations etaient invalides et silencieusement ignorees,
     ce qui expliquait des fonds de rarete inexistants. */
  --surface-subtle: #191510;

  /* Rarete des objets. Une couleur par palier, assez distincte pour se lire
     au coup d'oeil dans une grille. */
  --rarity-commun: #6e6656;
  --rarity-rare: #6d90b4;
  --rarity-legendaire: #c6ac74;
  --rarity-relique: #b8543a;
}

/* Couleurs d'attribut : elles doivent rester identifiables d'un coup d'oeil
   et ne pas se confondre avec l'accent. */
:root {
  --stat-vigor: #48a352;
  --stat-strength: #b8683c;
  --stat-dexterity: #dcb420;
  --stat-intelligence: #4b93d6;
}

* {
  box-sizing: border-box;
}

/* Barres de defilement. Sans ca le navigateur les rend en blanc et gris sur
   Windows, ce qui trouait la palette des que le contenu debordait. Firefox
   utilise scrollbar-color, les moteurs WebKit les pseudo-elements : les deux
   sont necessaires. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--border-strong) 80%, transparent);
  border: 2px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--accent) 55%, transparent);
  background-clip: content-box;
}

::-webkit-scrollbar-corner {
  background: transparent;
}

html,
body {
  min-height: 100%;
}

body {
  margin: 0;
  background: radial-gradient(
      circle at top left,
      rgba(197, 173, 115, 0.08),
      transparent 32%
    ),
    radial-gradient(
      circle at bottom right,
      rgba(118, 151, 190, 0.08),
      transparent 28%
    ), linear-gradient(180deg, #0f0d0b, #171410);
  color: var(--text);
  font-family: var(--body-font);
  overflow-x: hidden;
}

button,
input,
label,
select {
  font-family: inherit;
}

h1,
h2,
h3,
h4 {
  font-family: var(--header-font);
  color: var(--text);
  margin: 0;
}

p {
  margin: 0;
}

button {
  appearance: none;
  border: 1px solid var(--border-strong);
  background: linear-gradient(
    180deg,
    var(--surface-strong),
    var(--surface-muted)
  );
  color: var(--text);
  border-radius: 10px;
  padding: 0.8rem 1rem;
  font-weight: 700;
  cursor: pointer;
  transition: transform 0.18s ease, background-color 0.18s ease, border-color
    0.18s ease, box-shadow 0.18s ease;
  box-shadow: 0 10px 20px rgba(70, 54, 28, 0.08);
}

button:hover {
  transform: translateY(-1px);
  border-color: var(--accent);
  box-shadow: 0 14px 26px rgba(70, 54, 28, 0.12);
}

button:active {
  transform: translateY(1px);
}

button:disabled {
  opacity: 0.55;
  transform: none;
  cursor: not-allowed;
  box-shadow: none;
}

#fire-particles {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  overflow: hidden;
}

#fire-particles.hidden {
  opacity: 0;
}

/* Braises. Carrees et non rondes : un degrade radial jure a cote du pixel art.
   Chaque braise porte sa propre derive horizontale et sa propre teinte, tirees
   au sort a la creation — sans ca les 50 montent en colonnes paralleles. */
.particle {
  position: absolute;
  bottom: -30px;
  background-color: var(--ember-color, #ec984c);
  box-shadow: 0 0 6px var(--ember-color, #ec984c);
  animation: driftUp linear infinite;
  opacity: 0;
  image-rendering: pixelated;
}

@keyframes driftUp {
  from {
    transform: translate3d(0, 0, 0);
    opacity: 0;
  }
  12% {
    opacity: var(--ember-peak, 0.5);
  }
  70% {
    opacity: var(--ember-peak, 0.5);
  }
  to {
    transform: translate3d(var(--drift-x, 16px), -105vh, 0);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .particle {
    animation: none;
    opacity: 0.25;
  }
}

#game-container {
  position: relative;
  z-index: 1;
  width: min(1320px, calc(100vw - 24px));
  margin: 12px auto;
  min-height: calc(100vh - 24px);
  padding: 20px;
  border-radius: 28px;
  border: 1px solid var(--border-strong);
  background: var(--shell-sheen), var(--shell-veil);
  backdrop-filter: var(--shell-blur);
  box-shadow: var(--shadow-card);
}

/* Cette surcharge est plus specifique que la regle #game-container : elle doit
   utiliser les memes jetons, sinon le decor du camp reste cache derriere un
   fond a 94% d'opacite. */
#game-container {
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.14)),
    var(--shell-sheen), var(--shell-veil);
}

.app-header {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 18px;
  padding-bottom: 18px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--border);
}

.brand-kicker,
.screen-kicker,
.panel-kicker,
.slot-label,
.combat-card__eyebrow,
.log-column-title,
.detail-label,
.battle-intel-chip span,
#battle-meta {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.75rem;
  color: var(--text-faint);
}

.brand-lockup h1 {
  font-size: clamp(1.8rem, 3vw, 2.5rem);
}

.primary-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}

.nav-btn {
  min-width: 92px;
  border-radius: 8px;
}

.nav-btn.is-active {
  background: linear-gradient(
    180deg,
    var(--accent-soft),
    rgba(255, 255, 255, 0.65)
  );
  border-color: var(--accent);
}

.header-tools {
  display: flex;
  align-items: center;
  gap: 14px;
}

.currency-panel {
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: right;
}

.currency-panel span {
  display: inline-block;
  margin-left: 8px;
  font-weight: 700;
}

.risk {
  color: var(--danger);
}

.menu-button,
.safe-btn {
  width: 100%;
}

#camp-view,
.combat-view {
  display: block;
}

/* ------------------------------------------------------------------ */
/* Decor du camp                                                      */
/* ------------------------------------------------------------------ */

#camp-view {
  position: relative;
}

/* Fond fixe a l'echelle du viewport, et non absolu a la hauteur du camp :
   #camp-view peut faire 1700px de haut, et un background-size: cover sur une
   image de 480x270 y produisait un agrandissement absurde ou l'on ne voyait
   plus qu'un morceau de l'Arbre-Monde. En fixe, l'echelle reste raisonnable et
   le decor se comporte comme un veritable arriere-plan quand on defile. */
#camp-scene {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* Le decor s'efface vers le bas : les panneaux n'ont pas a se battre contre
     un sol charge. */
  -webkit-mask-image: linear-gradient(
    180deg,
    #000 0%,
    #000 52%,
    transparent 96%
  );
  mask-image: linear-gradient(180deg, #000 0%, #000 52%, transparent 96%);
  transition: opacity 400ms ease;
}

#camp-scene.hidden {
  opacity: 0;
}

.camp-scene__layer {
  position: absolute;
  inset: 0;
  /* Decale au defilement par ui.js, a un rythme different par calque. */
  transform: translate3d(0, var(--parallax, 0px), 0);
  will-change: transform;
  background-repeat: no-repeat;
  background-position: center top;
  /* La largeur pilote l'echelle, la hauteur suit. background-size: cover sur
     une image 16:9 dans un viewport presque carre recadrait a l'extreme : on ne
     voyait plus qu'une bande de ciel, l'Arbre-Monde etait hors cadre. */
  background-size: 100% auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* Les trois calques sont cales sur la meme image de 480x270 : ils doivent
   partager exactement le meme cadrage, sinon l'horizon se dedouble. */
.camp-scene__sky {
  background-image: url("assets/sprites/scene/camp-sky.png");
}

.camp-scene__mid {
  background-image: url("assets/sprites/scene/camp-mid.png");
}

.camp-scene__near {
  background-image: url("assets/sprites/scene/camp-near.png");
}

/* Halo de grace qui respire, cale sur l'Arbre-Monde de l'image (a 73% de la
   largeur, 26% de la hauteur). C'est ce qui empeche la scene de paraitre morte
   sans imposer d'animation lourde. */
.camp-scene__grace {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    circle at 73% 26%,
    rgba(232, 192, 106, 0.2),
    transparent 26%
  );
  animation: grace-breathe 7s ease-in-out infinite;
}

@keyframes grace-breathe {
  0%,
  100% {
    opacity: 0.45;
  }
  50% {
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .camp-scene__grace {
    animation: none;
    opacity: 0.7;
  }
}

/* Les ecrans passent devant le decor. */
#camp-view > .app-screen {
  position: relative;
  z-index: 1;
}

.app-screen {
  display: none;
}

.app-screen.is-active {
  display: block;
}

.screen-shell {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.screen-heading {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 14px;
}

.screen-heading h2 {
  margin-top: 4px;
  font-size: clamp(1.75rem, 2.6vw, 2.5rem);
  letter-spacing: 0.03em;
}

.screen-copy,
.section-head p,
.panel-note,
.detail-path-role,
.combat-card__sub,
#battle-hint {
  color: var(--text-soft);
}

.camp-panel,
.battle-panel,
.options-page {
  background: var(--panel-sheen), var(--panel-veil);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 18px;
  box-shadow: var(--shadow-soft);
  backdrop-filter: blur(3px);
}

.battle-panel {
  position: relative;
  overflow: hidden;
}

.battle-panel::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(255, 255, 255, 0.28),
    transparent
  );
  opacity: 0.55;
}

.camp-panel,
.battle-panel,
.options-page,
.combat-card {
  background: var(--panel-sheen), var(--panel-veil);
}

/* Les panneaux de combat n'ont pas de decor derriere eux : ils restent opaques,
   pour ne pas perdre en lisibilite pendant l'action. */
.battle-panel,
.combat-card {
  background: var(--panel-sheen), var(--surface-strong);
}

.section-head,
.panel-topline,
.biome-detail-header,
.biome-detail-actions,
.battle-heading,
.options-zone {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
}

.hub-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(340px, 0.9fr);
  gap: 18px;
}

/*
 * Colonne de droite du Hub : contrat puis actions principales.
 *
 * Les deux cartes etaient des enfants directs de .hub-grid. Le contrat se
 * retrouvait donc seul en face de l'equipement et, la grille etirant ses
 * cellules, il prenait toute la hauteur de celui-ci pour n'afficher que six
 * lignes — un grand vide sous le bouton « Abandonner ». Les actions, elles,
 * atterrissaient a gauche sur la rangee suivante.
 *
 * Les empiler dans un conteneur unique rend a chaque carte sa hauteur de
 * contenu. C'est le conteneur, et lui seul, qui s'etire sur la hauteur de la
 * rangee : `flex: 0 0 auto` sur les cartes leur interdit de se partager
 * l'espace restant, qui devient du vide sous la derniere plutot que du vide
 * dans les deux.
 */
.hub-side-stack {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-width: 0;
}

.hub-side-stack > .camp-panel {
  flex: 0 0 auto;
}

.hero-panel {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.hero-panel__figure {
  display: flex;
  align-items: center;
  gap: 18px;
}

/* Le portrait est pose sur un socle rond avec une lueur : le sprite fait
   32x32, il a besoin d'un cadre pour ne pas flotter dans le vide. */
.hero-portrait {
  position: relative;
  flex: 0 0 auto;
  width: 160px;
  height: 160px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: radial-gradient(
      circle at 50% 78%,
      var(--accent-soft),
      transparent 62%
    ), var(--surface-muted);
  border: 1px solid var(--border-strong);
  box-shadow: inset 0 -18px 40px rgba(0, 0, 0, 0.35);
  overflow: hidden;
}

.hero-portrait::after {
  /* Ombre portee sous les pieds du sprite. */
  content: "";
  position: absolute;
  bottom: 26px;
  width: 66px;
  height: 11px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.42);
  filter: blur(3px);
}

.hero-portrait__canvas {
  position: relative;
  z-index: 1;
  /* Le canvas a une taille en pixels fixe (32 x l'echelle). Sans ces deux
     regles il debordait du cadre rond aux petites largeurs et le sprite se
     faisait rogner par l'overflow: hidden. */
  max-width: 100%;
  height: auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

.hero-identity {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.hero-identity h3 {
  margin: 0;
  font-size: 1.35rem;
  line-height: 1.15;
  color: var(--accent);
}

.hero-identity__note {
  margin: 0;
  color: var(--text-soft);
  font-size: 0.9rem;
  line-height: 1.45;
}

.hero-stats {
  /* Grille commune aux quatre lignes plutot qu'une grille par ligne : la
     colonne de libelle s'adapte au plus long ("Intelligence", qui debordait
     sous la barre avec les 108px fixes d'avant) et les quatre barres restent
     alignees entre elles. */
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) 62px;
  gap: 10px 12px;
}

.hero-stats__head,
.hero-stats > :not(.hero-stat) {
  grid-column: 1 / -1;
}

.hero-stats__head {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  padding-bottom: 14px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}

.hero-stats__head > div {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.hero-stats__head strong {
  font-family: var(--header-font);
  font-size: 1.2rem;
}

.hero-stat {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}

.hero-stat__label {
  /* Sans ca le libelle se coupait en deux et passait sous la barre. */
  white-space: nowrap;
}

.hero-stat__label {
  font-size: 0.86rem;
  color: var(--text-soft);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.hero-stat__track {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
  overflow: hidden;
}

.hero-stat__fill {
  height: 100%;
  min-width: 2px;
  border-radius: 999px;
  transition: width 260ms ease;
}

.hero-stat__value {
  font-family: var(--header-font);
  font-size: 1rem;
  text-align: right;
}

.hero-stat__value small {
  color: var(--success);
  font-family: var(--body-font);
  font-size: 0.78rem;
  margin-left: 3px;
}

/* La stat dominante est celle qui decide de l'apparence : on le montre. */
.hero-stat.is-dominant .hero-stat__label {
  color: var(--accent);
}

.hero-stat.is-dominant .hero-stat__value {
  color: var(--accent);
}

.hub-focus-card {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.hub-focus-card__title {
  font-size: 1.55rem;
  margin-bottom: 6px;
}

.hub-focus-card__stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.hub-focus-card__stat {
  padding: 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

.hub-cta-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

#battle-header-panel {
  padding: 16px 18px;
}

#battle-meta {
  color: var(--accent);
}

#battle-hint {
  font-size: 0.92rem;
}

#current-biome-header {
  font-size: clamp(1.9rem, 3vw, 2.6rem);
}

.combat-card {
  min-height: 118px;
}

.combat-card__name {
  font-size: 1.55rem;
  line-height: 1.1;
}

.slots-container {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-top: 12px;
}

.slot-wrapper {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.slot {
  width: 100%;
  min-height: 118px;
  padding: 14px;
  display: flex;
  align-items: center;
  gap: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--text);
  text-align: left;
  transition: border-color 160ms ease, background 160ms ease;
}

.slot:hover {
  border-color: var(--border-strong);
  background: var(--surface-strong);
}

.slot-text {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

.slot-item-name {
  font-family: var(--header-font);
  font-size: 0.95rem;
  line-height: 1.35;
}

.slot-item-meta {
  color: var(--text-soft);
  font-size: 0.82rem;
}

.slot-empty .slot-item-name,
.slot-empty .slot-item-meta {
  color: var(--text-faint);
}

/* ------------------------------------------------------------------ */
/* Icones pixel                                                       */
/* ------------------------------------------------------------------ */

/* Toutes les icones sont des decoupes d'atlas 16x16 agrandies : sans
   image-rendering explicite le navigateur les lisse et le pixel art devient
   une bouillie. Les deux valeurs sont volontaires, les moteurs ne supportent
   pas le meme mot-cle. */
/* Cet element ne porte QUE le decoupage de l'atlas : pas de padding, pas de
   bordure, pas de raccourci `background`. Toute decoration va sur le cadre
   (.item-icon / .slot-icon), sinon les cellules voisines de l'atlas
   reapparaissent — un padding fait peindre le fond sur toute la boite, et le
   raccourci `background` reinitialise background-clip. */
.pixel-icon {
  display: block;
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* Emplacement vide, ou icone pas encore mappee : un cadre en pointilles
   plutot qu'un trou, pour que la grille reste lisible. */
.map-screen-panel {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.atlas-legend {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.legend-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0.4rem 0.75rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-muted);
  color: var(--text-soft);
  font-size: 0.78rem;
}

.legend-chip.unlocked {
  color: var(--success);
}

.legend-chip.reachable {
  color: var(--info);
}

.legend-chip.locked {
  color: var(--text-faint);
}

.legend-chip.current {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.legend-chip.selected {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
}

/* Carte et navigateur de chapitres cote a cote des qu'il y a la place. En une
   seule colonne, la carte et la liste s'empilaient et il fallait faire
   l'aller-retour en defilant pour comparer deux zones. */
.map-screen-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}

@media (min-width: 1000px) {
  .map-screen-grid {
    grid-template-columns: minmax(0, 1fr) minmax(340px, 400px);
    align-items: start;
  }
}

#world-map-shell {
  position: relative;
  min-height: 380px;
  height: clamp(380px, 58vh, 640px);
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
  background: linear-gradient(180deg, #f6efdf, #eaddc6);
  overflow: hidden;
}

/* La carte reste en vue pendant qu'on parcourt la fiche et les chapitres.
   Sans ca, la colonne de droite fait 1300px, la carte 520px, et on defile sur
   800px de vide a gauche.

   Ce bloc doit rester APRES la regle #world-map-shell ci-dessus : une media
   query n'ajoute pas de specificite, et `position: relative` reprendrait le
   dessus par simple ordre de source. */
@media (min-width: 1000px) {
  #world-map-shell {
    position: sticky;
    top: 16px;
    align-self: start;
    /* Elle occupe presque toute la hauteur de la fenetre : epinglee, une carte
       courte laissait un vide sous elle pendant tout le defilement. Le -140px
       garde la marge haute et empeche la carte de depasser la fenetre, ce qui
       annulerait l'epinglage. */
    height: min(760px, calc(100vh - 140px));
  }
}

#world-map {
  position: absolute;
  inset: 0;
}

#world-map canvas {
  border-radius: inherit;
}

#world-map::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    circle at center,
    rgba(255, 255, 255, 0.03),
    transparent 60%
  );
}

#world-map::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    circle at center,
    rgba(197, 173, 115, 0.04),
    transparent 38%
  );
  opacity: 0.8;
}

#world-map-shell {
  background: radial-gradient(
      circle at 25% 18%,
      rgba(160, 136, 89, 0.22),
      transparent 24%
    ),
    radial-gradient(
      circle at 77% 74%,
      rgba(95, 127, 159, 0.18),
      transparent 22%
    ), linear-gradient(180deg, #241d16, #15120f);
}

/*
 * Parchemin de fond.
 *
 * L'illustration porte le vocabulaire du jeu — pierre fissuree, filigrane
 * dore, arches, l'Arbre — la ou les deux degrades precedents ne disaient rien.
 *
 * Elle vit dans ce pseudo-element et non sur le conteneur, pour une raison
 * precise : `opacity` sur #world-map-shell aurait aussi delave le graphe, qui
 * est son enfant. Ici seule l'image est attenuee ; les noeuds et les libelles
 * gardent leur contraste plein.
 *
 * Elle est aussi ASSOMBRIE par un voile. Son centre est un beige clair, or les
 * libelles de la carte sont clairs eux aussi : telle quelle, le texte se
 * dissolvait dans le parchemin. C'est ce voile, plus que l'opacite, qui rend la
 * lecture possible.
 *
 * La trame de 54px est conservee par-dessus : elle donne l'echelle et survit a
 * l'image sans la concurrencer.
 */
#world-map-shell::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(rgba(0, 0, 0, 0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0, 0, 0, 0.04) 1px, transparent 1px),
    linear-gradient(
      180deg,
      rgba(16, 13, 10, 0.74),
      rgba(16, 13, 10, 0.58) 45%,
      rgba(16, 13, 10, 0.8)
    ), url("assets/ui/carte-fond.webp");
  background-size: 54px 54px, 54px 54px, cover, cover;
  background-position: 0 0, 0 0, center, center;
  opacity: 0.9;
  pointer-events: none;
}

#world-map-paths,
#world-map {
  position: absolute;
  inset: 0;
}

.atlas-path {
  stroke: rgba(102, 86, 56, 0.38);
  stroke-width: 0.36;
  stroke-dasharray: 2 1;
}

.atlas-path {
  stroke: rgba(219, 200, 137, 0.28);
}

.world-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 999px;
  background: rgba(255, 251, 243, 0.92);
  border: 1px solid var(--border-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 14px 26px rgba(82, 68, 39, 0.14);
}

.world-node {
  background: rgba(26, 21, 16, 0.92);
}

.world-node__dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: currentColor;
}

.world-node.unlocked {
  color: var(--success);
}

.world-node.reachable {
  color: var(--info);
}

.world-node.selected {
  transform: translate(-50%, -50%) scale(1.08);
  box-shadow: 0 0 0 4px rgba(134, 113, 66, 0.16), 0 18px 30px
    rgba(82, 68, 39, 0.18);
}

/* Empilee par defaut. Cette colonne etait une grille a deux colonnes, ce qui
   convenait quand elle occupait toute la largeur sous la carte ; depuis
   qu'elle est la colonne de droite, ses deux enfants s'y ecrasaient. Elle ne
   reprend deux colonnes que dans la fenetre ou elle repasse en pleine
   largeur. */
.map-side-column {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  min-height: 0;
  align-items: start;
}

@media (min-width: 720px) and (max-width: 999px) {
  .map-side-column {
    grid-template-columns: minmax(0, 1.15fr) minmax(280px, 0.85fr);
  }
}

/* ------------------------------------------------------------------ */
/* Commandes de la carte                                              */
/* ------------------------------------------------------------------ */

.map-controls {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 2;
  display: flex;
  gap: 6px;
  padding: 6px;
  border-radius: var(--radius-sm);
  background-color: var(--surface);
  border: 1px solid var(--border);
  backdrop-filter: blur(4px);
}

.map-controls button {
  padding: 0.4rem 0.7rem;
  font-size: 0.8rem;
  font-weight: 700;
  border-radius: 8px;
  box-shadow: none;
}

.map-controls button:first-child,
.map-controls button:nth-child(2) {
  /* Les deux boutons de zoom sont des carres : un "+" seul dans un bouton
     large flotterait au milieu de rien. */
  min-width: 34px;
  padding: 0.4rem 0;
  text-align: center;
}

/* ------------------------------------------------------------------ */
/* Navigateur par chapitres                                           */
/* ------------------------------------------------------------------ */

#biome-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
  max-height: clamp(320px, 52vh, 560px);
  overflow-y: auto;
  padding-right: 4px;
}

.chapter-group {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background-color: var(--surface-subtle);
  overflow: hidden;
}

.chapter-group__head {
  width: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  padding: 0.6rem 0.8rem;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  text-align: left;
}

.chapter-group__head:hover {
  background-color: var(--hover-btn);
  transform: none;
}

.chapter-group__name {
  font-family: var(--header-font);
  font-size: 0.92rem;
}

.chapter-group__count {
  font-size: 0.76rem;
  color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}

.chapter-group__chevron {
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--text-soft);
  transition: transform 160ms ease;
}

.chapter-group.is-collapsed .chapter-group__chevron {
  transform: rotate(-90deg);
}

.chapter-group__body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 8px 8px;
}

.chapter-group.is-collapsed .chapter-group__body {
  display: none;
}

/* Une zone verrouillee reste cliquable : on peut lire sa fiche pour preparer
   la suite. Elle est seulement mise en retrait. */
.biome-shortcut.is-locked {
  opacity: 0.5;
}

.biome-shortcut.is-current {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}

.biome-shortcut.is-wip .biome-shortcut__title::after {
  content: " · chantier";
  color: var(--text-faint);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
}

.biome-detail-card {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-height: 100%;
  background: var(--panel-sheen), var(--panel-veil);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 18px;
  box-shadow: var(--shadow-soft);
  backdrop-filter: blur(3px);
}

.biome-detail-card h4 {
  font-size: 1.5rem;
}

.danger-badge {
  padding: 0.45rem 0.8rem;
  border-radius: 8px;
  font-size: 0.76rem;
  font-weight: 700;
  border: 1px solid var(--border);
  background: var(--surface-muted);
}

/*
 * Danger RELATIF au personnage.
 *
 * Les anciennes classes suivaient l'etiquette fixe du biome — faible, modere,
 * eleve — qui ne bougeait jamais quel que soit le niveau atteint. Elles sont
 * remplacees par les cinq crans de getDangerRelatif, qui se repeignent a
 * chaque montee de niveau.
 */
.danger-badge.danger-promenade {
  color: var(--success);
}

.danger-badge.danger-mesure {
  color: var(--info);
}

.danger-badge.danger-dangereux {
  color: var(--accent);
}

.danger-badge.danger-mortel,
.danger-badge.danger-suicidaire {
  color: var(--danger);
}

/* Le pire cran se signale aussi par son fond : le texte seul se rate. */
.danger-badge.danger-suicidaire {
  border-color: color-mix(in srgb, var(--danger) 55%, var(--border));
  background: color-mix(in srgb, var(--danger) 14%, var(--surface-muted));
}

.biome-focus {
  color: var(--text-soft);
  line-height: 1.5;
}

.biome-detail-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.detail-block {
  padding: 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.detail-block strong {
  line-height: 1.45;
}

#biome-list {
  display: grid;
  gap: 8px;
  max-height: min(40vh, 420px);
  overflow-y: auto;
  padding-right: 6px;
}

#biome-shortcuts-panel {
  min-height: 0;
}

#biome-list::-webkit-scrollbar {
  width: 8px;
}

#biome-list::-webkit-scrollbar-track {
  background: transparent;
}

#biome-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--border-strong) 70%, transparent);
  border-radius: 999px;
}

#biome-list {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--border-strong) 70%, transparent)
    transparent;
}

.biome-shortcut {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  text-align: left;
  border-radius: var(--radius-sm);
  padding: 0.7rem 0.85rem;
}

.biome-shortcut.active-shortcut {
  border-color: var(--accent);
}

.biome-shortcut__title {
  font-family: var(--header-font);
  font-size: 0.84rem;
  line-height: 1.2;
}

.biome-shortcut__meta {
  font-size: 0.72rem;
  color: var(--text-soft);
}

.build-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(300px, 0.9fr);
  gap: 18px;
}

.build-side-stack {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

#character-sheet {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 12px;
}

.build-cap-status {
  display: none;
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: linear-gradient(
    180deg,
    rgba(245, 235, 212, 0.9),
    rgba(236, 225, 201, 0.92)
  );
  color: var(--text-soft);
  font-size: 0.92rem;
}

.build-cap-status.is-visible {
  display: block;
}

.build-cap-status.is-maxed {
  border-color: rgba(184, 141, 76, 0.34);
  background: linear-gradient(
    180deg,
    rgba(245, 229, 197, 0.95),
    rgba(232, 214, 184, 0.94)
  );
  color: var(--text);
}

.build-cap-status {
  background: linear-gradient(
    180deg,
    rgba(37, 31, 24, 0.96),
    rgba(28, 23, 18, 0.95)
  );
}

.build-cap-status.is-maxed {
  background: linear-gradient(
    180deg,
    rgba(53, 40, 24, 0.96),
    rgba(40, 29, 18, 0.95)
  );
}

.stat-line {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

.stat-line.secondary {
  background: color-mix(in srgb, var(--surface-muted) 70%, var(--accent-soft));
}

/* Grille a deux colonnes : l'icone tient la premiere sur les deux lignes, le
   nom et la valeur s'empilent dans la seconde. En colonne flex, l'icone
   prependue par decorateStatLines se retrouvait au-dessus du libelle.
   Les lignes sont declarees explicitement et le placement est force :
   sans grid-template-rows, `grid-row: 1 / -1` ne couvrait qu'une seule ligne
   et les valeurs retombaient dans la colonne de l'icone, dont la largeur
   variait alors selon la presence d'un bonus — les libelles ne s'alignaient
   plus d'une ligne a l'autre. */
.stat-info {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 12px;
  row-gap: 4px;
}

.stat-info > .stat-line-icon {
  grid-column: 1;
  grid-row: 1 / span 2;
  margin: 0;
}

.stat-info > :not(.stat-line-icon) {
  grid-column: 2;
}

.stat-values {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

.upgrade-action {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.mini-upgrade {
  min-width: 38px;
  padding: 0.55rem 0.75rem;
}

.cost-label,
.bonus-val {
  color: var(--text-faint);
  font-size: 0.8rem;
}

.bonus-val {
  display: inline-flex;
  align-items: center;
  padding: 0.16rem 0.5rem;
  border-radius: 999px;
  background: rgba(100, 122, 68, 0.1);
  border: 1px solid transparent;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.bonus-val.has-positive {
  color: var(--success) !important;
  background: rgba(96, 122, 69, 0.12);
  border-color: rgba(96, 122, 69, 0.24);
}

.bonus-val.has-negative {
  color: var(--danger) !important;
  background: rgba(170, 90, 72, 0.12);
  border-color: rgba(170, 90, 72, 0.22);
}

.cost-label.is-maxed {
  color: var(--accent);
  font-weight: 700;
}

.mini-upgrade.is-maxed {
  opacity: 0.8;
  color: var(--accent);
}

.hidden-panel {
  display: none;
}

#real-time-content {
  display: grid;
  gap: 8px;
  margin-top: 12px;
}

.rt-stat {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  color: var(--text-soft);
}

.rt-stat b {
  color: var(--text);
}

/* En-tete de panneau avec un bouton pour le replier (cendres de l'inventaire). */
.panel-collapse-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.panel-collapse-row h3 {
  margin: 0;
}

.inventory-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 14px;
}

.inventory-search {
  flex: 1 1 220px;
  max-width: 320px;
  min-width: 0;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.25);
  color: var(--text);
  font: inherit;
}

.inventory-search::placeholder {
  color: var(--text-soft);
}

.inventory-search:focus {
  outline: none;
  border-color: var(--accent);
}

.inventory-equipped-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  margin-top: 12px;
}

.inventory-equipped-card {
  min-height: 118px;
  padding: 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto auto;
  align-items: center;
  column-gap: 14px;
  row-gap: 4px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

/* L'icone occupe les trois lignes de la carte, le texte s'empile a droite. */
.inventory-equipped-card .item-icon {
  grid-row: 1 / -1;
}

.inventory-equipped-label,
.inventory-item-type {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text-soft);
}

.inventory-equipped-name,
.inventory-item-name {
  font-size: 1rem;
  line-height: 1.25;
}

.inventory-equipped-meta,
.inventory-item-meta,
.inventory-item-progress {
  color: var(--text-soft);
  font-size: 0.88rem;
}

.inventory-equipped-empty {
  opacity: 0.82;
}

/* ------------------------------------------------------------------ */
/* Socle d'icone, commun aux cartes d'inventaire                      */
/* ------------------------------------------------------------------ */

.item-icon,
.slot-icon {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  padding: 9px;
  border-radius: var(--radius-sm);
  background: radial-gradient(
      circle at 50% 42%,
      var(--item-glow, transparent),
      transparent 68%
    ), var(--surface-subtle);
  border: 1px solid var(--border);
}

/* Cadre sans icone : hachures en pointilles, pour que la grille garde son
   rythme au lieu de laisser un trou. */
.item-icon.is-empty,
.slot-icon.is-empty {
  border-style: dashed;
}

.pixel-icon--missing {
  background-image: repeating-linear-gradient(
    45deg,
    transparent,
    transparent 4px,
    var(--border) 4px,
    var(--border) 5px
  );
}

#filter-buttons {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

#inventory-grid {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 12px;
}

.inventory-item {
  min-height: 176px;
  padding: 14px;
  border-radius: var(--radius-sm);
  background: var(--panel-sheen), var(--surface-strong);
  border: 1px solid var(--border);
  cursor: pointer;
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  justify-items: center;
  gap: 8px;
  text-align: center;
  transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s
    ease;
}

.inventory-item:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
  box-shadow: 0 16px 28px rgba(0, 0, 0, 0.35);
}

.inventory-item-name {
  align-self: start;
}

.inventory-item-footer {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 8px;
  width: 100%;
}

/* ------------------------------------------------------------------ */
/* Rarete                                                             */
/* ------------------------------------------------------------------ */

/* Chaque palier pose sa couleur dans --rarity-color, que la bordure, la puce
   et la lueur derriere l'icone consomment ensuite. Un seul point a changer
   pour ajouter un palier. */
.rarity-commun {
  --rarity-color: var(--rarity-commun);
}

.rarity-rare {
  --rarity-color: var(--rarity-rare);
}

.rarity-legendaire {
  --rarity-color: var(--rarity-legendaire);
}

.rarity-relique {
  --rarity-color: var(--rarity-relique);
}

.inventory-item[class*="rarity-"] {
  border-color: color-mix(in srgb, var(--rarity-color) 38%, var(--border));
}

.inventory-item[class*="rarity-"] .item-icon {
  --item-glow: color-mix(in srgb, var(--rarity-color) 26%, transparent);
  border-color: color-mix(in srgb, var(--rarity-color) 32%, var(--border));
}

.inventory-item-rarity {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: color-mix(in srgb, var(--rarity-color) 72%, var(--text));
  background: color-mix(in srgb, var(--rarity-color) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--rarity-color) 28%, transparent);
}

/* Le palier commun ne merite pas d'etre souligne : on l'efface presque. */
.rarity-commun .inventory-item-rarity {
  color: var(--text-faint);
  background: transparent;
  border-color: var(--border);
}

.equipped-highlight {
  border-color: var(--success);
  box-shadow: inset 0 0 0 1px rgba(96, 122, 69, 0.26);
}

.inventory-item.item-type-weapon,
.inventory-equipped-card.item-type-weapon {
  background: linear-gradient(
    180deg,
    rgba(60, 38, 31, 0.96),
    rgba(44, 28, 23, 0.94)
  );
  border-color: rgba(181, 109, 83, 0.35);
}

.inventory-item.item-type-armor,
.inventory-equipped-card.item-type-armor {
  background: linear-gradient(
    180deg,
    rgba(31, 40, 55, 0.96),
    rgba(22, 29, 41, 0.94)
  );
  border-color: rgba(120, 140, 185, 0.34);
}

.inventory-item.item-type-accessory,
.inventory-equipped-card.item-type-accessory {
  background: linear-gradient(
    180deg,
    rgba(39, 46, 28, 0.96),
    rgba(29, 34, 21, 0.94)
  );
  border-color: rgba(132, 155, 82, 0.34);
}

.inventory-equipped-card.item-type-ash {
  background: linear-gradient(
    180deg,
    rgba(53, 40, 24, 0.96),
    rgba(40, 29, 18, 0.94)
  );
  border-color: rgba(184, 141, 76, 0.34);
}

.ashes-container {
  display: grid;
  gap: 10px;
  margin-top: 12px;
}

.ash-item {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
  border-radius: var(--radius-sm);
}

.active-ash {
  border-color: var(--accent);
}

.boot-notice {
  position: relative;
  z-index: 60;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 20px;
  margin: 0;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border-strong);
  background: var(--surface-strong);
  color: var(--text);
  font-family: var(--body-font);
  line-height: 1.45;
}

.boot-notice p {
  margin: 0;
  flex: 1 1 340px;
}

.boot-notice--warn {
  border-left: 4px solid var(--info);
}

/* Un deblocage n'est pas un avertissement : il se lit en accent, pas en rouge.
   Meme forme que les deux autres tons, seule la couleur du liseret change. */
.boot-notice--unlock {
  border-left: 4px solid var(--accent);
}

.boot-notice--danger {
  border-left: 4px solid var(--danger);
}

.boot-notice__close {
  flex: 0 0 auto;
  padding: 8px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font-family: var(--body-font);
  cursor: pointer;
}

.boot-notice__close:hover {
  background: var(--hover-btn);
}

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

.button-stack {
  display: grid;
  gap: 10px;
}

.save-buttons {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.option-row {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 12px;
}

/* Le curseur natif ressortait en magenta vif sur toute la palette : couleur
   d'accent imposee par le navigateur, sans rapport avec le jeu. On le redessine
   entierement. Les prefixes -webkit et -moz sont necessaires, aucun moteur
   n'accepte encore une syntaxe commune pour la piste et la poignee. */
.option-row input[type="range"] {
  width: 100%;
  appearance: none;
  -webkit-appearance: none;
  height: 22px;
  background: transparent;
  cursor: pointer;
}

.option-row input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

.option-row input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

.option-row input[type="range"]::-moz-range-progress {
  height: 6px;
  border-radius: 999px;
  background: var(--accent);
}

.option-row input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--accent);
  border: 1px solid var(--bg);
  box-shadow: var(--glow-accent);
}

.option-row input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--accent);
  border: 1px solid var(--bg);
  box-shadow: var(--glow-accent);
}

.option-row input[type="range"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.offline-controls,
.offline-controls-b {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}

.bank-text {
  color: var(--text-soft);
}

/*
 * Ce que la banque vaut reellement. En retrait : c'est une explication, pas
 * une valeur a surveiller — le compteur au-dessus joue ce role.
 */
.bank-note {
  margin: 4px 0 8px;
  font-size: 0.74rem;
  line-height: 1.4;
  color: var(--text-faint);
}

.danger-panel .reset-btn {
  border-color: color-mix(in srgb, var(--danger) 55%, var(--border-strong));
  color: var(--danger);
}

#biome-view {
  position: relative;
}

.combat-shell {
  gap: 18px;
}

#current-biome-header {
  display: flex;
  align-items: center;
  gap: 10px;
}

#cycle-count {
  color: var(--accent);
}

#dungeon-stepper-container {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

#stepper-track {
  position: relative;
  height: 12px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

#stepper-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: linear-gradient(90deg, var(--accent), var(--accent-strong));
}

#stepper-markers .marker {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 14px;
  height: 14px;
  border-radius: 999px;
  border: 2px solid var(--surface-strong);
}

.marker-grace {
  background: var(--success);
}

.marker-boss {
  background: var(--danger);
}

#combat-zone {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  position: sticky;
  /* Empilement : la barre d'actions est le plancher, la zone de combat se cale
     juste au-dessus. Sans ce decalage le bouton "Se replier" recouvrait les
     combattants. */
  bottom: calc(var(--combat-actions-height, 68px) + 10px);
  z-index: 3;
  padding: 16px 18px;
  border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--border));
  box-shadow: 0 18px 34px rgba(15, 10, 6, 0.22);
}

.combat-card {
  border-radius: var(--radius-md);
  padding: 16px;
  border: 1px solid var(--border);
  background: var(--surface-strong);
}

.combat-card.ally {
  box-shadow: inset 0 0 0 1px rgba(95, 127, 159, 0.14);
}

.combat-card.enemy {
  box-shadow: inset 0 0 0 1px rgba(183, 91, 59, 0.14);
}

.battle-intel-strip {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.battle-intel-chip {
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.battle-intel-chip strong {
  font-size: 1.05rem;
}

.battle-log-frame {
  padding: 14px;
}

#action-log {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}

.log-column {
  min-height: 320px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.log-column-title {
  position: sticky;
  top: 0;
  z-index: 1;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(
    180deg,
    var(--surface-strong),
    rgba(255, 255, 255, 0)
  );
  color: var(--text-soft);
}

.log-column-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 360px;
  overflow-y: auto;
  padding-right: 4px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.log-column-body::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* Le joueur occupe la colonne de gauche, l'ennemi celle de droite : c'est la
   convention des jeux de combat, et l'ordre du DOM a ete inverse en
   consequence dans ui.js. Les alignements suivent. */
.enemy-column .log-column-title {
  text-align: right;
}

.enemy-column .log-column-body {
  align-items: flex-end;
}

.player-column .log-column-body {
  align-items: flex-start;
}

.player-column {
  padding-right: 10px;
  border-right: 1px solid var(--border);
}

.enemy-column {
  padding-left: 10px;
}

.log-entry {
  position: relative;
  max-width: 92%;
  margin: 0;
  padding: 13px 14px;
  border-radius: 12px;
  border: 1px solid var(--border);
  line-height: 1.5;
  background: var(--surface-muted);
  box-shadow: 0 8px 18px rgba(82, 68, 39, 0.06);
}

.player-side {
  border-top-left-radius: 4px;
}

.enemy-side {
  border-top-right-radius: 4px;
}

.player-side.log-system {
  border-left: 3px solid var(--accent);
}

.enemy-side.log-system {
  border-right: 3px solid var(--info);
}

.log-crit {
  border-color: color-mix(in srgb, var(--danger) 44%, var(--border-strong));
}

.log-runes,
.log-heal,
.log-ash-activation {
  border-color: color-mix(in srgb, var(--success) 44%, var(--border-strong));
}

.log-entry {
  background: linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.04),
      rgba(255, 255, 255, 0.015)
    ), var(--surface-muted);
}

.log-entry-latest {
  box-shadow: 0 0 0 2px rgba(197, 173, 115, 0.16), 0 18px 30px
    rgba(0, 0, 0, 0.22);
}

.log-repeat-badge {
  display: inline-flex;
  margin-left: 8px;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--text);
  font-size: 0.75rem;
}

.log-kind-boss {
  border-color: color-mix(in srgb, var(--danger) 55%, var(--border-strong));
}

.log-kind-runes,
.log-kind-heal,
.log-kind-ash {
  border-color: color-mix(in srgb, var(--success) 48%, var(--border-strong));
}

.log-kind-loot {
  border-color: color-mix(in srgb, var(--accent) 48%, var(--border-strong));
}

/*
 * Le bouton de cendre n'a plus de couche collante.
 *
 * Il en avait une, calee sur la hauteur de la zone de combat via une variable
 * publiee par un ResizeObserver. Le pilage faisait donc TROIS etages — cendre,
 * combattants, barre d'actions — chacun dependant de la hauteur du precedent.
 * Des que la zone changeait de gabarit, un boss etant plus grand qu'un soldat,
 * le bouton retombait sur les combattants et sur la carte d'ennemi.
 *
 * Il est desormais un utilitaire parmi les autres dans la barre du bas, ce qui
 * ramene le pilage a deux etages.
 */
#ash-button.is-hidden {
  display: none;
}

#ash-button {
  min-width: 220px;
  padding: 14px 18px;
  border-radius: 999px;
  box-shadow: 0 14px 28px rgba(15, 10, 6, 0.32);
  pointer-events: auto;
}

#ash-button.ash-primed,
.active-offline {
  border-color: var(--accent);
  background: linear-gradient(180deg, var(--accent-soft), var(--surface-strong));
}

.hp-container {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.hp-container > span {
  font-family: var(--header-font);
  letter-spacing: 0.03em;
  font-size: 0.95rem;
  color: var(--text);
}

.hp-bar-bg {
  position: relative;
  width: 100%;
  height: 22px;
  overflow: hidden;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
  background: linear-gradient(
    180deg,
    var(--surface-muted),
    color-mix(in srgb, var(--surface-strong) 88%, black)
  );
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}

.hp-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 100%;
  background: linear-gradient(90deg, var(--danger), #d98c67);
}

.hp-text {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.82rem;
  font-weight: 700;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
  letter-spacing: 0.02em;
}

.status-container {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* ------------------------------------------------------------------ */
/* Combattants animes                                                 */
/* ------------------------------------------------------------------ */

/* Socle de chaque combattant. La hauteur est fixe pour que la ligne de sol
   reste la meme quelle que soit la taille du sprite : les boss sont 45% plus
   grands et debordent vers le haut, pas vers le bas. */
.fighter-stage {
  position: relative;
  /* Hauteur pilotee par le gabarit de l'ennemi, pose par mountCombatEnemy sur
     #combat-zone. Mesures reelles du bestiaire : standard 102px, rare 118px,
     boss 136px, et jusqu'a 148px pour les boss qui reutilisent une planche
     commune de 64px. Un cadre fige a 104px en tronquait les trois quarts.
     La zone de combat est collante en bas d'ecran, mais sa hauteur est publiee
     par le ResizeObserver de ui.js : la pile se readapte toute seule. */
  height: var(--fighter-stage-height, 104px);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  margin-bottom: 6px;
}

.fighter-stage::after {
  /* Ombre au sol, commune aux deux camps : c'est elle qui pose les
     combattants sur un plan plutot que de les laisser flotter. */
  content: "";
  position: absolute;
  bottom: 2px;
  width: 76px;
  height: 12px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  filter: blur(4px);
}

.fighter-sprite {
  position: relative;
  z-index: 1;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* Les sprites sont tous dessines tournes vers la droite. L'ennemi occupe la
   lane de droite : on le retourne pour qu'il fasse face au joueur. Seul le
   canvas est retourne, pas la scene : l'embleme resterait sinon en miroir. */
.fighter-stage--enemy .fighter-sprite {
  transform: scaleX(-1);
}

/* Marque de faction, posee en haut a droite du monstre. Elle ne distingue
   qu'une minorite de monstres : la plupart n'en ont pas, et l'emplacement
   reste alors vide. */
.fighter-emblem {
  position: absolute;
  top: 4px;
  right: 8px;
  z-index: 2;
  display: grid;
  place-items: center;
  padding: 4px;
  border-radius: var(--radius-sm);
  background-color: var(--surface-subtle);
  border: 1px solid var(--border);
  opacity: 0.92;
}

.fighter-emblem:empty {
  display: none;
}

/* Effet de cendre de guerre : superpose au centre de la zone de combat, sans
   intercepter les clics, et invisible tant qu'aucune animation ne tourne. */
#ash-effect-canvas {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  transition: opacity 120ms ease;
}

#ash-effect-canvas.is-active {
  opacity: 1;
}

/* Pastille de statut : icone 32px + compteur. Le nom de l'effet n'est plus
   ecrit, il vit dans le title et l'aria-label — huit noms cote a cote
   saturaient la ligne de combat. */
.status-icon {
  position: relative;
  display: inline-grid;
  place-items: center;
  padding: 5px;
  border-radius: 10px;
  background: var(--surface-subtle);
  border: 1px solid var(--border);
}

.status-icon__count {
  position: absolute;
  right: -3px;
  bottom: -4px;
  min-width: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--surface-strong);
  border: 1px solid var(--border-strong);
  color: var(--text);
  font-size: 0.64rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
}

/* Icones de stat posees dans un libelle : elles doivent suivre la ligne de
   base du texte, pas flotter au-dessus. */
.stat-line-icon,
.hero-stat__icon,
.rune-icon {
  display: inline-block;
  vertical-align: -3px;
  margin-right: 6px;
}

.stat-line-icon {
  /* Deja rendue en 2x (32px) par decorateStatLines : pas de transform, une
     mise a l'echelle non entiere floute le pixel art. */
  margin: 0 6px 0 2px;
}

.hero-stat__label {
  display: flex;
  align-items: center;
}

.rune-icon {
  margin-right: 4px;
}

/* Colle sous la zone de combat, au meme titre que le bouton de cendre : se
   replier est l'action la plus urgente d'un combat, elle ne doit pas demander
   de defiler. La hauteur de la zone de combat est publiee par ui.js. */
/* Fondu de raccord entre le journal qui defile et le bandeau opaque. */
.biome-actions-bottom::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 18px;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0), var(--surface-strong));
}

.biome-actions-bottom {
  position: sticky;
  bottom: 0;
  z-index: 4;
  /* Le journal defile derriere la barre. Un fond opaque, pas un degrade : un
     degre d'opacite intermediaire laissait passer les entrees du journal
     derriere le bouton. Le fondu est reporte sur le ::before, au-dessus. */
  padding: 12px 14px;
  margin: -4px -14px 0;
  background: var(--surface-strong);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.tooltip-hidden {
  display: none;
}

#tooltip {
  position: fixed;
  z-index: 60;
  max-width: 340px;
  padding: 14px 16px;
  border-radius: 8px;
  border: 1px solid rgba(122, 101, 64, 0.28);
  background: linear-gradient(
      180deg,
      rgba(255, 251, 243, 0.98),
      rgba(243, 234, 218, 0.96)
    ), var(--surface-strong);
  color: var(--text);
  box-shadow: 0 22px 46px rgba(69, 49, 26, 0.18), 0 4px 12px
    rgba(69, 49, 26, 0.08);
  pointer-events: none;
  line-height: 1.45;
  backdrop-filter: blur(12px);
}

#tooltip {
  border-color: rgba(205, 179, 115, 0.24);
  background: linear-gradient(
      180deg,
      rgba(30, 25, 20, 0.98),
      rgba(21, 18, 15, 0.97)
    ), var(--surface-strong);
  box-shadow: 0 22px 46px rgba(0, 0, 0, 0.34), 0 4px 12px rgba(0, 0, 0, 0.18);
}

.tooltip-title {
  display: block;
  margin-bottom: 4px;
  color: var(--accent);
  font-size: 1rem;
}

#tooltip strong {
  color: var(--accent) !important;
}

#tooltip small {
  display: block;
  color: var(--text-soft) !important;
  font-size: 0.9rem;
  line-height: 1.45;
}

#tooltip hr {
  border: 0;
  border-top: 1px solid var(--border-strong) !important;
  margin: 8px 0 !important;
}

#tooltip span,
#tooltip b {
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.06);
}

#tooltip span,
#tooltip b,
#tooltip small,
#tooltip strong {
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.34);
}

#tooltip [style*="color:#aaa"],
#tooltip [style*="color:beige"] {
  color: var(--text-soft) !important;
}

#tooltip [style*="color:grey"],
#tooltip [style*="color:#777"] {
  color: var(--text-faint) !important;
}

#tooltip [style*="color:#4dff4d"] {
  color: var(--success) !important;
}

#tooltip [style*="color:#ff4d4d"] {
  color: var(--danger) !important;
}

.tooltip-subtitle {
  display: block;
  color: var(--text-soft);
  font-size: 0.9rem;
  font-style: italic;
}

.tooltip-rule {
  border: 0;
  border-top: 1px solid var(--border-strong);
  margin: 8px 0;
}

.tooltip-section-title {
  display: block;
  margin-bottom: 4px;
  color: var(--text);
}

.tooltip-empty {
  color: var(--text-faint);
}

.tooltip-stat {
  color: var(--success);
}

.tooltip-stat-negative {
  color: var(--danger);
}

.tooltip-set-title {
  color: var(--accent);
}

.tooltip-set-bonus {
  color: var(--text-faint);
  font-size: 0.9rem;
}

.tooltip-set-bonus.is-active {
  color: var(--success);
}

.tooltip-compare-grid {
  display: grid;
  gap: 6px;
  margin-top: 8px;
}

.tooltip-compare-grid.compact {
  gap: 4px;
}

.tooltip-compare-row {
  display: grid;
  grid-template-columns: minmax(84px, 1fr) auto auto auto auto;
  gap: 8px;
  align-items: center;
  padding: 0.24rem 0;
  border-bottom: 1px dashed rgba(128, 107, 72, 0.18);
  font-size: 0.86rem;
}

.tooltip-compare-row.compact {
  grid-template-columns: minmax(90px, 1fr) auto auto;
  gap: 10px;
  padding: 0.12rem 0;
}

.tooltip-compare-row:last-child {
  border-bottom: 0;
}

.tooltip-compare-stat {
  color: var(--text-soft);
}

.tooltip-compare-current,
.tooltip-compare-next {
  font-weight: 700;
  color: var(--text);
}

.tooltip-compare-arrow {
  color: var(--text-faint);
}

.tooltip-compare-row.compact .tooltip-compare-current,
.tooltip-compare-row.compact .tooltip-compare-arrow {
  display: none;
}

.tooltip-compare-diff {
  font-weight: 700;
  color: var(--text-faint);
}

.tooltip-compare-diff.is-positive {
  color: var(--success);
}

.tooltip-compare-diff.is-negative {
  color: var(--danger);
}

#fade-overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
}

.shake-effect {
  animation: screenShake 0.35s linear;
}

.prep-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

.prep-label {
  margin: 0 0 10px;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.prep-option-list {
  display: grid;
  gap: 10px;
}

.prep-option {
  width: 100%;
  text-align: left;
  padding: 14px;
  border: 1px solid var(--border);
  background: var(--surface-subtle);
}

.prep-option strong,
.prep-option span {
  display: block;
}

.prep-option span {
  margin-top: 6px;
  color: var(--text-soft);
  font-size: 0.92rem;
}

.prep-option small {
  display: block;
  margin-top: 8px;
  color: var(--text-soft);
  line-height: 1.45;
  font-size: 0.82rem;
}

.prep-option.is-active {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--surface-subtle) 82%, var(--accent) 18%);
}

/* Les regles du panneau d'intention supprime vivaient ici. La severite se
   lit desormais sur .combat-lane__intent, en bas de ce fichier. */

.journal-panel {
  display: grid;
  gap: 14px;
  opacity: 0.92;
  background: linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.2),
      rgba(255, 255, 255, 0.06)
    ), color-mix(in srgb, var(--surface-strong) 92%, transparent);
  border-color: color-mix(in srgb, var(--border) 88%, transparent);
  margin-top: 18px;
}

.section-head-tight {
  margin-bottom: 0;
}

.journal-filters select {
  min-width: 160px;
  appearance: none;
  border: 1px solid var(--border-strong);
  background: linear-gradient(
    180deg,
    var(--surface-strong),
    var(--surface-muted)
  );
  color: var(--text);
  padding: 0.75rem 2.4rem 0.75rem 0.9rem;
  border-radius: 8px;
  box-shadow: 0 10px 20px rgba(58, 43, 20, 0.08);
  background-image: linear-gradient(
      180deg,
      var(--surface-strong),
      var(--surface-muted)
    ), linear-gradient(45deg, transparent 50%, var(--text-soft) 50%),
    linear-gradient(135deg, var(--text-soft) 50%, transparent 50%);
  background-position: 0 0, calc(100% - 18px) calc(50% - 3px), calc(100% - 12px)
    calc(50% - 3px);
  background-size: 100% 100%, 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

.journal-filters select:hover,
.journal-filters select:focus {
  border-color: var(--accent);
  outline: none;
}

.journal-entries {
  display: grid;
  gap: 10px;
  max-height: 210px;
  overflow-y: auto;
  padding-right: 4px;
}

.journal-entry,
.codex-entry {
  padding: 12px 14px;
  border: 1px solid var(--border);
  background: var(--surface-subtle);
}

.journal-entry-head {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}

.journal-entry p,
.codex-entry p {
  margin: 0;
  color: var(--text-soft);
}

.journal-empty,
.codex-empty {
  color: var(--text-soft);
  margin: 0;
}

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

.codex-span {
  grid-column: 1 / -1;
}

.codex-list {
  display: grid;
  gap: 10px;
  max-height: 320px;
  overflow-y: auto;
  padding-right: 4px;
}

.codex-entry strong,
.codex-entry span {
  display: block;
}

.codex-entry span {
  margin-top: 4px;
  margin-bottom: 6px;
  color: var(--text-soft);
  font-size: 0.88rem;
}

@keyframes screenShake {
  0%,
  100% {
    transform: translateX(0);
  }
  20% {
    transform: translateX(-6px);
  }
  40% {
    transform: translateX(4px);
  }
  60% {
    transform: translateX(-4px);
  }
  80% {
    transform: translateX(3px);
  }
}

@media (max-width: 1080px) {
  .app-header,
  .hub-grid,
  .map-screen-grid,
  .build-grid,
  .codex-grid,
  .options-grid,
  #combat-zone,
  .hero-stats__head,
  .hub-focus-card__stats {
    grid-template-columns: 1fr;
  }

  #combat-zone {
    /* relative et non static : on desactive l'epinglage tout en gardant un
       bloc conteneur pour l'overlay d'effet de cendre. */
    position: relative;
    bottom: auto;
  }

  #ash-button {
    width: 100%;
  }

  .app-header {
    grid-template-columns: 1fr;
  }

  .header-tools,
  .currency-panel {
    text-align: left;
  }

  .primary-nav {
    justify-content: flex-start;
  }
}

@media (max-width: 760px) {
  /* En etroit, minmax(160px) ne laissait passer qu'une colonne apres les
     paddings du panneau : on descend le plancher pour en tenir deux. */
  #inventory-grid {
    grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
    gap: 10px;
  }

  .inventory-item {
    min-height: 158px;
    padding: 10px;
  }

  /* Les cartes d'objets equipes passent en pleine largeur, icone a gauche. */
  .inventory-equipped-grid {
    grid-template-columns: 1fr;
  }

  /* En etroit, une echelle pilotee par la largeur reduit le decor a une bande
     de 210px : on pilote par la hauteur et on recadre sur l'Arbre-Monde. */
  /* Hauteur 280px -> image large de 498px pour un viewport de 375 : cale a
     droite, l'Arbre-Monde (a 73% de la largeur de l'image) tombe pile aux deux
     tiers de l'ecran au lieu d'etre coupe par le bord. */
  .camp-scene__layer {
    background-size: auto 280px;
    background-position: right top;
  }

  .camp-scene__grace {
    background: radial-gradient(
      circle at 65% 13%,
      rgba(232, 192, 106, 0.22),
      transparent 30%
    );
  }

  /* En dessous de 760px, portrait et texte cote a cote sont a l'etroit :
     le titre d'archetype se cassait sur trois lignes. On empile. */
  .hero-panel__figure {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .hero-portrait {
    width: 132px;
    height: 132px;
  }

  .hero-stat {
    grid-template-columns: minmax(0, 1fr) 58px;
    row-gap: 6px;
  }

  .hero-stat__track {
    grid-column: 1 / -1;
    grid-row: 2;
  }

  #game-container {
    width: 100vw;
    margin: 0;
    min-height: 100vh;
    border-radius: 0;
    padding: 16px;
  }

  .screen-heading,
  .section-head,
  .panel-topline,
  .biome-detail-header,
  .biome-detail-actions,
  .battle-heading,
  .inventory-toolbar,
  .offline-controls,
  .offline-controls-b,
  .biome-actions-bottom {
    flex-direction: column;
    align-items: stretch;
  }

  /* En colonne, la base flex s'appliquerait a la hauteur du champ. */
  .inventory-search {
    flex: none;
    max-width: none;
  }

  .hub-cta-grid,
  .slots-container,
  .inventory-equipped-grid,
  .prep-columns,
  .save-buttons,
  .biome-detail-grid,
  .battle-intel-strip,
  #action-log {
    grid-template-columns: 1fr;
  }

  #world-map-shell {
    min-height: 300px;
    height: 42vh;
    padding: 12px;
  }

  #world-map-shell::before,
  #world-map-paths {
    display: none;
  }

  #world-map {
    position: absolute;
    inset: 0;
    display: block;
  }

  .map-side-column {
    grid-template-columns: 1fr;
  }

  #biome-list {
    max-height: 30vh;
    padding-right: 2px;
  }

  .world-node {
    position: static;
    transform: none;
    width: 100%;
    height: 52px;
    justify-content: flex-start;
    padding-left: 16px;
  }

  .world-node.selected {
    transform: none;
  }

  .player-column {
    padding-left: 0;
  }

  .enemy-column {
    padding-right: 0;
    border-right: none;
    border-bottom: 1px solid var(--border);
    padding-bottom: 12px;
  }

  .player-column .log-column-title {
    text-align: left;
  }
}

/* ------------------------------------------------------------------ */
/* Points de competence critiques                                     */
/* ------------------------------------------------------------------ */

.crit-points {
  margin-top: 14px;
  padding: 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border));
  border-radius: var(--radius-md);
  background: var(--surface-subtle);
}

.crit-points__head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.crit-points__label {
  font-family: var(--header-font);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 0.82rem;
  color: var(--text-soft);
}

#crit-points-available {
  font-family: var(--header-font);
  font-size: 1.15rem;
  color: var(--accent);
}

.crit-points__hint {
  font-size: 0.78rem;
  color: var(--text-soft);
}

.crit-points__reset {
  margin-left: auto;
  padding: 4px 10px;
  font-size: 0.76rem;
  border-radius: 999px;
}

.crit-points__reset:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Les deux lignes de critique vivent dans .crit-points, un bloc simple, et non
   dans le conteneur flex a gap 12px qui espace les statistiques principales :
   sans cette regle elles se touchent. On reprend la meme valeur pour que les
   six lignes du panneau respirent pareil. */
.crit-points .stat-line + .stat-line {
  margin-top: 12px;
}

.crit-points__super {
  margin: 10px 0 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--text-soft);
}

/* Le super critique n'existe qu'au-dela de 100% de chance : quand il s'active,
   la ligne cesse d'etre une explication et devient une valeur a lire. */
.crit-points__super.is-active {
  color: var(--accent);
  font-weight: 600;
}

/* Cadre elargi pour les gabarits qui ne tiennent pas dans la hauteur standard.
   Pose sur la zone de combat pour que les deux lanes montent ensemble et que
   la ligne de sol reste alignee. */
#combat-zone.is-tier-rare {
  --fighter-stage-height: 124px;
}

#combat-zone.is-tier-boss {
  --fighter-stage-height: 156px;
}

/* ------------------------------------------------------------------ */
/* Fin de partie : renaissance et epreuves                            */
/* ------------------------------------------------------------------ */

.endgame-panel.is-hidden {
  display: none;
}

.endgame-panel {
  border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
  /* Plus de voisin sur sa rangee depuis que le contrat et les actions sont
     empiles a droite : il prend les deux colonnes. */
  grid-column: 1 / -1;
}

/* Renaissance disponible : le cadre doit se voir d'un coup d'oeil. */
.endgame-panel.is-ready {
  border-color: var(--accent);
  box-shadow:
    0 0 0 1px var(--accent),
    0 0 24px color-mix(in srgb, var(--accent) 35%, transparent);
  animation: endgame-lueur 2.4s ease-in-out infinite;
}

@keyframes endgame-lueur {
  50% {
    box-shadow:
      0 0 0 1px var(--accent),
      0 0 36px color-mix(in srgb, var(--accent) 55%, transparent);
  }
}

.endgame-ready-badge {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--accent);
  color: #1a1408;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: middle;
}

/* Pastille sur l'onglet Hub tant qu'une Renaissance attend. */
.nav-btn.has-alert {
  position: relative;
}

.nav-btn.has-alert::after {
  content: "";
  position: absolute;
  top: 4px;
  right: 4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .endgame-panel.is-ready {
    animation: none;
  }
}

.endgame-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px;
  margin-bottom: 14px;
}

.endgame-stats > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-subtle);
}

.endgame-stats span {
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.endgame-stats b {
  font-family: var(--header-font);
  font-size: 1.25rem;
  color: var(--accent);
}

.endgame-rebirth {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 12px 14px;
  margin-bottom: 16px;
  border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--border));
  border-radius: var(--radius-md);
  background: var(--surface-subtle);
}

.endgame-rebirth .panel-copy {
  flex: 1 1 320px;
  margin: 0;
}

.endgame-rebirth__btn {
  flex: 0 0 auto;
  padding: 12px 22px;
  border-radius: 999px;
  font-family: var(--header-font);
  letter-spacing: 0.04em;
}

.endgame-rebirth__btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.trial-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px;
}

.trial-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-subtle);
}

/* Une epreuve accomplie le reste pour toujours, renaissances comprises. */
.trial-card.is-cleared {
  border-color: color-mix(in srgb, var(--success) 45%, var(--border));
}

.trial-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.trial-card__badge {
  flex: 0 0 auto;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.trial-card.is-cleared .trial-card__badge {
  color: var(--success);
}

.trial-card__lore {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--text-soft);
}

.trial-card button {
  align-self: flex-start;
  padding: 7px 14px;
  font-size: 0.82rem;
  border-radius: 999px;
}

.trial-card button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ------------------------------------------------------------------ */
/* Automatisation d'expedition                                        */
/* ------------------------------------------------------------------ */

.automation-controls {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

#btn-auto-restart {
  padding: 6px 12px;
  font-size: 0.78rem;
  border-radius: 999px;
}

/* L'etat actif doit se lire sans relire le texte du bouton. */
#btn-auto-restart.is-active {
  border-color: color-mix(in srgb, var(--success) 55%, var(--border-strong));
  color: var(--success);
}

.automation-cycle {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.74rem;
  color: var(--text-soft);
}

/* Les fleches natives du champ nombre jurent avec le reste : minuscules,
   dessinees par le navigateur, differentes sur chaque plateforme. On les
   supprime au profit de deux boutons qui reprennent la grammaire des boutons
   d'amelioration. */
.automation-cycle input {
  width: 46px;
  padding: 5px 4px;
  font-size: 0.78rem;
  text-align: center;
  border: none;
  background: none;
  color: var(--text);
  appearance: textfield;
  -moz-appearance: textfield;
}

.automation-cycle input::-webkit-outer-spin-button,
.automation-cycle input::-webkit-inner-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

/* La bordure passe sur l'ensemble : le champ et ses deux boutons forment un
   seul objet, pas trois elements poses cote a cote. */
.cycle-stepper {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 6px);
  background: var(--surface-strong);
  overflow: hidden;
}

.cycle-stepper__btn {
  width: 24px;
  padding: 0;
  border: none;
  border-radius: 0;
  background: color-mix(in srgb, var(--surface-strong) 70%, var(--border));
  color: var(--text-soft);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.cycle-stepper__btn:hover {
  background: var(--accent-soft);
  color: var(--text);
}

.cycle-stepper__btn:active {
  background: color-mix(in srgb, var(--accent) 30%, var(--surface-strong));
}

/* Arbre de renaissance. Meme grammaire visuelle que les epreuves : ce sont
   deux moities du meme panneau de fin de partie. */
.rebirth-tree {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}

.tree-node {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-subtle);
}

.tree-node.is-full {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}

.tree-node__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.tree-node__rank {
  font-family: var(--header-font);
  font-size: 0.9rem;
  color: var(--accent);
}

.tree-node__detail {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--text-soft);
}

.tree-node button {
  align-self: flex-start;
  min-width: 46px;
  padding: 5px 12px;
  font-size: 0.82rem;
  border-radius: 999px;
}

.tree-node button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Regle de zone, sur la fiche de biome. Volontairement contrastee : c'est
   l'information qui doit decider de la preparation du joueur avant qu'il ne
   parte, pas un detail de plus dans la liste. */
.biome-trait {
  margin-top: 12px;
  padding: 12px 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  border-left-width: 3px;
  border-radius: var(--radius-md);
  background: var(--surface-subtle);
}

.biome-trait__label {
  display: block;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 2px;
}

.biome-trait strong {
  font-family: var(--header-font);
  font-size: 1.05rem;
}

.biome-trait p {
  margin: 4px 0 0;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--text-soft);
}

/* ------------------------------------------------------------------ */
/* Banniere d'evenement                                               */
/* ------------------------------------------------------------------ */

/*
 * Les evenements ne s'annonçaient que par une ligne dans le journal de
 * combat, noyee dans le flot des coups : le joueur ne les voyait pas passer.
 * La banniere se pose au-dessus de la zone de combat, reste cinq secondes et
 * disparait. Elle ne capte jamais le clic — l'expedition automatique doit
 * continuer sans intervention.
 */
.event-banner {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%) translateY(-8px);
  z-index: 6;
  display: flex;
  align-items: stretch;
  gap: 10px;
  width: min(420px, calc(100% - 24px));
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--surface-strong) 92%, var(--accent));
  box-shadow: 0 10px 26px rgb(0 0 0 / 45%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.28s ease, transform 0.28s ease;
}

.event-banner.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Barre de teinte a gauche : elle dit d'un coup d'oeil si c'est une aubaine
   ou une tuile, avant meme d'avoir lu le texte. */
.event-banner__kind {
  flex: 0 0 4px;
  border-radius: 999px;
  background: var(--accent);
}

.event-banner[data-tone="gain"] .event-banner__kind {
  background: #2ecc71;
}
.event-banner[data-tone="danger"] .event-banner__kind {
  background: #e74c3c;
}
.event-banner[data-tone="grace"] .event-banner__kind {
  background: #f1c40f;
}
.event-banner[data-tone="route"] .event-banner__kind {
  background: #54a0d4;
}

.event-banner__body {
  min-width: 0;
}

.event-banner__title {
  margin: 0 0 2px;
  font-family: var(--header-font);
  font-size: 0.92rem;
  letter-spacing: 0.03em;
  color: var(--text);
}

.event-banner__text {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-soft);
}

@media (max-width: 640px) {
  .event-banner {
    top: 6px;
    padding: 8px 10px;
  }
  .event-banner__title {
    font-size: 0.84rem;
  }
  .event-banner__text {
    font-size: 0.72rem;
  }
}

/* ------------------------------------------------------------------ */
/* Identite d'un combattant, sous son sprite                          */
/* ------------------------------------------------------------------ */

/*
 * Nom et statistiques vivaient dans une carte separee, au-dessus du bloc de
 * combat. Comme ce bloc est colle en bas d'ecran, il recouvrait la carte a
 * moitie : on lisait "FOR 506 · VIG 465 · ARM 2" avec la fin coupee. Tout ce
 * qui decrit un combattant tient maintenant dans sa lane, avec son sprite et
 * sa barre de vie.
 */
.combat-lane__name {
  margin: 0;
  font-family: var(--header-font);
  font-size: 1rem;
  letter-spacing: 0.03em;
  line-height: 1.2;
  color: var(--text);
  /* Un nom de boss ne doit pas pousser la barre de vie hors du cadre. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.combat-lane__meta {
  margin: 2px 0 0;
  font-size: 0.76rem;
  letter-spacing: 0.02em;
  color: var(--text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* La barre de vie reprend l'espace laisse par l'ancien libelle. */
.hp-container {
  gap: 6px;
}

@media (max-width: 640px) {
  .combat-lane__name {
    font-size: 0.9rem;
  }
  .combat-lane__meta {
    font-size: 0.7rem;
  }
}

/*
 * Le groupe de gauche de la barre d'actions.
 *
 * Il n'avait aucune regle : un div en bloc, donc ses boutons s'empilaient. En y
 * ajoutant la cendre, la barre est passee de 68 a 122 pixels de haut et a
 * mange le bas de l'ecran de combat.
 */
.biome-bottom-left {
  display: flex;
  align-items: center;
  gap: 10px;
  /*
   * Pas de flex-wrap ici, et une largeur automatique sur les boutons.
   *
   * .safe-btn s'etire sur toute la largeur disponible : avec un wrap, il
   * poussait la cendre a la ligne suivante et la barre passait de 68 a 132
   * pixels de haut, mangeant le bas de l'ecran de combat.
   */
  flex-wrap: nowrap;
}

.biome-bottom-left > button {
  width: auto;
  flex: 0 0 auto;
  white-space: nowrap;
}

/*
 * L'intention, sur UNE ligne, presente dans les deux lanes.
 *
 * Elle tenait sur deux lignes et n'existait que cote ennemi : sa lane comptait
 * cinq rangees contre trois pour le joueur, et les deux barres de vie ne
 * s'alignaient plus. La rangee est reservee des deux cotes — vide chez le
 * joueur, qui n'a pas d'intention a annoncer.
 *
 * Hauteur FIXE et non min-height : une ligne vide et une ligne remplie ne se
 * mesurent pas pareil, et il restait deux pixels d'ecart.
 */
.combat-lane__intent {
  margin: 6px 0 0;
  height: 1.2rem;
  line-height: 1.2rem;
  font-family: var(--header-font);
  font-size: 0.84rem;
  letter-spacing: 0.02em;
  color: var(--accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* La severite teinte le libelle : c'est le seul mot a lire en urgence. */
.combat-lane.intent-boss .combat-lane__intent {
  color: #e74c3c;
}
.combat-lane.intent-elite .combat-lane__intent {
  color: #f1c40f;
}
.combat-lane.intent-heavy .combat-lane__intent {
  color: #e67e22;
}

@media (max-width: 640px) {
  .combat-lane__intent {
    font-size: 0.78rem;
  }
}

/*
 * Barre d'actions de combat en etroit.
 *
 * `.biome-bottom-left` est en `nowrap` avec des boutons en `flex: 0 0 auto`,
 * et `#ash-button` porte un `min-width: 220px`. "Se replier au camp" et la
 * cendre reunis depassent donc 375 pixels : le bouton de cendre sortait du
 * cadre par la droite.
 *
 * Le `nowrap` protegeait la mise en page epinglee du bureau, ou une barre plus
 * haute mangeait le bas de l'ecran de combat. En dessous de 1080px,
 * `#combat-zone` repasse en `position: relative` : l'empilement ne coute plus
 * rien, on peut donc rendre la barre a la ligne.
 */
@media (max-width: 640px) {
  .biome-bottom-left {
    flex-wrap: wrap;
    width: 100%;
  }

  .biome-bottom-left > button {
    flex: 1 1 100%;
    min-width: 0;
    white-space: normal;
  }

  #ash-button {
    min-width: 0;
    width: 100%;
  }
}

/* ------------------------------------------------------------------ */
/* Transfert de sauvegarde                                            */
/* ------------------------------------------------------------------ */

/*
 * Le code scelle fait pres de 3000 signes : il faut pouvoir le selectionner
 * d'un coup, mais pas qu'il mange l'ecran. Trois lignes et un defilement.
 */
/* Un panneau masque par l'attribut `hidden` le reste, quelle que soit la mise
   en page que ses classes lui donnent par ailleurs. */
[hidden] {
  display: none !important;
}

#save-transfer-status.is-ok {
  color: #2ecc71;
}
#save-transfer-status.is-error {
  color: #e74c3c;
}

/* ------------------------------------------------------------------ */
/* Ferveur : la prime en jeu pendant une expedition                    */
/* ------------------------------------------------------------------ */

/*
 * Le bandeau doit se lire d'un coup d'oeil au milieu d'un combat : c'est sur
 * lui que repose la decision de se replier ou d'enchainer un cycle de plus.
 * D'ou le montant en gros, le reste en petit.
 */
.ferveur-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin: 0 0 10px;
  padding: 8px 12px;
  border: 1px solid color-mix(in srgb, var(--accent) 42%, transparent);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--accent-soft) 40%, transparent),
    transparent 70%
  );
  font-family: var(--body-font);
  line-height: 1.3;
}

/* Marqueurs de tete d'un contrat : etape de chaine, delai restant. */
.contract__chain,
.contract__deadline {
  flex: 0 0 auto;
  padding: 1px 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-family: var(--header-font);
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.contract__chain {
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
  color: var(--accent);
}

/* Sous trois cycles, le delai passe en alerte : c'est le moment ou la decision
   de changer de zone doit se prendre, pas quand il est trop tard. */
.contract__deadline.is-urgent {
  border-color: color-mix(in srgb, var(--danger) 60%, var(--border));
  color: var(--danger);
}

.contract.is-expired {
  opacity: 0.65;
}

.contract.is-expired .contract__progress-fill {
  background: var(--danger);
}

.contract__expired-note {
  margin: 6px 0 0;
  color: var(--danger);
  font-size: 0.8rem;
}

.ferveur-banner.is-hidden {
  display: none;
}

/* Le bandeau porte une explication : le curseur doit le dire, sinon personne
   ne pense a le survoler. */
.ferveur-banner {
  cursor: help;
}

.ferveur-banner__rank {
  flex: 0 0 auto;
  font-family: var(--header-font);
  font-size: 0.82rem;
  letter-spacing: 0.03em;
  color: var(--accent);
}

.ferveur-banner__bank {
  flex: 0 0 auto;
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.ferveur-banner__hint {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.72rem;
  color: var(--text-soft);
}

/*
 * Plafond de prime atteint : la menace continue de monter, plus la
 * recompense. Le bandeau vire au rouge pour que cela se voie sans lire.
 */
.ferveur-banner.is-overheated {
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
  border-left-color: var(--danger);
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--danger) 18%, transparent),
    transparent 70%
  );
}

.ferveur-banner.is-overheated .ferveur-banner__rank {
  color: var(--danger);
}

/*
 * En etroit, l'indice passe sous le montant plutot que de comprimer les deux :
 * le rang et la somme doivent rester sur la meme ligne, c'est le couple qui
 * porte la decision.
 */
@media (max-width: 640px) {
  .ferveur-banner {
    padding: 7px 10px;
    gap: 2px 8px;
  }

  .ferveur-banner__rank {
    font-size: 0.74rem;
  }

  .ferveur-banner__bank {
    font-size: 1rem;
  }

  .ferveur-banner__hint {
    flex: 1 0 100%;
    font-size: 0.68rem;
  }
}

/* ------------------------------------------------------------------ */
/* Panoplies enregistrees                                             */
/* ------------------------------------------------------------------ */

.loadouts {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.loadouts__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin-bottom: 10px;
}

.loadouts__title {
  font-family: var(--header-font);
  font-size: 0.95rem;
  letter-spacing: 0.03em;
  color: var(--text);
}

.loadouts__hint {
  font-size: 0.74rem;
  color: var(--text-soft);
}

/*
 * Une seule colonne par defaut, et non une grille de trois.
 *
 * Trois colonnes obligeaient chaque carte a tenir dans un tiers de la largeur :
 * sur telephone, les quatre boutons d'action passaient a la ligne un par un et
 * la carte montait a pres de 200 pixels. En colonne, la carte est large, les
 * boutons tiennent sur deux rangees, et la hauteur totale est plus faible.
 * Les colonnes reviennent au-dela de 900px, ou la largeur les rend gratuites.
 */
.loadouts__list {
  display: grid;
  gap: 10px;
}

@media (min-width: 900px) {
  .loadouts__list {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.loadout {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.loadout.is-active {
  border-color: var(--accent);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent-soft) 35%, transparent),
    var(--surface)
  );
}

/* Un emplacement vide ne doit pas peser autant qu'une panoplie remplie. */
.loadout.is-empty {
  padding: 0;
  border-style: dashed;
  background: none;
}

.loadout__save-empty {
  width: 100%;
  padding: 14px 12px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-soft);
  font-family: var(--body-font);
  font-size: 0.82rem;
  cursor: pointer;
}

.loadout__save-empty:hover {
  color: var(--text);
  background: var(--hover-btn);
}

.loadout__head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.loadout__name {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--header-font);
  font-size: 0.92rem;
  color: var(--text);
  /* Un nom long ne doit pas pousser le badge hors de la carte. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.loadout__badge {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--surface-strong);
  font-size: 0.66rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.loadout__pieces {
  margin: 0;
  font-size: 0.76rem;
  line-height: 1.4;
  color: var(--text-soft);
  /* Deux lignes au plus : quatre pieces ne doivent pas doubler la carte. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.loadout__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.loadout__actions button {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 10px;
  font-size: 0.76rem;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/*
 * Equiper est l'action principale : elle prend la place, les autres suivent.
 *
 * Selecteur volontairement aussi specifique que `.loadout__actions button`
 * (0,2,1) : ecrit `.loadout__load` seul (0,1,0), il perdait contre la regle
 * generique malgre l'ordre, et le bouton principal se retrouvait a la meme
 * largeur que les trois autres.
 */
.loadout__actions .loadout__load {
  flex: 2 1 100px;
}

.loadout__actions .loadout__load:disabled {
  opacity: 0.5;
  cursor: default;
}

/*
 * Sur telephone, les actions se rangent en rangees de deux plutot que de se
 * comprimer sur une seule : sous 44 pixels de large, une cible tactile n'est
 * plus fiable.
 */
/*
 * Sur telephone : Equiper sur toute la largeur, les trois actions secondaires
 * en dessous sur une seule rangee.
 *
 * Une rangee de trois plutot que deux rangees de deux : a deux par rangee,
 * "Effacer" se retrouvait seul sur une pleine largeur, aussi gros que l'action
 * principale alors qu'il est destructif — et la carte montait a 222 pixels.
 * A trois, chaque bouton fait encore 90 pixels de large et la carte redescend
 * sous 180.
 */
@media (max-width: 640px) {
  .loadout__actions button {
    flex: 1 1 calc(33.333% - 4px);
    /* 44px de haut : en dessous, une cible tactile devient peu fiable. */
    min-height: 44px;
    padding: 10px 6px;
    font-size: 0.72rem;
  }

  .loadout__actions .loadout__load {
    flex: 1 1 100%;
    font-size: 0.8rem;
  }
}

/* ------------------------------------------------------------------ */
/* Contrats de zone                                                   */
/* ------------------------------------------------------------------ */

.contract-empty {
  margin: 0 0 10px;
  color: var(--text-soft);
  font-size: 0.82rem;
}

/*
 * Faveur de contrat, affichee quand aucun contrat n'est en cours.
 *
 * Le pourcentage est la seule information du panneau vide qui aide a decider :
 * il porte donc la couleur d'accent, et l'explication reste en retrait.
 */
.contract-faveur {
  margin: 0 0 4px;
  font-size: 0.86rem;
  color: var(--text-soft);
}

.contract-faveur strong {
  color: var(--accent-strong, var(--accent));
  font-size: 0.95rem;
}

.contract-faveur__aide {
  margin: 0 0 12px;
  font-size: 0.76rem;
  line-height: 1.4;
  color: var(--text-faint, var(--text-soft));
  opacity: 0.85;
}

.contract {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.contract__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
  min-width: 0;
}

/*
 * La rarete se lit avant le titre : c'est elle qui dit au joueur combien de
 * temps l'engagement va lui couter, et donc s'il le prend maintenant.
 */
.contract__rarity {
  flex: 0 0 auto;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid currentColor;
  font-size: 0.66rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.contract--commune .contract__rarity {
  color: var(--text-soft);
}

.contract--rare .contract__rarity {
  color: var(--info, #6ba9ff);
}

.contract--legendaire .contract__rarity {
  color: var(--accent);
}

.contract__title {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--header-font);
  font-size: 1rem;
  color: var(--text);
}

.contract__text {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--text-soft);
}

.contract__progress {
  position: relative;
  height: 22px;
  overflow: hidden;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface-muted);
}

.contract__progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent-soft), var(--accent));
  transition: width 0.35s ease;
}

.contract__progress-text {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.74rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.65);
}

.contract__reward {
  margin: 0;
  font-size: 0.78rem;
  color: var(--text);
}

.contract__actions {
  display: flex;
  gap: 8px;
}

.contract__actions button {
  flex: 1 1 auto;
  min-height: 44px;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
}

/* Reclamer est une recompense : le bouton doit se voir comme telle. */
.contract__claim {
  border-color: var(--accent);
  background: linear-gradient(180deg, var(--accent-soft), var(--surface-strong));
  font-weight: 700;
}

/*
 * Contrat pret : le bouton doit se voir DE LOIN.
 *
 * Il ne se distinguait de l'abandon que par son libelle, et un joueur a lu
 * "Abandonner" comme un "Reclamer" grise. Le halo pulse deux fois puis
 * s'arrete : assez pour attirer l'oeil en arrivant sur le Hub, pas assez pour
 * clignoter en permanence dans un jeu qu'on laisse ouvert.
 */
.contract__claim.is-ready {
  animation: contract-ready 1.6s ease-out 2;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
}

@keyframes contract-ready {
  0% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
  }
  70% {
    box-shadow: 0 0 0 10px color-mix(in srgb, var(--accent) 0%, transparent);
  }
  100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .contract__claim.is-ready {
    animation: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
  }
}

.contract.is-done .contract__progress-fill {
  background: linear-gradient(90deg, var(--accent), var(--accent));
}

/* Relancer ne doit pas attirer l'oeil autant que reclamer. */
.contract__abandon {
  opacity: 0.8;
  font-size: 0.8rem;
}

/*
 * Relance disponible : teinte de rouge, discretement.
 *
 * Assez pour qu'on lise « ceci detruit le contrat en cours » sans regarder le
 * libelle, pas assez pour rivaliser avec le bouton de reclamation ni sortir de
 * la palette du camp — c'est le meme --danger que le reste de la page.
 */
.contract__abandon.is-armed {
  opacity: 1;
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--danger) 12%, transparent),
    var(--surface-strong)
  );
}

.contract__abandon.is-armed:hover {
  border-color: color-mix(in srgb, var(--danger) 70%, transparent);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--danger) 20%, transparent),
    var(--surface-strong)
  );
}

/* Delai en cours : le bouton dit pourquoi il ne fait rien. */
.contract__abandon[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

/*
 * Un contrat legendaire se distingue par le cadre, pas par une couleur de
 * texte de plus : le panneau contient deja trois niveaux de gris.
 */
.contract--legendaire {
  padding: 10px;
  margin: -10px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-radius: var(--radius-sm);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent-soft) 22%, transparent),
    transparent 60%
  );
}

@media (max-width: 640px) {
  .contract__title {
    font-size: 0.92rem;
  }

  .contract__text {
    font-size: 0.78rem;
  }

  /*
   * Le titre passe sous la pastille de rarete plutot que de partager sa ligne :
   * a 375px, un titre comme "Tete mise a prix : Profondeurs de la Souche" se
   * retrouvait comprime sur trois lignes etroites a cote de la pastille.
   */
  .contract__head {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }
}
