/*
  Tricoti — ui.css
  Barre du haut, grille 4 zones égales, modal de l'arbre de compétences en superposition
  Voir docs/tricoti-technical-spec.md section 7

  Tous les fonds et tous les cadres viennent d'assets (voir pixel-art.css).
  Les seules couleurs posées ici sont celles de la palette du pack, pour des
  éléments de 1 à 2 pixels de dessin (jauge, gouttes, ombre du texte).
*/

/* ------------------------------------------------------------------ */
/* Caché veut dire caché                                               */
/* ------------------------------------------------------------------ */
/*
  Le navigateur applique `display: none` aux éléments porteurs de l'attribut
  `hidden`, mais avec la spécificité la plus faible qui soit. Or presque tout
  ici est un `.panel` en `display: flex` — et une classe l'emporte. Un élément
  marqué caché restait donc à l'écran, cliquable.

  Ça s'est vu sur « passer la main » : le bouton s'affichait dès la première
  seconde d'une partie alors que le code le déclarait caché, et le joueur
  pouvait transmettre une ferme qui n'existait pas encore. Le chantier avait le
  même défaut.

  Chaque élément avait jusqu'ici sa propre règle `X[hidden] { display: none }`,
  qu'il fallait penser à écrire — les deux boutons les plus récents ont été
  oubliés. Une seule règle, en `!important` puisqu'elle doit battre n'importe
  quel `display` posé ailleurs, et le problème ne peut plus se reproduire.
*/
[hidden] { display: none !important; }

/* ------------------------------------------------------------------ */
/* Fond général : planches de bois                                     */
/* ------------------------------------------------------------------ */

body {
  background-image: url("../assets/sprites/ui/plank.png");
  background-size: calc(9px * var(--px)) calc(77px * var(--px));
  background-repeat: repeat;
}

/* Ombre portée nette (1 pixel de dessin), jamais floue */
.zone-title,
.zone-todo,
.res-val,
.float {
  text-shadow: calc(1px * var(--px)) calc(1px * var(--px)) 0 rgba(58, 42, 32, 0.35);
}

/* ------------------------------------------------------------------ */
/* Barre du haut persistante                                           */
/* ------------------------------------------------------------------ */

/* La barre passe à la ligne plutôt que de sortir de l'écran.
   Mesuré à 1280x720 : huit éléments demandaient 1366 px, et le bouton de
   l'arbre de compétences était purement et simplement hors champ — invisible
   et inatteignable. Les deux boutons ajoutés depuis (chantier, transmission)
   ont fait déborder une barre qui tenait tout juste. */
#topbar {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Centré quand ça se replie : sur une fenêtre étroite, tout ne tient pas sur
     une rangée — huit panneaux à l'échelle du jeu réclament 1455 px. Un second
     rang centré se lit comme un choix ; le même rang collé à gauche avec un
     grand vide à droite se lit comme un accident. */
  justify-content: center;
  gap: calc(3px * var(--px));
  padding: calc(3px * var(--px)) calc(4px * var(--px)) calc(6px * var(--px));

  background-image: url("../assets/sprites/ui/plank.png");
  background-size: calc(9px * var(--px)) calc(77px * var(--px));
  background-repeat: repeat;
}

/* Poutre horizontale qui referme la barre du haut */
#topbar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(10px * var(--px));
  background-image: url("../assets/sprites/ui/beam.png");
  background-size: calc(8px * var(--px)) calc(10px * var(--px));
  background-repeat: repeat-x;
}

.resources {
  display: flex;
  align-items: center;
  gap: calc(4px * var(--px));
  padding: 0 calc(2px * var(--px));
}

.res {
  display: inline-flex;
  align-items: center;
  gap: calc(2px * var(--px));
  /* Assez pour quatre chiffres, pas plus : c'est de la place gagnée pour les
     boutons quand l'écran est étroit. */
  min-width: calc(34px * var(--px));
}

/* Sursaut par paliers, jamais d'interpolation douce */
.res-val.bump { animation: bump 300ms steps(3); }

@keyframes bump {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(calc(-2px * var(--px))); }
  100% { transform: translateY(0); }
}

/* Ressource manquante : le compteur concerné clignote en rouge */
.res-val.lack { animation: lack 600ms steps(1) 2; }

@keyframes lack {
  0%, 100% { color: inherit; }
  50%      { color: var(--c-warn); }
}

/* ------------------------------------------------------------------ */
/* Jauge de combo (GDD section 6)                                      */
/* ------------------------------------------------------------------ */

/* Elle n'apparaît qu'à partir de deux éléments enchaînés, et disparaît dès
   que la chaîne casse : le reste du temps, la barre du haut ne change pas de
   largeur, donc rien ne saute à l'écran. */
.combo {
  display: inline-flex;
  align-items: center;
  gap: calc(3px * var(--px));
  flex: 0 0 auto;
  padding: 0 calc(3px * var(--px));
  color: var(--c-wheat);
}


.combo.is-max { color: var(--c-wheat-light); }

/* La barre est un rectangle plein, sans coin arrondi ni dégradé : deux
   aplats de la palette, comme les jauges des parcelles. */
.combo-bar {
  display: block;
  width: calc(24px * var(--px));
  height: calc(3px * var(--px));
  background: var(--c-brown-dark);
}

.combo-fill {
  display: block;
  height: 100%;
  width: 100%;
  background: currentColor;
}

/* Par paliers : le vidage se lit comme une jauge de pixel art, pas comme une
   animation fluide qui jurerait avec le reste. */
@keyframes combo-drain {
  from { width: 100%; }
  to   { width: 0%; }
}

/* Bouton de coupure du son. Le pack ne fournit pas d'icône de haut-parleur,
   et en dessiner une jurerait avec le reste : on écrit le mot dans la police
   du jeu, et le son coupé se lit à l'estompe — la même que celle des boutons
   inactifs de la boutique, donc rien de nouveau à apprendre. */
.sound-btn {
  flex: 0 0 auto;
  padding: 0 calc(4px * var(--px));
  cursor: inherit;
  color: var(--c-brown-dark);
}

.sound-btn[data-muted="true"] {
  opacity: 0.45;
  text-decoration: line-through;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;      /* sinon le bouton se fait écraser par les compteurs */
  padding: 0;
  cursor: inherit;
}

.icon-btn:disabled { opacity: 0.45; }

/* ------------------------------------------------------------------ */
/* Les 4 zones                                                         */
/* ------------------------------------------------------------------ */

.zone {
  position: relative;
  min-height: 0;
  overflow: hidden;

  /* La zone est son propre contexte d'empilement.
   *
   * C'est ce qui permet aux fleurs du décor de porter un `z-index: -1` qui
   * signifie vraiment « au fond de CETTE zone » : elles se peignent alors
   * juste au-dessus de l'herbe et sous absolument tout le reste, sans qu'il
   * faille numéroter chaque élément qu'on ajoutera un jour. Sans isolation,
   * un z-index négatif remonterait jusqu'à la racine de la page et la fleur
   * passerait sous le fond de la zone, donc disparaîtrait. */
  isolation: isolate;

  background-image: url("../assets/sprites/ui/grass.png");
  background-size: var(--tile) var(--tile);
  background-repeat: repeat;

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(4px * var(--px));
  padding: calc(3px * var(--px));
}

.zone-title {
  padding: 0 calc(3px * var(--px));
  letter-spacing: calc(1px * var(--px));
}

.zone-todo {
  margin-top: auto;
  margin-bottom: auto;
  padding: 0 calc(3px * var(--px));
  opacity: 0.8;
}

/* Fleurs animées du pack, en décor. Purement visuel, hors interaction.
 *
 * `z-index: -1`, et pas 0.
 *
 * Elles passaient d'abord par-dessus le cadre de l'enclos ; les mettre au même
 * niveau que les bâtiments n'a réglé que la moitié du problème, parce qu'un
 * `::after` est TOUJOURS le dernier enfant de son parent : à z-index égal,
 * c'est lui qui gagne. On voyait donc des fleurs d'hiver plantées sur le toit
 * du séchoir et sur les ailes du moulin.
 *
 * Un rang négatif règle le cas une fois pour toutes : quoi qu'on ajoute plus
 * tard dans une zone, le décor restera dessous. C'est ce qu'on demande à un
 * décor — un grand troupeau le recouvre, et c'est très bien ainsi. */
.zone::before,
.zone::after {
  content: "";
  position: absolute;
  z-index: -1;
  width: var(--tile);
  height: var(--tile);
  pointer-events: none;
  background-size: var(--tile) var(--tile);
  background-repeat: no-repeat;
}

/* Seules les fleurs « spring » ont exactement le vert de notre tuile d'herbe
   (131,146,76) ; les variantes summer/fall sont sur un autre vert et
   laisseraient un carré visible. */
.zone::before {
  left: calc(4px * var(--px));
  bottom: calc(3px * var(--px));
  background-image: url("../assets/cozy-farm/tiles/flower_spring_white.gif");
}

.zone::after {
  right: calc(5px * var(--px));
  bottom: calc(8px * var(--px));
  background-image: url("../assets/cozy-farm/tiles/flower_spring_blue.gif");
}

/* ------------------------------------------------------------------ */
/* Écran-titre (GDD section 11bis)                                     */
/* ------------------------------------------------------------------ */

/* Un pré, une clôture au fond, la pelote et le mot du jeu. Tout vient du
   pack ou de la police : rien n'est peint en CSS. */
.title {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;

  /* Le bloc titre remonte au-dessus de la haie : centré sur toute la
     hauteur, il se retrouvait le nez dans les arbres. */
  padding-bottom: calc(36px * var(--px));

  background-image: url("../assets/sprites/ui/grass.png");
  background-size: var(--tile) var(--tile);
  background-repeat: repeat;
}


.title-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(6px * var(--px));
  z-index: 2;
}

/* L'emblème, posé au-dessus du mot */
.title-emblem {
  width: calc(48px * var(--px));
  height: calc(48px * var(--px));
  background-image: url("../assets/sprites/ui/emblem-yarn.png");
  background-size: 100% 100%;
  image-rendering: pixelated;

  /* Il flotte à peine, par paliers d'un pixel : le seul mouvement de
     l'écran, sinon rien ne bouge et la page a l'air figée. */
  animation: title-float 1600ms steps(2) infinite alternate;
}

@keyframes title-float {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-2px * var(--px))); }
}

/* Le mot du jeu : la police, en multiple entier de son em de 16 px, donc
   parfaitement net. Aucune image, aucun logo dessiné. */
.title-word {
  margin: 0;
  font-size: calc(4 * var(--tile));
  letter-spacing: calc(2px * var(--px));
  color: var(--c-cream);
  text-shadow: calc(2px * var(--px)) calc(2px * var(--px)) 0 var(--c-brown-dark);
}

.title-menu {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: calc(3px * var(--px));
  margin-top: calc(4px * var(--px));
  min-width: calc(120px * var(--px));
}

.title-btn {
  padding: calc(2px * var(--px)) calc(6px * var(--px));
  text-align: center;
  cursor: inherit;
}

.title-btn:disabled { opacity: 0.45; }

/* Le bouton qui va effacer une partie prend la couleur d'alerte du jeu,
   celle des compteurs quand une ressource manque. */
.title-btn.is-warning { color: var(--c-warn); }

/* Ligne d'explication sous le menu. Hauteur réservée en permanence, sinon
   le menu remonte et redescend au passage de la souris. */
.title-note {
  min-height: var(--tile);
  color: var(--c-brown-dark);
}

/* Le décor du bas : haie, portail, fleurs, clôture, mouton.
   Trois plans, dans cet ordre : les arbres derrière, la clôture au milieu,
   le mouton devant. */
.title-scene {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.title-decor {
  position: absolute;
  bottom: calc(10px * var(--px));   /* les troncs mordent sur la clôture */
  transform: translateX(-50%);
  background-repeat: no-repeat;
  background-size: 100% 100%;
  image-rendering: pixelated;
}

.title-decor--tree,
.title-decor--pine {
  width: calc(32px * var(--px));
  height: calc(32px * var(--px));
}

.title-decor--tree { background-image: url("../assets/cozy-farm/tiles/tree_shake_spring.gif"); }
.title-decor--pine { background-image: url("../assets/cozy-farm/tiles/pinetree_shake_spring.gif"); }

/* Le portail est dans l'alignement de la clôture, pas derrière */
.title-decor--gate {
  width: var(--tile);
  height: var(--tile);
  bottom: 0;
  z-index: 3;
  background-image: url("../assets/cozy-farm/tiles/gate_wood_spring.gif");
}

.title-decor--flower {
  width: var(--tile);
  height: var(--tile);
  background-image: url("../assets/cozy-farm/tiles/flower_spring_white.gif");
}

/* Rocher et botte de foin : découpés dans tiles.png par background-position,
   comme les icônes de ressources. Pas besoin d'en faire des fichiers à part,
   ils ne se répètent pas. */
.title-decor--rock,
.title-decor--bale {
  width: var(--tile);
  height: var(--tile);
  background-image: url("../assets/cozy-farm/tiles/tiles.png");
  background-size: calc(864px * var(--px)) calc(800px * var(--px));
}

.title-decor--rock { background-position: calc(-144px * var(--px)) calc(-448px * var(--px)); } /* (9,28) */
.title-decor--bale { background-position: calc(-96px * var(--px)) calc(-448px * var(--px)); }  /* (6,28) */

/* Une fleur sur deux d'une autre couleur, pour que la rangée n'ait pas l'air
   copiée-collée. */
.title-decor--flower:nth-of-type(even) {
  background-image: url("../assets/cozy-farm/tiles/flower_spring_blue.gif");
}

/* La clôture ferme le bas de l'écran. Tuile (5,31) du pack, la seule dont
   les traverses touchent les deux bords : elle se répète sans couture. */
.title-fence {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--tile);
  z-index: 2;
  background-image: url("../assets/sprites/ui/fence.png");
  background-size: var(--tile) var(--tile);
  background-repeat: repeat-x;
  image-rendering: pixelated;
}

/* Le mouton mascotte, assis devant la clôture */
.title-sheep {
  position: absolute;
  right: calc(40px * var(--px));
  bottom: calc(4px * var(--px));
  width: calc(48px * var(--px));
  height: calc(48px * var(--px));
  z-index: 4;
  background-image: url("../assets/sprites/ui/title-sheep.png");
  background-size: 100% 100%;
  image-rendering: pixelated;
}

/* « comment jouer » — le seul tutoriel du jeu */
.title-help {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
}


.title-help-box {
  display: flex;
  flex-direction: column;
  gap: calc(3px * var(--px));
  padding: calc(4px * var(--px)) calc(6px * var(--px));
}

.title-rule {
  display: flex;
  align-items: center;
  gap: calc(4px * var(--px));
  margin: 0;
  white-space: nowrap;
}

.title-help-close { margin-top: calc(3px * var(--px)); }

/* ------------------------------------------------------------------ */
/* Infobulle (voir js/tooltip.js)                                      */
/* ------------------------------------------------------------------ */

/* Elle vient se coller à ce qu'on survole, dans l'arbre comme dans les
   succès. `position: fixed` parce qu'elle doit pouvoir déborder du panneau
   qui l'a demandée — une perle du bord de l'arbre a besoin de place au-delà
   du plateau. Sa position est calculée en JS, à l'entier près. */
.tip {
  position: fixed;
  z-index: 30;
  pointer-events: none;      /* elle ne doit jamais intercepter un survol */

  display: flex;
  flex-direction: column;
  gap: calc(1px * var(--px));
  padding: calc(1px * var(--px)) calc(4px * var(--px));
  max-width: 60vw;
  color: var(--c-brown-dark);

  /* `--px`, `--tile` et `font-size` sont posés en JS, recopiés de l'élément
     survolé : l'infobulle a ainsi toujours la taille du texte qu'elle
     explique, que ce soit une perle de l'arbre ou une ligne de succès. */
  animation: tip-in 120ms steps(2);
}


@keyframes tip-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.tip-name   { color: var(--c-brown-dark); }
.tip-effect { color: var(--c-brown); }
.tip-note   { color: var(--c-brown); }

/* Même code couleur que partout : doré = à portée, rouge = hors de portée. */
.tip-note[data-state="ready"] { color: var(--c-wheat); }
.tip-note[data-state="poor"]  { color: var(--c-warn); }
.tip-note[data-state="owned"] { color: var(--c-wheat); }
.tip-note[data-state="locked"],
.tip-note[data-state="todo"]  { color: var(--c-brown); }

/* ------------------------------------------------------------------ */
/* Panneau des succès                                                  */
/* ------------------------------------------------------------------ */

/* Comme l'arbre de compétences, le panneau a sa propre échelle. Vingt-sept
   libellés en cinq colonnes font beaucoup de texte : à l'échelle du jeu, le
   panneau mesurait 963 x 900 sur un 1024x768, donc plus haut que l'écran.
   Il descend d'un cran sur les petits écrans — le facteur reste entier, donc
   la police reste nette, simplement à sa taille de dessin. */
.succes {
  --px: 1;
  /* `--tile` vaut `calc(var(--unit) * var(--px))`, mais la substitution a
     déjà eu lieu sur `:root` : les descendants héritent d'une valeur figée,
     et redéfinir `--px` ici ne la change pas. Il faut donc redéclarer `--tile`
     dans la même portée pour qu'il se recalcule sur la nouvelle échelle —
     sinon le panneau rétrécit ses marges mais garde un texte deux fois trop
     gros, ce qui était exactement le symptôme. */
  --tile: calc(var(--unit) * var(--px));
  font-size: var(--tile);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(4px * var(--px));
}

/* Le panneau double d'échelle dès que ses cinq colonnes tiennent — seuil
   mesuré dans le navigateur, pas estimé. En dessous, il reste à sa taille de
   dessin : petit, mais net et complet. */
@media (min-width: 1800px) and (min-height: 700px) {
  .succes { --px: 2; }
}

.succes-head {
  display: flex;
  align-items: center;
  gap: calc(6px * var(--px));
  padding: 0 calc(4px * var(--px));
  letter-spacing: calc(1px * var(--px));
}

.succes-count { color: var(--c-wheat); }

.succes-close {
  padding: 0 calc(2px * var(--px));
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  cursor: inherit;
}

/* Les cinq familles restent sur UNE ligne, jamais plus.
   Laisser le navigateur choisir donnait 4 + 1 : quatre colonnes puis une
   toute seule en dessous, ce qui se lit comme un oubli. Cinq ou rien, donc,
   et c'est l'échelle du panneau qui s'adapte à la largeur disponible (voir
   la media query sur `.succes`). */
.succes-grid {
  display: flex;
  flex-wrap: nowrap;
  align-items: stretch;
  justify-content: center;
  gap: calc(4px * var(--px));
}

/* Colonnes de même largeur : sans ça, « champ » et « commerce » ont des
   libellés de longueurs très différentes et la rangée part de travers. */
.succes-col { flex: 1 1 0; }

.succes-col {
  display: flex;
  flex-direction: column;
  gap: calc(1px * var(--px));

  /* Chaque colonne est un bandeau : cinq cadres valent mieux que trente. */
  border-style: solid;
  border-width: calc(5px * var(--px));
  border-image-source: url("../assets/sprites/ui/banner.png");
  border-image-slice: 5 fill;
  border-image-repeat: stretch;
  padding: calc(2px * var(--px)) calc(3px * var(--px));
}

.succes-family {
  color: var(--c-brown);
  letter-spacing: calc(1px * var(--px));
  margin-bottom: calc(2px * var(--px));
}

.succes-item {
  display: flex;
  align-items: center;
  gap: calc(3px * var(--px));
  white-space: nowrap;
}

/* Un succès non obtenu reste lisible : son libellé EST l'objectif, le cacher
   priverait la liste de son seul intérêt. Il est simplement estompé, comme
   les offres inaccessibles de la boutique. */
.succes-item[data-state="todo"] { opacity: 0.4; }
.succes-item[data-state="todo"] .icon { filter: grayscale(1); }

.succes-item[data-state="done"] .succes-label { color: var(--c-brown-dark); }

/* Le bandeau d'aide en bas du panneau a disparu : l'objectif se lit
   maintenant dans l'infobulle, contre la ligne survolée. */

/* ------------------------------------------------------------------ */
/* Nuit — voile tramé (GDD section 9)                                  */
/* ------------------------------------------------------------------ */

/* Un aplat de bleu nuit dont l'opacité suit l'heure, en continu.
 *
 * C'est la seule entorse assumée à la règle « rien qui ne vienne d'un asset ».
 * La première version était un tramage de Bayer, comme les jeux 8 bits : quatre
 * densités de damier, aucun pixel semi-transparent. Fidèle, mais regardé à
 * l'usage, le quadrillage se voyait plus que la nuit, et le passage d'un
 * palier à l'autre sautait au lieu de fondre. Un vrai fondu demande des
 * valeurs intermédiaires, donc de l'alpha.
 *
 * L'opacité est posée par js/daynight.js à chaque tour de boucle, cinq fois
 * par seconde : la transition est calculée, pas déclarée en CSS, pour rester
 * juste même après un onglet mis en arrière-plan.
 *
 * Le voile se pose entre le décor et les panneaux. La terre, les moutons et
 * les aides sont dessous et s'assombrissent ; les bandeaux de bois sont
 * dessus et restent lisibles. */
.night-veil {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background-color: #1a203a;
  opacity: 0;
}


/* Ce qui dort s'affaisse d'un pixel et se fige : le mouton ne respire plus,
   l'aide ne sautille plus. C'est le seul indice qu'ils ne travaillent pas. */
.sheep.is-asleep,
.helper.is-asleep {
  animation: none;
  transform: translateY(calc(1px * var(--px)));
  opacity: 0.75;
}

/* L'heure et la saison, dans le même bandeau. Le pack n'a ni soleil ni lune,
   donc ce sont les mots ; la couleur suffit à les distinguer d'un coup d'œil,
   et le point les sépare sans prendre de place. */
.horloge {
  display: inline-flex;
  align-items: center;
  gap: calc(3px * var(--px));
}

.horloge #season::after {
  content: "·";
  padding-left: calc(3px * var(--px));
  color: var(--c-brown-dark);
}

#clock[data-phase="jour"] { color: var(--c-wheat); }
#clock[data-phase="nuit"] { color: var(--c-water); }

/* Ce qui doit rester lisible la nuit passe au-dessus du voile. */
.zone-title,
.shop-column,
.loom { position: relative; z-index: 6; }

/* ------------------------------------------------------------------ */
/* Les loups (GDD section 9)                                           */
/* ------------------------------------------------------------------ */

/* La meute se tient du côté opposé aux aides, qui occupent déjà le coin
   bas-gauche de chaque zone. Elle passe AU-DESSUS du voile de nuit : partout
   ailleurs on assombrit pour dire l'heure, mais des loups qu'on ne
   distinguerait pas de l'herbe ne seraient pas un événement, seulement une
   surprise.

   `--i` est posé par wolves.js : trois loups au plus, alignés vers la gauche
   depuis le bord droit. */
.wolf {
  position: absolute;
  right: calc((6px + 22px * var(--i, 0)) * var(--px));
  /* Plus haut que les aides, qui se tiennent à 12 px du bas : à cette hauteur
     le bandeau de narration, qui court en bas de l'écran, recouvrait
     entièrement la meute — regardé, corrigé. */
  bottom: calc(34px * var(--px));
  width: calc(24px * var(--px));
  height: calc(24px * var(--px));
  pointer-events: none;
  z-index: 7;

  background-image: url("../assets/sprites/characters/loup.png");
  background-repeat: no-repeat;
  background-size: calc(24px * var(--px)) calc(24px * var(--px));
  image-rendering: pixelated;

  /* Ils arrivent de la droite, hors du cadre, en quatre pas nets. */
  animation: wolf-in 480ms steps(4) both,
             wolf-prowl 900ms steps(2) 480ms infinite alternate;
}

@keyframes wolf-in {
  from { transform: translateX(calc(40px * var(--px))); }
  to   { transform: translateX(0); }
}

/* Le rôdeur ne marche pas : il se balance sur ses pattes, deux images, comme
   les moutons et les aides. Une vraie marche demanderait des images de
   marche, et le pack n'en fournit pas pour un loup. */
@keyframes wolf-prowl {
  from { transform: translate(0, 0); }
  to   { transform: translate(calc(-2px * var(--px)), calc(-1px * var(--px))); }
}

/* Recul sec à chaque mouton balayé : c'est le seul retour immédiat qui dit
   que le geste sert à quelque chose. */
/* Le balancement est relancé derrière le recul : une animation seule
   remplacerait la précédente, et le loup resterait figé jusqu'au balayage
   suivant. */
.wolf.is-hit {
  animation: wolf-hit 220ms steps(2),
             wolf-prowl 900ms steps(2) 220ms infinite alternate;
}

@keyframes wolf-hit {
  0%   { transform: translateX(0); }
  50%  { transform: translateX(calc(5px * var(--px))); }
  100% { transform: translateX(0); }
}

/* Et ils repartent par où ils sont venus. */
.wolf.is-fleeing {
  animation: wolf-out 700ms steps(5) forwards;
}

@keyframes wolf-out {
  from { transform: translateX(0); opacity: 1; }
  to   { transform: translateX(calc(60px * var(--px))); opacity: 0; }
}

/* L'enclos pendant l'attaque : le même liseré net qu'une parcelle mûre, mais
   clignotant et couleur d'alerte. Pas de halo ni d'ombre floue — la règle du
   projet vaut aussi pour les moments dramatiques.

   Il passe AU-DESSUS du voile de nuit, le temps de l'attaque. Sous le voile,
   essayé et regardé, le rouge d'alerte ressortait violet et les bêtes
   restaient dans le noir — or c'est précisément elles qu'il faut survoler.
   L'enclos s'éclaire donc pendant les six secondes, et rentre dans la nuit
   dès que la meute est partie. */
.pen.is-raided {
  position: relative;
  z-index: 7;
  animation: raid-pulse 500ms steps(2) infinite;
}

@keyframes raid-pulse {
  0%, 100% { box-shadow: inset 0 0 0 calc(1px * var(--px)) var(--c-warn); }
  50%      { box-shadow: none; }
}

/* Le compte à rebours, au-dessus de l'enclos et dans l'axe : c'est là que les
   yeux sont déjà, puisque c'est là qu'il faut balayer. */
.raid-alert {
  position: absolute;
  /* Juste sous l'enseigne de la zone : 3 px de marge, 26 px de bandeau, 3 px
     d'écart. Valeur mesurée à l'écran, pas devinée. */
  top: calc(32px * var(--px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 8;
  padding: 0 calc(4px * var(--px));
  white-space: nowrap;
  color: var(--c-warn);
  animation: raid-blink 500ms steps(2) infinite;
}


@keyframes raid-blink {
  0%, 100% { color: var(--c-warn); }
  50%      { color: var(--c-brown-dark); }
}

/* ------------------------------------------------------------------ */
/* Bâtiments (voir js/buildings.js)                                    */
/* ------------------------------------------------------------------ */

/* 48x48 de dessin : le double des personnages. Ce sont des masses de bois et
   de pierre, avec des toits et des portes qu'on doit reconnaître d'un coup
   d'œil ; à la taille d'une aide il n'en resterait qu'une tache brune.

   Ils passent SOUS le voile de nuit, comme le reste du décor : un bâtiment
   éclairé à trois heures du matin sortirait du tableau. */
.batiment {
  position: absolute;
  width: calc(48px * var(--px));
  height: calc(48px * var(--px));
  pointer-events: none;

  /* Derrière l'enclos et le lopin (z-index 1), devant les fleurs (0).
   *
   * Un bâtiment est un décor de fond : il ne doit jamais masquer une bête ni
   * une parcelle, qui sont les seules choses avec lesquelles on joue. Quand le
   * troupeau devient énorme et vient mordre sur le couloir, c'est la bergerie
   * qui passe dessous — elle a l'air posée derrière la clôture, et aucun
   * mouton ne disparaît. */
  z-index: 0;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  image-rendering: pixelated;
}


/* Les quatre coins. Aucun ne tombe sur une aide (bas-gauche) ni sur le
   marchand (bas-droite de la boutique) — vérifié par les tests. */
.batiment[data-coin="hg"] { left: calc(4px * var(--px));  top: calc(20px * var(--px)); }
.batiment[data-coin="hd"] { right: calc(4px * var(--px)); top: calc(20px * var(--px)); }
.batiment[data-coin="bd"] { right: calc(4px * var(--px)); bottom: calc(6px * var(--px)); }

/* La vignette du panneau de chantier : le sprite de 32 px, à sa TAILLE RÉELLE.
   Ce n'est pas celui de 48 px rétréci par le navigateur — ça lui mangeait un
   pixel sur deux et le moulin n'était plus qu'une tache. C'est un second
   sprite, sorti de la même source à 32 px par tools/import_sprite.py. On ne
   réduit jamais un sprite dans ce jeu ; on en refait un. */
.batiment-vignette {
  flex: 0 0 auto;
  width: calc(32px * var(--px));
  height: calc(32px * var(--px));
  background-repeat: no-repeat;
  background-size: 100% 100%;
  image-rendering: pixelated;
}

.batiment-vignette.batiment--moulin   { background-image: url("../assets/sprites/buildings/moulin-32.png"); }
.batiment-vignette.batiment--bergerie { background-image: url("../assets/sprites/buildings/bergerie-32.png"); }
.batiment-vignette.batiment--sechoir  { background-image: url("../assets/sprites/buildings/sechoir-32.png"); }
.batiment-vignette.batiment--etal     { background-image: url("../assets/sprites/buildings/etal-32.png"); }
.batiment-vignette.batiment--grenier  { background-image: url("../assets/sprites/buildings/grenier-32.png"); }
.batiment-vignette.batiment--niche    { background-image: url("../assets/sprites/buildings/niche-32.png"); }

.batiment--moulin   { background-image: url("../assets/sprites/buildings/moulin.png"); }
.batiment--bergerie { background-image: url("../assets/sprites/buildings/bergerie.png"); }
.batiment--sechoir  { background-image: url("../assets/sprites/buildings/sechoir.png"); }
.batiment--etal     { background-image: url("../assets/sprites/buildings/etal.png"); }
.batiment--grenier  { background-image: url("../assets/sprites/buildings/grenier.png"); }
.batiment--niche    { background-image: url("../assets/sprites/buildings/niche.png"); }

/* Le panneau de chantier */
.chantier {
  display: flex;
  flex-direction: column;
  gap: calc(2px * var(--px));
  /* Assez large pour que chaque effet tienne sur UNE ligne. À 330 px les six
     phrases se coupaient en deux ou trois, le panneau faisait 800 px de haut et
     la dernière ligne passait sous le bord de l'écran d'un portable. */
  width: calc(430px * var(--px));
  max-width: 92vw;
  padding: calc(1px * var(--px)) calc(4px * var(--px)) calc(4px * var(--px));
  color: var(--c-brown-dark);
}

.chantier-head {
  position: relative;
  display: flex;
  align-items: center;
  /* L'icône de l'en-tête fait 32 px de dessin, donc deux fois la hauteur du
     mot. Sans cette hauteur minimale, elle débordait sur la première ligne de
     la liste. */
  min-height: calc(32px * var(--px));
  gap: calc(4px * var(--px));
  letter-spacing: calc(1px * var(--px));
}

.chantier-compte { margin-left: auto; color: var(--c-brown); }

.chantier-close {
  padding: 0 calc(3px * var(--px));
  background: none;
  border: 0;
  cursor: inherit;
  color: var(--c-brown-dark);
}

/* Une ligne par bâtiment, du moins cher au plus cher : c'est l'ordre où on les
   construit, et ça évite de chercher ce qu'on peut s'offrir. */
/* Les lignes s'alignent en haut : la vignette fait 48 px et le texte deux
   lignes, donc un centrage laissait le sprite flotter à côté de rien. */
.chantier-ligne {
  display: flex;
  align-items: flex-start;
  gap: calc(4px * var(--px));
  padding: calc(2px * var(--px));
  background: none;
  border: 0;
  cursor: inherit;
  text-align: left;
  color: var(--c-brown-dark);
}

.chantier-texte {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  line-height: 1.1;
}

.chantier-effet { color: var(--c-brown); }
.chantier-prix { color: var(--c-wheat); }

/* Trop cher : la ligne s'estompe, comme les boutons inactifs de la boutique.
   Déjà bâti : elle reste pleine, et le prix devient « debout ». */
.chantier-ligne[data-state="poor"] { opacity: 0.5; }
.chantier-ligne[data-state="built"] .chantier-prix { color: var(--c-brown); }
.chantier-ligne[data-state="ready"]:active {
  transform: translate(calc(1px * var(--px)), calc(1px * var(--px)));
}

/* ------------------------------------------------------------------ */
/* Passer la main (voir js/heritage.js)                                */
/* ------------------------------------------------------------------ */

.heritage {
  display: flex;
  flex-direction: column;
  gap: calc(3px * var(--px));
  width: calc(300px * var(--px));
  max-width: 92vw;
  padding: calc(1px * var(--px)) calc(5px * var(--px)) calc(4px * var(--px));
  color: var(--c-brown-dark);
}

.heritage-head {
  display: flex;
  align-items: center;
  gap: calc(4px * var(--px));
  letter-spacing: calc(1px * var(--px));
}

.heritage-quoi {
  margin: 0;
  line-height: 1.1;
  color: var(--c-brown);
}

/* Ce qu'on gagne, en doré : c'est la seule bonne nouvelle d'un panneau qui
   parle surtout d'effacer. */
.heritage-gains {
  display: flex;
  flex-wrap: wrap;
  gap: calc(3px * var(--px)) calc(8px * var(--px));
  padding: calc(2px * var(--px)) 0;
}

.heritage-gain {
  display: inline-flex;
  align-items: center;
  gap: calc(3px * var(--px));
  color: var(--c-wheat);
}

/* Ce qu'il faut retenir, en doré. Pas en gras : la police du pack n'a pas de
   graisse, et le navigateur en fabriquerait une en doublant les pixels — le
   seul texte épaissi et flou de tout l'écran. */
.heritage-cle { color: var(--c-wheat); }

.heritage-note {
  margin: 0;
  line-height: 1.1;
  opacity: 0.8;
}

.heritage-actions {
  display: flex;
  gap: calc(5px * var(--px));
  padding-top: calc(2px * var(--px));
}

.heritage-go,
.heritage-annuler {
  padding: 0 calc(4px * var(--px));
  cursor: inherit;
  color: var(--c-brown-dark);
}

/* Le second clic est annoncé en rouge : c'est le seul geste du jeu, avec
   « nouvelle partie », qui efface une ferme. */
.heritage-go[data-confirm="true"] { color: var(--c-warn); }

/* ------------------------------------------------------------------ */
/* Saisons (voir js/seasons.js)                                        */
/* ------------------------------------------------------------------ */

/* Le sol suit la saison. Les quatre tuiles sont relevées dans le pack, pas
   teintées en CSS : un filtre ferait glisser toute la palette hors de celle du
   pack, et l'herbe ne serait plus la même matière que les sprites posés
   dessus. L'attribut est posé sur le corps par seasons.js, donc un seul
   changement repeint la ferme entière. */
body[data-season="printemps"] .zone,
body[data-season="printemps"] .pen {
  background-image: url("../assets/sprites/ui/grass-printemps.png");
}
body[data-season="ete"] .zone,
body[data-season="ete"] .pen {
  background-image: url("../assets/sprites/ui/grass-ete.png");
}
body[data-season="automne"] .zone,
body[data-season="automne"] .pen {
  background-image: url("../assets/sprites/ui/grass-automne.png");
}
body[data-season="hiver"] .zone,
body[data-season="hiver"] .pen {
  background-image: url("../assets/sprites/ui/grass-hiver.png");
}

/* Les fleurs du décor changent elles aussi : le pack en fournit deux par
   saison, jusqu'aux fleurs d'hiver. */
body[data-season="ete"] .zone::before {
  background-image: url("../assets/cozy-farm/tiles/flower_summer_orange.gif");
}
body[data-season="ete"] .zone::after {
  background-image: url("../assets/cozy-farm/tiles/flower_summer_red.gif");
}
body[data-season="automne"] .zone::before {
  background-image: url("../assets/cozy-farm/tiles/flower_fall_orange.gif");
}
body[data-season="automne"] .zone::after {
  background-image: url("../assets/cozy-farm/tiles/flower_fall_red.gif");
}
body[data-season="hiver"] .zone::before {
  background-image: url("../assets/cozy-farm/tiles/flower_winter_blue.gif");
}
body[data-season="hiver"] .zone::after {
  background-image: url("../assets/cozy-farm/tiles/flower_winter_purple.gif");
}

/* Le mot de la saison, dans la barre du haut. Chacune a sa couleur, prise dans
   la palette : on la reconnaît sans lire. */
body[data-season="printemps"] #season { color: var(--c-wheat-light); }
body[data-season="ete"] #season       { color: var(--c-wheat); }
body[data-season="automne"] #season   { color: var(--c-warn); }
body[data-season="hiver"] #season     { color: var(--c-water); }

/* Sur la neige, le brun du texte des enseignes reste lisible, mais les
   compteurs blancs des moutons se perdaient : on garde l'ombre portée. */
body[data-season="hiver"] .zone-title { text-shadow: calc(1px * var(--px)) calc(1px * var(--px)) 0 rgba(58, 42, 32, 0.5); }

/* ------------------------------------------------------------------ */
/* Bandeau de retour (voir js/offline.js)                              */
/* ------------------------------------------------------------------ */

/* Centré et au premier plan : c'est la première chose à lire en rentrant, et
   ça n'arrive qu'une fois par session. Il prend les clics, pour son bouton. */
.retour {
  position: fixed;
  left: 50%;
  /* Sous la poutre de la barre du haut, pas dessus : mesuré à l'écran, à 28 px
     le bandeau mordait sur les compteurs. */
  top: calc(40px * var(--px));
  transform: translateX(-50%);
  z-index: 19;

  display: flex;
  flex-direction: column;
  gap: calc(2px * var(--px));
  padding: calc(1px * var(--px)) calc(5px * var(--px)) calc(3px * var(--px));
  max-width: 92vw;
  color: var(--c-brown-dark);

  animation: toast-in 240ms steps(4);
}


.retour-head {
  display: flex;
  align-items: center;
  gap: calc(4px * var(--px));
  letter-spacing: calc(1px * var(--px));
}

.retour-ok {
  margin-left: calc(6px * var(--px));
  padding: 0 calc(4px * var(--px));
  cursor: inherit;
  color: var(--c-brown-dark);
}

.retour-note {
  margin: 0;
  line-height: 1.1;
  color: var(--c-brown);
}

/* Les gains sur une ligne, chacun derrière son icône : c'est ce qu'on vient
   lire, donc ça doit se compter d'un coup d'œil et non se déchiffrer. */
.retour-gains {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(3px * var(--px)) calc(8px * var(--px));
  padding-top: calc(2px * var(--px));
}

.retour-gain {
  display: inline-flex;
  align-items: center;
  gap: calc(3px * var(--px));
  color: var(--c-wheat);
}

/* ------------------------------------------------------------------ */
/* Réglages du son (voir js/soundpanel.js)                             */
/* ------------------------------------------------------------------ */

.son-panel {
  position: fixed;
  z-index: 17;             /* au-dessus du tutoriel : c'est un réglage */
  display: flex;
  flex-direction: column;
  gap: calc(3px * var(--px));
  padding: calc(2px * var(--px)) calc(5px * var(--px)) calc(4px * var(--px));
  color: var(--c-brown-dark);
  animation: tip-in 160ms steps(3);
}


.son-row {
  display: flex;
  align-items: center;
  gap: calc(4px * var(--px));
}

/* Largeur fixe pour les deux étiquettes : sans elle, « musique » et « effets »
   n'ont pas la même longueur et les deux jauges ne s'alignent pas. Un cran de
   plus que le mot le plus long, sinon « musique » vient toucher sa jauge. */
.son-label { width: calc(66px * var(--px)); }

/* Le chiffre est à droite, sur une largeur fixe lui aussi : à 100 il gagne un
   caractère, et toute la ligne se décalait d'un cran en passant de 90 à 100. */
.son-val {
  width: calc(24px * var(--px));
  text-align: right;
  color: var(--c-brown);
}

/* La jauge : dix cases, deux aplats de la palette — exactement la matière de
   la jauge de combo. Pas de curseur du système, qui serait le seul élément
   rond et ombré de l'écran. */
.son-jauge {
  display: flex;
  gap: calc(1px * var(--px));
  cursor: inherit;
}

.son-case {
  width: calc(6px * var(--px));
  height: calc(8px * var(--px));
  background-color: var(--c-brown-dark);
}

.son-case[data-on="true"] { background-color: var(--c-wheat); }

.son-couper {
  padding: 0 calc(4px * var(--px));
  cursor: inherit;
  color: var(--c-brown-dark);
}

.son-couper[data-muted="true"] { color: var(--c-warn); }

/* ------------------------------------------------------------------ */
/* Tutoriel — une bulle à côté de ce qu'elle explique                  */
/* ------------------------------------------------------------------ */

/* Elle ne bloque pas le jeu : la ferme continue de tourner derrière, et la
   bulle disparaît d'elle-même dès que le geste est fait. Elle prend quand
   même les clics, pour le bouton « compris » — c'est le seul élément flottant
   du jeu qui le fasse, et c'est voulu. */
/* Large exprès : chaque ligne de texte fait 16 px de hauteur de dessin, donc
   une bulle étroite devient vite plus haute qu'une zone. On préfère prendre de
   la largeur, qui est libre, plutôt que de la hauteur, qui ne l'est pas. */
.tuto {
  position: fixed;
  z-index: 16;
  width: calc(330px * var(--px));
  max-width: 92vw;
  padding: calc(1px * var(--px)) calc(5px * var(--px)) calc(3px * var(--px));

  display: flex;
  flex-direction: column;
  gap: calc(2px * var(--px));
  color: var(--c-brown-dark);

  animation: tip-in 160ms steps(3);
}


.tuto-head {
  display: flex;
  align-items: center;
  gap: calc(4px * var(--px));
  letter-spacing: calc(1px * var(--px));
}

/* Le bouton est poussé au bout de la ligne du titre : une ligne de moins,
   c'est de l'herbe du voisin que la bulle ne recouvre pas. */
.tuto-ok { margin-left: auto; }

/* Le titre ne se coupe jamais : vu à l'écran, « quand vous n'êtes pas là » se
   repliait sous lui-même et le dernier mot passait derrière le texte suivant.
   Les titres sont courts par construction ; c'est le bouton qui cède, pas eux. */
.tuto-titre { white-space: nowrap; }

/* Le panonceau « ? » du pack, découpé par tools/build_ui.py. Le pack le
   fournit déjà gravé sur bois : rien à dessiner. */
.tuto-sign {
  flex: 0 0 auto;
  width: var(--tile);
  height: var(--tile);
  background-image: url("../assets/sprites/ui/hint.png");
  background-size: 100% 100%;
  image-rendering: pixelated;
}

/* Deux voix distinctes : à quoi ça sert, puis comment s'en servir. La
   deuxième est celle qu'on relit, donc c'est elle qui reste en pleine
   couleur ; la première s'estompe une fois lue. */
.tuto-quoi {
  margin: 0;
  line-height: 1.1;
  opacity: 0.8;
}

.tuto-comment {
  margin: 0;
  line-height: 1.1;
  color: var(--c-brown);
}

.tuto-ok {
  flex: 0 0 auto;
  padding: 0 calc(4px * var(--px));
  cursor: inherit;
  color: var(--c-brown-dark);
}

/* La flèche, découpée du bouton « back » du pack et tournée d'un quart de
   tour. Elle est centrée sur l'ANCRE et non sur la bulle : quand un bord de
   l'écran repousse la bulle, c'est elle qui garde le lien avec ce qu'on
   explique. `--fleche` est posé par tutorial.js. */
.tuto::after {
  content: "";
  position: absolute;
  left: calc(var(--fleche, 50%) - 5px * var(--px));
  width: calc(10px * var(--px));
  height: calc(24px * var(--px));
  background-repeat: no-repeat;
  background-size: 100% 100%;
  image-rendering: pixelated;
  animation: tuto-point 700ms steps(2) infinite alternate;
}

/* Vers le contenu de la zone, sous la bulle. */
.tuto[data-sens="bas"]::after {
  top: 100%;
  background-image: url("../assets/sprites/ui/arrow-down.png");
}

/* Vers un bouton de la barre du haut, au-dessus de la bulle. */
.tuto[data-sens="haut"]::after {
  bottom: 100%;
  background-image: url("../assets/sprites/ui/arrow-up.png");
}

/* Elle avance et recule d'un pixel de dessin, par paliers : assez pour
   attraper l'œil, pas assez pour qu'on la regarde elle. */
@keyframes tuto-point {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(2px * var(--px))); }
}

.tuto[data-sens="haut"]::after {
  animation-name: tuto-point-haut;
}

@keyframes tuto-point-haut {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-2px * var(--px))); }
}

/* Ce que la leçon désigne : le même liseré net qu'une parcelle mûre, en deux
   pixels de dessin plutôt qu'un — mesuré à l'écran, un seul pixel d'or se
   perdait dans le brun de la poutre.

   Fixe, et non clignotant comme celui de l'enclos attaqué : la flèche bouge
   déjà. Deux choses qui s'agitent en même temps, et on ne sait plus laquelle
   regarder — une attaque de loups presse, une leçon non. */
.is-tuto-cible {
  box-shadow: inset 0 0 0 calc(2px * var(--px)) var(--c-wheat);
}

/* ------------------------------------------------------------------ */
/* Narration légère (GDD section 7)                                    */
/* ------------------------------------------------------------------ */

/* Le bandeau de palier, en bas de l'écran. Il ne prend aucun événement :
   le jeu se joue au survol, et une bande qui avale les survols au moment
   précis où l'on récolte serait la pire chose à faire. */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(8px * var(--px));
  transform: translateX(-50%);
  z-index: 15;
  pointer-events: none;

  display: flex;
  align-items: center;
  gap: calc(5px * var(--px));
  padding: calc(1px * var(--px)) calc(5px * var(--px));
  /* Deux bornes : ne jamais toucher les bords de la fenêtre, et ne jamais
     s'étirer sur toute la largeur d'un grand écran — une ligne de soixante-dix
     caractères d'un bord à l'autre ne se lit pas, l'œil perd le début. */
  max-width: min(88vw, calc(430px * var(--px)));
  color: var(--c-brown-dark);

  animation: toast-in 240ms steps(4);
}


@keyframes toast-in {
  from { transform: translate(-50%, calc(12px * var(--px))); }
  to   { transform: translate(-50%, 0); }
}

/* 24 px et non var(--tile) : le portrait fait 24x24 de dessin, le réduire à
   16 le rendrait flou — c'est le seul endroit du jeu où l'échelle ne serait
   pas un multiple entier. */
.toast-face {
  flex: 0 0 auto;
  width: calc(24px * var(--px));
  height: calc(24px * var(--px));
  background-image: url("../assets/sprites/characters/marchand.png");
  background-size: 100% 100%;
  image-rendering: pixelated;
}

/* La réplique se plie plutôt que de fuir hors de l'écran.
 *
 * En `nowrap`, le `max-width` du bandeau ne servait à rien : l'annonce d'un
 * bâtiment fait jusqu'à quatre-vingt-dix caractères — près de 1400 px à
 * l'échelle du jeu — et se coupait au bord de la fenêtre. Réduire la police
 * était l'autre issue, mais elle briserait la règle d'échelle unique : ce
 * texte doit avoir exactement les mêmes pixels que ses voisins.
 *
 * `min-width: 0` est indispensable : un enfant de conteneur flexible refuse
 * par défaut de descendre sous la largeur de son contenu, et aurait continué
 * à déborder malgré l'autorisation de revenir à la ligne. */
.toast-line {
  white-space: normal;
  min-width: 0;
  text-align: center;
}

/* Le marchand, posté dans la boutique — à droite, et non à gauche comme les
   aides : le coin bas-gauche de la boutique tombe au milieu de l'écran, juste
   là où passe le bandeau de narration, qui l'aurait recouvert. */
.merchant {
  position: absolute;
  right: calc(6px * var(--px));
  bottom: calc(14px * var(--px));
  width: calc(24px * var(--px));
  height: calc(24px * var(--px));
  z-index: 3;

  background-image: url("../assets/sprites/characters/marchand.png");
  background-repeat: no-repeat;
  background-size: calc(24px * var(--px)) calc(24px * var(--px));
  image-rendering: pixelated;

  animation: helper-idle 1800ms steps(2) infinite alternate;
}

/* Sa réplique, au-dessus de sa tête et alignée à droite : il est lui-même
   contre le bord droit de l'écran, une bulle qui partirait vers la droite
   sortirait du cadre.

   Elle est posée dans la zone, à côté du portrait et non dedans (voir
   js/story.js), donc ses coordonnées partent du coin de la zone : 6 px de
   marge comme le marchand, et 36 px de haut pour passer au-dessus de sa tête.
   Au niveau 9, elle domine les bandeaux de la boutique, qui sont au 6. */
.merchant-line {
  position: absolute;
  right: calc(6px * var(--px));
  bottom: calc(36px * var(--px));
  z-index: 9;
  padding: 0 calc(4px * var(--px));
  color: var(--c-brown-dark);

  /* Elle se plie au lieu de fuir par la gauche.
   *
   * En `nowrap`, une réplique longue faisait sept cents pixels de large : elle
   * sortait de la zone, le texte se coupait au bord de l'écran, et — bien
   * pire — ce ruban invisible s'étalait par-dessus toute la boutique. En
   * z-index 9 et sans `pointer-events`, il interceptait les survols et les
   * clics des lignes d'achat : c'est l'origine des « des fois ça clique pas ». */
  max-width: calc(190px * var(--px));
  white-space: normal;
  text-align: center;

  /* Rien de ce qui parle ne doit prendre un geste. Le marchand, lui, garde ses
     survols : c'est lui qu'on approche pour le faire parler. */
  pointer-events: none;
}


/* ------------------------------------------------------------------ */
/* Aides — un petit personnage posté dans sa zone (GDD section 5)      */
/* ------------------------------------------------------------------ */

/* Les trois aides se tiennent dans le coin bas-gauche de leur zone, hors du
   chemin : le champ, l'enclos et l'atelier sont centrés, et tout ce coin est
   de l'herbe. Elles ne prennent aucun clic ni survol — ce ne sont pas des
   boutons, juste la preuve visible qu'un travail se fait tout seul. */
.helper {
  position: absolute;
  left: calc(4px * var(--px));
  bottom: calc(12px * var(--px));
  width: calc(24px * var(--px));
  height: calc(24px * var(--px));
  pointer-events: none;
  z-index: 3;

  background-repeat: no-repeat;
  background-size: calc(24px * var(--px)) calc(24px * var(--px));
  image-rendering: pixelated;

  /* Respiration lente et par paliers, pour qu'elles aient l'air vivantes
     sans attirer l'œil plus que les moutons. */
  animation: helper-idle 1400ms steps(2) infinite alternate;
}


.helper--gardener { background-image: url("../assets/sprites/characters/aide-jardinier.png"); }
.helper--shepherd { background-image: url("../assets/sprites/characters/aide-berger.png"); }
.helper--weaver   { background-image: url("../assets/sprites/characters/aide-tisserand.png"); }

@keyframes helper-idle {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-1px * var(--px))); }
}

/* Sursaut au moment où l'aide agit : c'est ce qui relie le sprite à l'effet
   qu'on voit ailleurs dans la zone (gouttes, cœur, point de tricot). */
.helper.is-working { animation: helper-work 320ms steps(3); }

@keyframes helper-work {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(calc(-3px * var(--px))); }
  100% { transform: translateY(0); }
}

/* ------------------------------------------------------------------ */
/* Champ de blé                                                        */
/* ------------------------------------------------------------------ */

/* Le lopin : terre labourée, ceinturée du même cadre en poutres que les
   zones. Donne au champ sa place à l'écran et laisse voir où les parcelles
   supplémentaires de la Phase 2 viendront s'ajouter. */
.field-bed {
  position: relative;
  z-index: 1;                 /* au-dessus des fleurs du décor */
  margin: auto;
  padding: calc(4px * var(--px));
  /* Mêmes couloirs réservés que l'enclos : le grenier et le moulin s'y posent. */
  max-width: calc(100% - 64px * var(--px));

  background-image: url("../assets/sprites/ui/soil.png");
  background-size: var(--tile) var(--tile);
  background-repeat: repeat;
  image-rendering: pixelated;
  /* cadre et clip du fond : voir .field-bed dans pixel-art.css */
}

.field-grid {
  display: grid;
  grid-template-columns: repeat(3, var(--tile));
  gap: calc(2px * var(--px));
  justify-content: center;
  align-content: center;
}

.plot {
  position: relative;
  width: var(--tile);
  height: var(--tile);
}

/* Sillon d'un pixel de dessin qui détache la parcelle du lopin.
   Il vit dans un ::after et non sur .plot : une ombre interne se peint
   sous les enfants, et .plot-soil (inset: 0) la recouvrait entièrement. */
.plot::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 calc(1px * var(--px)) var(--c-brown-dark);
}

/* Terre plus sombre et plus froide une fois arrosée */
.plot.is-watered .plot-soil { filter: brightness(0.78) saturate(0.85) hue-rotate(-8deg); }

/* Le sillon change de couleur selon ce qu'on peut faire : bleu tant que
   l'arrosage est possible, doré quand le blé est mûr. Traits nets, sans flou. */
.plot.is-watering::after {
  animation: waterPulse 900ms steps(2) infinite;
}

@keyframes waterPulse {
  0%, 100% { box-shadow: inset 0 0 0 calc(1px * var(--px)) var(--c-water); }
  50%      { box-shadow: inset 0 0 0 calc(1px * var(--px)) var(--c-brown-dark); }
}

.plot[data-state="ready"]::after {
  box-shadow: inset 0 0 0 calc(1px * var(--px)) var(--c-wheat);
}

.plot[data-state="ready"] .plot-crop {
  animation: sway 1200ms steps(2) infinite alternate;
}

@keyframes sway {
  from { transform: translateX(calc(-1px * var(--px))); }
  to   { transform: translateX(calc(1px * var(--px))); }
}

/* Les enfants ne doivent jamais intercepter le survol de la parcelle */
.plot > * { pointer-events: none; }

/* Jauge de pousse : un seul pixel de dessin, posé dans l'écart entre les
   parcelles pour ne jamais recouvrir le sprite de blé.
   Masquée sur une parcelle vide — il n'y a rien à mesurer. */
.plot-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(-1px * var(--px));
  height: calc(1px * var(--px));
  background-color: var(--c-brown-dark);
}

.plot[data-state="empty"] .plot-bar { display: none; }

.plot-bar-fill {
  height: 100%;
  width: 0;
  background-color: var(--c-wheat);
  transition: width 200ms linear;
}

.plot.is-watering .plot-bar-fill { background-color: var(--c-water); }
.plot[data-state="ready"] .plot-bar-fill { background-color: var(--c-wheat-light); }

/* ------------------------------------------------------------------ */
/* Enclos des moutons                                                  */
/* ------------------------------------------------------------------ */

/* Le pré : même cadre en poutres que le lopin du champ, pour que les deux
   zones parlent la même langue. Sa taille minimale fait qu'un enclos vide
   reste visible — c'est ce qui donne envie d'acheter une bête. */
.pen {
  position: relative;
  z-index: 1;                 /* au-dessus des fleurs du décor */

  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-content: center;
  /* De la place au-dessus de chaque bête pour le badge d'action, qui fait
     une tuile entière et se pose 18 px de dessin plus haut. */
  gap: calc(21px * var(--px)) calc(5px * var(--px));
  margin: auto;
  padding: calc(19px * var(--px)) calc(6px * var(--px)) calc(4px * var(--px));

  min-width: calc(96px * var(--px));
  min-height: calc(44px * var(--px));

  /* Deux couloirs réservés aux bâtiments, et un enclos qui se contient.
   *
   * Sans la borne, un troupeau de dix-sept bêtes étirait l'enclos d'un bord à
   * l'autre de la zone et la bergerie se retrouvait plantée au milieu des
   * moutons. Le couloir vaut 32 px de dessin et non les 48 du sprite : les
   * bâtiments passent DERRIÈRE l'enclos (voir .batiment), donc un léger
   * recouvrement se lit comme une cabane posée derrière la clôture, ce qui est
   * juste. Ce qui ne l'était pas, c'est qu'une bête disparaisse dessous.
   *
   * `overflow: hidden` répare un défaut plus ancien : passé une trentaine de
   * bêtes, la zone comprime l'enclos, les rangs débordaient et des moutons se
   * retrouvaient dessinés DEHORS, au-dessus et en dessous des poutres. */
  max-width: calc(100% - 64px * var(--px));
  overflow: hidden;

  background-image: url("../assets/sprites/ui/grass.png");
  background-size: var(--tile) var(--tile);
  background-repeat: repeat;
  image-rendering: pixelated;
  /* cadre et clip du fond : voir .pen dans pixel-art.css */
}

.sheep > * { pointer-events: none; }

/* Une bête qui dépérit : liseré rouge net, et elle se met à trembler. */
.sheep[data-health="weak"] .sheep-body {
  animation: shiver 320ms steps(2) infinite alternate;
}

@keyframes shiver {
  from { transform: translateX(calc(-1px * var(--px))); }
  to   { transform: translateX(calc(1px * var(--px))); }
}

/* Le badge d'action fait un petit va-et-vient pour attirer l'œil */
.sheep-badge--feed,
.sheep-badge--shear {
  animation: bob 900ms steps(2) infinite alternate;
}

@keyframes bob {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-2px * var(--px))); }
}

/* Jauge sous la bête : repousse de la laine, ou compte à rebours avant
   la mort quand elle dépérit. */
.sheep-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(-2px * var(--px));
  height: calc(1px * var(--px));
  background-color: var(--c-brown-dark);
}

.sheep-bar-fill {
  height: 100%;
  width: 0;
  background-color: var(--c-cream);
  transition: width 200ms linear;
}

.sheep[data-health="weak"] .sheep-bar-fill { background-color: var(--c-warn); }

.sheep-fx {
  position: absolute;
  inset: 0;
  overflow: visible;
  z-index: 2;
}

/* Sprite qui s'envole (le cœur du pack quand on nourrit) */
.pop {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  image-rendering: pixelated;
  background-repeat: no-repeat;
  animation: floatUp 900ms steps(9) forwards;
  opacity: 0;
}

/* ------------------------------------------------------------------ */
/* Atelier                                                             */
/* ------------------------------------------------------------------ */

.workshop {
  display: flex;
  flex-direction: column;
  margin: auto;
}

/* Le métier : même bandeau que la boutique, mêmes lignes cliquables. */
.loom {
  display: flex;
  flex-direction: column;
  min-width: calc(96px * var(--px));
}

.loom-recipe {
  display: grid;
  grid-template-columns: var(--tile) 1fr auto var(--tile);
  align-items: center;
  gap: calc(5px * var(--px));
  padding: calc(1px * var(--px)) calc(2px * var(--px));
  border: 0;
  background: none;
  text-align: left;
  cursor: inherit;
}

.loom-recipe + .loom-recipe {
  border-top: calc(1px * var(--px)) solid var(--c-brown);
}

.loom-label { justify-self: start; }
.loom-cost  { justify-self: end; }

.loom-recipe:disabled { opacity: 0.55; }

.loom-recipe:not(:disabled):active {
  transform: translate(calc(1px * var(--px)), calc(1px * var(--px)));
}

/* ------------------------------------------------------------------ */
/* Métier à picots (voir js/crafting.js)                               */
/* ------------------------------------------------------------------ */

.rail-head {
  display: grid;
  grid-template-columns: var(--tile) 1fr auto;
  align-items: center;
  gap: calc(4px * var(--px));
  padding: 0 calc(2px * var(--px)) calc(2px * var(--px));
}

.rail-count { color: var(--c-brown); }

/* La série est encore propre : le compteur passe en doré, comme un épi mûr.
   Une faute le rend au brun, sans autre annonce — on perd un bonus, on n'a
   rien cassé, et un message d'échec serait hors de proportion. */
.rail-count[data-clean="true"] { color: var(--c-wheat); }

/* Le cadre : une planche du pack, aux dimensions posées par crafting.js selon
   la longueur du tricot. */
.pegboard {
  position: relative;
  z-index: 6;
  width: calc(var(--board-w) * 1px * var(--px));
  height: calc(var(--board-h) * 1px * var(--px));
  margin: 0 auto calc(2px * var(--px));

  background-image: url("../assets/sprites/ui/plank.png");
  background-size: calc(9px * var(--px)) calc(77px * var(--px));
  background-repeat: repeat;
  image-rendering: pixelated;
}

/* Le patron, tracé au canvas : un trait SVG serait lissé, et ce serait la
   seule chose floue de l'écran. */
.pegboard-thread {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  image-rendering: pixelated;
}

/* Les picots réutilisent les perles de l'arbre de compétences : c'est déjà de
   la laine, déjà à la bonne taille, et les deux endroits du jeu où l'on suit
   un fil se ressemblent alors volontairement. */
.peg {
  position: absolute;
  width: calc(24px * var(--px));
  height: calc(24px * var(--px));
  image-rendering: pixelated;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  background-image: url("../assets/sprites/ui/bead-locked.png");
}

.peg[data-state="done"],
.peg[data-state="here"] {
  background-image: url("../assets/sprites/ui/bead-owned.png");
}

/* Celui qu'on cherche. Il est le seul à bouger : c'est ce qui permet de le
   trouver sans lire, en enchaînant. */
.peg[data-state="next"] {
  background-image: url("../assets/sprites/ui/bead-ready.png");
  animation: peg-call 600ms steps(2) infinite alternate;
}

@keyframes peg-call {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-2px * var(--px))); }
}

/* ------------------------------------------------------------------ */
/* Boutique                                                            */
/* ------------------------------------------------------------------ */

/* Deux colonnes, ventes puis achats. Un bandeau par colonne et non par
   ligne : chaque cadre coûte deux bordures, et six lignes empilées ne
   tenaient plus dans le quart d'écran d'un portable. */
.shop {
  display: flex;
  align-items: flex-start;
  gap: calc(3px * var(--px));
  margin: auto;
}

.shop-column {
  display: flex;
  flex-direction: column;
}

/* Au clic, pas au survol : le survol est réservé aux gestes de terrain. */
.shop-offer {
  display: grid;
  grid-template-columns: var(--tile) 1fr auto var(--tile);
  align-items: center;
  gap: calc(5px * var(--px));
  padding: calc(1px * var(--px)) calc(2px * var(--px));
  border: 0;
  background: none;
  text-align: left;
  cursor: inherit;
}

/* Séparateur d'un pixel de dessin entre les lignes */
.shop-offer + .shop-offer {
  border-top: calc(1px * var(--px)) solid var(--c-brown);
}

.shop-label { justify-self: start; }
.shop-price { justify-self: end; }

.shop-offer--gain .shop-price { color: var(--c-wheat); }

/* Marché Dynamique : le cours se lit à la couleur du prix. Doré clair quand
   il est haut, brun terne quand il est bas — les deux teintes que le jeu
   utilise déjà pour « ça va bien » et « ça va moins bien », donc rien de
   nouveau à apprendre. Sans la compétence, aucun `data-trend` n'est posé et
   le prix garde sa couleur d'origine. */
.shop-offer[data-trend="high"] .shop-price { color: var(--c-wheat-light); }
.shop-offer[data-trend="low"]  .shop-price { color: var(--c-brown); }

/* Le prix au plus haut respire, par paliers : c'est ce qui attire l'œil sur
   la boutique alors qu'on est en train de jouer ailleurs. */
.shop-offer[data-trend="high"]:not(:disabled) .shop-price {
  animation: bump 900ms steps(3) infinite;
}

/* Assez pâle pour dire « pas maintenant », assez lisible pour qu'on voie
   quand même le prix à atteindre. */
.shop-offer:disabled { opacity: 0.55; }

/* Enfoncement d'un pixel de dessin quand on presse — pas de transition douce */
.shop-offer:not(:disabled):active {
  transform: translate(calc(1px * var(--px)), calc(1px * var(--px)));
}

/* ------------------------------------------------------------------ */
/* Retours visuels : textes flottants et gouttes                       */
/* ------------------------------------------------------------------ */

.plot-fx,
.plot-drops {
  position: absolute;
  inset: 0;
  overflow: visible;
  z-index: 2;      /* au-dessus du sillon dessiné par .plot::after */
}

.float {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  white-space: nowrap;
  animation: floatUp 900ms steps(9) forwards;
  opacity: 0;
}

.float--gain    { color: var(--c-wheat); }
.float--seed    { color: var(--c-brown); }
.float--water   { color: var(--c-water); }
.float--warn    { color: var(--c-warn); }
.float--neutral { color: var(--c-cream); }
.float--wool    { color: var(--c-cream); }
.float--double  { color: var(--c-wheat-light); }   /* récolte ou tonte doublée */

@keyframes floatUp {
  0%   { opacity: 0; transform: translate(-50%, 0); }
  15%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, calc(-11px * var(--px))); }
}

/* Gouttes : carrés nets de 1x2 pixels de dessin */
.drop {
  position: absolute;
  top: calc(-2px * var(--px));
  width: calc(1px * var(--px));
  height: calc(2px * var(--px));
  background-color: var(--c-water);
  animation: dropFall 480ms steps(8) forwards;
}

@keyframes dropFall {
  from { transform: translateY(0); opacity: 1; }
  to   { transform: translateY(calc(19px * var(--px))); opacity: 0; }
}

/* ------------------------------------------------------------------ */
/* Modal arbre de compétences (Phase 5)                                */
/* Pattern « faux viewport » : pas de position: fixed (spec section 7) */
/* ------------------------------------------------------------------ */


/* Au-dessus de la bulle de tutoriel et du bandeau de narration : ce sont des
   choses qui arrivent, un panneau est une chose qu'on a ouverte. Vu sur une
   capture, la leçon du champ barrait l'arbre de compétences. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 18;
  display: flex;
  /* `align-items: center` coupait le haut du panneau dès qu'il dépassait la
     hauteur de la fenêtre, ET rendait la partie coupée inatteignable : c'est
     le défaut classique du centrage flex avec débordement. `margin: auto` sur
     l'enfant centre aussi bien et laisse le défilement faire son travail.
     Vu sur une fenêtre basse, où le chantier était rogné des deux bouts. */
  align-items: flex-start;
  justify-content: flex-start;
  overflow: auto;
  padding: calc(4px * var(--px));
  background-image: url("../assets/sprites/ui/plank.png");
  background-size: calc(9px * var(--px)) calc(77px * var(--px));
  background-repeat: repeat;
}

.modal > * { margin: auto; }

/* La barre de défilement, dans la palette du pack et sans coin arrondi.
   La cacher aurait été plus pur, mais on ne devine pas qu'un panneau
   continue plus bas s'il ne le montre pas. */
.modal::-webkit-scrollbar { width: calc(5px * var(--px)); }
.modal::-webkit-scrollbar-track { background-color: var(--c-brown-dark); }
.modal::-webkit-scrollbar-thumb { background-color: var(--c-cream); }

/* L'arbre a sa propre échelle, indépendante de celle du jeu. Le plateau a
   une taille fixe en pixels de dessin : à l'échelle 3 il dépasserait la
   hauteur de presque tous les écrans. Tout ce que contient la modal partage
   cette échelle, donc elle reste cohérente avec elle-même. */
.tree {
  --px: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(3px * var(--px));
}

@media (min-width: 1500px) and (min-height: 1250px) {
  .tree { --px: 3; }
}

.tree-head {
  display: grid;
  grid-template-columns: 1fr auto auto var(--tile) auto;
  align-items: center;
  gap: calc(6px * var(--px));
  padding: 0 calc(2px * var(--px));
  white-space: nowrap;
  min-width: calc(200px * var(--px));
}

.tree-level {
  color: var(--c-brown);
  margin-right: calc(4px * var(--px));
}

.tree-close {
  border: 0;
  background: none;
  padding: 0 calc(2px * var(--px));
  cursor: inherit;
}

/* Le plateau : sa taille est celle de la grille de dessin, multipliée.
   Tout ce qu'il contient est positionné en pixels de dessin. */
.tree-board {
  position: relative;
  /* content-box, contre le réglage global : la largeur déclarée doit être
     celle de la zone de dessin, bordure en plus. En border-box le cadre
     rognait le canvas, qui n'était donc plus à l'échelle 1:1 avec les
     positions des perles — et les fils passaient à côté d'elles. */
  box-sizing: content-box;
  width: calc(var(--board-w) * 1px * var(--px));
  height: calc(var(--board-h) * 1px * var(--px));

  /* Poutre sombre plutôt que planches claires : les fils de laine ont besoin
     d'un fond calme pour se détacher, et sur le bois clair du reste de la
     modal ils disparaissaient. */
  background-image: url("../assets/sprites/ui/beam.png");
  background-size: calc(8px * var(--px)) calc(10px * var(--px));
  background-repeat: repeat;
  image-rendering: pixelated;

  border-style: solid;
  border-width: calc(10px * var(--px)) calc(9px * var(--px));
  border-image-source: url("../assets/sprites/ui/frame.png");
  border-image-slice: 10 9;
  border-image-repeat: stretch;
  background-clip: padding-box;
}

.tree-threads {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
}

/* La pelote d'où part tout le fil. Ce n'est plus un décor : c'est la perle
   racine, la première et la seule au premier ouvrage. Elle porte le sprite du
   curseur de laine — déjà l'objet du jeu, et à la bonne échelle — par-dessus
   la perle, pour se distinguer des trente et une autres. */
.tree-node--root::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  image-rendering: pixelated;
  background-image: url("../assets/sprites/ui/cursor-yarn@2x.png");
  background-size: 100% 100%;
}

/* Elle n'affiche pas de rang : « 0/1 » sous la pelote ne veut rien dire. */
.tree-node--root .tree-node-rank { display: none; }

/* Une perle par compétence */
.tree-node {
  position: absolute;
  width: calc(24px * var(--px));
  height: calc(24px * var(--px));
  padding: 0;
  border: 0;
  background: none;
  cursor: inherit;
  image-rendering: pixelated;
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

.tree-node[data-state="owned"] { background-image: url("../assets/sprites/ui/bead-owned.png"); }
.tree-node[data-state="ready"] { background-image: url("../assets/sprites/ui/bead-ready.png"); }
.tree-node[data-state="poor"],
.tree-node[data-state="locked"],
.tree-node[data-state="soon"]  { background-image: url("../assets/sprites/ui/bead-locked.png"); }

/* L'icône de la compétence, posée au centre de la perle */
.tree-node .icon {
  position: absolute;
  left: calc(4px * var(--px));
  top: calc(4px * var(--px));
  pointer-events: none;
}

.tree-node[data-state="locked"] .icon,
.tree-node[data-state="soon"] .icon { opacity: 0.5; }

/* Le coût, juste sous la perle */
.tree-node-rank {
  position: absolute;
  left: 50%;
  top: calc(23px * var(--px));
  transform: translateX(-50%);
  font-size: calc(2px * var(--unit));
  color: var(--c-cream);
  text-shadow: calc(1px * var(--px)) calc(1px * var(--px)) 0 rgba(58, 42, 32, 0.6);
  pointer-events: none;
}

.tree-node[data-state="ready"] .tree-node-rank { color: var(--c-wheat-light); }

/* La perle qu'on peut prendre respire, pour attirer l'œil */
.tree-node[data-state="ready"] {
  animation: beadPulse 900ms steps(2) infinite alternate;
}

@keyframes beadPulse {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-1px * var(--px))); }
}

/* Le bandeau de détail sous le plateau a disparu : lire en bas ce qu'on
   survolait en haut obligeait à un aller-retour des yeux à chaque perle. Le
   texte est passé dans l'infobulle (voir js/tooltip.js). */

/* Le nom du jeu, en haut à gauche, peint à même les planches.
   Sans cadre : un panneau de plus dans une rangée qui en compte déjà six
   ferait une enseigne parmi les enseignes, alors que c'est un titre. La
   couleur du blé et l'ombre portée d'un pixel suffisent à le détacher du bois. */
.jeu-titre {
  margin: 0;
  margin-right: auto;      /* il reste à gauche, quoi qu'il arrive */
  padding: 0 calc(2px * var(--px));
  font-size: inherit;
  font-weight: inherit;
  letter-spacing: calc(1px * var(--px));
  color: var(--c-wheat-light);
  text-shadow: calc(1px * var(--px)) calc(1px * var(--px)) 0 rgba(58, 42, 32, 0.6);

  display: flex;
  align-items: center;
  gap: calc(3px * var(--px));
  flex: 0 0 auto;
}

/* Un bouton de la barre qui existe mais ne sert pas encore. Il garde sa place
   et son cadre — c'est un but à viser — mais son icône s'éteint. */
.icon-btn[data-state="locked"] { opacity: 0.45; }
.icon-btn[data-state="locked"]:active { transform: none; }

/* Ce qui manque pour transmettre, dans la couleur d'alerte du jeu. */
.heritage-manque { color: var(--c-warn); }

/* Le bouton d'action éteint : même traitement que les lignes de boutique hors
   de portée, pour qu'on reconnaisse « pas maintenant » d'un coup d'œil. */
.heritage-go:disabled { opacity: 0.5; }

/* ------------------------------------------------------------------ */
/* Barrage tactile : le jeu ne peut pas fonctionner sans survol        */
/* ------------------------------------------------------------------ */
/*
  Tricoti n'a pas de clic. Semer, arroser, tondre, tricoter, chasser les
  loups : tout se fait en promenant le curseur. Un écran tactile n'a pas de
  survol — le doigt est posé ou il ne l'est pas — et il n'y a donc aucune
  version dégradée à proposer. Mieux vaut le dire franchement à l'entrée que
  laisser quelqu'un tapoter une ferme qui ne répondra jamais.

  La détection porte sur la CAPACITÉ, pas sur la largeur de l'écran : une
  fenêtre étroite sur un ordinateur reste jouable, et un portable à écran
  tactile garde sa souris (il annonce `hover: hover`). Seuls les appareils qui
  n'ont vraiment que le doigt sont écartés.

  Tout est en CSS, sans une ligne de script : le message est déjà dans le
  document, donc il n'y a ni chargement à attendre ni éclair de jeu avant lui.
  Et comme le voile recouvre l'écran-titre, la partie ne démarre jamais.
*/
.tactile { display: none; }

@media (hover: none) and (pointer: coarse) {
  .tactile {
    position: fixed;
    inset: 0;
    z-index: 30;              /* au-dessus de l'écran-titre, qui est à 20 */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: calc(6px * var(--px));
    padding: calc(10px * var(--px));
    overflow: auto;

    /* Le voile porte SA propre échelle, et la redéclare pour de bon.
     *
     * `--tile` et la taille du texte sont calculés une fois sur `:root` : les
     * redéclarer ici ne changerait que la moitié des mesures, et le cadre
     * rétrécirait pendant que l'herbe et la police resteraient grandes. Tout
     * ce qui suit est donc écrit en `16px * var(--px)` plutôt qu'en `--tile`,
     * et la police est refixée.
     *
     * L'échelle reste celle du jeu : c'est la LARGEUR du cadre qui s'adapte au
     * téléphone, pas la taille des pixels. Essayé à --px 1 pour tenir plus de
     * texte — tout devenait minuscule et la clôture n'était plus qu'une ligne
     * de points. */
    --px: 2;
    font-size: calc(16px * var(--px));

    background-image: url("../assets/sprites/ui/grass.png");
    background-size: calc(16px * var(--px)) calc(16px * var(--px));
    background-repeat: repeat;
  }

  .tactile .title-fence {
    height: calc(16px * var(--px));
    background-size: calc(16px * var(--px)) calc(16px * var(--px));
  }
}

.tactile-emblem,
.tactile-mouton {
  flex: 0 0 auto;
  width: calc(48px * var(--px));
  height: calc(48px * var(--px));
  background-size: 100% 100%;
  image-rendering: pixelated;
}

.tactile-emblem { background-image: url("../assets/sprites/ui/emblem-yarn.png"); }

/* Le mouton mascotte, comme sur l'écran-titre : le refus reste accueillant.
   Il se retire quand la hauteur manque — le message passe avant la mascotte. */
.tactile-mouton {
  background-image: url("../assets/sprites/ui/title-sheep.png");
  margin-bottom: calc(16px * var(--px));   /* il s'assoit sur la clôture */
}

/* Sur un téléphone, l'emblème et le mouton ne tiennent pas tous les deux
   au-dessus et en dessous du cadre. On garde le mouton : c'est lui la
   mascotte, et un refus vaut mieux avec un visage. */
@media (max-width: 560px) { .tactile-emblem { display: none; } }

/* Et si la hauteur manque vraiment, le message passe avant la mascotte. */
@media (max-height: 620px) { .tactile-mouton { display: none; } }

.tactile-mot {
  max-width: min(calc(300px * var(--px)), 90vw);
  padding: calc(6px * var(--px)) calc(8px * var(--px));
  color: var(--c-brown-dark);
  line-height: 1.35;
  text-align: center;
}

.tactile-mot p { margin: 0 0 calc(6px * var(--px)); }
.tactile-mot p:last-child { margin-bottom: 0; }

/* Le titre du message : la couleur du blé, celle des libellés importants. */
.tactile-titre {
  color: var(--c-wheat);
  letter-spacing: calc(1px * var(--px));
}

.tactile-fin { color: var(--c-brown); }
