/* =========================================================================
 * CATAN — thème « mer & parchemin »
 *
 * Même famille que Carcassonne (parchemin, or, bois) mais l'identité vient
 * d'ailleurs : le bleu profond du cadre maritime remplace le bois sombre, et
 * l'accent passe du cramoisi médiéval au rouge terre cuite des jetons 6 et 8.
 * ========================================================================= */

:root {
  --sand:        #f2e4c4;   /* le beige exact du pourtour des tuiles */
  --sand-dark:   #e0cda2;
  --sand-line:   #c9b489;
  --ink:         #3b2a16;
  --ink-soft:    #7a6136;

  --sea-deep:    #0a2233;
  --sea:         #123a52;
  --sea-panel:   #10314a;
  --sea-light:   #1b4a6b;
  --sea-line:    #072030;

  --terracotta:  #b3261e;   /* le rouge des jetons 6 et 8 */
  --terracotta-d:#8a1a14;
  --gold:        #d9a441;
  --gold-bright: #ffd24a;

  /* Le dos des cartes développement, relevé sur img/dev/ed6_dos.png : un champ
   * terracotta clair cerné d'un liseré crème, entre le rouge des jetons et le
   * sable des tuiles. Les teintes ne sont pas inventées — ce sont celles que le
   * joueur a sous les yeux chaque fois qu'il en pioche une. */
  --card-back:   #e07f63;
  --card-cream:  #f7eed1;
  --card-line:   #a8503a;
  --card-ink:    #4a1c0e;

  --serif:   'Crimson Pro', Georgia, 'Times New Roman', serif;
  --display: 'Grenze', Georgia, serif;
}

* { box-sizing: border-box; }

/* Le seul zoom de la partie est celui du plateau, à la molette, aux boutons ou
 * au pincement sur le canevas. Celui du navigateur, lui, agrandit l'interface
 * entière et n'a aucun moyen de revenir en arrière une fois déclenché : ni
 * bouton, ni geste inverse fiable, le joueur reste avec un panneau à moitié
 * hors écran. On lui retire donc ses deux entrées, le double-tape et le
 * pincement, en ne laissant que le défilement des panneaux :
 *
 *   pan-x pan-y  =  manipulation  moins  pinch-zoom
 *
 * `manipulation`, employé jusqu'ici, gardait le pincement ouvert.
 *
 * Sur le sélecteur universel, et non sur body : WebKit n'honore pas
 * touch-action posé sur html ou body, et la valeur ne redescend donc pas sur
 * les descendants. Chaque élément doit la porter lui-même. #board garde son
 * `none` — un sélecteur d'identifiant l'emporte sur celui-ci.
 *
 * La feuille sert aussi les règles, une page de texte où le zoom du navigateur
 * est le seul moyen de grossir les caractères : elle en est exclue.
 *
 * Safari ne s'en tient pas là — il ignore user-scalable=no depuis iOS 10 et
 * n'honore pas toujours touch-action face à ses propres gestes ; js/input.js
 * refuse en plus les événements gesture* et le second tape d'un double. */
body:not(.rules) * { touch-action: pan-x pan-y; }

html, body {
  margin: 0; padding: 0;
  /* dvh, et non vh : sur mobile, vh est la hauteur barres d'outils rétractées,
   * si bien que le bas de la page reste caché sous la barre du navigateur.
   * La ligne height: 100% sert de repli aux navigateurs sans dvh. */
  height: 100%; height: 100dvh;
  font-family: var(--serif); font-size: 17px;
  background: var(--sea-deep); color: #eae0c8;
  overflow: hidden;
  -webkit-text-size-adjust: 100%;

  /* Sur mobile, un appui un peu long ou un glissé qui part de travers déclenche
   * la sélection de texte du navigateur : une bande bleue recouvre la moitié de
   * l'interface et ne s'en va qu'en tapant à côté, au risque de jouer un coup.
   * Rien ici ne se recopie à la main — le code de partie se copie d'un clic —
   * hormis les champs de saisie, rétablis juste en dessous. */
  user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none;              /* ni menu « Copier / Partager » iOS */
  -webkit-tap-highlight-color: transparent; /* ni flash gris à chaque tape */
}

input, textarea, [contenteditable] {
  user-select: text; -webkit-user-select: text;
}

.hidden { display: none !important; }

/* ============================ Pictogrammes ============================
 * Les emoji ont été remplacés par du SVG écrit dans js/icons.js : la moitié
 * d'entre eux — ⚔ 🏛 🛤 — sont rangés côté « texte » par Unicode, et Windows
 * les tirait alors d'une police de symboles monochromes, à côté de voisins en
 * couleur. Voir l'en-tête de js/icons.js.
 *
 * Taille en em, et non en pixels : le picto suit le corps du texte qui le
 * porte — pastille à .68em, bouton à .82em, titre de modale à 1.3em — sans
 * qu'aucun de ces endroits ait à le redire.
 *
 * L'alignement se fait au ras du texte plutôt que par vertical-align, qui
 * n'aurait rien donné dans les boutons et les pastilles : ce sont des boîtes
 * flex, où l'alignement vertical n'est plus l'affaire de la ligne de base. */
.ic {
  width: 1.15em; height: 1.15em;
  flex: none;                    /* jamais comprimé par un libellé trop long */
  vertical-align: -.19em;        /* pour les rares emplois au fil du texte */
}

#app { display: flex; height: 100%; }

/* ============================== Plateau ============================== */

#boardWrap { position: relative; flex: 1; min-width: 0; }

#board {
  position: absolute; inset: 0; width: 100%; height: 100%;
  cursor: grab; touch-action: none; display: block;
}
#board.dragging { cursor: grabbing; }

#zoom {
  position: absolute; top: 12px; left: 12px; z-index: 4;
  display: flex; flex-direction: column; gap: 6px;
}
#zoom button {
  width: 40px; height: 40px; font: 700 20px/1 var(--serif);
  display: flex; align-items: center; justify-content: center;
  border-radius: 8px; cursor: pointer;
  background: rgba(12, 44, 66, .85); color: var(--sand);
  border: 1px solid var(--sea-line);
  box-shadow: 0 2px 0 var(--sea-line), 0 4px 12px rgba(0,0,0,.4);
}
#zoom button:active { transform: translateY(2px); box-shadow: 0 0 0 var(--sea-line); }

/* ------------------------- Le bandeau du tour ---------------------------
 * Le passage d'un joueur à l'autre n'était qu'un texte réécrit dans un coin du
 * panneau : à quatre, on ne voyait plus la partie tourner. Une bande s'ouvre
 * donc en travers du plateau, le temps de lire un nom, puis s'efface.
 *
 * Elle vit dans #boardWrap et non dans la surcouche des effets : c'est une
 * annonce de l'interface, pas une pièce du jeu — elle ne suit ni la caméra ni
 * le plateau.
 *
 * Elle se tient en haut du plateau, où l'île commence : assez dans le champ
 * pour marquer le temps, assez haut pour ne pas couvrir le centre — le voleur
 * et les tuiles qu'on regarde. Sur bureau le haut est libre (seuls les boutons
 * de zoom, dans le coin) ; sur mobile il est occupé par la marque et le bandeau
 * des joueurs, et la bande passe dessous — voir --topH plus bas.
 *
 * Elle ne capte aucun geste : on peut construire pendant qu'elle passe.
 *
 * Le mouvement tient en trois temps décalés — la bande s'ouvre, le nom arrive,
 * un reflet la traverse. Rien n'est repeint : tout est transform et opacity, et
 * chaque couche est composée à part. Les durées sont relatives à --tb-ms, que
 * js/ui.js pose d'après sa propre constante : changer la scène se fait d'un
 * seul endroit, et le ménage du nœud reste calé dessus. */

#turnBanner {
  position: absolute; inset: 0; z-index: 4;
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: 20px;
  pointer-events: none;
}

.tb-band {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: .62em;
  max-width: 88%;
  padding: .66em 1.9em;
  font: 700 clamp(1.15rem, 3.4vw, 1.7rem)/1.1 var(--display);
  letter-spacing: .055em;
  color: var(--sand);
  text-shadow: 0 2px 6px rgba(0, 0, 0, .55);
  white-space: nowrap;
}

/* Le fond, en une seule couche à trois étages : le voile sombre, puis les deux
 * filets aux couleurs du joueur, haut et bas. Tous trois s'éteignent aux bouts
 * — la bande n'a pas de bord, elle naît de la mer et y retourne.
 *
 * C'est lui qui s'ouvre, et non la bande : mise à l'échelle, celle-ci aurait
 * étiré le nom avec elle. Le voile s'écarte donc seul, sous un texte qui ne
 * bouge pas de sa place. */
.tb-band::before {
  content: ''; position: absolute; inset: 0;
  background:
    linear-gradient(90deg, transparent, var(--tbc, var(--gold)), transparent) top / 100% 1.5px no-repeat,
    linear-gradient(90deg, transparent, var(--tbc, var(--gold)), transparent) bottom / 100% 1.5px no-repeat,
    linear-gradient(90deg,
      rgba(6, 20, 31, 0) 0, rgba(6, 20, 31, .88) 16%,
      rgba(6, 20, 31, .93) 50%, rgba(6, 20, 31, .88) 84%, rgba(6, 20, 31, 0) 100%);
  transform: scaleX(.26);
  animation: tbBand var(--tb-ms, 1500ms) cubic-bezier(.2, .86, .25, 1) var(--tb-wait, 0ms) both;
}

/* Les filets gagnent en clarté ce que certaines couleurs de pion n'ont pas : le
 * brun et le bleu, posés tels quels sur un voile presque noir, ne se voyaient
 * pas. La déclaration nue au-dessus sert de recours là où color-mix manque. */
@supports (color: color-mix(in srgb, red 50%, white)) {
  .tb-band::before {
    background:
      linear-gradient(90deg, transparent,
        color-mix(in srgb, var(--tbc, #d9a441) 72%, #fff), transparent) top / 100% 1.5px no-repeat,
      linear-gradient(90deg, transparent,
        color-mix(in srgb, var(--tbc, #d9a441) 72%, #fff), transparent) bottom / 100% 1.5px no-repeat,
      linear-gradient(90deg,
        rgba(6, 20, 31, 0) 0, rgba(6, 20, 31, .88) 16%,
        rgba(6, 20, 31, .93) 50%, rgba(6, 20, 31, .88) 84%, rgba(6, 20, 31, 0) 100%);
  }
}

/* Le reflet qui court sur la bande une fois ouverte. On ne le remarque pas ; on
 * remarquerait son absence — sans lui la bande est un rectangle imprimé, avec
 * lui c'est une étoffe qui prend la lumière. */
.tb-sheen {
  position: absolute; top: 0; bottom: 0; left: 0; width: 34%;
  background: linear-gradient(102deg,
    transparent, rgba(255, 255, 255, .07) 42%, rgba(255, 255, 255, .12) 56%, transparent);
  transform: translateX(-150%);
  animation: tbSheen calc(var(--tb-ms, 1500ms) * .6) ease-in-out
             calc(var(--tb-wait, 0ms) + var(--tb-ms, 1500ms) * .14) both;
}

/* La pastille du joueur, cernée de sombre pour tenir sur le voile comme sur la
 * mer, et posée dans son propre halo. */
.tb-dot {
  position: relative; flex: none;
  width: .56em; height: .56em; border-radius: 50%;
  background: var(--tbc, var(--gold));
  box-shadow: 0 0 0 2px rgba(6, 20, 31, .8), 0 0 14px var(--tbc, var(--gold));
}

.tb-text { position: relative; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Le nom tire vers la couleur du joueur sans s'y perdre : teinté, jamais peint.
 * Le brun et le bleu foncé, écrits pleins sur le voile, ne se lisaient plus. */
.tb-text b { color: var(--gold-bright); font-weight: 700; }
@supports (color: color-mix(in srgb, red 50%, white)) {
  .tb-text b { color: color-mix(in srgb, var(--tbc, #d9a441) 40%, var(--sand)); }
}

/* La pastille et le nom entrent ensemble, une fois la bande ouverte : le décalage
 * (14 % de la scène) est ce qui fait qu'on la voit s'ouvrir plutôt que paraître. */
.tb-dot, .tb-text {
  animation: tbText var(--tb-ms, 1500ms) cubic-bezier(.2, .9, .3, 1) var(--tb-wait, 0ms) both;
}

@keyframes tbBand {
  0%   { opacity: 0; transform: scaleX(.26); }
  13%  { opacity: 1; transform: scaleX(1); }
  78%  { opacity: 1; transform: scaleX(1); }
  100% { opacity: 0; transform: scaleX(1); }
}
@keyframes tbText {
  0%   { opacity: 0; transform: translateY(9px); }
  14%  { opacity: 0; transform: translateY(9px); }
  30%  { opacity: 1; transform: translateY(0); }
  76%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-6px); }
}
@keyframes tbSheen {
  from { transform: translateX(-150%); }
  to   { transform: translateX(400%); }
}

/* -------------------------- Main du joueur --------------------------- */

#bottom {
  position: absolute; left: 12px; right: 12px; bottom: 10px; z-index: 3;
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  pointer-events: none;
}

#hand { display: flex; gap: 8px; pointer-events: auto; }
#hand .card {
  position: relative;
  /* rapport des images de img/card/ : 240 × 356 */
  width: 62px; height: 92px;
  border-radius: 6px; overflow: hidden;
  border: 2px solid var(--sand-line);
  box-shadow: 0 6px 16px rgba(0,0,0,.5);
  background: var(--sand);
  transition: transform .12s, filter .12s;
}
#hand .card img { width: 100%; height: 100%; object-fit: cover; display: block; }
#hand .card:hover { transform: translateY(-5px); }
#hand .card.empty { filter: grayscale(1) brightness(.6); opacity: .5; }

/* ------------------------- La main en devis ---------------------------------
 * Toucher un bouton de construction retourne la main côté prix (js/ui.js,
 * `costPeek`). Les cinq piles sont déjà là, juste au-dessus de la barre
 * d'actions : le devis s'y lit sans voler un pixel de plateau, ce qui sur un
 * téléphone est la seule place disponible — les vignettes de coût des boutons
 * n'y tiennent pas, et l'infobulle qui les remplace n'existe pas au doigt.
 *
 * Rien ne bouge de la carte ordinaire : même cadre, même compteur au même coin.
 * Le devis n'ajoute qu'une rangée de jetons sur la tranche haute, et retire la
 * lumière aux matières qui ne le concernent pas. Une pile qui compte reste donc
 * exactement la pile qu'on connaît — c'est le reste qui s'efface autour.
 *
 *   idle — hors devis : éteinte, mais toujours lisible. Elle n'est pas
 *          désactivée, elle est simplement hors sujet, et une carte qu'on croit
 *          disparue de sa main est pire qu'une carte trop visible.
 *   need — à payer : liseré d'or, jetons pleins.
 *   short — il en manque : liseré terre cuite, jetons creux pour le compte qui
 *          reste à trouver. */
#hand .card.idle { filter: grayscale(.9) brightness(.62); opacity: .42; }
#hand .card.need { border-color: var(--gold-bright); }
#hand .card.short { border-color: var(--terracotta); }

/* La rangée de jetons : autant que de cartes à payer, posée sur un bandeau
 * sombre qui la détache aussi bien d'un ciel de pâturage que d'un fond de mine.
 * Trois au plus (le minerai d'une ville) — elle tient sur 44 px sans se serrer.
 *
 * Le bandeau ne descend pas : il s'arrête à sa propre hauteur et laisse
 * l'illustration entière en dessous. C'est une carte à jouer, pas un formulaire.
 */
#hand .card .req {
  position: absolute; top: 0; left: 0; right: 0;
  display: flex; justify-content: center; align-items: center; gap: 3px;
  padding: 3px 2px;
  background: linear-gradient(180deg, rgba(6,20,31,.88), rgba(6,20,31,.5) 70%, transparent);
}
/* Creux : ce qu'il reste à trouver. Le cerne suffit à le dire — un jeton vide
 * se distingue d'un jeton plein sans qu'aucun des deux ait à crier. */
#hand .card .req i {
  width: 7px; height: 7px; border-radius: 50%;
  border: 1.5px solid var(--terracotta);
  background: rgba(6,20,31,.55);
  box-shadow: 0 1px 2px rgba(0,0,0,.6);
}
/* Plein : payé d'avance, il est déjà dans la pile sous lui. */
#hand .card .req i.got {
  border-color: var(--gold-bright);
  background: var(--gold);
}

/* ------------------------------ La main respire ------------------------------
 * Une pile qui grossit se soulève et prend la lumière ; une pile qui paie
 * s'enfonce et se ternit. Le compteur seul ne disait rien : on voyait un
 * chiffre différent sans avoir vu le mouvement.
 *
 * `both` retient la première image pendant le délai : js/ui.js retarde le gain
 * jusqu'à ce que la carte en vol se pose (js/fx.js dit quand), et sans cela la
 * pile s'illuminerait d'abord, puis attendrait. La transition de .card est
 * neutralisée le temps du geste, sinon les deux se disputeraient la transform.
 * L'ombre portée fait le décollage — l'échelle seule ne soulève rien. */
#hand .card.fx-take,
#hand .card.fx-drop { transition: none; }
#hand .card.fx-take { animation: fxTake .62s cubic-bezier(.2, 1.5, .4, 1) both; z-index: 2; }
#hand .card.fx-drop { animation: fxDrop .46s ease-out both; }

@keyframes fxTake {
  0%   { transform: none; }
  38%  { transform: translateY(-13px) scale(1.11);
         box-shadow: 0 14px 26px rgba(0,0,0,.6), 0 0 0 2px var(--gold-bright),
                     0 0 20px rgba(255,210,74,.7);
         filter: brightness(1.28); }
  100% { transform: none; }
}
@keyframes fxDrop {
  0%   { transform: none; }
  45%  { transform: translateY(7px) scale(.9);
         filter: grayscale(.55) brightness(.72); }
  100% { transform: none; }
}
#hand .card .ct {
  position: absolute; right: -1px; bottom: -1px;
  min-width: 26px; padding: 1px 5px;
  font-family: var(--display); font-weight: 700; font-size: 1.05em;
  text-align: center;
  color: var(--sand); background: rgba(12, 34, 51, .92);
  border-top-left-radius: 7px;
  border-top: 1px solid var(--sand-line); border-left: 1px solid var(--sand-line);
}

/* La rangée des cartes développement tenait ici. Elle disait deux fois ce que la
 * barre d'actions dit déjà : toute carte jouable y est un bouton, et une carte
 * qu'on ne peut pas jouer n'a pas à occuper le bas de l'écran. Restaient les
 * Points de victoire, qui ne se jouent jamais — ils ont rejoint la pastille du
 * joueur, à côté du score qu'ils gonflent (voir renderPlayers). */

#resLeft {
  font-size: .76em; color: #9fb6c6; letter-spacing: .5px;
  text-shadow: 0 1px 3px rgba(0,0,0,.8);
}

/* ============================== Panneau ============================== */

#side {
  width: 300px; flex: none;
  display: flex; flex-direction: column;
  background: linear-gradient(180deg, rgba(255,255,255,.05), transparent 140px), var(--sea-panel);
  border-left: 4px solid var(--sea-line);
  box-shadow: -8px 0 24px rgba(0,0,0,.5);
  overflow: hidden;
}

.brand { padding: 14px 16px 10px; border-bottom: 1px solid var(--sea-light); text-align: center; }
.brand .title {
  font-family: var(--display); font-weight: 700;
  font-size: 2.1em; letter-spacing: 6px;
  color: var(--gold-bright);
  text-shadow: 0 2px 0 #6b3a06, 0 4px 12px rgba(0,0,0,.55);
}
.brand .tag { font-style: italic; color: #8fa9bb; font-size: .85em; margin-top: -2px; }
.brand .links { margin-top: 8px; display: flex; justify-content: center; gap: 18px; }
.brand .links a {
  cursor: pointer; color: var(--sand-line); font-size: .85em;
  text-decoration: underline; text-underline-offset: 3px;
}
.brand .links a:hover { color: var(--gold-bright); }
/* Le réglage du son, seul pictogramme parmi deux liens de texte : pas de
 * soulignement — on ne souligne pas un dessin — et un cran de gris en moins
 * quand il est coupé, pour que l'état se lise sans avoir à déchiffrer la barre
 * sur le haut-parleur. */
.brand .links .linkic {
  display: inline-flex; align-items: center; text-decoration: none;
  color: var(--sand-line);
}
.brand .links .linkic .ic { width: 1.15em; height: 1.15em; }
.brand .links .linkic.off { color: #5d7385; }
/* Bouton chronique : n'existe que sur mobile (le panneau montre déjà la
 * chronique sur bureau). La surcharge mobile le rétablit et le stylise. */
.brand-log { display: none; }

.pad { padding: 12px 14px; }
.pad.grow { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.sectlabel {
  font-family: var(--display); font-size: .78em; letter-spacing: 2.5px;
  color: #7f9db1; text-transform: uppercase; margin-bottom: 8px;
}

/* ----------------------------- Les joueurs ---------------------------- */

#players { display: flex; flex-direction: column; gap: 6px; }
.player {
  display: flex; align-items: center; gap: 8px;
  background: var(--sea-light);
  padding: 7px 10px; border-radius: 8px;
  border: 1.5px solid var(--sea-line);
  transition: border-color .2s, background .2s;
}
.player.current {
  border-color: var(--gold-bright);
  background: #2a5470;
  box-shadow: inset 0 0 14px rgba(255, 210, 74, .18);
}
/* Le fanion est posé par renderPlayers(), à côté du nom : en ::after il était
 * dans le nom, et un pseudonyme trop long l'emportait avec son ellipse. */
.pflag { color: var(--gold-bright); width: .95em; height: .95em; }
.pdot {
  width: 16px; height: 16px; border-radius: 50%; flex: none;
  border: 2px solid rgba(255,255,255,.7);
  box-shadow: 0 1px 3px rgba(0,0,0,.6);
  display: inline-block; vertical-align: -3px;
}
.player .pname {
  flex: 1; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pbadges { display: flex; gap: 4px; flex: none; }
.badge {
  font-size: .68em; letter-spacing: .5px;
  display: inline-flex; align-items: center; gap: 3px;
  background: rgba(0,0,0,.28); border: 1px solid rgba(255,255,255,.12);
  border-radius: 20px; padding: 2px 6px; color: var(--gold-bright);
}
/* Un joueur qui tient les deux titres aligne trois pastilles dans un panneau
 * de 300 px : le pictogramme y est ramené au corps du texte, faute de quoi il
 * mange le nom à coups d'ellipse. */
.badge .ic { width: 1em; height: 1em; }
/* Les signes qui remplacent un mot — la tête de robot ici, la couronne de
 * l'hôte dans la salle d'attente — vont nus. La pastille est ce qui donne à un
 * mot son statut d'étiquette ; un dessin l'est déjà, et le cadre ne faisait que
 * l'enfermer et le rapetisser pour tenir dedans. Sans lui, le picto reprend le
 * cadratin des autres et retrouve ses yeux. */
.pbot, .lhost {
  flex: none; display: inline-flex; align-items: center; align-self: center;
}
.pbot { color: #a9bfcd; }
.lhost { color: var(--gold-bright); }
.badge.dim { color: #a9bfcd; }
.badge.gone { color: #ffb3a7; border-color: rgba(255,140,120,.35); }
/* Les deux compteurs de main adverse (cf. handCount, js/ui.js) : le dos réel de
 * la carte, et son nombre posé dessus. Pas de pastille — une carte a le format
 * d'une carte, elle dépasse le corps du texte, et le chip qui l'enfermait
 * montait plus haut que ses voisins ; la rangée y perdait sa ligne. Posé nu sur
 * le panneau, le carton tient dans la hauteur de ligne du score et rien ne
 * grandit. Il garde son rapport réel, 0,68 : plus étroit, terre cuite et sable
 * cessent de se départager et les deux comptes redeviennent jumeaux.
 *
 * Le nombre était d'abord écrit à côté du carton : il coûtait sa propre largeur
 * plus l'écart, la moitié du compteur pour un seul chiffre. Sur la carte, le
 * compteur ne mesure plus que le carton, et le bandeau mobile y gagne une
 * quinzaine de pixels par joueur — de quoi rendre à un pseudonyme les lettres
 * que la demi-largeur lui prenait. Le carton est élargi juste assez pour qu'une
 * main à deux chiffres — au-delà de sept, c'est la règle de la défausse qui les
 * amène — tienne sans mordre le bord.
 *
 * Le voile est ce qui rend le chiffre lisible sur une illustration : à moitié
 * transparent, il assombrit le carton sans le décolorer, si bien que le coup
 * d'œil distingue toujours le sable de la terre cuite avant même de lire. */
.phand, .ptitle {
  position: relative; flex: none; display: inline-block;
  height: 1.65em;
  font-size: .72em; color: #f6ecd2;
}
.phand { width: 1.12em; }
.phand img {
  width: 100%; height: 100%; display: block;
  border-radius: 2px; object-fit: cover;
  box-shadow: 0 1px 2px rgba(0,0,0,.45);
}
/* Mes Points de victoire ne comptent pas des cartes mais des points : c'est le
 * médaillon du livret qu'ils portent, un disque, et non un carton de plus. La
 * pastille devient donc carrée — un disque n'a pas de format à respecter, et à
 * la largeur d'une carte il serait ovale.
 *
 * Le chiffre garde l'encre claire de ses voisins. En or, il se posait sur le
 * laurier et sur l'emblème doré du centre : trois ors dans une pastille de vingt
 * pixels, et le seul qu'il fallait lire était le plus petit. Le médaillon dit
 * déjà que ce sont des points ; le chiffre n'a qu'à se compter.
 *
 * Le voile qui le porte suit le disque : carré, il aurait dessiné une vignette
 * là où le médaillon est détouré. Le laurier fait le liseré, rien à ajouter
 * autour ; l'ombre portée, elle, tient de sa forme ronde. */
.phand.pvp { width: 1.65em; }
.phand.pvp img,
.phand.pvp b { border-radius: 50%; }
.phand.pvp img { box-shadow: 0 1px 3px rgba(0,0,0,.55); }

.phand b, .ptitle b {
  position: absolute; inset: 0; border-radius: 2px;
  display: flex; align-items: center; justify-content: center;
  font-size: .92em; font-weight: 700; letter-spacing: 0;
  /* Chasse fixe : sans elle, « 11 » se resserre et le compteur d'à côté paraît
   * porter un autre corps que son jumeau. */
  font-variant-numeric: tabular-nums;
  /* Le voile est uniforme, et léger : à moitié opaque il faisait du carton un
   * rectangle sombre où l'illustration ne se lisait plus. Il n'a pas à faire
   * tout le contraste — le halo du chiffre s'en charge, et lui ne noircit que
   * ce que le glyphe recouvre. */
  background: rgba(6, 22, 35, .32);
  text-shadow: 0 0 3px rgba(4, 16, 26, .95), 0 1px 2px rgba(0, 0, 0, .9);
}

/* Le titre porte sa carte entière, au rapport de l'image (280 × 319), et le
 * compte qui le justifie par-dessus — le nombre de segments de la route, celui
 * de chevaliers pour l'armée. La pastille et son pictogramme disaient la même
 * chose en deux fois plus large.
 *
 * Le chiffre est centré sur le carton, comme sur les paquets de ressources et
 * de développement : les quatre comptes de la rangée se lisent alors à la même
 * hauteur, et c'est cet alignement qui les donne pour ce qu'ils sont — quatre
 * nombres du même ordre, et non des marques disparates. Il tombe donc en
 * partie sur le pavé de texte, où le halo se charge de le détacher.
 *
 * Ce texte, illisible à cette taille, ne nomme de toute façon plus la carte :
 * ici c'est la couleur qui la nomme, le vert du chemin contre l'ocre des
 * cavaliers, comme le sable et la terre cuite séparent les deux paquets. Il est
 * donc écarté du cadre. Montrée entière, la carte donnait 45 % de sa surface à
 * un pavé de texte blanc identique sur les deux titres : presque tout le carton
 * était du bruit commun, et le seul signal qui les départageait tenait dans une
 * bande de dix pixels. Cadré sur la seule vignette, ce signal occupe la
 * surface entière.
 *
 * La fenêtre est la bande d'illustration au complet — de 67 à 213 en largeur,
 * de 6 à 172 en hauteur, sur une image de 280 × 319. Serrer davantage sur la
 * charrette ou sur les cavaliers grossit le sujet mais rapproche les deux
 * cartons : de près, ce ne sont plus qu'un attelage brun et des chevaux bruns.
 * De loin, c'est un pré vert contre un ciel d'ocre, et c'est cela qui se lit.
 *
 * Les décalages sont en pourcentages de la boîte, donc l'échelle suit la taille
 * de police sans qu'aucun nombre soit à reprendre. */
.ptitle {
  width: 1.45em; overflow: hidden;
  border-radius: 2px; box-shadow: 0 1px 2px rgba(0,0,0,.45);
}
.ptitle img {
  position: absolute; display: block;
  width: 192%; height: auto; left: -46%; top: -3.6%;
}
/* Le carton d'un poursuivant : les chevaliers sont posés, la carte n'est pas
 * encore devant lui. L'illustration s'efface donc vers le fond du panneau —
 * c'est un carton en attente, pas un carton acquis — et le liseré d'ombre part
 * avec elle, puisqu'il n'y a plus de carte à décoller de la rangée.
 *
 * Le chiffre, lui, reste lisible : c'est la seule chose que la marque a à dire.
 * Il perd juste assez d'encre pour ne pas rivaliser avec celui du porteur, et
 * son voile s'allège d'autant — sur une image déjà pâle, le voile d'origine
 * dessinait une vignette sombre plus visible que la carte qui la portait. */
.ptitle.dim { box-shadow: none; }
.ptitle.dim img { opacity: .32; filter: grayscale(.7); }
.ptitle.dim b { color: rgba(246, 236, 210, .8); background: rgba(6, 22, 35, .16); }

.player .pscore {
  font-family: var(--display); font-weight: 700; font-size: 1.25em;
  color: var(--gold-bright); min-width: 26px; text-align: right;
}

/* ------------------------------- Le tour ------------------------------ */

.turnbox {
  background: rgba(0,0,0,.18);
  border-top: 1px solid var(--sea-light);
  border-bottom: 1px solid var(--sea-light);
}

#dice { display: flex; align-items: center; justify-content: center; gap: 9px; min-height: 0; }
#dice:not(:empty) { margin-bottom: 10px; }
/* La paire du jeu : un dé jaune à points rouges et un dé rouge à points jaunes.
 * Les deux couleurs se répondent, ce qui permet de suivre chaque dé du regard
 * pendant le jet — et de relire le tirage d'un coup d'œil une fois posés. */
.die {
  width: 38px; height: 38px; padding: 4px;
  display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr);
  border: 1px solid var(--die-line); border-radius: 7px;
  background: linear-gradient(160deg, var(--die-hi), var(--die-lo));
  box-shadow: 0 3px 0 var(--die-base), 0 5px 12px rgba(0,0,0,.45);
}
.die.gold {
  --die-hi: #ffe64a; --die-lo: #f5c400; --die-line: #c99500; --die-base: #a87a00;
  --die-pip: #d81f26;
}
.die.red {
  --die-hi: #ee2b26; --die-lo: #c1161c; --die-line: #8f0f13; --die-base: #780c10;
  --die-pip: #ffd400;
}
.die i {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--die-pip); align-self: center; justify-self: center;
}
.dsum {
  font-family: var(--display); font-weight: 700; font-size: 1.5em;
  color: var(--sand); min-width: 34px; text-align: center;
}
.dsum.hot { color: var(--terracotta); text-shadow: 0 0 14px rgba(179,38,30,.6); }

/* Le temps que les cubes roulent sur le plateau, le panneau n'a pas encore de
 * réponse à donner : il garde le tirage précédent, en retrait. Le vider ferait
 * sauter la consigne et les boutons à chaque tour, pour une seconde et demie.
 *
 * Le résultat s'inscrit ensuite d'un ressaut, à l'instant même où les dés
 * s'immobilisent — c'est le seul moment où l'on découvre le nombre, ici comme
 * sur le carton. */
#dice { transition: opacity .3s ease, filter .3s ease; }
#dice.rolling { opacity: .22; filter: saturate(.3); }
.die.fx-land, .dsum.fx-land { animation: fxLand .34s cubic-bezier(.2, 1.5, .4, 1) both; }
@keyframes fxLand {
  0%   { transform: scale(.55); opacity: 0; }
  100% { transform: none; opacity: 1; }
}

#turnLabel {
  text-align: center; font-size: .93em; line-height: 1.35;
  min-height: 2.5em; color: #dfd4b8; margin-bottom: 10px;
}

/* Les points de suspension d'un tour qui n'est pas le mien, mais qui battent :
 * un texte figé ne dit pas si l'autre réfléchit ou si la partie a calé. Ils
 * gardent leur place à vide (opacity, jamais visibility) pour que la consigne
 * ne se décale pas trois fois par seconde. */
.thinking { display: inline-block; margin-left: 1px; }
.thinking i {
  display: inline-block; width: .3em; font-style: normal;
  animation: think 1.25s infinite ease-in-out;
}
.thinking i::before { content: "."; }
.thinking i:nth-child(2) { animation-delay: .18s; }
.thinking i:nth-child(3) { animation-delay: .36s; }
@keyframes think {
  0%, 60%, 100% { opacity: .25; }
  30%           { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .thinking i { animation: none; opacity: 1; }
}

#actions { display: flex; flex-direction: column; gap: 6px; }
/* Le bouton devient une boîte flex : il porte désormais un pictogramme, un
 * libellé et un coût, trois éléments qu'un simple text-align ne saurait plus
 * espacer. L'écart vient du gap, jamais d'une espace dans le texte — le coût
 * disparaît sur mobile, et une espace typographique aurait laissé sa trace. */
button.act {
  font: inherit; font-size: .82em; cursor: pointer; text-align: left;
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; border-radius: 7px;
  background: var(--sand-dark); color: var(--ink);
  border: 1px solid var(--sand-line);
  box-shadow: 0 2px 0 #a08850;
  transition: filter .1s, transform .06s;
}
/* Le libellé cède la place au coût plutôt que de le pousser hors du bouton. */
button.act .blab { min-width: 0; }
/* Le coût : les vignettes de matière du livret, une par carte à payer, alignées
 * à droite du bouton — d'un bouton à l'autre elles tombent ainsi sur la même
 * colonne, et la rangée se lit d'un trait plutôt qu'en cherchant où chaque coût
 * commence. Elles ne rétrécissent pas (flex 0 0 auto) : c'est au libellé de
 * céder, il est en toutes lettres et se relit, une image écrasée non.
 *
 * Pas de séparateur : le point que portait le coût en toutes lettres n'avait de
 * sens qu'entre deux textes. Entre un mot et des images, l'alignement à droite
 * sépare déjà, et le point n'était plus qu'une poussière au milieu du bouton. */
button.act .bcost {
  display: flex; align-items: center; gap: 2px;
  margin-left: auto; flex: 0 0 auto;
}
/* Une seule hauteur pour les cinq, et pourtant elles ne font pas la même
 * taille : tools/build_res.py les pose dans une bande transparente commune, où
 * chacune garde la hauteur que lui donne le livret — la gerbe de blé debout
 * remplit la bande, le galet de minerai couché en occupe les deux tiers. C'est
 * ce rapport-là qui les distingue au premier coup d'œil, et le fixer ici d'un
 * seul chiffre suffit à le respecter. La largeur suit le dessin. */
button.act .bcost .cost-res { height: 1.25em; width: auto; display: block; }
button.act:hover:not(:disabled) { filter: brightness(1.06); }
button.act:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 0 0 #a08850; }
button.act:disabled { opacity: .35; cursor: default; }
/* Bloqué, mais pas mort : le bouton d'une construction hors de portée s'éteint
 * comme un :disabled et reste cliquable — c'est lui qui va dire ce qui manque
 * (js/ui.js, `addBtn`). Un peu moins effacé que le vrai désactivé, parce qu'il
 * répond encore ; assez pour qu'on ne l'essaie pas par erreur. */
button.act.short { opacity: .5; }
/* Le bouton dont la main affiche le devis. Il n'est pas armé — rien ne se
 * posera sur le plateau —, il est seulement celui qu'on interroge : un liseré
 * d'or, pas le fond d'or plein de `.on`, qui promettrait un geste. */
button.act.peek { border-color: var(--gold); box-shadow: 0 2px 0 #8a6207; }
button.act.short.peek { opacity: .78; }
button.act.on {
  background: linear-gradient(180deg, #f4c86a, var(--gold));
  border-color: #8a6207; box-shadow: 0 2px 0 #8a6207;
  font-weight: 700;
}
button.act.gold {
  background: linear-gradient(180deg, #cf3a2c, var(--terracotta) 50%, var(--terracotta-d));
  color: #ffeacb; font-family: var(--display); font-weight: 700;
  letter-spacing: 1px; justify-content: center; font-size: .95em;
  border-color: #5f1009; box-shadow: 0 3px 0 #5f1009;
  text-shadow: 0 1px 2px rgba(0,0,0,.45);
}
button.act.gold:active:not(:disabled) { transform: translateY(3px); box-shadow: 0 0 0 #5f1009; }
/* Une carte développement n'est pas une action de construction : c'est la carte
 * elle-même, posée à plat dans la colonne. Elle en reprend donc l'anatomie —
 * cadre crème, champ terre cuite, pictogramme au centre d'un médaillon rond —
 * mais couchée à la largeur de ses voisines plutôt que debout : une pile de
 * vignettes verticales aurait demandé sa propre rangée, et le format portrait
 * ne tient pas dans une barre qui empile des bandes.
 *
 * Le cadre se fait en deux temps : la bordure porte le crème, l'outline pose le
 * trait sombre juste au-dehors. Un box-shadow n'aurait pas convenu — il sert
 * déjà l'épaisseur du bouton, celle qui s'écrase au clic.
 *
 * Pas de :hover ici : .act éclaircit déjà, et un soulèvement de plus aurait
 * répété le geste de la main sans qu'il veuille dire la même chose. */
button.act.dev {
  padding: 6px 12px 6px 6px;
  background: var(--card-back); color: var(--card-ink);
  border: 2px solid var(--card-cream);
  outline: 1px solid var(--card-line);
  box-shadow: 0 2px 0 var(--card-line);
}
button.act.dev:active:not(:disabled) { box-shadow: 0 0 0 var(--card-line); }

/* Le médaillon : un disque crème cerné du même trait que le cadre, et le
 * pictogramme au milieu. Il donne au bouton le seul repère rond d'une colonne
 * qui n'a que des angles — c'est lui, plus que la couleur, qui dit « carte ».
 *
 * flex-shrink à 0 : un nom long — Construction de routes — pousse sur la ligne,
 * et sans cela le disque s'aplatirait en ovale avant que le texte cède. */
.act.dev .dmed {
  width: 28px; height: 28px; flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: 50%; background: var(--card-cream);
  border: 1px solid var(--card-line);
}
/* La rangée qui regroupe les actions secondaires (js/ui.js) ne forme pas de
 * boîte sur bureau : ses boutons coulent dans la colonne de #actions comme
 * avant. La surcharge mobile plus bas en fait une barre de pastilles défilante. */
.actrow { display: contents; }

/* ------------------------------ Chronique ----------------------------- */

#log {
  flex: 1; min-height: 70px; overflow-y: auto;
  background: rgba(4, 20, 32, .6);
  border: 1px solid var(--sea-line); border-radius: 8px;
  padding: 8px 11px; font-size: .82em; line-height: 1.5;
  color: #b8c9d5;
}
#log div { padding: 3px 0; border-bottom: 1px solid rgba(255,255,255,.05); }
#log div:last-child { border-bottom: 0; color: #f0e6cd; }

#side ::-webkit-scrollbar { width: 8px; }
#side ::-webkit-scrollbar-thumb { background: var(--sea-light); border-radius: 4px; }

/* =============================== Modales ============================== */

#modal, #final {
  position: fixed; inset: 0; z-index: 30;
  background: rgba(4, 16, 26, .74);
  backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center;
  padding: 18px;
}

.mcard, #modalBody > * {
  background: linear-gradient(160deg, rgba(255,255,255,.4), transparent 40%), var(--sand);
  color: var(--ink);
  border: 1px solid var(--sand-line);
  outline: 6px solid var(--sea-panel);
  box-shadow: 0 0 0 8px #0d2c42, 0 24px 60px rgba(0,0,0,.7);
  border-radius: 6px;
  padding: 22px 26px;
  width: min(420px, 94vw);
  max-height: 88vh; overflow-y: auto;
  animation: pop .25s ease-out;
}
/* Assez large pour que les deux colonnes de cinq ressources tiennent côte à
 * côte sans que les vignettes se resserrent. */
.mcard.wide, #modalBody > .wide { width: min(640px, 94vw); }
@keyframes pop { from { transform: translateY(16px); opacity: 0; } }

.mcard h3 {
  font-family: var(--display); font-weight: 700; font-size: 1.5em;
  margin: 0 0 6px; color: var(--terracotta);
  display: flex; align-items: center; justify-content: center; gap: 10px;
}
.msub { text-align: center; color: var(--ink-soft); font-size: .9em; margin: 4px 0 12px; }
.msep { height: 1px; background: var(--sand-line); margin: 16px 0 12px; }

.mbtn {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%; margin-top: 9px;
  font: inherit; font-size: .95em; cursor: pointer;
  padding: 11px 14px; border-radius: 7px;
  background: var(--sand-dark); color: var(--ink);
  border: 1px solid var(--sand-line); box-shadow: 0 2px 0 #a08850;
  text-align: center;
}
.mbtn:hover:not(:disabled) { filter: brightness(1.06); }
.mbtn:active:not(:disabled) { transform: translateY(2px); box-shadow: 0 0 0 #a08850; }
.mbtn:disabled { opacity: .4; cursor: default; }
.mbtn.gold {
  background: linear-gradient(180deg, #cf3a2c, var(--terracotta) 50%, var(--terracotta-d));
  color: #ffeacb; font-family: var(--display); font-weight: 700; letter-spacing: 1px;
  border-color: #5f1009; box-shadow: 0 3px 0 #5f1009;
}

.discard-grid {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin: 10px 0;
}
.discard-grid.small { gap: 6px; }
.dc {
  width: 74px; padding: 5px; border-radius: 8px; cursor: pointer; text-align: center;
  background: rgba(255,255,255,.45);
  border: 2px solid transparent;
  transition: transform .1s, border-color .1s;
}
.discard-grid.small .dc { width: 48px; padding: 3px; }
.dc:hover { transform: translateY(-3px); }
.dc.on { border-color: var(--terracotta); background: rgba(179,38,30,.14); }
.dc.off { opacity: .35; cursor: default; }
/* Vignette de simple lecture : elle montre un lot, elle ne se clique pas. */
.dc.plain { cursor: default; }
.dc.off:hover, .dc.plain:hover { transform: none; }
.dc img { width: 100%; border-radius: 4px; display: block; border: 1px solid var(--sand-line); }
.dc b { display: block; font-size: .74em; margin-top: 3px; font-weight: 600; }
.resimg { width: 100%; border-radius: 4px; }

/* Deux lots côte à côte, séparés par un filet : ce qu'on donne d'un côté, ce
 * qu'on reçoit de l'autre. Le retrait est porté par les colonnes elles-mêmes
 * plutôt que par un espacement, pour que le trait tombe au milieu et que les
 * deux moitiés gardent la même largeur utile. */
.offer-row { display: flex; justify-content: center; }
.offer-row > div { flex: 1 1 0; min-width: 0; padding: 0 14px; }
.offer-row > div + div { border-left: 1px solid var(--sand-line); }
/* Dans une colonne « je donne / je reçois », une vignette vaut au plus un
 * cinquième de la colonne — les cinq ressources tiennent donc toujours sur une
 * ligne, en se resserrant avec elle, plutôt que d'en renvoyer une seule à la
 * ligne suivante. Le lot reste centré quand il compte moins de cinq cartes :
 * une seule ressource se pose au milieu de sa moitié, pas contre le filet.
 * min-width: 0 — sans lui, la largeur propre des illustrations imposerait son
 * minimum et la fenêtre déborderait de l'écran plutôt que de resserrer. */
.offer-row .discard-grid.small { flex-wrap: nowrap; }
.offer-row .discard-grid.small .dc {
  width: auto; min-width: 0;
  flex: 0 1 calc((100% - 24px) / 5);   /* 24px = les 4 espaces entre vignettes */
}

/* Sous cette largeur, deux colonnes de cinq vignettes deviendraient illisibles :
 * les lots passent l'un sous l'autre, et le filet qui les sépare bascule à
 * l'horizontale. Le seuil est celui de la fenêtre large elle-même — au-delà,
 * les deux moitiés tiennent toujours côte à côte, jamais de repli à mi-chemin. */
@media (max-width: 640px) {
  .offer-row { display: block; }
  .offer-row > div { padding: 0; }
  .offer-row > div + div {
    border-left: 0; border-top: 1px solid var(--sand-line);
    margin-top: 10px; padding-top: 8px;
  }
}
.olab { text-align: center; font-size: .8em; font-weight: 700; color: var(--ink-soft); }

ul.rules { margin: 6px 0 0; padding-left: 20px; font-size: .9em; line-height: 1.55; }
ul.rules li { margin-bottom: 7px; }

/* --------------------------- Tableau de fin ---------------------------- */
/* Le décompte final n'est pas une modale comme les autres : c'est le dernier
 * écran de la partie, celui qu'on regarde une minute au lieu d'y cliquer. Il a
 * donc droit à un peu de cérémonie — médaillon, sous-titre, rangées — là où le
 * reste de l'interface va au plus court.
 *
 * Un peu plus large que ses sœurs : une rangée y aligne un rang, un nom, deux
 * cartons et un total, et le détail des points court sous eux d'un bord à
 * l'autre. À 420 px, tout cela se serait replié. */
/* Élargie depuis que les statistiques la suivent : le tableau des joueurs y
 * aligne un nom et cinq colonnes de chiffres, qui se seraient serrées à 470. */
.fcard { width: min(520px, 94vw); padding-top: 26px; }

/* Le médaillon du vainqueur. Un disque d'or cerné de sable puis de terre
 * cuite : le liseré clair détache l'or du parchemin, qui en est trop proche
 * pour trancher tout seul, et le cercle rouge lui donne le cadre d'une
 * décoration plutôt que d'un simple picto agrandi. */
.fcrest {
  width: 58px; height: 58px; margin: 0 auto 12px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 28%, #ffe79c, var(--gold) 58%, #a3752099);
  color: #5c3c08;
  box-shadow: 0 0 0 3px var(--sand), 0 0 0 5px var(--terracotta),
              0 5px 14px rgba(0, 0, 0, .4);
}
.fcrest .ic { width: 32px; height: 32px; }
/* Quand c'est un autre qui l'emporte, le médaillon salue sans se réjouir : la
 * couronne du vainqueur, dans les bleus du plateau plutôt que dans l'or. Le
 * même disque en or aurait fêté ma propre défaite. */
.fcrest.lost {
  background: radial-gradient(circle at 34% 28%, #3a6a8d, var(--sea-panel) 62%, #071d2c);
  color: var(--gold);
  box-shadow: 0 0 0 3px var(--sand), 0 0 0 5px var(--sea-panel),
              0 5px 14px rgba(0, 0, 0, .4);
}

#finalList {
  list-style: none; padding: 0; margin: 16px 0 18px;
  display: flex; flex-direction: column; gap: 7px;
}

/* Une rangée de tableau. Elle s'enroule sur deux lignes : la première porte le
 * rang, le nom, les cartons et le total ; la seconde, le détail des points, qui
 * prend toute la largeur (flex: 1 0 100%) plutôt que de partager la colonne du
 * nom — une énumération de bâtiments coupée d'une ellipse ne dit plus rien.
 *
 * Le filet de gauche est à la couleur du joueur, posée par js/ui.js. C'est la
 * seule couleur de la rangée : le parchemin ne supporte pas qu'on lui pose
 * quatre teintes vives dessus, et un filet suffit à rendre les quatre lignes
 * distinctes du bord de l'œil. */
.frow {
  display: flex; flex-wrap: wrap; align-items: center; gap: 3px 10px;
  padding: 7px 12px; border-radius: 5px;
  background: rgba(255, 255, 255, .38);
  border: 1px solid var(--sand-line);
  border-left: 5px solid var(--sand-line);
  /* Le filet intérieur détache le filet de couleur du fond de la rangée. Sans
   * lui, le joueur blanc — un sable à peine plus clair que le parchemin —
   * n'avait pas de filet du tout, et sa rangée seule restait anonyme. */
  box-shadow: inset 1px 0 0 rgba(0, 0, 0, .2);
}
/* La rangée du vainqueur : l'or lui sert de fond, et son nom passe au rouge des
 * jetons. Elle ne grossit pas — c'est le premier de la liste, il n'a pas besoin
 * d'être plus gros pour qu'on le trouve ; il a besoin de briller. */
.frow.win {
  background:
    linear-gradient(100deg, rgba(217, 164, 65, .42), rgba(255, 255, 255, .5) 70%);
  border-color: var(--gold);
  box-shadow: inset 1px 0 0 rgba(0, 0, 0, .2), 0 1px 6px rgba(179, 38, 30, .16);
}

.frank {
  flex: none; width: 1.5em; text-align: center;
  font-family: var(--display); font-weight: 700; font-size: 1em;
  color: var(--ink-soft);
}
.frow.win .frank { color: var(--terracotta); }

.fname {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--display); font-weight: 700; font-size: 1.2em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.frow.win .fname { color: var(--terracotta); }

/* Le titre se montre par son carton, celui-là même qu'on pose devant soi sur la
 * table. Il tient la place d'un pictogramme — le tableau se lit d'abord en noms
 * et en points, la carte n'est qu'une mention au bout de la ligne — mais
 * l'illustration dit du premier coup d'œil de quel titre il s'agit, là où deux
 * dessins au trait de la même encre demandaient de lire leur forme.
 *
 * Entier et sans compteur : ce n'est plus la vignette du bandeau, qui recadrait
 * pour loger un chiffre. La partie est finie, il n'y a plus de compte à suivre.
 *
 * En rem, pour que toutes les rangées posent le même carton quelle que soit la
 * taille de leurs lettres. */
.ftitles { flex: none; display: flex; gap: 5px; }
.ftitle {
  height: 2.15rem; width: auto; display: block;
  border-radius: 2px; box-shadow: 0 1px 3px rgba(0, 0, 0, .35);
}

/* Le total, colonne de droite. Chasse fixe et largeur minimale : les quatre
 * nombres tombent alors exactement l'un sous l'autre, à un ou deux chiffres,
 * et l'écart entre deux joueurs se lit à la longueur des colonnes.
 *
 * L'unité reste sur la ligne du nombre plutôt qu'en dessous : empilée, elle
 * faisait de la colonne un bloc de deux lignes, que l'alignement vertical de la
 * rangée centrait alors sur son milieu — le chiffre remontait au-dessus du nom
 * qu'il accompagne. */
.fvp {
  flex: none; min-width: 2.6em; text-align: right; line-height: 1;
  font-family: var(--display); font-weight: 700; font-size: 1.6em;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.frow.win .fvp { color: var(--terracotta); }
.fvp small {
  font-family: var(--serif); font-weight: 600; margin-left: .16em;
  font-size: .42em; letter-spacing: 1.2px; color: var(--ink-soft);
}

.fdetail {
  flex: 1 1 100%; min-width: 0; padding-left: calc(1.5em + 10px);
  font-size: .8em; line-height: 1.4; color: var(--ink-soft);
}

/* ------------------------ Le récit chiffré de la partie ------------------
 * Deux tableaux sous le classement : ce que les dés ont donné, puis ce que
 * chacun en a fait. Ils viennent après le podium et avant le bouton — c'est
 * l'ordre dans lequel on les lit, et le bouton reste là où on le cherche, tout
 * en bas. La carte défile déjà (max-height: 88vh), ils n'ont donc pas à tenir
 * dans un écran. */

.fsec { margin-top: 2px; }

/* Un titre de section, deux crans sous celui de la carte : ce n'est pas une
 * nouvelle modale, c'est la suite de la même page. En petites capitales
 * espacées plutôt qu'en gros — le tableau qu'il annonce porte déjà le regard,
 * l'intitulé n'a qu'à le nommer. */
.fsectitle {
  display: flex; align-items: center; gap: 7px;
  margin: 0 0 10px;
  font-family: var(--display); font-weight: 700;
  font-size: .84em; letter-spacing: 1.1px; text-transform: uppercase;
  color: var(--ink-soft);
}
.fsectitle .ic { width: 1.25em; height: 1.25em; color: var(--terracotta); }

/* L'histogramme des jets. Onze colonnes de largeur égale : à cette échelle, le
 * profil en cloche se reconnaît d'un coup d'œil, et c'est lui qu'on vient
 * chercher — le détail se lit dans l'infobulle de chaque colonne. */
.fdice {
  display: flex; align-items: flex-end; gap: 3px;
  margin-bottom: 8px;
}
.fdcol { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; }

/* Le compte, au-dessus de sa barre. Toujours présent, même à zéro : une colonne
 * vide est une information — c'est le numéro qui n'est jamais tombé. */
.fdn {
  font-family: var(--display); font-weight: 700; font-size: .72em;
  font-variant-numeric: tabular-nums; color: var(--ink-soft);
  line-height: 1; margin-bottom: 3px;
}

.fdtrack {
  position: relative; width: 100%; height: 58px;
  background: rgba(0, 0, 0, .05);
  border-radius: 2px 2px 0 0;
  border-bottom: 1px solid var(--sand-line);
}
.fdbar {
  position: absolute; left: 0; right: 0; bottom: 0;
  min-height: 1px;                      /* un zéro garde un trait, pas un vide */
  background: linear-gradient(180deg, var(--sea-light), var(--sea-panel));
  border-radius: 2px 2px 0 0;
}
/* Le repère de l'espérance. En pointillé et par-dessus la barre : c'est une
 * mesure portée sur le dessin, pas une seconde barre — s'il était plein, on
 * lirait deux séries au lieu d'une série et sa référence. */
.fdexp {
  position: absolute; left: -1px; right: -1px; height: 0;
  border-top: 2px dashed var(--terracotta);
  opacity: .75;
}
/* Le 7 ne produit rien : il chasse. Sa barre prend l'encre du voleur pour qu'on
 * ne la compare pas aux dix autres, qui parlent de récolte. */
.fdcol.seven .fdbar {
  background: linear-gradient(180deg, #6b625a, #3f3832);
}
.fdlab {
  font-family: var(--display); font-weight: 700; font-size: .78em;
  color: var(--ink); line-height: 1; margin-top: 4px;
}
.fdcol.seven .fdlab { color: var(--terracotta); }

.fscap {
  font-size: .74em; line-height: 1.4; color: var(--ink-soft);
  text-align: center; margin-bottom: 4px;
}

/* Le tableau des joueurs. Une grille plutôt qu'un <table> : les colonnes de
 * chiffres sont fixées à la même largeur, le nom prend ce qui reste, et les
 * cinq totaux tombent exactement l'un sous l'autre — c'est cet alignement qui
 * rend l'écart lisible, comme dans le classement au-dessus. */
.fsgrid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) repeat(5, 2.5em);
  align-items: stretch;
  gap: 3px;
  font-variant-numeric: tabular-nums;
}

/* La ligne d'en-tête : des pictogrammes seuls, l'intitulé dans l'infobulle. */
.fsh {
  display: flex; align-items: center; justify-content: center;
  padding: 4px 0; color: var(--ink-soft);
  border-bottom: 1px solid var(--sand-line);
}
.fsh .ic { width: 1.15em; height: 1.15em; }

/* Le nom porte le filet de couleur du joueur, comme sa rangée du classement :
 * les deux tableaux se relisent alors l'un par l'autre sans qu'on ait à
 * chercher qui est qui. */
.fsname {
  display: flex; align-items: center;
  min-width: 0; padding: 5px 8px;
  border-left: 4px solid var(--sand-line);
  box-shadow: inset 1px 0 0 rgba(0, 0, 0, .18);
  background: rgba(255, 255, 255, .3);
  border-radius: 0 4px 4px 0;
  font-family: var(--display); font-weight: 700; font-size: .95em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fsval {
  display: flex; align-items: center; justify-content: center;
  padding: 5px 0; border-radius: 4px;
  background: rgba(255, 255, 255, .3);
  font-family: var(--display); font-weight: 700; font-size: 1em;
  color: var(--ink);
}
/* Un zéro s'efface : sur cinq colonnes et quatre lignes, ce sont les nombres
 * qui portent quelque chose qu'on veut voir ressortir. */
.fsval.nil { color: var(--ink-soft); opacity: .45; }

/* ================================ Menu ================================ */

#menu {
  position: fixed; inset: 0; z-index: 40;
  display: flex; flex-direction: column; align-items: center;
  gap: 4px; padding: 24px;
  /* En fenêtre courte — un téléphone couché — le menu défile. D'où les marges
   * automatiques plutôt qu'un justify-content: center : centré tant qu'il y a
   * de la place, mais sans jamais rendre le haut du contenu inatteignable, ce
   * que le centrage fait dans un conteneur qui défile. */
  justify-content: flex-start;
  overflow-y: auto;
  background:
    radial-gradient(ellipse at 50% 32%, rgba(255,190,80,.22), transparent 55%),
    radial-gradient(ellipse at center, #1a5478 0%, #0d3049 55%, #06141f 100%);
}
#menuHome, #lobby {
  display: flex; flex-direction: column; align-items: center;
}
/* Même largeur pour les deux : la carte ne change pas de taille en passant de
 * l'accueil à la salle d'attente. */
#menuHome, #lobby { width: min(400px, 92vw); }
/* Les deux bouts du menu, pour le centrage : l'un des deux blocs est toujours
 * masqué, ils portent donc la marge du bas à eux deux. */
.mlogo { margin-top: auto; }
#menuHome, #lobby { margin-bottom: auto; }
.mlogo {
  font-family: var(--display); font-weight: 700;
  font-size: clamp(3rem, 13vw, 5.5rem); letter-spacing: 12px;
  color: var(--gold-bright);
  text-shadow: 0 3px 0 #8a4c06, 0 8px 30px rgba(0,0,0,.6);
}
.mtag { color: #b8cddb; font-style: italic; margin-bottom: 6px; text-align: center; }
.msun { font-size: 2.2rem; color: #ffcb5c; margin: 4px 0 14px; line-height: 1; }

.pinput {
  width: min(340px, 86vw); margin: 4px 0 14px;
  /* 16 px au minimum : en dessous, Safari iOS zoome sur le champ dès qu'on y
   * pose le doigt, et la page reste agrandie ensuite. Sur téléphone la racine
   * fait 15 px, donc 1em n'y suffirait pas ; sur bureau elle en fait 17 et le
   * max ne change rien. */
  font: inherit; font-size: max(16px, 1em); text-align: center;
  padding: 10px 12px; border-radius: 8px;
  background: rgba(255,255,255,.94); color: var(--ink);
  border: 1.5px solid var(--sand-line);
  box-shadow: inset 0 2px 5px rgba(0,0,0,.12);
}
.pinput:focus { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(217,164,65,.25); }

.mdesc { color: #a9c2d2; font-size: .88em; margin: 10px 0 4px; text-align: center; }
.menulink {
  margin-top: 14px; color: var(--sand-line); font-size: .9em;
  display: flex; align-items: center; gap: 7px;
  text-decoration: underline; text-underline-offset: 4px;
}
/* Le livre ne fait pas partie du mot : le souligné s'arrête au texte. */
.menulink .ic { text-decoration: none; }
.menulink:hover { color: var(--gold-bright); }

/* Un <a> stylé comme un bouton de modale (lien vers la page de règles). */
a.mbtn { text-decoration: none; }
#menu .mbtn { width: min(340px, 86vw); margin-top: 14px; }

/* ============================ Accueil =================================
 * Une seule carte, empilée : le nom d'abord — il sert aux deux actions —, puis
 * créer, puis rejoindre, tous deux dans le même style. C'est le « ou » qui dit
 * l'alternative, pas une différence de bouton. Rien d'autre : jouer seul, c'est
 * créer une partie et y ajouter des bots. */

.mblock-title {
  font-family: var(--display); font-weight: 700; font-size: 1.15em;
  letter-spacing: 2px; color: var(--gold-bright); margin-bottom: 4px;
}
/* Les boutons du menu font 340 px de large ; dans une carte, ils suivent leur
 * colonne plutôt que de la déborder. */
#menu .lcard .mbtn { width: 100%; margin-top: 10px; }

/* Le champ du nom : un vrai libellé au-dessus, aligné à gauche comme une
 * fiche à remplir — c'est la première chose qu'on saisit. */
.mfield {
  display: block; width: 100%; margin-bottom: 6px; text-align: left;
  font-weight: 600; font-size: .95em; color: #cfe0ea;
}
.mfield .pinput { display: block; width: 100%; margin: 6px 0 0; text-align: left; }

/* Le « ou » entre les deux actions : deux filets qui se rejoignent sur le mot,
 * plutôt qu'une simple ligne — c'est une alternative, pas une séparation. */
.mor {
  display: flex; align-items: center; gap: 12px;
  width: 100%; margin: 14px 0 12px;
  font-style: italic; font-size: .88em; color: #8fa8b8;
}
.mor::before, .mor::after {
  content: ''; flex: 1; height: 1px; background: rgba(255,255,255,.16);
}

/* Code et bouton sur une ligne. `stretch` par défaut : le bouton prend
 * exactement la hauteur du champ, sans qu'aucun des deux ne la fixe. */
.joinrow { display: flex; gap: 8px; width: 100%; }
.joinrow .pinput { flex: none; width: 7.5em; margin: 0; }
#menu .joinrow .mbtn { flex: 1; width: auto; margin-top: 0; }

/* Le code de partie : lettres capitales espacées, faciles à dicter. */
.pinput.code {
  font-family: var(--display); font-weight: 700; font-size: 1.15em;
  letter-spacing: 4px; text-transform: uppercase;
}

.merr { color: #ff9d8f; font-size: .88em; text-align: center; margin: 10px 0 0; min-height: 1em; }

/* ====================== Multijoueur : salle d'attente ==================== */

.lcard {
  width: 100%; padding: 18px 20px 16px;
  display: flex; flex-direction: column; align-items: center;
  border: 1px solid rgba(255,255,255,.16); border-radius: 10px;
  background: rgba(3, 20, 33, .55);
  box-shadow: 0 18px 50px rgba(0,0,0,.5);
}
/* Le code de la partie : de l'or sur le bleu profond, dans un cadre en
 * pointillés qui le désigne comme la chose à prendre et à dicter. */
.roomcode {
  display: inline-block;
  font-family: var(--display); font-weight: 700;
  font-size: 2.6rem; letter-spacing: .24em;
  /* Centrage vertical, mesuré sur les métriques de la police et non à l'œil :
   * Grenze porte de longues hampes (1,10 em) pour des capitales courtes
   * (0,61 em), si bien que l'encre d'un code tout en capitales tombe sous le
   * centre de sa boîte — l'ombre portée l'y enfonçant encore de 2 px. D'où
   * l'interligne ramené à 1 et le padding bas plus épais que le haut. La
   * Cinzel de Carcassonne, elle, est assez équilibrée pour se contenter d'un
   * padding symétrique.
   * Horizontalement : l'interlettrage s'ajoute après la dernière lettre, on le
   * rend à gauche — d'où .58em = .34em + .24em. */
  line-height: 1;
  padding: .19em .34em .35em .58em;
  margin: 8px 0 2px;
  color: var(--gold-bright);
  text-shadow: 0 2px 0 #8a4c06;
  background: rgba(0, 0, 0, .22);
  border: 2px dashed var(--sand-line);
  border-radius: 8px;
  cursor: pointer;
}
.roomcode:hover { color: #fff0c0; background: rgba(0, 0, 0, .34); }
.roomcode:active { transform: scale(.97); }
.copyfb {
  font-size: .82em; color: #9fe0a8; height: 1.2em;
  opacity: 0; transition: opacity .2s;
}
.copyfb.show { opacity: 1; }

.lplayers { list-style: none; margin: 12px 0 6px; padding: 0; width: 100%; }
.lplayer {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; margin-bottom: 5px; border-radius: 7px;
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.1);
}
.lname { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lkick {
  font: inherit; font-size: .85em; line-height: 1; cursor: pointer;
  padding: 3px 7px; border-radius: 5px;
  background: rgba(179,38,30,.7); color: #ffe6df; border: 1px solid #5f1009;
}
.lkick:hover { background: var(--terracotta); }

.colorpicker { display: flex; gap: 8px; margin: 4px 0 2px; }
.swatch {
  width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
  border: 2px solid rgba(255,255,255,.55);
  box-shadow: 0 2px 5px rgba(0,0,0,.5);
  transition: transform .12s;
}
.swatch:hover { transform: translateY(-2px); }
.swatch.mine { border-color: var(--gold-bright); box-shadow: 0 0 0 3px rgba(255,210,74,.35); }
.swatch.taken { opacity: .3; cursor: default; transform: none; }

.linklike {
  margin-top: 12px; font: inherit; font-size: .88em; cursor: pointer;
  background: none; border: none; color: var(--sand-line);
  text-decoration: underline; text-underline-offset: 4px;
}
.linklike:hover { color: var(--gold-bright); }

/* ============================ Avis réseau ============================= */

#netmsg {
  position: fixed; z-index: 50; left: 50%; top: 14px; transform: translateX(-50%);
  max-width: min(460px, 92vw); padding: 10px 16px;
  border-radius: 8px; font-size: .92em; text-align: center;
  background: rgba(138, 26, 20, .94); color: #ffe6df;
  border: 1px solid #5f1009; box-shadow: 0 8px 24px rgba(0,0,0,.5);
}

/* ====================== La réponse du plateau ========================= */

/* Une bulle posée à l'endroit cliqué, quand le geste était juste mais l'instant
 * mauvais — viser une tuile pendant que la table se défausse. Elle n'emprunte
 * pas le rouge du bandeau réseau : celui-là signale ce qui échappe au joueur,
 * un coup refusé, une partie disparue. Ici il n'y a qu'à attendre, et le même
 * clic aboutira dans un instant ; le bleu du panneau et l'encre sable disent
 * cela du ton qu'a l'interface partout ailleurs.
 *
 * Elle ne capte rien : elle se pose là où l'on vient de cliquer, et le clic
 * suivant tombera au même endroit — il doit traverser jusqu'au plateau.
 *
 * L'ancre est le point touché, et la bulle monte au-dessus de lui : sous le
 * doigt, la main qui vient de cliquer la cacherait. js/ui.js la rentre dans
 * l'écran quand le clic tombe près d'un bord. */
#boardmsg {
  position: fixed; z-index: 45; pointer-events: none;
  transform: translate(-50%, calc(-100% - 16px));
  max-width: min(260px, 70vw); padding: 7px 12px;
  border-radius: 8px; font-size: .84em; line-height: 1.35; text-align: center;
  background: rgba(16, 49, 74, .96); color: var(--sand);
  border: 1px solid var(--sea-light); box-shadow: 0 6px 18px rgba(0,0,0,.5);
  animation: hintIn .16s ease-out;
  /* Sa sortie, elle, se fait en fondu (js/ui.js pose `out` le temps de lecture
   * passé). Plus lente que son entrée : on la découvre d'un coup parce qu'on
   * vient de la demander, on la quitte sans y penser. */
  transition: opacity .45s ease;
}
#boardmsg.out { opacity: 0; }
/* Un clic tout en haut de l'écran : il n'y a plus de place au-dessus, la bulle
 * passe dessous. Cachée un instant par le doigt, mais dans la vue. */
#boardmsg.below { transform: translate(-50%, 16px); }
@keyframes hintIn { from { opacity: 0; } }
/* Animations réduites : la bulle paraît et s'en va sans fondu, comme les tuiles
 * du tirage — c'est une information, pas un ornement. Le temps de lecture ne
 * change pas, seul le voile qui l'emporte. */
@media (prefers-reduced-motion: reduce) {
  #boardmsg { animation: none; transition: none; }
}

/* ============================== Animations ==============================
 * La surcouche des pièces volantes (js/fx.js) couvre toute la page : une carte
 * part de sa tuile, sur le plateau, et se pose sur la ligne du joueur, dans le
 * panneau. Elle est transparente aux gestes et se glisse entre le plateau et
 * les modales. Ses enfants sont posés en (0,0) et déplacés par transform —
 * moins coûteux qu'animer left/top, et c'est ce que la boucle met à jour. */
#fxLayer {
  position: fixed; inset: 0; z-index: 25;
  pointer-events: none; overflow: hidden;
}
#fxLayer > * { position: absolute; top: 0; left: 0; will-change: transform, opacity; }

/* Une carte de ressource en vol : la même vignette que dans la main. */
.fx-card {
  width: 46px; height: 68px; border-radius: 6px;
  border: 2px solid var(--sand-line); background: var(--sand);
  object-fit: cover;
  box-shadow: 0 8px 20px rgba(0,0,0,.55);
}
/* La carte volée : elle sort de la colonie que le voleur muselle, s'arrête au
 * milieu du plateau pour se laisser lire, puis se range dans la main. Deux
 * plans dos à dos dans la même carte, que js/fx.js retourne d'un demi-tour —
 * le voleur et sa victime en voient la face, les autres n'ont que le dos, et
 * c'est le navigateur qui masque le plan tourné de l'autre côté. */
/* La boîte fait la taille à laquelle la carte se lit, au milieu du plateau, et
 * le mouvement ne fait plus que la réduire — jamais l'agrandir. C'est ce qui la
 * garde nette : le nœud porte un retournement, donc une scène en trois
 * dimensions, et une couche composée est tramée une fois pour toutes à sa
 * taille de mise en page avant d'être étirée par la transform. Posée à 46 px et
 * grossie de moitié, l'illustration montrait la trame plutôt que la pierre. */
.fx-steal { width: 69px; height: 102px; perspective: 900px; }
/* Et qu'on la trame à l'échelle où elle se montre, non à celle où elle part :
 * la surcouche annonce ses transforms (#fxLayer > *), ce qui invite le
 * navigateur à garder la première texture. Une carte, ici, ne vaut pas cette
 * économie — elle est seule à l'écran et s'arrête pour être lue. */
#fxLayer > .fx-steal { will-change: auto; }
.fx-sflip {
  position: relative; width: 100%; height: 100%;
  transform-style: preserve-3d;
}
/* `fx-sface`, et non `fx-face` : ce nom-là est déjà celui des six faces du dé,
 * plus bas. Une carte et un cube ne se ressemblent en rien — arrondi, liseré,
 * ombre portée —, et la face du dé refuse justement l'arrondi, qui la percerait
 * dans ses coins. */
.fx-sface {
  position: absolute; inset: 0; border-radius: 6px; overflow: hidden;
  border: 2px solid var(--sand-line); background: var(--sand);
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  box-shadow: 0 10px 26px rgba(0,0,0,.6);
}
.fx-sface.front { transform: rotateY(180deg); }
.fx-sface img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Le péage du sept : la carte que je jette au voleur. Elle emprunte les deux
 * plans de la carte volée — c'est le même carton qui se retourne, et il n'y a
 * pas de raison qu'il soit écrit deux fois —, mais la boîte est celle de la
 * carte en vol : celle-ci ne s'arrête pas pour se laisser lire, elle part.
 *
 * Le point de fuite est plus proche que celui du vol, et il le faut : sur une
 * carte deux fois plus petite, une perspective lointaine aplatit le demi-tour
 * au point qu'on ne le voit plus tourner — la face changerait sans qu'aucun
 * geste ne l'ait retournée. Ici le carton prend de l'épaisseur en basculant.
 * Rien à retirer à `will-change`, contrairement au vol : la carte ne fait que
 * rétrécir, la texture posée à sa taille de mise en page lui suffit donc de
 * bout en bout — et elles peuvent être quatorze à voler ensemble. */
.fx-toll { width: 46px; height: 68px; perspective: 620px; }
.fx-robber { filter: drop-shadow(0 6px 9px rgba(0,0,0,.55)); }
.fx-robber svg { width: 100%; height: 100%; display: block; }

/* ---------------------------- La pose d'une pièce ---------------------------
 * Des couches empilées sur le point de contact, que js/fx.js tient à jour :
 * l'ombre et l'onde de choc restent au sol, seuls les corps bougent. Les rendre
 * solidaires écraserait l'ombre avec la pièce, or une ombre ne s'aplatit pas —
 * c'est justement elle qui dit le sol.
 *
 * Un corps d'ordinaire, deux pour la relève d'une ville : la colonie qu'on
 * démonte et la ville qui pousse à sa place, au même point de contact.
 *
 * Le nœud extérieur n'a ni taille ni contenu : c'est un point. Les enfants s'y
 * accrochent, et le tassement se fait par le bas (transform-origin en 0 0, qui
 * est le point d'ancrage de la pièce, donc son assise), car c'est le sol qui la
 * retient — une colonie qui s'enfonce et une ville qui sort de terre partent du
 * même principe, pris dans un sens puis dans l'autre. */
.fx-build { width: 0; height: 0; }
.fx-bbody { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.fx-bpart { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.fx-bpart svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* L'ombre portée : une tache qui s'élargit et pâlit à mesure que la pièce
 * monte. Elle est décalée vers le bas — la lumière du plateau ne tombe jamais
 * tout à fait d'aplomb, comme pour les dés. */
.fx-bshadow {
  position: absolute; top: 0; left: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(0,0,0,.5), rgba(0,0,0,0) 78%);
}

/* L'onde de choc : un anneau qui s'ouvre au sol à l'instant précis où la pièce
 * touche, puis s'efface. --delay dit cet instant, compté depuis la naissance du
 * nœud (js/fx.js le pose à la milliseconde). Le carton encaisse.
 *
 * L'anneau part de rien plutôt que de sa pleine lumière, comme le liseré des
 * tuiles : `both` retient la première image pendant tout le délai, et un premier
 * cran déjà lumineux poserait un point brillant sur le chantier bien avant que
 * la pièce ne touche. La montée tient en un vingtième du geste — on ne la voit
 * pas, on ne voit que le choc. */
.fx-bring {
  position: absolute; top: 0; left: 0;
  border-radius: 50%;
  border: 2px solid rgba(255, 226, 150, .8);
  opacity: 0;
  animation: fxBuildRing 460ms ease-out var(--delay, 0ms) both;
}
@keyframes fxBuildRing {
  0%   { opacity: 0;   transform: translate(-50%,-50%) scale(.18) scaleY(.42); }
  5%   { opacity: .85; transform: translate(-50%,-50%) scale(.22) scaleY(.42); }
  100% { opacity: 0;   transform: translate(-50%,-50%) scale(1.15) scaleY(.42); }
}

/* L'arrivée d'une carte : la ligne du joueur s'illumine un instant. */
.player.fx-pulse { animation: fxPulse .6s ease-out; }
@keyframes fxPulse {
  0%   { box-shadow: 0 0 0 0 rgba(255,210,74,0); }
  35%  { box-shadow: 0 0 0 3px rgba(255,210,74,.55), inset 0 0 18px rgba(255,210,74,.35);
         border-color: var(--gold-bright); }
  100% { box-shadow: 0 0 0 0 rgba(255,210,74,0); }
}

/* ------------------- Les tuiles que le tirage réveille -----------------------
 * Posé sur la tuile, à sa taille exacte (js/fx.js la mesure sur le plateau) :
 * un liseré d'or qui bat trois fois pendant que les cartes s'envolent, et une
 * lueur assez discrète pour laisser lire le jeton chiffre et les pièces.
 *
 * Tout est en pourcentage du repère du SVG, jamais en pixels : le même nœud
 * s'ajuste au zoom du plateau sans qu'aucune valeur ne soit à reprendre ici.
 * --dur et --delay viennent de js/fx.js — le battement démarre à la seconde où
 * les dés se couchent, décalé d'une tuile à l'autre. L'animation ne touche que
 * l'opacité : la transform, elle, porte la position et reste au JS. */
.fx-hexfire {
  animation: fxHexFire var(--dur, 2400ms) ease-out var(--delay, 0ms) both;
  /* Le halo est porté par la boîte, pas par le tracé : un filtre posé sur le
   * polygone lui-même mange son arête et le liseré s'éteint. Deux passes — une
   * serrée qui fait le néon, une large qui déborde sur la mer et les tuiles
   * voisines, celle qu'on attrape du coin de l'œil sans regarder. */
  filter: drop-shadow(0 0 3px rgba(255, 226, 130, .9))
          drop-shadow(0 0 11px rgba(255, 184, 46, .7));
}
.fx-hexfire svg { display: block; width: 100%; height: 100%; overflow: visible; }
.fx-hexfire .glow { fill: rgba(255, 214, 92, .22); }
/* Un souffle d'ombre sous le liseré, débordant à peine d'un demi-point de part
 * et d'autre : juste de quoi que l'or ne se dissolve pas dans les champs et le
 * désert, qui sont de sa teinte. Au-delà, on ne voit plus que le cerne noir. */
.fx-hexfire .edge {
  fill: none; stroke: rgba(40, 24, 6, .26); stroke-width: 6.6; stroke-linejoin: round;
}
.fx-hexfire .ring {
  fill: none; stroke: #ffd24a; stroke-width: 5; stroke-linejoin: round;
}
/* La tuile sous le voleur : désignée par le nombre, mais muette. Elle s'assombrit
 * au lieu de s'allumer, sous le liseré rouge du voleur — on voit d'un coup
 * d'œil ce que le voleur coûte ce tour-ci. */
.fx-hexfire.blocked { filter: drop-shadow(0 0 8px rgba(0, 0, 0, .8)); }
.fx-hexfire.blocked .glow { fill: rgba(18, 14, 12, .44); }
.fx-hexfire.blocked .edge { stroke: rgba(0, 0, 0, .3); }

/* ------------------------- Le liseré du voleur ------------------------------
 * Un seul trait rouge, partout où le voleur se montre sur le plateau : sur la
 * tuile qu'il musèle quand son nombre sort, et sur celle qu'il s'apprête à
 * quitter quand tombe un 7 (.fx-ember, plus bas). Les deux disent la même
 * chose — « il est là » —, et rien ne gagnait à ce qu'elles le disent de deux
 * façons ; écrites en un seul endroit, elles ne peuvent plus diverger.
 *
 * Il était en pointillés sur la tuile muselée. Un pointillé se lit comme une
 * interdiction, or le voleur n'interdit rien : il est posé là, c'est tout, et
 * c'est le noircissement de la tuile qui dit qu'elle ne rapporte pas. Le trait
 * plein le désigne sans le commenter.
 *
 * Un peu plus fin que le liseré d'or (4 contre 5) : le rouge sur une tuile
 * assombrie porte déjà plus loin que l'or sur une tuile éclairée. */
.fx-hexfire.blocked .ring,
.fx-ember .ring {
  fill: none; stroke: #e8562c; stroke-width: 4; stroke-linejoin: round;
}

@keyframes fxHexFire {
  0%   { opacity: 0; }
  6%   { opacity: 1; }
  24%  { opacity: .3; }
  42%  { opacity: 1; }
  60%  { opacity: .3; }
  78%  { opacity: 1; }
  100% { opacity: 0; }
}

/* Un point de victoire de plus : le score rebondit. */
.pscore.fx-score { animation: fxScore .6s cubic-bezier(.2, 1.5, .4, 1); }
@keyframes fxScore {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.55); color: #fff2c8; }
  100% { transform: scale(1); }
}

/* ------------------------ Le lancer de dés sur le plateau --------------------
 * Deux vrais dés en volume (js/fx.js) jetés sur l'île : ils arrivent du bord
 * bas, traversent le carton en culbutant, rebondissent et se couchent vers le
 * milieu. Chaque dé est un cube de six faces ; c'est le pilotage image par
 * image qui l'immobilise pile sur la bonne face. Le plateau étant vu d'aplomb,
 * la face tournée vers nous est celle du dessus : c'est elle qui porte le
 * tirage.
 *
 * La scène couvre la page et porte à elle seule la perspective, dont le point
 * de fuite — posé par js/fx.js — est le centre du plateau. Un dé posé loin du
 * centre montre donc un peu ses côtés, et un dé en l'air (translaté en Z vers
 * l'œil) grossit de lui-même : sa hauteur est vraie, pas mimée par une échelle.
 * --sz (posé lui aussi par js/fx.js, d'après le zoom du plateau) donne l'arête
 * du cube. */
#fxLayer > .fx-dice-stage { width: 100%; height: 100%; }
/* Les enfants sont tous posés en (0,0) et déplacés par transform ; la scène
 * n'étant pas un espace 3D commun (pas de preserve-3d), chacun est aplati dans
 * le plan de la page et peint dans l'ordre du DOM — d'où les ombres d'abord. */
.fx-dice-stage > * { position: absolute; top: 0; left: 0; }
/* Le fondu porte sur la scène, jamais sur un cube : une opacité < 1 (comme un
 * filtre) sur un élément preserve-3d l'aplatirait et tuerait le relief. */
#fxLayer > .fx-dice-stage.out { transition: opacity .35s ease; opacity: 0; }

/* L'ombre du dé sur le carton : ronde et floue, elle s'étale et pâlit à mesure
 * qu'il monte — c'est elle qui donne la hauteur du jet. */
.fx-die-shadow {
  width: var(--sz, 40px); height: var(--sz, 40px);
  background: radial-gradient(closest-side, rgba(0, 0, 0, .5), rgba(0, 0, 0, .3) 52%, rgba(0, 0, 0, 0) 78%);
}

/* Le cube ne porte que sa position et sa rotation : ni opacité, ni filtre, ni
 * will-change — tout cela romprait le preserve-3d et remettrait le dé à plat. */
.fx-cube {
  width: var(--sz, 40px); height: var(--sz, 40px);
  transform-style: preserve-3d;
}
/* Les deux teintes du jeu, portées par le cube et lues par ses six faces : le
 * dé jaune creuse ses points en rouge, le dé rouge les creuse en jaune. Le
 * galbe (liseré, ombre des bords, lumière du coin haut-gauche) est le même de
 * part et d'autre, seulement accordé à la couleur du corps. */
.fx-cube.gold {
  --face-hi: #ffee6b; --face-mid: #ffd400; --face-lo: #e5ac00;
  --face-edge: rgba(160, 112, 0, .5); --face-shade: rgba(150, 102, 0, .3);
  --face-soft: rgba(214, 156, 0, .5);
  --pip-hi: #e8262c; --pip-lo: #a5121a;
}
.fx-cube.red {
  --face-hi: #f7493f; --face-mid: #d81f26; --face-lo: #a3121a;
  --face-edge: rgba(92, 8, 12, .5); --face-shade: rgba(76, 6, 10, .34);
  --face-soft: rgba(150, 20, 26, .5);
  --pip-hi: #ffe14a; --pip-lo: #f0b800;
}
.fx-face {
  /* Débord d'1 px : les faces se recouvrent le long des arêtes, ce qui supprime
   * la couture qu'on voyait entre deux faces jointes pile bord à bord. */
  position: absolute; inset: -1px; padding: 11%;
  background: linear-gradient(150deg,
    var(--face-hi, #fffef8) 0%, var(--face-mid, #ece0c4) 60%, var(--face-lo, #d8c8a4) 100%);
  /* Aucun border-radius : il ne bomberait pas le coin, il le découperait — la
   * face y deviendrait transparente, et sur un vrai dé un coin arrondi est de
   * la matière, pas un trou. Le plateau étant vu d'aplomb, l'encoche de la face
   * du dessus et celle de la face du dessous se superposent exactement : le
   * regard passait par les deux et voyait la tuile au travers du dé, les faces
   * latérales étant vues sur la tranche et ne bouchant rien. L'arrondi est donc
   * seulement suggéré, par l'ombre qui suit le bord.
   *
   * Ce dégradé interne tient lieu de bord et de galbe, sans agrandir la boîte
   * comme le ferait une bordure (qui doublerait au recouvrement) : un liseré
   * sable, puis une ombre plus large qui creuse les quatre côtés, et une
   * lumière au bord haut-gauche, du côté d'où vient le jour.
   *
   * La première ombre, elle, est extérieure, et c'est le lissage des arêtes.
   * Une face est un plan tourné dans l'espace : la carte graphique la dessine
   * d'abord d'aplomb, puis incline l'image obtenue. Un bord franc donne alors
   * un escalier de pixels, que rien dans le CSS ne peut adoucir après coup —
   * ni opacité ni filtre, qui écraseraient le relief. En revanche l'image
   * dessinée inclut les ombres portées : un halo flou de la teinte du dé
   * dégrade le bord sur deux pixels avant l'inclinaison, et l'escalier
   * disparaît. Le halo est de la couleur du corps, pas noir — c'est l'arête du
   * dé qu'on prolonge, pas une ombre qu'on ajoute. */
  box-shadow:
    0 0 2px .3px var(--face-soft, rgba(178, 150, 104, .5)),
    inset 0 0 0 1px var(--face-edge, rgba(178, 150, 104, .5)),
    inset 0 0 6px var(--face-shade, rgba(120, 96, 52, .34)),
    inset 2px 2px 3px rgba(255, 255, 255, .45),
    inset -2px -2px 4px var(--face-shade, rgba(120, 96, 52, .3));
}
.fx-pips { width: 100%; height: 100%; display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr); }
.fx-pips i {
  width: 62%; height: 62%; align-self: center; justify-self: center; border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, var(--pip-hi, #4c4c4c), var(--pip-lo, #1a1a1a) 78%);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, .2);
}
/* Les six faces, chacune poussée d'une demi-arête sur son côté. */
.fx-face.f { transform: translateZ(calc(var(--sz, 40px) / 2)); }
.fx-face.b { transform: rotateY(180deg) translateZ(calc(var(--sz, 40px) / 2)); }
.fx-face.r { transform: rotateY(90deg)  translateZ(calc(var(--sz, 40px) / 2)); }
.fx-face.l { transform: rotateY(-90deg) translateZ(calc(var(--sz, 40px) / 2)); }
.fx-face.u { transform: rotateX(90deg)  translateZ(calc(var(--sz, 40px) / 2)); }
.fx-face.d { transform: rotateX(-90deg) translateZ(calc(var(--sz, 40px) / 2)); }

/* ------------------------------ Le sept ------------------------------------
 * La nuit qui se referme sur le tirage. Un seul nœud, posé par js/fx.js : un
 * carré centré sur les deux cubes, assez grand pour que le coin d'écran le plus
 * lointain tombe dedans, et dont le dégradé est clair au milieu, sombre au bord.
 * Le `closest-side` prolongeant sa dernière teinte jusqu'aux angles du carré, la
 * page entière s'assombrit — seule la poche de lumière est ronde.
 *
 * Tout le mouvement est dans l'échelle : à 1,9 la couronne sombre est rejetée
 * bien au-delà de l'écran et il fait grand jour, à 1 elle en atteint les bords.
 * La nuit ne se pose donc pas sur l'image, elle vient du dehors et se resserre.
 * Rien n'est repeint au passage : le compositeur ne fait que transformer une
 * couche, plateau et cubes intacts dessous.
 *
 * --pocket (js/fx.js, d'après la taille des cubes) est le rayon de la clairière
 * qui reste autour des dés ; --delay est l'instant où la paire s'immobilise, à
 * la milliseconde où le son du sept s'ouvre lui aussi.
 *
 * Le noir tire sur le bleu plutôt que sur le gris : posé sur le sable et la mer
 * du carton, un noir neutre grise le plateau au lieu de l'assombrir. */
.fx-seven {
  background: radial-gradient(circle closest-side,
    rgba(7, 5, 14, 0) 0,
    rgba(7, 5, 14, 0) var(--pocket, 160px),
    rgba(7, 5, 14, .36) 48%,
    rgba(7, 5, 14, .74) 100%);
  opacity: 0;
  animation: fxSeven var(--dur, 1950ms) ease-in-out var(--delay, 0ms) both;
}

/* Les trois temps de js/fx.js, en pourcentages de leur somme : la fermeture
 * (400 ms, soit 21 %), la tenue (850, jusqu'à 64 %), la réouverture (700).
 * Changer une de ces trois durées là-bas demande de reprendre ces bornes ici.
 *
 * Le battement de la tenue n'est pas décoratif : le son du sept est fait de
 * partiels dédoublés et désaccordés qui se renforcent puis s'annulent, et le
 * voile ondule du même pas. Personne ne le remarquera, tout le monde le sentira.
 *
 * Et la réouverture est plus lente que la fermeture, en écartant les bords
 * au-delà de leur point de départ : ce qui se resserre doit se relâcher, pas se
 * couper. */
@keyframes fxSeven {
  0%   { opacity: 0;   transform: translate(-50%, -50%) scale(1.9); }
  21%  { opacity: 1;   transform: translate(-50%, -50%) scale(1); }
  35%  { opacity: .87; transform: translate(-50%, -50%) scale(1.035); }
  50%  { opacity: 1;   transform: translate(-50%, -50%) scale(1); }
  64%  { opacity: .93; transform: translate(-50%, -50%) scale(1.02); }
  100% { opacity: 0;   transform: translate(-50%, -50%) scale(1.45); }
}

/* La couleur qui quitte le carton. Le noir seul assombrit l'île ; la
 * désaturation la vide, et c'est autre chose — le bois, les blés et l'argile
 * s'en vont, il ne reste qu'un relief gris sous une lumière rouge.
 *
 * Le masque n'est pas un ornement : `backdrop-filter` s'applique à tout ce qui
 * est derrière l'élément, d'un bloc et sans dégradé. Sans lui, les deux cubes
 * — le seul endroit qui doit garder ses couleurs — seraient lavés comme le
 * reste. Le masque redonne au filtre la forme du voile : rien au milieu, tout
 * au bord. Il porte alors les deux à la fois, et le fond redevient plat.
 *
 * Sous @supports, et le dégradé de fond reste écrit au-dessus : là où le masque
 * ou le filtre manquent, la nuit tombe comme avant, simplement sans le
 * délavage. Un fond plat servi sans son masque, lui, noircirait l'écran entier,
 * dés compris — c'est la panne qu'on ne veut pas.
 *
 * Le coût est réel et c'est le seul de tout le fichier : un filtre de fond
 * oblige le navigateur à relire ce qu'il y a dessous à chaque image, plein
 * écran, pendant les deux secondes de la scène. Sur un téléphone modeste, c'est
 * ici que ça se verra — nulle part ailleurs. */
@supports ((backdrop-filter: saturate(1)) or (-webkit-backdrop-filter: saturate(1))) and
          ((mask-image: radial-gradient(#000, #000)) or (-webkit-mask-image: radial-gradient(#000, #000))) {
  .fx-seven {
    background: rgba(7, 5, 14, .74);
    -webkit-backdrop-filter: saturate(.35);
    backdrop-filter: saturate(.35);
    /* Le même profil que le dégradé qu'il remplace : transparent jusqu'à la
     * clairière, deux tiers de nuit à mi-chemin, pleine nuit au bord. */
    -webkit-mask-image: radial-gradient(circle closest-side,
      transparent 0, transparent var(--pocket, 160px),
      rgba(0, 0, 0, .49) 48%, #000 100%);
    mask-image: radial-gradient(circle closest-side,
      transparent 0, transparent var(--pocket, 160px),
      rgba(0, 0, 0, .49) 48%, #000 100%);
  }
}

/* ------------------------- La braise du voleur ------------------------------
 * Pendant que l'île s'éteint, la tuile qu'il occupe encore garde une lueur. Ce
 * n'est pas un ornement de plus : c'est la seule chose que le 7 annonce, et
 * elle désigne la case que le voleur s'apprête à quitter.
 *
 * Même géométrie que les tuiles réveillées par le tirage (js/fx.js pose le même
 * hexagone à la même échelle), mais l'inverse comme intention : là-bas de l'or
 * qui dit « ceci rapporte », ici une braise qui dit « ceci va bouger ».
 *
 * Son liseré est celui du voleur, écrit plus haut avec celui de la tuile qu'il
 * musèle : c'est la même marque, et elle n'est faite qu'une fois. Ce qui change
 * ici, c'est le halo — chaud au lieu de noir, parce qu'une braise éclaire ce
 * qu'elle touche au lieu de le creuser. */
.fx-ember {
  animation: fxEmber var(--dur, 1950ms) ease-in-out var(--delay, 0ms) both;
  filter: drop-shadow(0 0 4px rgba(255, 138, 74, .75))
          drop-shadow(0 0 16px rgba(198, 48, 20, .6));
}
.fx-ember svg { display: block; width: 100%; height: 100%; overflow: visible; }
.fx-ember .glow { fill: rgba(150, 34, 16, .34); }

/* En phase avec le voile : la braise est au plus vif quand la nuit est au plus
 * noir, et faiblit quand elle respire. Deux battements contraires feraient
 * entendre deux évènements là où il n'y en a qu'un. */
@keyframes fxEmber {
  0%   { opacity: 0; }
  21%  { opacity: 1; }
  35%  { opacity: .5; }
  50%  { opacity: 1; }
  64%  { opacity: .58; }
  100% { opacity: 0; }
}

/* Réglage système « animations réduites » : js/fx.js ne lance alors aucune
 * pièce volante ni aucun dé ; on coupe aussi les petits effets posés en CSS.
 * Les tuiles réveillées par le tirage restent, elles : sans battement ni fondu,
 * simplement allumées le temps qu'on les voie — c'est une information, pas un
 * ornement. */
@media (prefers-reduced-motion: reduce) {
  .fx-pulse, .fx-score, .fx-take, .fx-drop, .fx-bring,
  .fx-land { animation: none !important; }
  /* Aucun dé ne roule : le tirage n'a personne à attendre et s'inscrit d'emblée.
   * La règle reste, par sûreté — un panneau en retrait qui ne se rallumerait
   * jamais rendrait le résultat illisible. */
  #dice { transition: none; }
  #dice.rolling { opacity: 1; filter: none; }
  .fx-hexfire { animation: none !important; opacity: 1; }
  /* Le bandeau du tour dit qui prend la main : il reste, posé d'un coup et
   * retiré par js/ui.js à l'heure dite. Seul son reflet s'en va — celui-là
   * n'apprend rien à personne. */
  .tb-band::before, .tb-dot, .tb-text {
    animation: none !important; opacity: 1; transform: none;
  }
  .tb-sheen { display: none; }
}

/* =============================== Mobile =============================== */

/* ============================ Mobile : plein écran =======================
 * Sur téléphone, le plateau prend tout l'écran et l'interface passe par-dessus
 * en surimpression : bandeau des joueurs et chronique en haut, dock d'état et
 * d'actions en bas, main flottante juste au-dessus. Le panneau latéral n'est
 * plus une colonne à faire défiler pour atteindre un bouton — chaque contrôle
 * est posé là où le pouce le cherche.
 *
 * Le panneau latéral devient une surcouche plein écran, transparente aux
 * gestes ; ses blocs (marque, joueurs, chronique) s'empilent en flux depuis le
 * haut, et le dock d'actions se détache en bas. Rien ne change au balisage ni
 * au rendu de bureau, tout tient dans cette requête média. */
@media (max-width: 820px) {
  html { font-size: 15px; }

  #app { display: block; position: relative; }
  /* z-index posé : boardWrap devient un contexte d'empilement, ses propres
   * surcouches (zoom, main) restent sous celles du panneau en surimpression. */
  #boardWrap { position: absolute; inset: 0; z-index: 0; }

  /* Le bandeau du tour se range juste sous la liste des joueurs. Cette hauteur
   * n'est pas devinable : les pastilles passent à la ligne dès qu'un joueur
   * cumule titres et compteurs, et le bandeau s'épaissit en cours de partie.
   * js/ui.js la relève donc à chaque rendu et la publie en --topH, comme il le
   * fait déjà de --handH et --dockH pour l'empilement du bas. Le repli sert le
   * tout premier rendu, avant qu'aucune mesure n'ait été prise.
   *
   * Sans quoi la bande passerait *derrière* la liste : le panneau mobile est en
   * surimpression à z-index 5, et #boardWrap, qui la porte, est un contexte
   * d'empilement à 0 — aucun z-index ne l'en ferait sortir.
   *
   * Elle s'étale aussi davantage : un pseudonyme n'a pas moins de lettres sur
   * un téléphone que sur un écran. */
  #turnBanner { padding-top: calc(var(--topH, 124px) + 12px); }
  .tb-band { max-width: 94%; padding: .5em 1.2em; gap: .5em; }

  /* Le panneau devient une surcouche plein écran, transparente aux gestes : ses
   * blocs s'empilent naturellement depuis le haut (marque, joueurs, chronique)
   * et le dock d'actions se détache tout en bas. Empiler en flux plutôt que de
   * poser chaque bloc à un « top » deviné supprime tout chevauchement. */
  #side {
    position: absolute; inset: 0; z-index: 5;
    width: auto; display: flex; flex-direction: column;
    background: none; border: none; box-shadow: none; overflow: visible;
    pointer-events: none;
  }
  /* Les intitulés de section diraient ce que la lecture donne déjà. */
  .sectlabel { display: none; }

  /* ----------------------------- Bandeau haut ---------------------------- */
  /* Marque discrète à gauche, accès Règles / Menu à droite ; le fond fond vers
   * le plateau et ne capte les gestes que sur les liens. */
  .brand {
    display: flex; align-items: center; gap: 10px;
    padding: calc(5px + env(safe-area-inset-top, 0px)) 12px 12px; text-align: left;
    background: linear-gradient(180deg, rgba(6,20,31,.92), rgba(6,20,31,.4) 55%, transparent);
    border: none; box-shadow: none; pointer-events: none;
  }
  .brand .title { font-size: 1.05em; letter-spacing: 3px; }
  .brand .tag { display: none; }
  .brand .links { margin: 0 0 0 auto; display: flex; gap: 16px; pointer-events: auto; }
  /* Le bouton chronique, en tête du bandeau donc à gauche. */
  .brand-log {
    display: flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; flex: none; padding: 0;
    border-radius: 8px; cursor: pointer; pointer-events: auto;
    background: rgba(12,44,66,.85); color: var(--sand);
    border: 1px solid var(--sea-line); box-shadow: 0 2px 0 var(--sea-line);
  }
  .brand-log:active { transform: translateY(2px); box-shadow: 0 0 0 var(--sea-line); }
  .brand-log.on { background: var(--sea-light); border-color: var(--gold); color: var(--gold-bright); }
  .brand-log .ic { width: 1.2em; height: 1.2em; }

  /* --------------------------- Bandeau joueurs --------------------------- */
  #side > .pad:not(.turnbox):not(.grow) { padding: 2px 6px 0; pointer-events: none; }
  /* Deux joueurs par rangée, quoi qu'ils portent : une grille à colonnes égales
   * plutôt qu'un enroulement au gré des largeurs. Laissée à sa largeur
   * naturelle, une pastille chargée — titres, chevaliers, deux mains — passait
   * la moitié de l'écran et s'emparait de la ligne entière ; la table perdait
   * sa symétrie à mesure que la partie s'étoffait, et d'autant plus vite que le
   * bandeau grandissait sur le plateau. La case est désormais une donnée, et
   * c'est le contenu qui s'y plie. */
  #players {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 6px;
  }
  /* Un effectif impair : le dernier reste à la mesure des autres, centré sous
   * eux, plutôt que de s'étaler sur deux colonnes qu'il ne remplit pas. */
  #players .player:last-child:nth-child(odd) {
    grid-column: 1 / -1; width: calc(50% - 3px); justify-self: center;
  }
  .player {
    min-width: 0; gap: 4px; padding: 3px 7px;
    background: rgba(16,49,74,.92); backdrop-filter: blur(2px);
  }
  /* Dans une case à largeur imposée, c'est le nom qui cède : il s'abrège d'une
   * ellipse plutôt que de pousser marques et score hors du cadre. Sur bureau il
   * s'étire (flex:1) faute de concurrence ; ici il prend ce qui reste.
   *
   * Le coefficient de retrait démesuré (100 contre 1) fait céder le nom seul :
   * à parts égales, la rangée de marques aurait rendu du terrain en même temps
   * que lui et serait passée à la ligne alors qu'un nom encore long attendait
   * d'être abrégé. Le nom fond d'abord, les marques ensuite. */
  .player .pname { flex: 1 100 auto; min-width: 0; max-width: none; }
  /* Les marques se resserrent d'un cran : quatre d'entre elles doivent tenir
   * dans une demi-largeur de téléphone à côté d'un nom encore lisible. Et si un
   * joueur les cumule toutes — les deux titres, les deux mains — elles passent
   * à la ligne dans la pastille, qui grandit d'un rang plutôt que de déborder
   * de son cadre ou d'escamoter un compte. */
  .pbadges {
    flex: 0 1 auto; min-width: 0;
    flex-wrap: wrap; justify-content: flex-end; gap: 2px 3px;
  }
  .badge { font-size: .64em; letter-spacing: .3px; padding: 2px 5px; gap: 2px; }
  .player .pscore { font-size: 1.05em; min-width: 14px; }
  .pdot { width: 12px; height: 12px; }

  /* ------------------------------ Chronique ------------------------------ */
  /* Fermée par défaut ; le bouton en haut à gauche l'ouvre comme une fenêtre de
   * chat, posée sous le bandeau des joueurs (top réglé par js/ui.js), sans
   * repousser le plateau. Le cadre laisse passer les gestes, seul le log capte. */
  #side > .pad.grow { display: none; }
  #side > .pad.grow.log-open {
    display: block; position: absolute; left: 6px; right: 6px; top: 70px; z-index: 9;
    padding: 0; pointer-events: none;
  }
  #log {
    flex: none; min-height: 0; max-height: 50vh;
    padding: 7px 11px; pointer-events: auto;
    background: rgba(4,20,32,.92); backdrop-filter: blur(3px);
    box-shadow: 0 10px 30px rgba(0,0,0,.5);
  }

  /* --------------------------- Dock du bas ------------------------------- */
  /* La barre d'actions seule. Le fond ne capte les gestes que sur la barre
   * elle-même — ailleurs, le plateau reste saisissable.
   *
   * Rien au-dessus des boutons : l'écart qui les sépare des cartes appartient à
   * la pile flottante, qui le pose elle-même (voir #bottom). Un rembourrage ici
   * s'y serait ajouté sans se voir, et le bas de la pile aurait respiré plus
   * large que le haut. */
  .turnbox {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 8;
    padding: 0 8px calc(6px + env(safe-area-inset-bottom, 0px));
    border: none; pointer-events: none;
    background: linear-gradient(0deg, rgba(6,20,31,.96), rgba(6,20,31,.62) 72%, transparent);
  }

  /* ---------------------------- État du tour ----------------------------- */
  /* Le tirage est ce qu'on cherche des yeux en premier : il commande ce que la
   * main devient, et non l'inverse. Il quitte donc le flux du dock pour se
   * poser au-dessus des cartes — dés, puis consigne juste dessous, l'ordre de
   * lecture d'une annonce. La main reste entre les deux et la barre d'actions.
   *
   * `bottom: 100%` le pose sur le bord haut du dock ; la hauteur de la main,
   * publiée en --handH par updateInsets(), l'en écarte d'autant. Repli si la
   * variable manque (premier rendu).
   *
   * 16 = deux fois l'écart de la pile : celui qui sépare la main du dock, puis
   * celui qui doit rester ici. L'ancre est le dock, pas la main. */
  #turnState {
    position: absolute; left: 8px; right: 8px;
    bottom: calc(100% + var(--handH, 78px) + 16px);
    pointer-events: none;
  }
  #dice { pointer-events: none; }
  #dice:not(:empty) { margin-bottom: 5px; }
  .die { width: 30px; height: 30px; }
  .die i { width: 5px; height: 5px; }
  /* Les dés portent leur propre ombre ; la somme et la consigne, elles, ne sont
   * que du texte posé sur le plateau — une tuile claire ou un chiffre de jeton
   * suffirait à les avaler. L'ombre portée les détache de tout fond, comme elle
   * le fait déjà pour la réserve sur bureau.
   *
   * Le 7 garde son halo rouge : les deux ombres se cumulent dans la même liste,
   * la lueur d'abord, le relief ensuite. Sans cette reprise, la règle plus
   * spécifique de .dsum.hot emporterait la mise et le 7 — justement celui qu'on
   * lit le plus vite — resterait le seul illisible. */
  .dsum {
    font-size: 1.25em; min-width: 26px;
    text-shadow: 0 1px 3px rgba(0,0,0,.85);
  }
  .dsum.hot {
    text-shadow: 0 0 14px rgba(179,38,30,.6), 0 1px 3px rgba(0,0,0,.85);
  }
  #turnLabel {
    min-height: 0; margin: 0; font-size: .86em; line-height: 1.3;
    color: #e7dcc0; pointer-events: none;
    text-shadow: 0 1px 3px rgba(0,0,0,.85);
  }

  /* La barre d'actions : les actions secondaires tiennent dans une rangée de
   * pastilles toujours visibles, l'action or seule en pleine largeur dessous. */
  #actions { display: flex; flex-direction: column; gap: 6px; pointer-events: auto; }
  /* Cinq vignettes n'entrent pas dans une pastille de 60 px. Le coût ne
   * disparaît pas pour autant : il passe dans la main, deux doigts au-dessus,
   * dès qu'on touche le bouton — armé s'il est payable, en simple devis sinon
   * (js/ui.js, `costPeek`). L'infobulle, elle, ne sert plus ici : rien ne
   * survole sur un écran tactile. */
  button.act .bcost { display: none; }

  /* Rangée défilante : chaque action secondaire devient une pastille icône +
   * libellé, à largeur naturelle ; ce qui déborde se fait défiler du doigt. */
  .actrow {
    display: flex; flex-wrap: nowrap; gap: 6px;
    overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
    padding-bottom: 2px; -webkit-overflow-scrolling: touch;
  }
  .actrow::-webkit-scrollbar { display: none; }
  /* flex-grow:1 : les pastilles s'étirent pour remplir toute la largeur, sans
   * marge à droite ; shrink:0 : si elles débordent (cartes dev en plus), elles
   * gardent leur taille et la rangée défile au lieu de les écraser. */
  .actrow .act {
    flex: 1 0 auto; flex-direction: column; gap: 3px;
    min-width: 60px; padding: 7px 6px; text-align: center; font-size: .72em;
  }
  .actrow .act .ic { width: 1.55em; height: 1.55em; }
  .actrow .act .blab {
    max-width: 74px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* La carte redevient une pastille comme ses voisines : médaillon en haut, nom
   * dessous. Trois reprises sont nécessaires — le rembourrage asymétrique du
   * format couché n'a plus de sens une fois la pastille verticale, et le
   * pictogramme grossi de la rangée déborderait de son disque. Ces sélecteurs
   * sont plus spécifiques que le bloc bureau, sans quoi ils ne passeraient pas. */
  .actrow .act.dev { padding: 7px 6px; }
  .actrow .act.dev .dmed { width: 26px; height: 26px; }
  .actrow .act.dev .dmed .ic { width: 1.05em; height: 1.05em; }

  /* ------------------------------- La main ------------------------------- */
  /* Elle flotte juste au-dessus du dock, dont la hauteur est publiée en
   * --dockH par updateInsets(), et sous l'état du tour qui se cale sur elle.
   * Repli si la variable manque (premier rendu).
   *
   * Un seul écart, 8px, règle toute la pile du bas : entre l'état du tour et les
   * cartes, entre les cartes et les boutons. Ces bandes sont des voisines de
   * même rang — rien ne justifie qu'une frontière se voie plus qu'une autre. */
  #bottom {
    left: 6px; right: 6px; top: auto; bottom: calc(var(--dockH, 120px) + 8px);
    gap: 8px; z-index: 5;
  }
  #hand { gap: 6px; }
  #hand .card { width: 44px; height: 66px; }
  /* C'est ici que le devis compte vraiment : les vignettes de coût des boutons
   * sont cachées plus bas faute de place, et rien ne survole au doigt. Les
   * jetons se resserrent à la mesure d'une carte de 44 px, contre 62 sur
   * bureau ; trois d'entre eux y tiennent encore sans se toucher. */
  #hand .card .req { gap: 2px; padding: 2px 2px; }
  #hand .card .req i { width: 6px; height: 6px; border-width: 1.5px; }
  /* La réserve et la pioche restent dans les infobulles : la barre du bas n'a
   * pas la place de les dire en clair. */
  #resLeft { display: none; }

  /* -------------------------------- Zoom --------------------------------- */
  /* Sur le bord droit, à mi-hauteur, hors des surcouches du haut et du bas. */
  #zoom {
    top: 50%; bottom: auto; left: auto; right: 8px; z-index: 6;
    transform: translateY(-50%);
  }
  #zoom button { width: 34px; height: 34px; }
}
