:root {
  /*
   * La carte est l'étalon de toute l'interface : sa largeur suit celle de la fenêtre — posée
   * en pixels par le script à l'ouverture et au redimensionnement — et tout le reste s'y
   * proportionne. Plus l'écran est grand — téléviseur, grand moniteur — plus le jeu prend de
   * place ; l'étalon des proportions reste la carte de 68px, le secours sans script en vaut 102.
   */
  --card-w: 102px;
  --ui-scale: 1;
  /* Le pas du pli : la carte chevauche sa voisine d'un tiers. */
  --card-step: calc(var(--card-w) * 0.676);
  --felt: #14532d;
  --felt-dark: #0d3b21;
  --ink: #1c1917;
  --paper: #faf7f2;
  --line: #d6cfc4;
  --accent: #b45309;
  --muted: #78716c;
}

* {
  box-sizing: border-box;
}

/* La page est la table : l'en-tête en haut, le feutre partout ailleurs. */
body {
  margin: 0;
  font:
    15px/1.5 system-ui,
    -apple-system,
    'Segoe UI',
    sans-serif;
  color: var(--ink);
  background: var(--felt-dark);
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

h2 {
  margin: 0 0 0.5rem;
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.controls {
  display: flex;
  gap: 0.5rem;
}

select,
input,
button {
  font: inherit;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
}

button {
  cursor: pointer;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

button:hover {
  filter: brightness(1.08);
}

/* Le tapis prend toute la place : la partie n'est pas une carte dans une page. */
main {
  flex: 1;
  display: flex;
  min-height: 0;
}

/* Le tapis du jeu de 2013 : feutre vert et enseignes en filigrane, d'un bord à l'autre. */
.board {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  /* Le feutre suit le jeu de cartes choisi : ses enseignes sont gravées dans le fond. */
  background:
    linear-gradient(rgba(6, 40, 20, 0.34), rgba(6, 40, 20, 0.34)),
    var(--carpet, url('cards/tapis-ro.jpg')) center / cover no-repeat;
  color: #f5f5f4;
  padding: 0.6rem 1rem 0.3rem;
  box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.45);
}

.status {
  display: flex;
  gap: 1.25rem;
  flex-wrap: wrap;
  align-items: center;
  font-size: 0.9rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}

/* Quitter, c'est revenir au menu : le bouton vit sur le plateau, au bout du bandeau. */
.status .quit {
  order: 10;
  font: inherit;
  color: #fff;
  background: rgba(150, 40, 40, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  padding: 0.15rem 0.8rem;
  cursor: pointer;
}
.status .quit:hover {
  filter: none;
  background: rgba(150, 40, 40, 0.8);
}

/* Le panneau des scores : le tenir appuyé fait apparaître le détail des plis de chacun. */
.status .score {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  font: inherit;
  color: inherit;
  background: rgba(0, 0, 0, 0.25);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 999px;
  padding: 0.15rem 0.7rem;
  cursor: pointer;
  user-select: none;
}
.status .score:hover {
  filter: none;
  background: rgba(0, 0, 0, 0.4);
}

/*
 * La table, vue depuis le siège 0. Le jeu tourne dans le sens trigonométrique et la main passe
 * au voisin de droite (RD-014) : le siège suivant occupe donc la case de droite.
 */
.felt {
  position: relative;
  /* Le tapis remplit tout l'espace entre le bandeau et la main. */
  flex: 1;
  /* Ce qui vit hors vue — paquets lancés, plis ramassés, tas pris en main — sort par les bords
     du tapis, jamais par-dessus le reste de la page. */
  overflow: hidden;
  display: grid;
  grid-template-areas:
    '.    top    .'
    'left center right'
    '.    bottom .';
  grid-template-columns: minmax(7rem, 1fr) minmax(0, 1.6fr) minmax(7rem, 1fr);
  gap: 0.5rem;
  align-items: center;
  justify-items: center;
  /*
   * Le tapis est une image de fond : sa hauteur ne doit pas bouger en cours de partie. Elle n'est
   * pas figée pour autant — la table à trois n'a pas de siège en haut et se contente de moins.
   * Ce sont les sièges qui gardent une hauteur constante, ligne d'annotations réservée comprise.
   */
  min-height: 340px;
  padding: 0.5rem 0;
}

.seats {
  display: contents;
}

/*
 * Le pli : les cartes côte à côte, dans l'ordre de jeu, à peine chevauchantes et légèrement
 * inclinées.
 *
 * L'emplacement est **réservé d'avance**, un par joueur, et la carte est placée à celui qui
 * correspond à son rang de dépôt. Une carte posée ne bouge donc plus jusqu'au ramassage : centrer
 * une rangée qui s'allonge décalerait au contraire toutes les précédentes à chaque dépôt. Du même
 * coup l'empreinte est fixe, et poser une quatrième carte ne fait pas grandir le tapis.
 */
.trick {
  grid-area: center;
  position: relative;
  width: calc(var(--card-w) + (var(--seats, 4) - 1) * var(--card-step));
  height: calc(var(--card-w) * 2.2);
}

.slot {
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--card-w);
  display: flex;
  align-items: center;
  /* Le pas est plus court que la carte : le chevauchement laisse voir l'ordre de dépôt. */
  left: calc(var(--index, 0) * var(--card-step));
}

.slot .card {
  transform: rotate(var(--tilt, 0deg));
}

/*
 * L'affichage étant reconstruit à chaque changement d'état, une transition ne se déclencherait
 * jamais : on passe par des images clés, qui se rejouent à chaque insertion.
 */
.slot.entering .card {
  animation: throw-in 300ms ease-out;
}

/*
 * Une carte n'est jamais transparente : elle arrive de loin, elle ne se matérialise pas. Et
 * elle vole **déjà dans sa position finale** — orientation (le hasch déterministe de `tiltOf`)
 * et taille — pour ne pas se poser puis pivoter.
 */
@keyframes throw-in {
  from {
    transform: translate(var(--from-x), var(--from-y)) rotate(var(--tilt, 0deg));
  }
  to {
    transform: rotate(var(--tilt, 0deg));
  }
}

/* Le pli entier glisse hors vue, vers celui qui le ramasse — il ne s'évapore pas. */
.slot.gathering .card {
  animation: gather 460ms ease-in forwards;
}

@keyframes gather {
  from {
    transform: rotate(var(--tilt, 0deg));
  }
  to {
    transform: translate(var(--to-x, 0), var(--to-y, 0)) rotate(var(--tilt, 0deg)) scale(0.7);
  }
}

/*
 * Le coup d'œil : trois positions, et des **transitions** entre elles — jamais d'images clés.
 * A : groupées hors tapis (invisibles une fois posées là). B : groupées sur le tapis, chacune
 * sa rotation subtile. C : éclatées selon qui a joué. La cible peut changer en plein vol —
 * cliquer vite fait hésiter les cartes entre dehors et dedans, sans jamais sauter.
 */
.peek[hidden] {
  display: none;
}
.peek {
  position: absolute;
  right: calc(var(--card-w) * 0.45);
  bottom: calc(var(--card-w) * 1.15);
  width: calc(var(--card-w) * 2.65);
  height: calc(var(--card-w) * 2.8);
  z-index: 28;
  pointer-events: none;
}
.peek.resting .peek-card {
  visibility: hidden;
}
.peek .peek-card {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--card-w);
  /* Le centre du groupe : la moitié d'une carte de 68 × 104. */
  margin: calc(var(--card-w) * -0.765) 0 0 calc(var(--card-w) * -0.5);
  transition: transform 300ms ease;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.peek .peek-card.winner {
  box-shadow:
    0 0 0 2px #fbbf24,
    0 2px 8px rgba(0, 0, 0, 0.5);
}
/* A — groupées juste au-delà du bord : la translation d'entrée reste courte, réaliste. */
.peek.state-a .peek-card {
  transform: translate(calc(var(--card-w) * 4.1), 0px) rotate(var(--tilt, 0deg));
}
/* B — le groupe posé sur le tapis, serré, chaque carte son décalage et sa rotation subtile. */
.peek.state-b .peek-card {
  transform: translate(var(--bx, 0px), var(--by, 0px)) rotate(var(--tilt, 0deg));
}
/* C — éclatées, chacune un pas vers la place de son joueur, chevauchées dans l'ordre de pose. */
.peek.state-c .peek-card {
  transform: translate(var(--dx, 0px), var(--dy, 0px)) rotate(var(--tilt, 0deg));
}

/*
 * La carte en cours de déplacement entre la main et le pli. Sa place au pli est déjà occupée mais
 * reste vide pendant le trajet, sinon la même carte serait visible à deux endroits.
 */
.in-flight {
  z-index: 60;
  pointer-events: none;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.5);
}

.slot.handoff .card {
  visibility: hidden;
}

/* Les cartes sont celles du jeu romand de 2013, en 134x205. */
.card {
  position: relative;
  width: var(--card-w);
  border-radius: 6px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
  line-height: 0;
}

.card img {
  width: 100%;
  height: auto;
  border-radius: 6px;
  display: block;
}

/* Cœur et carreau se lisent en rouge partout où une carte est nommée en texte. */
.red-suit {
  color: #b91c1c;
}

.card .owner {
  position: absolute;
  bottom: -1.25rem;
  font-size: 0.7rem;
  color: rgba(245, 245, 244, 0.85);
  white-space: nowrap;
}

.seat {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  padding: 0.4rem 0.6rem;
  border-radius: 6px;
  font-size: calc(0.8rem * var(--ui-scale));
  text-align: center;
}

.seat.pos-top {
  grid-area: top;
}
.seat.pos-right {
  grid-area: right;
}
.seat.pos-bottom {
  grid-area: bottom;
}
.seat.pos-left {
  grid-area: left;
}

.seat {
  transition: background 180ms ease;
}

.seat.active {
  background: rgba(255, 255, 255, 0.16);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
}

/* Qui est déjà entré dans le tour : son nom s'estompe une fois sa carte posée. */
.seat.played .who {
  opacity: 0.55;
}

/* L'étoile marque qui prend la main. */
/*
 * Toujours présente, invisible quand elle ne s'applique pas : la faire apparaître changerait la
 * largeur du nom, et la bulle ancrée au siège glisserait avec lui.
 */
.star {
  color: #fbbf24;
  margin-right: 0.25rem;
}

.star.absent {
  visibility: hidden;
}

.seat .who {
  font-weight: 600;
  font-size: calc(0.9rem * var(--ui-scale));
}

.seat .info {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  color: rgba(245, 245, 244, 0.72);
}

/* Réservée même vide : sans cela, le siège changerait de hauteur et ferait sauter le tapis. */
.seat .marks {
  min-height: 1.2em;
}

/*
 * Aucune main adverse à l'écran : leurs cartes sont supposées tenues hors champ, comme leurs
 * joueurs fictifs. Le siège ne montre que qui est là et où il en est (2026-08-31).
 */

/*
 * La main est posée sur le même feutre que le tapis : elle fait partie de la table, elle n'est pas
 * une barre d'outils. Les cartes se chevauchent en éventail léger, comme on les tient.
 */
/*
 * La main est posée au bas du tapis, dans la même vue que le jeu : elle n'a pas de fond propre,
 * c'est le feutre du plateau qui la porte.
 *
 * Au repos une carte n'est visible qu'à moitié — le reste est coupé par le bord, comme des cartes
 * tenues serrées. Ce sont les jouables qui remontent. `overflow` découpe donc la main, sauf le
 * temps d'un glissé : la carte tirée doit pouvoir en sortir.
 */
/*
 * Le tapis reste rectangulaire et les cartes se posent en bas, entières : rien n'est rogné. La
 * hauteur laisse la place à une carte complète, à la courbure de l'éventail et au dégagement des
 * jouables.
 */
.hand {
  /* La main vit dans la même vue que le jeu : posée au bas du tapis, dans le felt. */
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(var(--card-w) * 2.5);
  z-index: 15;
  /* Le calque couvre tout le bas du tapis : lui ne capte rien, seules ses cartes se cliquent. */
  pointer-events: none;
}

.hand .card {
  pointer-events: auto;
}

/*
 * Chaque carte est placée par sa **transformation**, non par le flux : `--x` donne son rang dans
 * l'éventail. C'est ce qui permet aux voisines de glisser vers leur nouvelle place quand l'une
 * d'elles est jouée — une position issue du flux changerait d'un coup, sans transition possible.
 *
 * `--sink` mesure le retrait vers le bas, `--arc` la courbure : les cartes du milieu montent,
 * celles des bords descendent, et l'ensemble décrit un arc de cercle.
 */
.hand .card {
  --sink: 0px;
  --arc: 0px;
  --x: 0px;
  position: absolute;
  left: 50%;
  /* Une marge sous la ligne de base : baissée ou inclinée, une carte ne passe jamais sous le
     bord du tapis — le felt clippe ce qui le quitte, pas la main du joueur. */
  bottom: calc(var(--card-w) * 0.2);
  transform-origin: bottom center;
  transform: translateX(calc(var(--x) - 50%)) rotate(var(--tilt, 0deg))
    translateY(calc(var(--sink) + var(--arc)));
  touch-action: none;
}

.hand .card {
  transition:
    transform 220ms ease,
    box-shadow 140ms ease;
}

/* La carte jouable sort du jeu, comme le pouce la dégage avant de choisir. */
.hand .playable {
  cursor: grab;
  --sink: calc(var(--card-w) * -0.32);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.45);
}

.hand .playable:hover {
  --sink: calc(var(--card-w) * -0.5);
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.5);
}

/*
 * La carte tirée est passée au-dessus du tapis : les autres se rangent, la main s'efface pour
 * laisser voir où l'on pose. Ramenée sur la main, elles remontent — c'est le geste qui commande.
 */
.hand.away .playable {
  --sink: 0px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}

/* La carte tirée suit le doigt : ni inclinaison ni retrait, elle est libre. */
.hand .card.dragged {
  cursor: grabbing;
  z-index: 20;
  transform: translateX(calc(var(--x) - 50%)) translate(var(--drag-x, 0px), var(--drag-y, 0px))
    scale(1.06);
  transition: none;
  box-shadow: 0 12px 26px rgba(0, 0, 0, 0.5);
}

/*
 * Le paquet pris revient du bas en **petit paquet serré** — les cartes superposées, comme on
 * les tient — puis les cartes **s'ordonnent** : l'éventail s'ouvre sur place. Sans fondu : une
 * carte n'est jamais transparente.
 */
.hand.arriving .card {
  animation: pickup 620ms ease-out backwards;
}

@keyframes pickup {
  from {
    transform: translateX(-50%) rotate(0deg)
      translateY(calc(var(--sink) + var(--arc) + var(--card-w) * 3.1));
  }
  50% {
    transform: translateX(-50%) rotate(0deg) translateY(calc(var(--sink) + var(--arc)));
  }
  to {
    transform: translateX(calc(var(--x) - 50%)) rotate(var(--tilt, 0deg))
      translateY(calc(var(--sink) + var(--arc)));
  }
}

/*
 * Les paquets que le donneur pose devant chacun : ils partent de sa place et vont à celle du
 * servi, trois cartes à la fois (RD-014). Hors du flux, sans quoi leur passage ferait respirer
 * le tapis.
 */
.flying {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
}

.flying .packet {
  position: absolute;
  left: 50%;
  top: 50%;
  /* Le jet arrive encore lancé — la courbe ne s'annule pas à l'impact : c'est la glissade
     d'inertie qui prendra le relais et s'éteindra, elle, en ease-out. */
  animation: deal-fly var(--dur, 130ms) cubic-bezier(0.3, 0, 0.75, 0.7) forwards;
}

/* Toutes les cartes ont la même taille à l'écran : celle du jeu. */
.flying .packet .card {
  position: absolute;
  width: var(--card-w);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
}

/* Le vol reste opaque de bout en bout : le paquet vient de loin, il ne se matérialise pas. */
@keyframes deal-fly {
  from {
    transform: translate(calc(-50% + var(--from-x)), calc(-50% + var(--from-y))) scale(0.85);
  }
  to {
    transform: translate(calc(-50% + var(--to-x)), calc(-50% + var(--to-y))) scale(1);
  }
}

/*
 * Les paquets de la distribution, un tas posé devant chaque joueur. Pris en main, un tas glisse
 * vers son propriétaire et sort de vue ; celui de qui attend le chibre reste posé.
 */
.pile {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

.pile-seat {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(calc(-50% + var(--x, 0px)), calc(-50% + var(--y, 0px)));
}

.pile-seat .card {
  position: absolute;
  width: var(--card-w);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  /* Chaque carte garde le désordre tiré à son atterrissage : position et orientation. */
  transform: translate(var(--tx, 0px), var(--ty, 0px)) rotate(var(--trot, 0deg));
}

/* Au toucher, l'inertie : les cartes glissent dans l'axe du jet — et les unes sur les autres —
   en décélérant (ease-out) jusqu'à l'arrêt. */
.pile-seat .card.settle {
  animation: card-settle 360ms ease-out;
}

@keyframes card-settle {
  from {
    transform: translate(var(--fx, 0px), var(--fy, 0px)) rotate(var(--frot, 0deg));
  }
  to {
    transform: translate(var(--tx, 0px), var(--ty, 0px)) rotate(var(--trot, 0deg));
  }
}

.pile-seat.leaving {
  animation: pile-take 380ms ease-in forwards;
}

@keyframes pile-take {
  to {
    transform: translate(calc(-50% + var(--away-x, 0px)), calc(-50% + var(--away-y, 0px)));
  }
}

/*
 * Une carte injouable n'est pas grisée : elle reste dans la main, simplement en retrait. Ce sont
 * les jouables qui sortent du jeu — c'est le geste du joueur qui les dégage, pas une teinte.
 */
.hand .disabled {
  cursor: default;
}

/*
 * Vos options : une case posée sur le tapis, sans fond blanc. Elle n'existe que lorsqu'une
 * décision vous revient — vide, elle disparaît sans laisser de trou.
 */
.options {
  position: absolute;
  left: 50%;
  bottom: calc(var(--card-w) * 2.8);
  transform: translateX(-50%);
  z-index: 25;
  max-width: min(640px, 92%);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
  /* Les boutons de texte gardent leur hauteur propre à côté des cartes. */
  align-items: flex-start;
  background: rgba(6, 28, 17, 0.82);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 10px;
  padding: 0.55rem 0.7rem;
}

.options:empty {
  display: none;
}

.opt {
  background: rgba(0, 0, 0, 0.45);
  color: #f2ede4;
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 8px;
}

.opt:hover {
  filter: none;
  background: rgba(150, 40, 40, 0.55);
  border-color: rgba(255, 255, 255, 0.5);
}

.card-opt {
  padding: 0;
  width: var(--card-w);
  border-radius: 6px;
  overflow: hidden;
  line-height: 0;
  border: 2px solid transparent;
}

.card-opt img {
  width: 100%;
  height: auto;
  display: block;
}

.card-opt:hover {
  border-color: var(--accent);
  transform: translateY(-3px);
}

/*
 * L'ardoise ne vit pas à demeure : en fin de manche elle glisse depuis le bord droit, se laisse
 * lire un instant — la pause entre les manches — puis repart. En fin de partie, elle reste.
 */
.ardoise {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translate(110%, -50%);
  transition: transform 450ms ease;
  width: min(300px, 42vw);
  z-index: 45;
  pointer-events: none;
}

.ardoise.showing {
  transform: translate(-10px, -50%);
}

.slate-side + .slate-side {
  border-left: 1px solid rgba(255, 255, 255, 0.18);
  padding-left: 0.5rem;
}

.slate-name {
  font-size: 0.75rem;
  font-weight: 600;
  margin-bottom: 0.35rem;
}

.stroke {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 1.1rem;
}

.stroke-value {
  width: 1.9rem;
  font-size: 0.7rem;
  text-align: right;
  color: rgba(245, 245, 244, 0.6);
  font-variant-numeric: tabular-nums;
}

.stroke .marks {
  display: flex;
  gap: 3px;
  flex-wrap: wrap;
}

/* Une coche : un bâton. La cinquième les barre en diagonale. */
.tally {
  width: 3px;
  height: 14px;
  background: #f5f5f4;
  display: inline-block;
}

.tally.five {
  position: relative;
  width: 15px;
  background: transparent;
  border-left: 3px solid #f5f5f4;
  border-right: 3px solid #f5f5f4;
  margin-right: 4px;
}

.tally.five::before,
.tally.five::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 5px;
  width: 3px;
  background: #f5f5f4;
}

.tally.five::after {
  left: 9px;
  transform: rotate(24deg);
  background: #fbbf24;
  left: 4px;
  width: 15px;
  height: 3px;
  top: 6px;
  bottom: auto;
}

/* Les 50 : la première coche est une diagonale, la seconde la croise. Un paquet vaut 100. */
.tally.slash,
.tally.cross {
  position: relative;
  width: 14px;
  height: 14px;
  background: transparent;
}

.tally.slash::before,
.tally.cross::before,
.tally.cross::after {
  content: '';
  position: absolute;
  left: 0;
  top: 6px;
  width: 16px;
  height: 3px;
  background: #f5f5f4;
}

.tally.slash::before,
.tally.cross::before {
  transform: rotate(-45deg);
}

.tally.cross::after {
  transform: rotate(45deg);
  background: #fbbf24;
}

/*
 * La bulle d'annonce : ce que le joueur dit à voix haute. Sa queue pointe vers lui, comme les
 * bulles du jeu de 2013, mais tracée en CSS plutôt qu'en image — plus nette à cette taille.
 */
.bubble {
  position: absolute;
  z-index: 3;
  max-width: 9rem;
  padding: 0.3rem 0.55rem;
  border-radius: 10px;
  background: var(--paper);
  color: var(--ink);
  font-size: 0.78rem;
  font-weight: 600;
  line-height: 1.25;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
  /*
   * Le décalage qui centre la bulle sur son siège vit dans une variable, et non dans une règle
   * `transform` à part : l'animation ci-dessous doit le reconduire à chaque image, faute de quoi
   * elle l'écraserait et la bulle sauterait de la moitié de sa largeur le temps de paraître.
   */
  transform: translate(var(--shift-x, 0), var(--shift-y, 0));
}

/*
 * Ne s'anime qu'une parole neuve. L'affichage est reconstruit à chaque événement : sans cette
 * distinction, chaque carte posée ferait rejouer la bulle d'un voisin qui n'a rien dit de plus.
 */
.bubble.fresh {
  animation: speak 200ms ease-out;
}

@keyframes speak {
  from {
    opacity: 0;
    transform: translate(var(--shift-x, 0), calc(var(--shift-y, 0px) + 4px)) scale(0.9);
  }
  to {
    opacity: 1;
    transform: translate(var(--shift-x, 0), var(--shift-y, 0)) scale(1);
  }
}

.bubble::after {
  content: '';
  position: absolute;
  border: 7px solid transparent;
}

/* La queue pointe vers celui qui parle. */
.bubble-bottom {
  bottom: calc(100% + 8px);
  left: 50%;
  --shift-x: -50%;
}

.bubble-bottom::after {
  top: 100%;
  left: 50%;
  margin-left: -7px;
  border-top-color: var(--paper);
}

.bubble-top {
  top: calc(100% + 8px);
  left: 50%;
  --shift-x: -50%;
}

.bubble-top::after {
  bottom: 100%;
  left: 50%;
  margin-left: -7px;
  border-bottom-color: var(--paper);
}

.bubble-left {
  left: calc(100% + 8px);
  top: 50%;
  --shift-y: -50%;
}

.bubble-left::after {
  right: 100%;
  top: 50%;
  margin-top: -7px;
  border-right-color: var(--paper);
}

.bubble-right {
  right: calc(100% + 8px);
  top: 50%;
  --shift-y: -50%;
}

.bubble-right::after {
  left: 100%;
  top: 50%;
  margin-top: -7px;
  border-left-color: var(--paper);
}

/* Le rappel du dernier pli : permis, contrairement aux plis plus anciens. */
.last-trick {
  position: absolute;
  right: 0;
  bottom: 0;
  /* Au-dessus du calque de la main : le coin doit rester cliquable. */
  z-index: 20;
  font-size: 0.75rem;
  padding: 0.25rem 0.55rem;
  background: rgba(0, 0, 0, 0.4);
  border-color: rgba(255, 255, 255, 0.25);
  color: #f5f5f4;
}

.last-trick:hover {
  background: rgba(0, 0, 0, 0.6);
}

/* Signaler et les sons, repliés derrière un petit bouton-menu flottant, en bas à gauche. */
.fab {
  position: absolute;
  left: 14px;
  bottom: 14px;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.45rem;
}
.fab #fab-toggle {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  font-size: 1.15rem;
  line-height: 1;
  padding: 0;
  color: #f5f5f4;
  background: rgba(0, 0, 0, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.3);
}
.fab #fab-toggle:hover {
  filter: none;
  background: rgba(0, 0, 0, 0.7);
}
.fab-menu {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.fab-menu[hidden] {
  display: none;
}
.fab-menu button {
  color: #f5f5f4;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 8px;
}
.fab-menu button:hover {
  filter: none;
  background: rgba(0, 0, 0, 0.75);
}

/* Le réseau est tombé : un sigle, bien voyant, le temps que ça revienne. */
.offline {
  position: absolute;
  top: 52px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  background: #7f1d1d;
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 999px;
  padding: 0.3rem 1rem;
  font-weight: 600;
}
.offline[hidden] {
  display: none;
}

/* Le déroulement de débogage : en surimpression discrète, replié par défaut. */
.debug {
  position: absolute;
  top: 46px;
  right: 12px;
  z-index: 30;
  opacity: 0.75;
}

.debug summary {
  cursor: pointer;
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(245, 245, 244, 0.6);
  text-align: right;
  margin-bottom: 0.3rem;
}

.slate-carry {
  margin-top: 0.4rem;
  font-size: 0.72rem;
  color: rgba(245, 245, 244, 0.7);
  font-variant-numeric: tabular-nums;
}

.log {
  width: 300px;
  height: 260px;
  overflow-y: auto;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 6px;
  padding: 0.6rem;
  background: rgba(0, 0, 0, 0.6);
  color: #e7e5e4;
  font-size: 0.82rem;
}

.log div {
  padding: 0.1rem 0;
}

/* Réservée même vide : le verdict de fin de donne ne doit pas repousser la main du joueur. */
/* L'interrupteur des sons : discret, et barré quand le silence est demandé. */
.sound {
  min-width: 2.2rem;
  font-size: 1rem;
  line-height: 1;
}

.sound.off {
  opacity: 0.45;
  text-decoration: line-through;
}

.outcome {
  margin: 0.3rem 0 0;
  min-height: 1.4em;
  font-weight: 600;
  color: #fde68a;
  text-align: center;
}

.muted {
  color: var(--muted);
  margin: 0;
}

/* Le panneau de connexion, sous la barre : deux temps, l'adresse puis le code. */
.login-panel[hidden],
.picker[hidden] {
  display: none;
}
.login-panel {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.55rem 1.2rem;
  background: #0d3321;
  color: #e8efe9;
}
.login-panel input {
  padding: 0.35rem 0.5rem;
  border-radius: 4px;
  border: 1px solid #2c5a41;
}
.login-note {
  font-size: 0.85rem;
  opacity: 0.85;
}

/* Le choix de sa place : par-dessus le tapis, un bouton par siège. */
.picker {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  background: rgba(6, 28, 17, 0.82);
  z-index: 30;
}
.picker h3 {
  color: #e8efe9;
  margin: 0 0 0.4rem;
  font-size: 1.05rem;
}
.picker button {
  min-width: 260px;
  padding: 0.55rem 1rem;
}
.picker .taken {
  min-width: 260px;
  padding: 0.55rem 1rem;
  text-align: center;
  color: #9db8a8;
  border: 1px dashed #2c5a41;
  border-radius: 6px;
  font-size: 0.9rem;
}

/* Dans quel monde on joue : local, ou en ligne — pour ne jamais se le demander. */
.context {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  opacity: 0.9;
}

/* Le salon : la liste des tables du moment, puis le formulaire d'une nouvelle. */
.lobby-panel {
  flex-direction: column;
  align-items: stretch;
}
.lobby-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.lobby-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}
.lobby-row .muted {
  opacity: 0.75;
  font-size: 0.88rem;
}
.lobby-create {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.lobby-check {
  font-size: 0.85rem;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

/* Le temps qui reste pour jouer : une barre qui fond, à côté du badge. */
.countdown {
  width: 90px;
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.18);
  overflow: hidden;
  align-self: center;
}
.countdown[hidden] {
  display: none;
}
.countdown .fill {
  display: block;
  height: 100%;
  background: #e8b13c;
  width: 100%;
}
@keyframes countdown-drain {
  from {
    width: 100%;
  }
  to {
    width: 0%;
  }
}

/* La conversation de la table : posée sur le tapis, en bas à droite, sobre. */
.chat {
  position: absolute;
  right: 12px;
  bottom: 12px;
  width: 270px;
  z-index: 35;
  background: rgba(6, 28, 17, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 10px;
  padding: 0.5rem 0.6rem;
  color: #ece7dc;
}
.chat[hidden] {
  display: none;
}
.chat-log {
  max-height: 220px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.88rem;
  padding: 0.4rem 0;
}
.chat-log .who {
  font-weight: 600;
  margin-right: 0.35rem;
}
.chat-send {
  display: flex;
  gap: 0.4rem;
}
.chat-send input {
  flex: 1;
  padding: 0.35rem 0.5rem;
  border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(0, 0, 0, 0.35);
  color: #ece7dc;
}

/* La fin de partie : le verdict au centre du tapis. */
.finale h3 {
  font-size: 1.5rem;
  margin-bottom: 0.2rem;
}
.finale .detail {
  color: #cfe0d4;
  margin-bottom: 0.8rem;
}
/* Les gestes de l'admin, discrets sous le siège. */
.seat .admin-acts {
  display: flex;
  gap: 0.3rem;
  justify-content: center;
  margin-top: 0.2rem;
}
.seat .admin-acts button {
  font-size: 0.68rem;
  padding: 0.1rem 0.45rem;
  background: rgba(0, 0, 0, 0.35);
  color: #e8efe9;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 4px;
}

/* Les places disposées comme autour du tapis : on voit où l'on va s'asseoir. */
.picker-ring {
  display: grid;
  grid-template-areas: '. top .' 'left mid right' '. bottom .';
  gap: 0.6rem 1rem;
  align-items: center;
  justify-items: center;
}
.picker-ring .pos-top {
  grid-area: top;
}
.picker-ring .pos-right {
  grid-area: right;
}
.picker-ring .pos-bottom {
  grid-area: bottom;
}
.picker-ring .pos-left {
  grid-area: left;
}
.picker-ring .mid {
  grid-area: mid;
  width: 130px;
  height: 84px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.18);
}
.picker-ring button,
.picker-ring .taken {
  min-width: 170px;
}

/* L'étoile et les fanions de 2013, à hauteur de nom — ils grandissent avec le jeu. */
.who img.star {
  width: calc(var(--card-w) * 0.235);
  height: calc(var(--card-w) * 0.235);
  vertical-align: calc(var(--card-w) * -0.045);
  margin-right: 0.25rem;
}
.who img.star.absent {
  visibility: hidden;
}
/* Les fanions portent l'atout et le chibre à eux seuls : ils se lisent de loin. */
.who img.flag {
  width: calc(var(--card-w) * 0.47);
  height: calc(var(--card-w) * 0.47);
  vertical-align: calc(var(--card-w) * -0.15);
  margin-left: 0.35rem;
}

/* La police du jeu de 2013, pour les écrans hors partie. */
@font-face {
  font-family: 'Fragma';
  src: url('ui/Fragma.ttf') format('truetype');
  font-display: swap;
}

/* Le splash : l'écusson brodé, seul, le temps de charger. */
#splash {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: #0d3321;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.4s ease;
}
/* `display: flex` l'emporterait sur l'attribut hidden — même parade que pour #menu. */
#splash[hidden] {
  display: none;
}
#splash img {
  max-height: 70vh;
  max-width: 80vw;
  object-fit: contain;
}
#splash.fading {
  opacity: 0;
  pointer-events: none;
}

/* Le menu : la table de 2013 — l'ardoise porte les boutons. */
#menu {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: #14522e url('ui/menu.jpg') center / cover no-repeat;
  display: flex;
  align-items: center;
}
#menu[hidden] {
  display: none;
}
.menu-board {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  margin-left: 8vw;
  width: min(360px, 38vw);
}
.menu-board button {
  font-family: 'Fragma', 'IBM Plex Sans', sans-serif;
  font-size: 1.6rem;
  letter-spacing: 0.04em;
  padding: 0.7rem 1rem;
  color: #f2ede4;
  background: rgba(0, 0, 0, 0.45);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
  transition:
    transform 0.08s ease,
    background 0.15s ease;
}
.menu-board button:hover {
  background: rgba(150, 40, 40, 0.55);
  transform: translateX(4px);
}
.menu-sub {
  margin-left: 4vw;
  max-width: 380px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 10px;
  padding: 1.2rem 1.4rem;
  color: #ece7dc;
}
.menu-sub[hidden] {
  display: none;
}
.menu-sub h3 {
  font-family: 'Fragma', sans-serif;
  margin: 0 0 0.6rem;
  font-size: 1.4rem;
}
.menu-sub label {
  display: block;
  margin: 0.7rem 0;
}
.menu-sub input {
  margin-left: 0.5rem;
  padding: 0.3rem 0.5rem;
  border-radius: 4px;
  border: 1px solid #777;
}
.menu-sub .prefs-cards-title {
  margin: 0.9rem 0 0.4rem;
  font-weight: 600;
}
/* Les quatre jeux de cartes : l'élu reste marqué à la craie rouge. */
.menu-sub .choices {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.menu-sub .choices button {
  font-family: 'Fragma', 'IBM Plex Sans', sans-serif;
  font-size: 1.05rem;
  padding: 0.45rem 0.9rem;
  color: #f2ede4;
  background: rgba(0, 0, 0, 0.45);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease;
}
.menu-sub .choices button:hover {
  background: rgba(150, 40, 40, 0.55);
}
.menu-sub .choices button.active {
  background: rgba(150, 40, 40, 0.55);
  border-color: rgba(255, 255, 255, 0.5);
}
/* La modale de reprise : la question avant d'écraser une partie qui dort. */
#resume-dialog {
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 10px;
  background: #10231a;
  color: #ece7dc;
  padding: 1.2rem 1.4rem;
  max-width: 380px;
}
#resume-dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
}
#resume-dialog h3 {
  font-family: 'Fragma', sans-serif;
  margin: 0 0 0.5rem;
}
#resume-dialog .dialog-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.9rem;
  flex-wrap: wrap;
}

/*
 * La configuration d'une nouvelle partie, posée sur le vrai tapis : les sièges autour montrent
 * qui jouera, le panneau règle le nombre, la variante, et lance.
 */
/* Sans cartes, le tapis retomberait à sa hauteur minimale : sièges et panneau doivent tenir. */
.felt.configuring {
  min-height: 560px;
}
.setup[hidden] {
  display: none;
}
.setup {
  position: absolute;
  left: 50%;
  bottom: 0.75rem;
  /* Ancré en bas : il couvre votre place — la seule sans rien à choisir — et laisse
     chaque bot visible à son siège. */
  transform: translateX(-50%);
  z-index: 30;
  width: min(320px, 92%);
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  background: rgba(6, 28, 17, 0.88);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 10px;
  padding: 0.9rem 1.1rem;
  color: #ece7dc;
}
.setup h3 {
  font-family: 'Fragma', sans-serif;
  margin: 0;
  font-size: 1.15rem;
}
.setup .tabs {
  display: flex;
  gap: 0.4rem;
}
.setup .tabs button,
.setup .choices button {
  font-family: 'Fragma', 'IBM Plex Sans', sans-serif;
  color: #f2ede4;
  background: rgba(0, 0, 0, 0.45);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s ease;
}
.setup .tabs button {
  flex: 1;
  font-size: 0.95rem;
  padding: 0.35rem 0.2rem;
  text-align: center;
}
.setup .choices {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.setup .choices button {
  font-size: 1rem;
  padding: 0.4rem 0.8rem;
  text-align: left;
}
.setup .tabs button:hover,
.setup .choices button:hover {
  background: rgba(150, 40, 40, 0.55);
}
/* Le choix arrêté — un onglet, une variante — reste marqué à la craie rouge. */
.setup .tabs button.active,
.setup .choices button.active {
  background: rgba(150, 40, 40, 0.55);
  border-color: rgba(255, 255, 255, 0.5);
}
.setup .seed {
  font-size: 0.85rem;
  opacity: 0.9;
}
.setup .seed input {
  margin-left: 0.5rem;
  padding: 0.25rem 0.45rem;
  border-radius: 4px;
  border: 1px solid #2c5a41;
}
.setup .actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.2rem;
}
.setup .actions .go {
  flex: 1;
}

/* Le bot d'un siège en configuration : son nom, changeable là où il jouera. */
.seat select.bot-pick {
  font: inherit;
  font-weight: 600;
  font-size: 0.9rem;
  color: #f5f5f4;
  background: rgba(0, 0, 0, 0.35);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 6px;
  padding: 0.25rem 0.4rem;
  cursor: pointer;
}

/* L'ardoise de 2013 : le cadre bois et ses deux Z affrontés portent les craies. */
.slate {
  position: relative;
  width: 100%;
  aspect-ratio: 700 / 1020;
  background: url('ui/Ardoise.png') center / contain no-repeat;
  /* L'image est l'ardoise nue, parfaitement détourée : l'ombre se pose ici — et se
     rasterisera si le filtre se révélait lent. */
  filter: drop-shadow(0 10px 22px rgba(0, 0, 0, 0.45));
}
.slate-half {
  position: absolute;
  left: 16%;
  right: 16%;
  display: flex;
  flex-direction: column;
  gap: 2%;
  color: #e8e2d2;
  font-family: 'Fragma', sans-serif;
}
.slate-half.north {
  top: 9%;
  height: 38%;
}
.slate-half.south {
  bottom: 9%;
  height: 38%;
  /* Le camp d'en face écrit de son côté : tout son coin est renversé. */
  transform: rotate(180deg);
}
.slate .slate-name {
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  opacity: 0.9;
  white-space: nowrap;
  overflow: hidden;
}
.slate .z {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.slate .stroke {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 24px;
}
.slate .stroke-value {
  font-size: 0.72rem;
  opacity: 0.7;
  width: 1.7rem;
}
.slate .marks {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-wrap: wrap;
}
.slate img.chalk {
  height: 22px;
  width: auto;
}
.slate .chalk-5 {
  position: relative;
  display: inline-flex;
  gap: 2px;
  margin-right: 4px;
}
.slate .chalk-5 img.dash {
  position: absolute;
  left: -2px;
  top: 4px;
  height: 15px;
  width: calc(100% + 4px);
}
.slate .chalk-x {
  position: relative;
  display: inline-flex;
  margin-right: 3px;
}
.slate .chalk-x img.over {
  position: absolute;
  left: 0;
  top: 0;
}
.slate .slate-carry {
  font-size: 0.72rem;
  opacity: 0.85;
}

/* Le dernier pli : l'icône de 2013, enfoncée quand on le montre (RD-025). */
.last-trick img {
  width: calc(var(--card-w) * 0.6);
  height: calc(var(--card-w) * 0.6);
  display: block;
}
.last-trick {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
}
.last-trick.showing img {
  filter: brightness(0.75) sepia(0.4);
}

/* Les têtes : rondes aux sièges, cadre vide par défaut. */
.who img.face {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  object-fit: cover;
  vertical-align: -9px;
  margin-right: 0.35rem;
  border: 1px solid rgba(255, 255, 255, 0.5);
  background: rgba(0, 0, 0, 0.25);
}
.face-preview {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  vertical-align: middle;
  margin: 0 0.5rem;
  border: 1px solid #888;
}
