/* ─── منچ (Ludo) — circular wooden Mencherz board ─────────────── */

:root {
	--main: #24363f;
	--red: #e51b23;
	--red-dark: #9b0f17;
	--red-light: #ff7e8c;
	--red-glow: rgba(229, 27, 35, 0.55);
	--purple: #a040ff;
	--purple-dark: #5c2291;
	--purple-light: #d5b3fd;
	--purple-glow: rgba(160, 64, 255, 0.55);
	--green: #43a047;
	--green-dark: #1b5e20;
	--green-light: #a5d6a7;
	--green-glow: rgba(67, 160, 71, 0.55);
	--blue: #1e88e5;
	--blue-dark: #0d47a1;
	--blue-light: #90caf9;
	--blue-glow: rgba(30, 136, 229, 0.55);
	--u: calc(100% / 15);
	/* strong custom curves — built-in CSS easings are too weak to read as intentional */
	--ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);
	--ease-in-out-strong: cubic-bezier(0.77, 0, 0.175, 1);
	/* springy overshoot for the dynamic-island banner pop */
	--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
	/* seat square is the single source; --midw derives the free center gap of a seat
	   row, so the turn banner and vc buttons auto-shrink on ANY viewport change and
	   can never overlap the seat cards — pure CSS, no resize listener needed */
	--seatw: clamp(96px, 26vw, 128px);
	--midw: calc(100vw - 2 * var(--seatw) - 16px);
	/* vc button diameter. Buttons are a 2×2 grid (.vc-btns), so 2 per row/column:
	   width fits in --midw (2·46+gap < any --midw), height fits in --seatw so the
	   grid never spills onto the board. Sized off --seatw (the tighter of the two)
	   → ~1.5× the old single-row size, bigger tap targets, no seat/board overlap.
	   ponytail: raise the 46px cap for true 2× only if you accept a few px board overlap */
	--vcb: clamp(34px, calc((var(--seatw) - 12px) / 2), 34px);
}

body {
	/* the wooden table fills the WHOLE screen, like Mencherz */
	background:
		repeating-linear-gradient(97deg, rgba(59, 29, 12, 0.10) 0 5px, rgba(0, 0, 0, 0) 5px 13px),
		radial-gradient(circle at 30% 18%, rgba(255, 222, 166, 0.22), transparent 55%),
		linear-gradient(155deg, #a5683f, #7d4a2a);
	/* NO background-attachment: fixed — the app never scrolls (identical render) and
	   fixed-attachment backgrounds break backdrop-filter sampling in Chromium, which
	   killed the glass effect on the seat cards and vc buttons */
	user-select: none;
	-webkit-user-select: none;
	-webkit-touch-callout: none;
	/* iOS still shows its long-press Copy menu without this, even with user-select: none */
	-webkit-tap-highlight-color: transparent;
	touch-action: manipulation;
}

.game {
	position: relative;
	display: flex;
	flex-direction: column;
	height: 100vh;
	/* fallback for WebViews without dvh (Chrome <108 / iOS <15.4) */
	height: 100dvh;
	/* Telegram's own viewport var, so the board circle centers on the VISIBLE area */
	height: var(--tg-viewport-stable-height, 100dvh);
	direction: rtl;
}

/* ─── seats — one per screen corner, avatar above the name ────── */

.seat-row {
	position: absolute;
	left: 0;
	right: 0;
	/* NO z-index: the base tokens live in .bases-layer (z-index 1, direct .game
	   child) and must paint above the seat cards they now sit inside — any positive
	   z-index here would put the cards back on top of them. Cards still paint above
	   the board's cells via plain DOM order (both rows come after .board-wrap). */
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
	/* no padding at all — seats sit flush against every screen wall they touch */
	box-sizing: border-box;
	color: #fff;
	pointer-events: none;
	/* LTR so each seat sits on its own board corner: red/green left, purple/blue right */
	direction: ltr;
}

#row-top {
	top: 0;
	padding-top: env(safe-area-inset-top);
}

#row-bottom {
	bottom: 0;
	padding-bottom: env(safe-area-inset-bottom);
}

.seat {
	position: relative;
	/* quarter-disc glued into its screen corner: an ALWAYS-SQUARE box (same clamp on
	   both axes, so no aspect-ratio dependency) whose board-facing corner is rounded
	   100% — set per corner below. Big enough for avatar + name + the 4-token arc
	   layoutBases() draws along the rim; shrinks with the viewport on narrow phones */
	flex: 0 0 auto;
	width: var(--seatw);
	height: var(--seatw);
	/* avatar size — shrinks with the viewport so the avatar+name column stays
	   clear of the base-token arc on the rim; .savatar and .sinfo both build on it */
	--av: clamp(42px, 13vw, 58px);
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	gap: 3px;
	/* pads > 8/10 pull the avatar+name column off the screen corner — it used to
	   hug the corner and crowd the card. Kept under 14px so the column still clears
	   the base-token arc layoutBases() draws on the rim of the smallest (96px) card */
	padding: 12px 13px;
	border-radius: 0;
	/* liquid glass (same language as #winner-card): translucent white gradient over
	   the blurred table, specular top edge + soft inner glow instead of flat borders.
	   backdrop-filter lives on ::before below, NOT here: on .seat it makes the card a
	   stacking context, which would trap .sbubble's z-index and drop the bubble under
	   the base tokens (.bases-layer, z1) and the pawns3d canvas (z6) */
	background: linear-gradient(160deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.04) 55%, rgba(255, 255, 255, 0.10));
	/* rim line lives on ::after (so layoutBases() can punch socket holes in it via
	   --rim-mask), NOT here — masking .seat itself would create a stacking context */
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.4),
		inset -1px -1px 1px -0.5px rgba(255, 255, 255, 0.3),
		inset 0 0 12px 4px rgba(255, 255, 255, 0.06),
		0 0 10px rgba(20, 8, 0, 0.28);
	/* even, no downward offset — a 2px-down drop cast a hard rectangular band
	   into the bottom seats' safe-area gap; a symmetric soft shadow hugs the disc */
	transition: background 0.25s, border-color 0.25s, box-shadow 0.25s;
	pointer-events: auto;
}

/* green/blue corners stay empty until 4-player networking exists */
/* non-playing seats (the 2 colors NOT in the random pair for this match) are dimmed.
   Which 2 are empty is decided in JS at boot by colorPairFor(glcode); the empty class is
   toggled on the seat div, so any color can be either live or empty per game */
.seat.empty {
	opacity: 0.5;
}

/* carries the card's backdrop-filter so .seat itself stays free of stacking
   contexts (see .seat comment) — same box, painted behind the card's background */
.seat::before {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: inherit;
	/* near-clear glass: tiny blur + saturation/brightness lift so the wood grain
	   stays READABLE through the card — heavier blur smeared the 5px grain streaks
	   into a flat tint and the card read as opaque */
	backdrop-filter: blur(2px) saturate(170%) brightness(1.06);
	-webkit-backdrop-filter: blur(2px) saturate(170%) brightness(1.06);
	z-index: -1;
}

/* the rim line, moved off .seat: z-index 0 keeps it painted ABOVE the board (tree
   order) but under the base sockets (.bases-layer, z1). layoutBases() sets
   --rim-mask per seat — 4 punched holes where the token sockets sit on the arc,
   so the line reads as interrupted sockets instead of a continuous stroke */
.seat::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	border-radius: inherit;
	border: 1px solid rgba(255, 255, 255, 0.28);
	transition: border-color 0.25s;
	pointer-events: none;
	-webkit-mask-image: var(--rim-mask, none);
	-webkit-mask-composite: source-in;
	mask-image: var(--rim-mask, none);
	mask-composite: intersect;
}

/* the 100% corner faces the board, the other 3 sit flush on the screen walls —
   a quarter circle wedged into the corner. Side is .sleft/.sright (set per-POV by
   arrangeSeats() in app.js, NOT tied to color) crossed with which row the seat is
   in. Wall-side borders dropped so only the arc shows a rim. Content hugs its
   corner: rows align it to the wall wall-side, bottom row stacks in reverse so the
   avatar sits nearest the bottom wall and the tokens' arc faces the board. */
#row-top .seat.sleft {
	border-radius: 0 0 100% 0;
}

#row-top .seat.sleft::after {
	border-top: none;
	border-left: none;
}

#row-top .seat.sright {
	border-radius: 0 0 0 100%;
}

#row-top .seat.sright::after {
	border-top: none;
	border-right: none;
}

#row-bottom .seat.sleft {
	border-radius: 0 100% 0 0;
}

#row-bottom .seat.sleft::after {
	border-bottom: none;
	border-left: none;
}

#row-bottom .seat.sright {
	border-radius: 100% 0 0 0;
}

#row-bottom .seat.sright::after {
	border-bottom: none;
	border-right: none;
}

/* avatar sits dead-center in the seat (middle of the quarter-disc); the name is
   lifted out of the flow (absolute, below) and pinned INSIDE the card at its screen
   wall — top wall for top seats, bottom wall for bottom seats — so it stays visible
   and never shoves the centered avatar off-center */
.seat {
	justify-content: center;
}

#row-top .sinfo {
	top: 6px;
}

#row-bottom .sinfo {
	bottom: 6px;
	flex-direction: column-reverse;
	/* name lowest, hugging the bottom wall */
}

.seat.sleft {
	align-items: flex-start;
	text-align: left;
}

.seat.sright {
	align-items: flex-end;
	text-align: right;
}

.savatar {
	position: relative;
	width: var(--av);
	height: var(--av);
	border-radius: 50%;
	/* recessed medallion: dark inset well + gold ring, echoes .m-center's socket */
	background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.16), rgba(43, 18, 3, 0.25));
	border: 2px solid rgba(240, 193, 113, 0.55);
	box-shadow:
		inset 0 2px 4px rgba(43, 18, 3, 0.55),
		0 0 0 3px rgba(43, 18, 3, 0.22);
	box-sizing: border-box;
	flex-shrink: 0;
	overflow: visible;
	transition: opacity 0.25s, filter 0.25s;
}

/* whoever's turn it ISN'T: fade their avatar so the active player's stands out */
.seat:not(.turn) .savatar {
	opacity: 0.5;
	filter: saturate(0.65);
}

/* the visible quarter-disc bulges toward its screen corner, so a box-centered avatar
   reads as off-center. Nudge it toward the row's wall (up for top, down for bottom)
   and in from the side wall (left for right seats, right for left seats) so it lands
   in the middle of the disc. One knob to tune the distance: --nudge.
   Defined on .seat (not .savatar) so the sibling .sinfo inherits it and tracks the
   avatar by the same amount — otherwise the name's translateX(var(--nudge)) is void. */
.seat {
	--nudge: 12px;
}

#row-top .seat.sright .savatar {
	transform: translate(calc(-1 * var(--nudge)), calc(-1 * var(--nudge)));
}

#row-top .seat.sleft .savatar {
	transform: translate(var(--nudge), calc(-1 * var(--nudge)));
}

#row-bottom .seat.sright .savatar {
	transform: translate(calc(-1 * var(--nudge)), var(--nudge));
}

#row-bottom .seat.sleft .savatar {
	transform: translate(var(--nudge), var(--nudge));
}

.savatar img {
	width: 100%;
	height: 100%;
	display: block;
	object-fit: cover;
	border-radius: 50%;
}

/* offline / not-yet-connected peer: grey out the avatar + a dark "disconnected"
   veil over it, instead of an آفلاین text label. Shows during "waiting for
   opponent" too, so it's immediately clear which seat is offline. */
.seat.offline .savatar img {
	filter: grayscale(1) brightness(0.5);
}

.seat.offline .savatar::after {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: 50%;
	pointer-events: none;
	background: rgba(0, 0, 0, 0.42) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='2' y1='2' x2='22' y2='22'/%3E%3Cpath d='M8.5 16.5a5 5 0 0 1 7 0'/%3E%3Cpath d='M5 12.9a10 10 0 0 1 5.2-2.8'/%3E%3Cpath d='M13.8 10.2A10 10 0 0 1 19 12.9'/%3E%3Cline x1='12' y1='20' x2='12.01' y2='20'/%3E%3C/svg%3E") center / 52% no-repeat;
}

/* fixed box centered on the AVATAR's axis (not the card's): avatar width + 10px
   slack each side, pulled toward the wall by the same 10px the card pads — so the
   name sits exactly under (top row) / over (bottom row) the corner-glued avatar.
   Long names first auto-shrink (layoutBases() in app.js), then ellipsize. */
.sinfo {
	position: absolute;
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	min-width: 0;
	width: calc(var(--av) + 20px);
}

/* The avatar is NOT flex-centered: .seat.sleft/.sright override align-items to
   flex-start/flex-end, pinning it to its side wall at (padding 13px + --nudge),
   then --nudge shifts it in. So anchor .sinfo to the SAME wall: its inner edge at
   padding+nudge, pulled back 10px (half its --av+20 width) so its center lands on
   the avatar's center. left:50% math was ~22px off — the avatar was never at seatw/2. */
.seat.sleft .sinfo {
	left: calc(13px + var(--nudge) - 10px);
	right: auto;
	transform: none;
}

.seat.sright .sinfo {
	right: calc(13px + var(--nudge) - 10px);
	left: auto;
	transform: none;
}

.sname {
	font-weight: 700;
	font-size: clamp(11px, 3.4vw, 13px);
	max-width: 100%;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	text-shadow: 0 1px 2px rgba(43, 18, 3, 0.6);
	transition: color 0.2s;
}

.sstatus {
	font-size: clamp(9.5px, 2.9vw, 10.5px);
	opacity: 0.85;
	min-height: 12px;
	text-shadow: 0 1px 2px rgba(43, 18, 3, 0.5);
	transition: color 0.2s;
}

.sstatus.online {
	color: #7df0a8;
}

.sstatus.offline {
	color: #000000;
}

/* turn indicator: the active seat's plaque warms up and breathes a glow in
   the player's OWN color (--turn-glow, set per color below) — same visual
   language as .m-center.glow-<color>/.my-turn on the board's dice hub, so
   the whole page reads as one lit-up system instead of two unrelated cues */
.seat.turn {
	/* gold-tinted liquid glass — same recipe as the idle card, warmed up */
	background: linear-gradient(160deg, rgba(255, 226, 173, 0.35), rgba(240, 193, 113, 0.10) 55%, rgba(255, 226, 173, 0.22));
	animation: seat-glow 2.6s ease-in-out infinite;
}

.seat.turn::after {
	border-color: rgba(255, 213, 79, 0.75);
}

.seat.red.turn {
	--turn-glow: var(--red-glow);
}

.seat.purple.turn {
	--turn-glow: var(--purple-glow);
}

.seat.green.turn {
	--turn-glow: var(--green-glow);
}

.seat.blue.turn {
	--turn-glow: var(--blue-glow);
}

@keyframes seat-glow {

	0%,
	100% {
		box-shadow:
			inset 0 2px 5px rgba(43, 18, 3, 0.35),
			0 0 16px 1px var(--turn-glow, rgba(240, 193, 113, 0.45));
	}

	50% {
		box-shadow:
			inset 0 2px 5px rgba(43, 18, 3, 0.35),
			0 0 26px 5px var(--turn-glow, rgba(240, 193, 113, 0.45));
	}
}

.seat.turn .sname {
	color: #fff5df;
	text-shadow: 0 1px 3px rgba(43, 18, 3, 0.75);
}

/* permanent turn/status banner — floating dynamic-island pill between the top
   seats (detached from the screen edge, near-black, full radius); turn texts
   moved here so seat .sstatus only shows connectivity. Shows/hides and swaps
   text with a springy blur+scale pop, like the iOS island morph */
#turn-banner {
	position: absolute;
	left: 50%;
	top: calc(env(safe-area-inset-top, 0px) + 20px);
	transform: translate(-50%, -12px) scale(0.6);
	opacity: 0;
	filter: blur(6px);
	background: rgba(14, 9, 5, 0.9);
	backdrop-filter: blur(20px) saturate(180%);
	-webkit-backdrop-filter: blur(20px) saturate(180%);
	border: 1px solid rgba(255, 255, 255, 0.12);
	color: #fff;
	font-size: clamp(11px, 3.3vw, 13px);
	padding: 9px 16px;
	border-radius: 999px;
	/* can never reach the seat cards: capped to the row's free center gap;
	   the inner .bt span ellipsizes anything longer (long names) */
	max-width: calc(var(--midw) - 4px);
	box-sizing: border-box;
	direction: rtl;
	white-space: nowrap;
	display: flex;
	align-items: center;
	gap: 6px;
	pointer-events: none;
	transition: transform 0.5s var(--ease-spring), opacity 0.25s ease, filter 0.3s ease;
	/* backdrop-filter puts this on its own compositor layer — without pinning
	   it, the layer gets re-settled right as the transform transition ends,
	   showing as a few-px snap */
	will-change: transform;
	z-index: 30;
}

/* the text node lives in .bt (setBanner in app.js) so it can ellipsize —
   text-overflow doesn't apply to bare text inside a flex container */
#turn-banner .bt {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}

#turn-banner.show {
	transform: translate(-50%, 0) scale(1);
	opacity: 1;
	filter: blur(0);
	/* shadow only while shown — while hidden, its blur bled into the viewport */
	box-shadow: 0 8px 24px rgba(20, 8, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}

/* active player's color — set (and cleared) in updateTurnUI, only while the
   banner reads «نوبت فلانیه...». Exact same gradients as the home cells
   (.cell.home.c-*) so the banner matches that player's corridor on the board */
#turn-banner.red {
	background: radial-gradient(circle at 40% 32%, var(--red-dark), #5e0c10);
}

#turn-banner.green {
	background: radial-gradient(circle at 40% 32%, var(--green-dark), #103413);
}

#turn-banner.purple {
	background: radial-gradient(circle at 40% 32%, var(--purple-dark), #3a1666);
}

#turn-banner.blue {
	background: radial-gradient(circle at 40% 32%, var(--blue-dark), #0a2a63);
}

/* full-screen blurred tint in the winner's color, shown to both players on
   gameOver — sits under the confetti canvas (z-index 10002) */
#winner-backdrop {
	position: fixed;
	inset: 0;
	z-index: 10000;
	pointer-events: none;
	opacity: 0;
	backdrop-filter: blur(10px) saturate(100%);
	-webkit-backdrop-filter: blur(24px) saturate(200%);
	display: flex;
	align-items: center;
	justify-content: center;
	animation: winner-backdrop-in 0.6s ease forwards;
}

@keyframes winner-backdrop-in {
	to {
		opacity: 1;
	}
}

/* the "«رنگ» برنده شد" card — liquid glass: translucent white gradient over the
   blurred color tint, specular top edge + soft inner glow instead of flat borders */
#winner-card {
	background: linear-gradient(160deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.06) 55%, rgba(255, 255, 255, 0.12));
	backdrop-filter: blur(28px) saturate(200%);
	-webkit-backdrop-filter: blur(28px) saturate(200%);
	border: 1px solid rgba(255, 255, 255, 0.32);
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.5),
		inset 0 -1px 1px rgba(255, 255, 255, 0.1),
		inset 0 0 24px rgba(255, 255, 255, 0.08),
		0 12px 40px rgba(0, 0, 0, 0.35);
	color: #fff;
	text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
	font-weight: 700;
	font-size: 20px;
	padding: 20px 36px;
	border-radius: 24px;
	white-space: nowrap;
}

#winner-backdrop.red {
	background: radial-gradient(circle at 50% 42%, rgba(229, 27, 35, 0.45), rgba(10, 4, 2, 0.85));
}

#winner-backdrop.green {
	background: radial-gradient(circle at 50% 42%, rgba(67, 160, 71, 0.45), rgba(10, 4, 2, 0.85));
}

#winner-backdrop.purple {
	background: radial-gradient(circle at 50% 42%, rgba(160, 64, 255, 0.45), rgba(10, 4, 2, 0.85));
}

#winner-backdrop.blue {
	background: radial-gradient(circle at 50% 42%, rgba(30, 136, 229, 0.45), rgba(10, 4, 2, 0.85));
}

/* ─── board ───────────────────────────────────────────────────── */

.board-wrap {
	flex: 1;
	display: flex;
	justify-content: center;
	align-items: center;
	min-height: 0;
}

#board {
	/* transparent square that positions the plate/cells on the full-screen wood;
	   fitBoard() in app.js keeps it an exact square and sets --bs (its px size) */
	position: relative;
	aspect-ratio: 1 / 1;
	width: min(100vw, 100%);
	max-height: 100%;
	direction: ltr;
	/* coordinates are LTR math; base columns sit outside the square, so no clipping.
	   perspective on #board so its DIRECT child .surface tilts as a 3D plane in
	   view3d; tokens/base-slots/dice are also direct children but have no 3D rotation
	   so perspective has no visible effect on them */
	perspective: 1000px;
}

.m-plate {
	position: absolute;
	left: 50%;
	top: 50%;
	width: 95%;
	height: 95%;
	/* counter-rotate against the layout-toggle spin (--spin on .surface): the ring
	   GEOMETRY is rotation-symmetric but the wood grain / off-center highlight are
	   NOT — without this the texture visibly jumps 45° the instant the toggle snaps.
	   Held screen-fixed, the felt stays put while the cell ring spins on it. */
	transform: translate(-50%, -50%) rotate(calc(-1 * var(--spin, 0deg)));
	border-radius: 50%;
	background:
		repeating-linear-gradient(115deg, rgba(97, 52, 24, 0.08) 0 4px, rgba(0, 0, 0, 0) 4px 11px),
		radial-gradient(circle at 42% 34%, #dda87a, #c2854f 62%, #a1683c);
	box-shadow:
		inset 0 0 0 7px rgba(93, 48, 21, 0.4),
		inset 0 8px 22px rgba(43, 18, 3, 0.3),
		0 3px 10px rgba(43, 18, 3, 0.35);
	/* concave bites pressed into the rim at the 4 cardinal points, pinching the
	   plate toward the cell ring. SVG mask (single layer, no mask-composite —
	   safest across WebViews): white keeps, black circles poking in from each
	   edge midpoint carve the dents. Dent depth = r(16) - offset(9) = 7% of the
	   plate; tweak those two numbers to deepen/soften. Outer drop shadow is
	   masked away with the rim — the inset shadows still read as the edge. */
	-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Crect width='100' height='100' fill='white'/%3E%3Ccircle cx='50' cy='-9' r='16' fill='black'/%3E%3Ccircle cx='50' cy='109' r='16' fill='black'/%3E%3Ccircle cx='-9' cy='50' r='16' fill='black'/%3E%3Ccircle cx='109' cy='50' r='16' fill='black'/%3E%3C/svg%3E") center / 100% 100%;
	mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Crect width='100' height='100' fill='white'/%3E%3Ccircle cx='50' cy='-9' r='16' fill='black'/%3E%3Ccircle cx='50' cy='109' r='16' fill='black'/%3E%3Ccircle cx='-9' cy='50' r='16' fill='black'/%3E%3Ccircle cx='109' cy='50' r='16' fill='black'/%3E%3C/svg%3E") center / 100% 100%;
}

/* center hub under the dice — glows in the active player's color */
.m-center {
	position: absolute;
	/* z-index makes this a stacking context so the ::after aura (z-index: -1)
		 sits behind this element's own disc, not behind unrelated siblings */
	z-index: 0;
	left: 50%;
	top: 50%;
	/* sized to the table, not to the home column — just a bit larger than the dice */
	width: calc(var(--u) * 2.8);
	height: calc(var(--u) * 2.8);
	/* same counter-rotation as .m-plate — the 40%/32% highlight isn't symmetric */
	transform: translate(-50%, -50%) rotate(calc(-1 * var(--spin, 0deg)));
	border-radius: 50%;
	background: radial-gradient(circle at 40% 32%, #8a5330, #5f3517 70%);
	box-shadow:
		inset 0 4px 12px rgba(43, 18, 3, 0.55),
		0 0 0 5px rgba(240, 193, 113, 0.3);
	transition: box-shadow 0.35s;
}

/* soft blurred aura behind the hub — livelier than the box-shadow ring alone,
	 and it breathes even on the opponent's turn, not just .my-turn */
.m-center::after {
	content: "";
	position: absolute;
	inset: -12%;
	border-radius: 50%;
	background: radial-gradient(circle, var(--turn-glow, transparent) 0%, transparent 72%);
	filter: blur(1.5px);
	pointer-events: none;
	z-index: -1;
	animation: center-aura 2.6s ease-in-out infinite;
}

.m-center.my-turn::after {
	animation-duration: 1.4s;
}

@keyframes center-aura {

	0%,
	100% {
		transform: scale(0.95);
		opacity: 0.3;
	}

	50% {
		transform: scale(1.08);
		opacity: 0.6;
	}
}

.m-center.glow-red {
	--turn-glow: var(--red-glow);
	box-shadow:
		inset 0 4px 12px rgba(43, 18, 3, 0.55),
		0 0 0 5px rgba(240, 193, 113, 0.3),
		0 0 26px 9px var(--red-glow);
}

.m-center.glow-purple {
	--turn-glow: var(--purple-glow);
	box-shadow:
		inset 0 4px 12px rgba(43, 18, 3, 0.55),
		0 0 0 5px rgba(240, 193, 113, 0.3),
		0 0 26px 9px var(--purple-glow);
}

.m-center.glow-green {
	--turn-glow: var(--green-glow);
	box-shadow:
		inset 0 4px 12px rgba(43, 18, 3, 0.55),
		0 0 0 5px rgba(240, 193, 113, 0.3),
		0 0 26px 9px var(--green-glow);
}

.m-center.glow-blue {
	--turn-glow: var(--blue-glow);
	box-shadow:
		inset 0 4px 12px rgba(43, 18, 3, 0.55),
		0 0 0 5px rgba(240, 193, 113, 0.3),
		0 0 26px 9px var(--blue-glow);
}

/* track & home cells */
.cell {
	position: absolute;
	width: calc(var(--u) * 0.8);
	height: calc(var(--u) * 0.8);
	box-sizing: border-box;
	transform: translate(-50%, -50%);
	/* cells are positioned by their center */
	border-radius: 50%;
	background: radial-gradient(circle at 40% 32%, #93603c, #6c4126);
	box-shadow: inset 0 2px 5px rgba(43, 18, 3, 0.55), 0 1px 0 rgba(255, 255, 255, 0.14);
	display: flex;
	justify-content: center;
	align-items: center;
	color: #e9bd5e;
	font-size: calc(var(--bs, min(100vw, 100vh)) / 15 * 0.34);
	text-shadow: 0 1px 2px rgba(43, 18, 3, 0.6);
	/* anti-jitter during the .surface tilt: force each cell to its own raster layer so
	   sub-pixel rounding doesn't shift it frame-to-frame */
	backface-visibility: hidden;
	opacity: 0.8;
	/* 20% lighter than full */
}

/* empty/seatless seats keep their 4 sockets VISIBLE (just dimmed) — the token
   places on every seat's rim always read as reserved, occupied or not */
.base-slot.inactive {
	opacity: 0.45;
}

/* seatless colors' spawn point + home column, dimmed 10% further than base-slots */
.cell.inactive {
	opacity: 0.25;
}

/* colored track cells (spawn + home column): same recessed "safe lane" tone —
   the spawn cell and the home cells share a single dark gradient so the
   player's color reads as one quiet corridor from entry to finish. The ✦
   glyph on the spawn cell still distinguishes it from the home column */
.cell.c-red,
.cell.home.c-red {
	background: radial-gradient(circle at 40% 32%, var(--red-dark), #5e0c10);
	box-shadow: inset 0 3px 6px rgba(43, 18, 3, 0.7), 0 0 3px var(--red-glow);
}

.cell.c-purple,
.cell.home.c-purple {
	background: radial-gradient(circle at 40% 32%, var(--purple-dark), #3a1666);
	box-shadow: inset 0 3px 6px rgba(43, 18, 3, 0.7), 0 0 3px var(--purple-glow);
}

.cell.c-green,
.cell.home.c-green {
	background: radial-gradient(circle at 40% 32%, var(--green-dark), #103413);
	box-shadow: inset 0 3px 6px rgba(43, 18, 3, 0.7);
}

.cell.c-blue,
.cell.home.c-blue {
	background: radial-gradient(circle at 40% 32%, var(--blue-dark), #0a2a63);
	box-shadow: inset 0 3px 6px rgba(43, 18, 3, 0.7);
}

/* base slots — the empty glass sockets under each seat's name */
.base-slot {
	position: absolute;
	/* slightly SMALLER than .token (0.8u) — this is a separate DOM element sitting
	   under the resting piece; it used to be 0.9u (bigger than the token), so its own
	   glow/inset-shadow ring peeked out around every base piece no matter what shadow
	   the token itself had. Shrunk to stay fully hidden under an occupied slot; still
	   shows as an empty glass marker once that piece spawns out */
	width: calc(var(--u) * 0.78);
	height: calc(var(--u) * 0.78);
	border-radius: 50%;
	transform: translate(-50%, -50%);
	/* same liquid-glass fill + 1px white stroke as the seat card / .seat::after rim line,
	   so an empty socket reads as a bead strung ON the arc — not a separate wooden disc */
	background: rgba(255, 255, 255, 0.07);
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.35),
		0 0 0 1px rgba(255, 255, 255, 0.28);
}

/* base-slot.red/purple/green/blue keep the plain glass look above on purpose — an empty
   socket matches the seat's arc line instead of glowing in the player's own color */

/* ─── tokens (same size as the board cells) ───────────────────── */

.token {
	position: absolute;
	width: calc(var(--u) * 0.8);
	height: calc(var(--u) * 0.8);
	box-sizing: border-box;
	/* border-box, so the token's outer size matches the cells exactly */
	border-radius: 50%;
	/* transform (not the standalone `scale` prop) for pre-Chrome-104 WebViews.
	   NEVER add scale() here: pawns3d (app.js) derives depth from this element's own
	   getBoundingClientRect(), so a self-applied transform scale feeds back into that
	   measurement and compounds every frame — tokens shrink to nothing. --pawn-fit
	   (below, in the ring's box-shadow only) is the safe way to apply that shrink. */
	transform: translate(-50%, -50%);
	/* fallback only — animateMove() in app.js sets transitionDuration inline from
	   TUNE.PAWN_HOP_MS for every hop, so the move sound always lands with the glide */
	transition: left 0.3s ease-out, top 0.3s ease-out, transform 0.2s, box-shadow 0.3s ease;
	border: 0;
	box-shadow: 0 2px 6px rgba(43, 18, 3, 0.55), 0 0 8px rgba(255, 255, 255, 0.14);
	z-index: 5;
	cursor: pointer;
}

/* no shadow while parked at base (unspawned); animateMove()'s spawn branch and
   applyMove()'s capture FLIP fade it in/out in sync with the position glide */
.token.at-base {
	box-shadow: none;
}

.token::after {
	content: "";
	position: absolute;
	inset: 30%;
	border-radius: 50%;
	background: #fff;
	/* solid white core — reads like the 3D pawn's white head seen from above */
	box-shadow: inset 0 1px 3px rgba(43, 18, 3, 0.3);
}

/* invisible hit area — the click on this pseudo-element bubbles up to the
   .token (its click handler), so a near-miss on the cell still picks the
   right token. -8px gives every token a forgiveness margin; .movable blows it
   up to -22px so the touch target is large enough for fat-finger picks during
   the dice-result beat. z-index lifts the hit pad above the token's own ::after
   dot so the whole pad is the active surface */
.token::before {
	content: "";
	position: absolute;
	inset: -4px;
	border-radius: 50%;
	z-index: 1;
}

.token.movable::before {
	inset: -22px;
	z-index: 2;
}

/* base slots sit only ~0.95 units apart (barely more than a token's own 0.8-unit
   width), so all 4 tokens' -22px pads badly overlap when every base token becomes
   movable at once (rolling a 6) — a tap near one token would spawn its neighbor
   instead. Keep the tighter default pad there. */
.bases-layer .token.movable::before {
	inset: -8px;
}

.token.red {
	background: radial-gradient(circle at 35% 30%, var(--red-light), var(--red) 55%, var(--red-dark));
}

.token.purple {
	background: radial-gradient(circle at 35% 30%, var(--purple-light), var(--purple) 55%, var(--purple-dark));
}

.token.green {
	background: radial-gradient(circle at 35% 30%, var(--green-light), var(--green) 55%, var(--green-dark));
}

.token.blue {
	background: radial-gradient(circle at 35% 30%, var(--blue-light), var(--blue) 55%, var(--blue-dark));
}

/* colored footprint a pawn leaves in each cell it hops through mid-move —
	 quick fade so it reads as motion, not clutter */
.trail-dot {
	position: absolute;
	/* starts smaller than the cell; trail-fade's scale(2) grows it to exactly
	   cell size (0.4 × 2 = 0.8, the .cell diameter) by the time it fades out —
	   0.5 overshot the cell and the ripple read as spilling past its edge */
	width: calc(var(--u) * 0.4);
	height: calc(var(--u) * 0.4);
	border-radius: 50%;
	transform: translate(-50%, -50%);
	pointer-events: none;
	z-index: 4;
	animation: trail-fade 1.8s ease-out forwards;
}

@keyframes trail-fade {
	from {
		opacity: 0.95;
		transform: translate(-50%, -50%) scale(1);
	}

	to {
		opacity: 0;
		transform: translate(-50%, -50%) scale(2);
	}
}

.trail-dot.trail-red {
	background: var(--red);
	box-shadow: 0 0 10px 2px var(--red);
}

.trail-dot.trail-purple {
	background: var(--purple);
	box-shadow: 0 0 10px 2px var(--purple);
}

.trail-dot.trail-green {
	background: var(--green);
	box-shadow: 0 0 10px 2px var(--green);
}

.trail-dot.trail-blue {
	background: var(--blue);
	box-shadow: 0 0 10px 2px var(--blue);
}

/* ─── cinematic capture FX — spawned by cinematicCapture() in app.js ──
   .impact-fx is a 0×0 anchor at the killed cell inside .surface; children
   size off --bs (board px, set by fitBoard) since % of a 0-box is useless */
.impact-fx {
	position: absolute;
	width: 0;
	height: 0;
	pointer-events: none;
	z-index: 7;
}

.impact-flash {
	position: absolute;
	width: calc(var(--bs, 360px) / 15 * 3.2);
	height: calc(var(--bs, 360px) / 15 * 3.2);
	border-radius: 50%;
	background: radial-gradient(circle, rgba(255, 255, 255, 0.95), rgba(255, 214, 120, 0.55) 40%, transparent 70%);
	mix-blend-mode: screen;
	animation: impact-flash 0.45s var(--ease-out-strong) forwards;
}

@keyframes impact-flash {
	from {
		opacity: 1;
		transform: translate(-50%, -50%) scale(0.4);
	}

	to {
		opacity: 0;
		transform: translate(-50%, -50%) scale(1.2);
	}
}

.impact-ring {
	position: absolute;
	width: calc(var(--bs, 360px) / 15);
	height: calc(var(--bs, 360px) / 15);
	border-radius: 50%;
	border: calc(var(--bs, 360px) / 15 * 0.12) solid rgba(255, 255, 255, 0.9);
	opacity: 0;
	animation: impact-ring 0.7s var(--ease-out-strong) forwards;
}

/* second, gold ring trails the white one for a double-shockwave read */
.impact-ring.r2 {
	border-color: rgba(255, 214, 120, 0.85);
	animation-delay: 0.12s;
}

@keyframes impact-ring {
	0% {
		opacity: 0.95;
		transform: translate(-50%, -50%) scale(0.3);
	}

	100% {
		opacity: 0;
		transform: translate(-50%, -50%) scale(3.4);
		border-width: 1px;
	}
}

/* debris of the killed pawn — --spark (its color) and --dx/--dy (fling vector,
   px) are set inline per particle in JS */
.impact-spark {
	position: absolute;
	width: calc(var(--bs, 360px) / 15 * 0.22);
	height: calc(var(--bs, 360px) / 15 * 0.22);
	border-radius: 50%;
	background: var(--spark, #fff);
	box-shadow: 0 0 8px 2px var(--spark, #fff);
	animation: impact-spark 0.8s var(--ease-out-strong) forwards;
}

@keyframes impact-spark {
	from {
		opacity: 1;
		transform: translate(-50%, -50%) scale(1);
	}

	to {
		opacity: 0;
		transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.25);
	}
}

/* camera thump at the moment of impact — lives on .board-wrap (no transform of
   its own) so it can't fight #board's inline zoom transform */
.board-wrap.shake {
	animation: cine-shake 0.5s var(--ease-out-strong);
}

@keyframes cine-shake {
	0% {
		transform: none;
	}

	14% {
		transform: translate(-7px, 4px) rotate(-0.4deg);
	}

	30% {
		transform: translate(6px, -5px) rotate(0.35deg);
	}

	46% {
		transform: translate(-5px, 3px) rotate(-0.25deg);
	}

	62% {
		transform: translate(4px, -2px);
	}

	78% {
		transform: translate(-2px, 1px);
	}

	100% {
		transform: none;
	}
}

/* spotlight vignette while the capture beat runs (body.cine) — darkens the
   table edges so the zoomed cell reads like a camera close-up. Above tokens
   and the pawns3d canvas (z6), below the banner/controls (z30) */
.game::after {
	content: "";
	position: absolute;
	inset: 0;
	pointer-events: none;
	background: radial-gradient(circle at 50% 50%, transparent 45%, rgba(10, 4, 2, 0.55) 100%);
	opacity: 0;
	transition: opacity 0.3s ease;
	z-index: 25;
}

body.cine .game::after {
	opacity: 1;
}

/* 3D view: base tokens (inside .bases-layer) stay flat like the seats; table tokens
   (inside .surface, which tilts rotateX(32deg)) inherit the tilt because .surface is
   preserve-3d — they read as real 3D discs lying on the angled table, NOT lifted off it.
   Previous translateZ+rotateX(-32deg) shifted the token UP off its cell ("upper") AND
   cancelled the tilt so it looked flat to the camera ("not 3d"). Now only the transform
   inherited from .surface applies, plus a stronger drop-shadow to anchor it on the table. */
body.view3d .surface .token {
	filter: drop-shadow(0 calc(var(--bs, 360px) / 15 * 0.55) calc(var(--bs, 360px) / 15 * 0.45) rgba(20, 8, 0, 0.65));
}

/* 3D hit-area fix: on the tilted table the three.js pawn STANDS UP, so its visible
   silhouette (body + head) rises well above the flat base disc the pad is centred on —
   tapping the obvious target (the standing pawn) landed above the pad and missed. Grow
   the movable pad UPWARD only, toward the board's top (where rotateX(32deg) throws the
   pawn's head on screen). Sideways/bottom stay tight, and the token's OWN box is untouched
   so pawns3d's getBoundingClientRect billboard placement is unaffected. Scales with --u so
   it tracks the pawn's on-screen height at every board size. Base tokens (flat) keep the
   default pad. */
body.view3d .surface .token.movable::before {
	top: calc(var(--u) * -2.2);
	right: -20px;
	bottom: -24px;
	left: -20px;
	border-radius: 42%;
}

/* pawns3d (JS-added when WebGL is available): the three.js mesh IS the pawn now — the
   DOM .token div becomes an invisible hit-area/pulse-ring host, still positioned and
   sized exactly as before, still receiving clicks. Keeps token-pulse (.movable) and
   token-capture (box-shadow/scale, both content-free properties) working unchanged. */
/* scoped to .surface: unspawned tokens (in .bases-layer) keep their flat solid
   CSS disc — the standing mesh + contact shadow only exist ON the table */
body.pawns3d .surface .token {
	background: none;
	border: none;
	box-shadow: none;
	filter: none;
}

body.pawns3d .surface .token::after {
	display: none;
}

/* The board pieces here are the darker PBR mesh (--color cut by PAWN_DARKEN in
   app.js); the flat CSS discs still parked in the seats read lighter/pinker than
   their spawned selves. Drop the light highlight so a seat piece matches the mesh
   (its white ::after core still reads as the pawn's white head).
   ponytail: four color knobs — nudge the stops if the mesh tint shifts. */
body.pawns3d .bases-layer .token.red {
	background: radial-gradient(circle at 35% 30%, var(--red), var(--red) 45%, var(--red-dark));
}

body.pawns3d .bases-layer .token.purple {
	background: radial-gradient(circle at 35% 30%, var(--purple), var(--purple) 45%, var(--purple-dark));
}

body.pawns3d .bases-layer .token.green {
	background: radial-gradient(circle at 35% 30%, var(--green), var(--green) 45%, var(--green-dark));
}

body.pawns3d .bases-layer .token.blue {
	background: radial-gradient(circle at 35% 30%, var(--blue), var(--blue) 45%, var(--blue-dark));
}

.token.movable {
	z-index: 8;
	animation: token-pulse 0.9s ease-in-out infinite;
}

@keyframes token-pulse {

	0%,
	100% {
		box-shadow: 0 2px 5px rgba(43, 18, 3, 0.4), 0 0 0 0 rgba(255, 255, 255, 0.95);
	}

	50% {
		/* ring spread scales with board size (--bs) and the live pawns3d depth-fit
		   (--pawn-fit, app.js) so it stays proportional to the pawn in every condition */
		box-shadow: 0 2px 5px rgba(43, 18, 3, 0.4), 0 0 0 calc(var(--bs, 360px) / 15 * 0.29 * var(--pawn-fit, 1)) rgba(255, 255, 255, 0);
	}
}

.token.captured-anim {
	animation: token-capture 0.35s var(--ease-out-strong);
}

@keyframes token-capture {
	0% {
		transform: translate(-50%, -50%) scale(1);
	}

	50% {
		transform: translate(-50%, -50%) scale(1.35);
		filter: brightness(1.6);
	}

	100% {
		transform: translate(-50%, -50%) scale(1);
	}
}

/* move-selection countdown: lives on the ACTIVE seat's BOTTOM edge (appended to
   seats[turn] in startMoveTimeout), shrinks over the timeout, then a random piece
   moves; colored in the active player's color so the indicator sits with the player
   whose clock is ticking */
#move-timer {
	position: absolute;
	inset: -4px;
	/* ring wraps just outside the avatar's gold rim */
	z-index: 2;
	/* over the avatar image (the .sdot is gone) */
	pointer-events: none;
}

#move-timer svg {
	width: 100%;
	height: 100%;
	/* seam (start = end) at the TOP of the avatar — the ring starts full and
	   drains one full clockwise turn back to there as the turn's time runs out */
	transform: rotate(-90deg);
	overflow: visible;
}

#move-timer circle {
	fill: none;
	stroke: var(--ring-color, #f0c171);
	stroke-width: 10;
	stroke-linecap: round;
	filter: drop-shadow(0 0 4px var(--ring-color, #f0c171)) drop-shadow(0 0 2px rgba(0, 0, 0, 0.6));
	stroke-dasharray: 289.03;
	/* 2π·46 */
	stroke-dashoffset: 289.03;
	/* EMPTY by default (tucked away); the transition drains whatever is left
	   back here when .run is removed early (player moved) — "goes under the avatar" */
	transition: stroke-dashoffset 0.45s ease;
}

#move-timer.run circle {
	/* on this player's turn: snap to FULL instantly, then drain to empty over
	   the timeout — when it hits empty the random auto-move fires */
	animation: move-countdown 10s linear forwards;
}

#move-timer.t-red {
	--ring-color: var(--red);
}

#move-timer.t-purple {
	--ring-color: var(--purple);
}

#move-timer.t-green {
	--ring-color: var(--green);
}

#move-timer.t-blue {
	--ring-color: var(--blue);
}

@keyframes move-countdown {
	from {
		stroke-dashoffset: 0;
		/* instantly FULL the moment the turn starts */
	}

	to {
		stroke-dashoffset: 289.03;
		/* drained EMPTY at timeout — random move fires */
	}
}

/* ─── white center dice — 3D faces ────────────────────────────── */

/* transparent click target + glow; the 3D die art is a three.js canvas overlay on body
   (outside #board's transform stack — see app.js positionDice3D). No perspective/3D here —
   that dead CSS used to create a nested 3D context that jittered inside the tilting #board */
.dice {
	position: absolute;
	left: 50%;
	top: 50%;
	/* container is bigger than the die itself so the 3D hop never clips against the button
	   edge; the three.js canvas overlay (on body, not in here) is sized to the content box */
	width: calc(var(--u) * 2.4);
	height: calc(var(--u) * 2.4);
	padding: calc(var(--u) * 0.35);
	box-sizing: border-box;
	border-radius: 18%;
	border: none;
	background: transparent;
	z-index: 9;
	overflow: visible;
	/* no perspective / --tilt / transform-transition here anymore — those were for the old
	   CSS cube + counter-rotation. The three.js die canvas lives on body (outside #board's
	   transform stack) and handles all 3D. Keeping dead 3D CSS on this button created a nested
	   3D rendering context inside the tilting #board, which sub-pixel-jittered during the
	   0.4s perspective animation */
	transform: translate(-50%, -50%);
	transition: opacity 0.2s;
}

.dice:disabled {
	opacity: 0.45;
}

/* press feedback on the die art itself (the button's own transform carries the tilt,
   and the my-turn glow animation owns it during the pulse) */
.dice3d {
	transition: transform 120ms var(--ease-out-strong);
}

.dice:not(:disabled):active .dice3d {
	transform: scale(0.94);
}

/* the turn halo lives on .m-center (the wooden hub surrounding the dice).
   .m-center.glow-<color> sets the active player's color glow + publishes --turn-glow;
   .my-turn (toggled in updateTurnUI when it is the local player's turn) gently pulses the
   halo in that same color — a soft breathing ring (≈1.4s, outline spread grows 8→17px,
   glow grows 22→36px). Subtle enough not to distract, color-coded to the player so it
   reinforces "whose turn" at a glance, and clear enough to be noticed mid-conversation. */
.m-center.my-turn {
	animation: center-pulse 1.4s ease-in-out infinite;
}

@keyframes center-pulse {

	0%,
	100% {
		box-shadow:
			inset 0 4px 12px rgba(43, 18, 3, 0.55),
			0 0 0 5px rgba(240, 193, 113, 0.3),
			0 0 22px 8px var(--turn-glow);
	}

	50% {
		box-shadow:
			inset 0 4px 12px rgba(43, 18, 3, 0.55),
			0 0 0 8px rgba(240, 193, 113, 0.3),
			0 0 36px 14px var(--turn-glow);
	}
}

/* roll-ready affordance: when it is the local player's turn AND the die can be rolled
   (not mid-roll, not picking a piece), the die itself breathes a soft golden halo — a
   "tap me" cue that complements the center-hub pulse. Uses filter (NOT transform/box-shadow)
   so it never fights positionDice3D's inline transform on the .dice3d canvas. The class is
   toggled in updateTurnUI on both the canvas (WebGL die) and the .dice button (CSS-cube fallback). */
.dice3d.my-turn {
	animation: die-breathe 1.7s ease-in-out infinite;
}

@keyframes die-breathe {

	0%,
	100% {
		filter: drop-shadow(0 2px 3px rgba(43, 18, 3, 0.5));
	}

	50% {
		filter: drop-shadow(0 0 11px rgba(240, 193, 113, 0.9));
	}
}

/* CSS-cube fallback (no WebGL): breathe the cube gently in place instead */
.dice.my-turn .cube {
	animation: cube-breathe 1.7s ease-in-out infinite;
}

@keyframes cube-breathe {

	0%,
	100% {
		transform: scale(1);
	}

	50% {
		transform: scale(1.06);
	}
}

.cube {
	width: 100%;
	height: 100%;
	position: relative;
	transform-style: preserve-3d;
	transition: transform 0.567s cubic-bezier(0.25, 0.6, 0.3, 1);
}

/* three.js dice canvas — a position:fixed overlay on body (NOT inside #dice, so it
   escapes the board's CSS transform stack); size is set in px by positionDice3D */
.dice3d {
	display: block;
	pointer-events: none;
	/* the #dice button handles the clicks */
}

.face {
	position: absolute;
	inset: 0;
	border-radius: 18%;
	background: linear-gradient(145deg, #ffffff, #f0f0f0 55%, #d8d8d8);
	box-shadow:
		inset 0 0 8px rgba(0, 0, 0, 0.15),
		inset 0 2px 4px rgba(0, 0, 0, 0.12),
		inset 0 -2px 4px rgba(255, 255, 255, 0.4);
	padding: 16%;
	box-sizing: border-box;
	backface-visibility: hidden;
}

.pips {
	width: 100%;
	height: 100%;
	display: grid;
	grid-template: repeat(3, 1fr) / repeat(3, 1fr);
}

.pips span {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: #1a1a1a;
	place-self: center;
	box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.5), inset 0 -1px 1px rgba(255, 255, 255, 0.15);
}

/* pip placement per value: cells are 1..9 in the 3×3 grid */
.pips[data-v="1"] span:nth-child(1) {
	grid-area: 2 / 2;
}

.pips[data-v="2"] span:nth-child(1) {
	grid-area: 1 / 1;
}

.pips[data-v="2"] span:nth-child(2) {
	grid-area: 3 / 3;
}

.pips[data-v="3"] span:nth-child(1) {
	grid-area: 1 / 1;
}

.pips[data-v="3"] span:nth-child(2) {
	grid-area: 2 / 2;
}

.pips[data-v="3"] span:nth-child(3) {
	grid-area: 3 / 3;
}

.pips[data-v="4"] span:nth-child(1) {
	grid-area: 1 / 1;
}

.pips[data-v="4"] span:nth-child(2) {
	grid-area: 1 / 3;
}

.pips[data-v="4"] span:nth-child(3) {
	grid-area: 3 / 1;
}

.pips[data-v="4"] span:nth-child(4) {
	grid-area: 3 / 3;
}

.pips[data-v="5"] span:nth-child(1) {
	grid-area: 1 / 1;
}

.pips[data-v="5"] span:nth-child(2) {
	grid-area: 1 / 3;
}

.pips[data-v="5"] span:nth-child(3) {
	grid-area: 2 / 2;
}

.pips[data-v="5"] span:nth-child(4) {
	grid-area: 3 / 1;
}

.pips[data-v="5"] span:nth-child(5) {
	grid-area: 3 / 3;
}

.pips[data-v="6"] span:nth-child(1) {
	grid-area: 1 / 1;
}

.pips[data-v="6"] span:nth-child(2) {
	grid-area: 1 / 3;
}

.pips[data-v="6"] span:nth-child(3) {
	grid-area: 2 / 1;
}

.pips[data-v="6"] span:nth-child(4) {
	grid-area: 2 / 3;
}

.pips[data-v="6"] span:nth-child(5) {
	grid-area: 3 / 1;
}

.pips[data-v="6"] span:nth-child(6) {
	grid-area: 3 / 3;
}

/* ─── toast ───────────────────────────────────────────────────── */

#toast {
	position: fixed;
	left: 50%;
	bottom: 110px;
	transform: translateX(-50%) translateY(20px);
	background: rgba(26, 15, 6, 0.55);
	backdrop-filter: blur(20px) saturate(180%);
	-webkit-backdrop-filter: blur(20px) saturate(180%);
	border: 1px solid rgba(255, 255, 255, 0.14);
	box-shadow: 0 2px 8px rgba(20, 8, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.08);
	color: #fff;
	font-size: 13px;
	padding: 9px 18px;
	border-radius: 20px;
	direction: rtl;
	white-space: nowrap;
	display: flex;
	align-items: center;
	gap: 6px;
	opacity: 0;
	pointer-events: none;
	/* stronger ease-out on entry so the toast reads as instant; opacity stays plain */
	transition: opacity 0.2s ease, transform 0.25s var(--ease-out-strong);
	z-index: 100;
}

#toast.show {
	opacity: 1;
	transform: translateX(-50%) translateY(0);
}

#toast svg,
#turn-banner svg {
	flex-shrink: 0;
}

/* ─── chat — bubble beside the sender's seat card + input bar ── */

.sbubble {
	position: absolute;
	/* above the base tokens (.bases-layer z1) and the pawns3d canvas (z6); reaches
	   the root stacking context because neither .seat-row nor .seat creates one */
	z-index: 7;
	left: 50%;
	/* --bx: horizontal shift; zeroed on .sleft/.sright so the bubble hugs its
	   screen edge. width: max-content frees it from the seat card's width —
	   an abspos child otherwise shrink-to-fits inside the card and breaks
	   even short words mid-word on small screens */
	--bx: -50%;
	transform: translateX(var(--bx)) scale(0.85);
	width: max-content;
	max-width: min(180px, 48vw);
	background: linear-gradient(160deg, #fffdf6, #ffe9c4);
	color: #2b1203;
	font-size: 12px;
	font-weight: 600;
	line-height: 1.5;
	padding: 7px 12px;
	border-radius: 14px;
	border: 1.5px solid var(--bub, #f0c171);
	box-shadow: 0 4px 14px rgba(43, 18, 3, 0.4), 0 0 14px -2px var(--bub-glow, rgba(240, 193, 113, 0.55));
	direction: rtl;
	word-break: break-word;
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.35s ease, transform 0.35s var(--ease-out-strong);
	z-index: 20;
}

/* bubble carries the sender seat's color on border + glow */
.seat.red .sbubble {
	--bub: var(--red);
	--bub-glow: var(--red-glow);
}

.seat.purple .sbubble {
	--bub: var(--purple);
	--bub-glow: var(--purple-glow);
}

.seat.green .sbubble {
	--bub: var(--green);
	--bub-glow: var(--green-glow);
}

.seat.blue .sbubble {
	--bub: var(--blue);
	--bub-glow: var(--blue-glow);
}

.sbubble.show {
	opacity: 1;
	transform: translateX(var(--bx)) scale(1);
	animation: bubble-pop 0.45s var(--ease-out-strong);
}

@keyframes bubble-pop {
	0% {
		transform: translateX(var(--bx)) scale(0.4);
	}

	60% {
		transform: translateX(var(--bx)) scale(1.08);
	}

	100% {
		transform: translateX(var(--bx)) scale(1);
	}
}

/* corner seats (side classes set in arrangeSeats): the avatar hugs its screen
   wall (sleft→left, sright→right) at 13px pad + av/2, so anchor the bubble's tail
   over the avatar and grow the bubble INWARD toward the table center. --bx pulls
   the bubble so its 16px tail lands on that avatar-center anchor. */
.seat.sleft .sbubble {
	left: calc(13px + var(--av) / 2);
	--bx: -16px;
}

.seat.sright .sbubble {
	left: auto;
	right: calc(13px + var(--av) / 2);
	--bx: 16px;
}

.seat.sleft .sbubble::after {
	left: 16px;
	transform: rotate(45deg);
}

.seat.sright .sbubble::after {
	left: auto;
	right: 16px;
	transform: rotate(45deg);
}

/* tail: rotated square sharing the bubble's border, pointing at the seat card */
.sbubble::after {
	content: '';
	position: absolute;
	left: 50%;
	width: 9px;
	height: 9px;
	background: inherit;
	pointer-events: none;
}

/* top seats: bubble hangs below the card (tail up, grows downward);
   bottom seats: bubble above it (tail down, grows upward) */
#row-top .sbubble {
	/* extra clearance so the tail never lands on the base tokens sitting on
	   the seat's rim arc */
	top: calc(100% + 30px);
	transform-origin: top center;
}

#row-top .sbubble::after {
	top: -5.5px;
	transform: translateX(-50%) rotate(45deg);
	/* solid top-of-gradient tone so the tail reads as one piece with the
	   bubble (the gradient sampled a mismatched slice on this tiny box) */
	background: #fffdf6;
	border-top: 1.5px solid var(--bub, #f0c171);
	border-left: 1.5px solid var(--bub, #f0c171);
	border-top-left-radius: 3px;
}

#row-bottom .sbubble {
	/* extra clearance so the tail never lands on the base tokens sitting on
	   the seat's rim arc */
	bottom: calc(100% + 30px);
	transform-origin: bottom center;
}

#row-bottom .sbubble::after {
	bottom: -5.5px;
	transform: translateX(-50%) rotate(45deg);
	/* solid bottom-of-gradient tone so the tail reads as one piece with the
	   bubble (the gradient sampled a mismatched slice on this tiny box) */
	background: #ffe9c4;
	border-bottom: 1.5px solid var(--bub, #f0c171);
	border-right: 1.5px solid var(--bub, #f0c171);
	border-bottom-right-radius: 3px;
}

@media (prefers-reduced-motion: reduce) {
	.sbubble.show {
		animation: none;
	}
}

/* positioned out of flow, pinned right under .vc-btns — it never reserves
   layout space, so it's free to slide in fully visible the whole way (a
   height/clip-based reveal would hide it until almost fully open) */
#chat-bar {
	position: absolute;
	top: 100%;
	left: 50%;
	width: min(250px, 80vw);
	margin-top: 16px;
	/* clear gap from .vc-btns so the bar never crowds the buttons */
	display: flex;
	align-items: center;
	gap: 4px;
	direction: rtl;
	/* the bar itself IS the input pill now — cancel + send buttons ride inside it,
	   the <input> sits transparent in the middle (glass recipe from #chat-input) */
	padding: 4px;
	border-radius: 24px;
	background: rgba(26, 15, 6, 0.75);
	backdrop-filter: blur(20px) saturate(180%);
	-webkit-backdrop-filter: blur(20px) saturate(180%);
	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 4px 14px rgba(20, 8, 0, 0.4);
	opacity: 0;
	/* hidden state sits well below its resting spot — opens rising up from
	   the bottom, closes sinking back down */
	transform: translate(-50%, 28px);
	pointer-events: none;
	transition: transform 0.3s var(--ease-out-strong), opacity 0.25s ease;
}

#chat-bar.open {
	opacity: 1;
	transform: translate(-50%, 0);
	pointer-events: auto;
}

#chat-input {
	flex: 1;
	min-width: 0;
	border: none;
	border-radius: 0;
	font-family: inherit;
	font-size: 14px;
	/* transparent — the glass now lives on #chat-bar (the pill wrapper) */
	background: transparent;
	color: #fff;
	outline: none;
	user-select: text;
	-webkit-user-select: text;
	-webkit-touch-callout: default;
}

#chat-input::placeholder {
	color: rgba(255, 255, 255, 0.5);
}

/* send + close: same liquid-glass recipe as the .vc-btns, round icon buttons */
#chat-send,
#chat-cancel {
	flex-shrink: 0;
	width: 34px;
	height: 34px;
	display: grid;
	place-items: center;
	padding: 0;
	border-radius: 50%;
	cursor: pointer;
	border: 1px solid rgba(255, 255, 255, 0.3);
	background: linear-gradient(160deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.04) 55%, rgba(255, 255, 255, 0.10));
	backdrop-filter: blur(2px) saturate(170%) brightness(1.06);
	-webkit-backdrop-filter: blur(2px) saturate(170%) brightness(1.06);
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.5),
		inset 1px 1px 1px -0.5px rgba(255, 255, 255, 0.35),
		inset -1px -1px 1px -0.5px rgba(255, 255, 255, 0.35),
		inset 0 0 6px 3px rgba(255, 255, 255, 0.08),
		0 2px 8px rgba(20, 8, 0, 0.35),
		0 0 12px rgba(255, 255, 255, 0.1);
	color: #fff;
	font-weight: 700;
	font-family: inherit;
	line-height: 1;
	transition: transform 140ms var(--ease-out-strong), background 0.2s ease, box-shadow 140ms var(--ease-out-strong);
}

#chat-send .send-icon,
#chat-cancel .cancel-icon {
	display: block;
}

/* cooling down: hide the plane, show the seconds-left digit (JS toggles
   .counting + data-count, so the SVG stays in the DOM) */
#chat-send.counting .send-icon {
	display: none;
}

#chat-send.counting::after {
	content: attr(data-count);
	font-size: 15px;
}

#chat-send:disabled {
	opacity: 0.55;
	cursor: default;
}

#chat-send:not(:disabled):active,
#chat-cancel:active {
	transform: scale(0.94);
	background: linear-gradient(160deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0.12) 55%, rgba(255, 255, 255, 0.2));
	box-shadow: inset 0 1px 4px rgba(20, 8, 0, 0.45), inset 0 0 6px 3px rgba(255, 255, 255, 0.1);
}

/* ─── loading spinner — plain ring inside the overlay ── */

/* a normal loading ring, centered on the board. The real 3D die is kept hidden
   until it's fully rendered (app.js), then drops onto the board on connect —
   so the user never sees a half-rendered die, only this simple spinner. */
#loading-dice {
	position: absolute;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
	display: grid;
	place-items: center;
}

.ring-spinner {
	width: 54px;
	height: 54px;
	border-radius: 50%;
	border: 4px solid rgba(255, 255, 255, 0.18);
	border-top-color: rgba(255, 226, 173, 0.95);
	animation: ring-spin 0.9s linear infinite;
}

@keyframes ring-spin {
	to {
		transform: rotate(360deg);
	}
}

@media (prefers-reduced-motion: reduce) {
	.ring-spinner {
		animation-duration: 1.6s;
	}
}

/* loading + reconnect share one POV-cube look — only the container size differs */
#loading-dice .cube,
#reconnect-dice .cube {
	animation: loading-tumble 3.2s linear infinite;
}

#loading-dice .face:nth-child(1),
#reconnect-dice .face:nth-child(1) {
	transform: translateZ(var(--ldz));
}

#loading-dice .face:nth-child(2),
#reconnect-dice .face:nth-child(2) {
	transform: rotateX(90deg) translateZ(var(--ldz));
}

#loading-dice .face:nth-child(3),
#reconnect-dice .face:nth-child(3) {
	transform: rotateY(90deg) translateZ(var(--ldz));
}

#loading-dice .face:nth-child(4),
#reconnect-dice .face:nth-child(4) {
	transform: rotateY(-90deg) translateZ(var(--ldz));
}

#loading-dice .face:nth-child(5),
#reconnect-dice .face:nth-child(5) {
	transform: rotateX(-90deg) translateZ(var(--ldz));
}

#loading-dice .face:nth-child(6),
#reconnect-dice .face:nth-child(6) {
	transform: rotateX(180deg) translateZ(var(--ldz));
}

/* pips scale with the big cube (the base 6px is sized for the 60px button die) */
#loading-dice .pips span,
#reconnect-dice .pips span {
	width: 46%;
	height: 46%;
}

@keyframes loading-tumble {
	from {
		transform: rotateX(0deg) rotateY(0deg);
	}

	to {
		transform: rotateX(360deg) rotateY(720deg);
	}
}

/* ─── reconnect overlay (own connection drops mid-game) ──────────── */

#reconnect-overlay {
	position: fixed;
	inset: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 14px;
	background: rgba(26, 15, 6, 0.55);
	backdrop-filter: blur(20px) saturate(180%);
	-webkit-backdrop-filter: blur(20px) saturate(180%);
	z-index: 500;
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	transition: opacity 0.3s ease, visibility 0s linear 0.3s;
}

#reconnect-overlay.show {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transition: opacity 0.3s ease;
}

/* same size as #loading-dice — cube/face/pip rules are shared above */
#reconnect-dice {
	position: relative;
	width: 22vmin;
	height: 22vmin;
	--ldz: 11vmin;
	perspective: 90vmin;
}

.reconnect-msg {
	color: #fff;
	font-size: 14px;
	direction: rtl;
}

/* load finished: the spinner scales down to the table die's size at the same spot
   (the board is viewport-centered) — a POV-style scale, not a translate from the top */
#loading-overlay.done #loading-dice {
	transform: translate(-50%, -50%) scale(0.45);
}

/* ─── 2D / 3D table view ──────────────────────────────────────── */

.view-controls {
	/* pinned to row center, out of flex flow — seat width changes redistribute
	   space-between among the OTHER row children and must not shift this */
	position: absolute;
	left: 50%;
	top: 60%;
	transform: translate(-50%, calc(-50% - 12px));
	/* lift off the bottom safe-area edge */
	display: flex;
	flex-direction: column;
	align-items: center;
	pointer-events: auto;
	z-index: 30;
	transition: transform 0.3s var(--ease-out-strong);
	/* #chat-bar is absolutely positioned below this, so it never affects this
	   box's own size — open chat bar may overlap the seat cards beside it */
}

/* the 3 buttons ride up together with #chat-bar's own reveal instead of
   sitting still while only the bar slides out beneath them */
/* lift higher when open so the bar clears the buttons AND keeps an equal
   gap to the bottom of the screen (matches the widened margin-top above) */
.view-controls.chat-open {
	transform: translate(-50%, calc(-50% - 54px));
}

.vc-btns {
	/* 2×2 grid, not a 4-wide row: lets each button be ~1.5× bigger (better mobile
	   tap targets) while the block still fits the center pocket — 2 wide < --midw,
	   2 tall < --seatw. Row-major order: layout | view / mute | chat. */
	display: grid;
	grid-template-columns: 1fr 1fr;
	place-items: center;
	gap: clamp(4px, 2vw, 10px);
}

#layout-btn,
#view-btn,
#mute-btn,
#chat-btn {
	/* --vcb (root) sizes each button so the 2×2 grid fits the center pocket at
	   any screen size — no seat or board overlap */
	width: var(--vcb);
	height: var(--vcb);
	border-radius: 50%;
	/* liquid glass: translucent white gradient over the blurred table,
	   specular top edge + bright rim insets instead of a flat tint */
	border: 1px solid rgba(255, 255, 255, 0.3);
	background: linear-gradient(160deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.04) 55%, rgba(255, 255, 255, 0.10));
	/* near-clear glass (matches .seat::before): tiny blur keeps the board/wood
	   pattern readable through the button instead of a flat frosted tint */
	backdrop-filter: blur(2px) saturate(170%) brightness(1.06);
	-webkit-backdrop-filter: blur(2px) saturate(170%) brightness(1.06);
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.5),
		inset 1px 1px 1px -0.5px rgba(255, 255, 255, 0.35),
		inset -1px -1px 1px -0.5px rgba(255, 255, 255, 0.35),
		inset 0 0 6px 3px rgba(255, 255, 255, 0.08),
		0 2px 8px rgba(20, 8, 0, 0.35),
		0 0 12px rgba(255, 255, 255, 0.1);
	color: #fff;
	font-weight: 700;
	font-size: clamp(12px, calc(var(--vcb) * 0.42), 20px);
	font-family: inherit;
	line-height: 1;
	padding: 0;
	flex-shrink: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	pointer-events: auto;
	transition: transform 140ms var(--ease-out-strong), background-color 0.2s ease, border-color 0.2s ease, box-shadow 140ms var(--ease-out-strong);
}

#view-btn {
	font-family: "IranSansEnNum";
	/* "2D"/"3D" in Latin digits, not the FaNum default */
	/* IRANSansX's tall Persian descent makes the Latin glyphs ride HIGH in the
	   line box, so flex-centering leaves "2D"/"3D" looking above center — nudge
	   the text back down to the optical middle. box-sizing keeps the circle at
	   --vcb. ponytail: this is a font-metric knob — flip the sign / tweak the
	   0.14em if it reads low on your device. */
	box-sizing: border-box;
	padding-top: 0.14em;
}

/* layout toggle icon: 4 seat dots on the table ring, showing the CURRENT layout —
   .x turns the dots onto the diagonals, easing in sync with the
   board's own 0.9s spin */
#layout-btn svg,
#chat-btn svg {
	/* HTML width/height attrs are fixed px; override so icons scale with --vcb
	   instead of staying tiny inside the bigger circle */
	width: 50%;
	height: 50%;
}

#layout-btn svg {
	display: block;
	transition: transform 0.9s var(--ease-in-out-strong);
}

#layout-btn.x svg {
	transform: rotate(45deg);
}

#layout-btn:active,
#view-btn:active,
#mute-btn:active,
#chat-btn:active {
	transform: scale(0.94);
	background: linear-gradient(160deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0.12) 55%, rgba(255, 255, 255, 0.2));
	box-shadow: inset 0 1px 4px rgba(20, 8, 0, 0.45), inset 0 0 6px 3px rgba(255, 255, 255, 0.1);
}

/* hover only on real pointers — touch taps would leave it stuck highlighted */
@media (hover: hover) and (pointer: fine) {

	#layout-btn:hover,
	#view-btn:hover,
	#mute-btn:hover,
	#chat-btn:hover {
		background: linear-gradient(160deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1) 55%, rgba(255, 255, 255, 0.18));
		border-color: rgba(255, 255, 255, 0.45);
	}
}

/* the surface wrapper: plate/cells/m-center live inside this. One rotateX on it in 3D
   view tilts them as a single plane (cells move position via perspective, not just shrink
   in place). Built in JS (buildBoard). Sibling of tokens/base-slots/dice inside #board */
.surface {
	position: absolute;
	inset: 0;
	/* --spin: transient counter-rotation driven by the layout toggle (app.js) —
	   0 outside the spin, so this stays the identity transform */
	transform: rotate(var(--spin, 0deg));
	transition: transform 0.4s var(--ease-in-out-strong);
	/* force .surface onto its own GPU compositing layer so the whole tilted plane
	   rasterizes as one operation — without this the browser recalculates each cell's
	   perspective projection per frame and sub-pixel rounding makes cells vibrate */
	will-change: transform;
	/* hidden backface during the tilt prevents the cells from flickering through */
	backface-visibility: hidden;
	/* children (cells, tokens) render in true 3D space, not flattened onto the plane —
	   needed for the elevated/counter-rotated table tokens in view3d. Without this,
	   translateZ on a token is collapsed to the plane (the lift was invisible: "just shadows") */
	transform-style: preserve-3d;
}

/* 3D view: tilt the surface plane; tokens/base-slots/dice stay flat (siblings, not inside
   .surface). #board's perspective on its direct child .surface makes the cells move as a
   single tilted plane */
body.view3d .surface {
	/* spin composes AFTER the tilt (right side = applied first, in the board plane),
	   so the layout rotation happens in the tilted table plane, not screen space */
	transform: rotateX(32deg) rotate(var(--spin, 0deg));
}

/* layout toggle in flight: app.js drives --spin per frame — the 0.4s transform
   transition would smear those raw updates */
.surface.spin {
	transition: none;
}

/* the snap frame of the toggle: tokens must jump with their cells to the new GEO,
   not glide the 0.3s left/top chord while the surface counter-rotates */
.surface.snap .token {
	transition: none;
}

/* base layer — holds base-slots AND tokens currently at base (p == -1); lives in
   .game (NOT #board) so it is NEVER tilted in 3D and paints above the seat cards the
   tokens sit inside (rows are z-auto). No inset: layoutBases() glues its box to
   #board's so % coords keep resolving as board units. Stays below the pawns3d
   canvas (z-index 6). Pieces on the track live inside .surface instead and tilt
   with the table. */
.bases-layer {
	position: absolute;
	z-index: 1;
	pointer-events: none;
}

.bases-layer .base-slot,
.bases-layer .token {
	pointer-events: auto;
}

/* Re-anchoring to the seat rim on resize/rotate is a LAYOUT change, not a move:
   the piece's screen spot barely shifts, but its left/top %-of-layer value jumps
   because the layer width changed under it — a plain transition then slides it,
   the "drag on rotate". Kill the positional transition so base pieces only resize
   in place. Spawn and return-to-base still glide: animateMove() and the capture
   FLIP set an inline transition on the element, and inline beats this rule. */
.bases-layer .token,
.bases-layer .base-slot {
	transition: box-shadow 0.3s ease;
}

/* reduced motion: keep the informative cues (loading, movable highlight) but as
   gentler/static versions — drop the attention-grabbing movement */
@media (prefers-reduced-motion: reduce) {

	#loading-dice .cube,
	#reconnect-dice .cube {
		animation-duration: 12s;
	}

	#reconnect-overlay {
		transition-duration: 0.01s;
	}

	.token.movable {
		animation: none;
		/* static white ring replaces the pulse — the affordance must survive.
		   Spread scales with board size (--bs) and --pawn-fit, same as the pulse ring. */
		box-shadow: 0 2px 5px rgba(43, 18, 3, 0.4), 0 0 0 calc(var(--bs, 360px) / 15 * 0.17 * var(--pawn-fit, 1)) rgba(255, 255, 255, 0.85);
	}

	.dice.my-turn,
	.m-center.my-turn {
		animation: none;
		box-shadow:
			inset 0 4px 12px rgba(43, 18, 3, 0.55),
			0 0 0 6px rgba(240, 193, 113, 0.32),
			0 0 28px 11px var(--turn-glow);
	}

	.seat.turn {
		animation: none;
		box-shadow:
			inset 0 2px 5px rgba(43, 18, 3, 0.35),
			0 018px 3px var(--turn-glow, rgba(240, 193, 113, 0.45));
	}

	.dice3d.my-turn,
	.dice.my-turn .cube {
		animation: none;
		filter: drop-shadow(0 0 8px rgba(240, 193, 113, 0.85));
	}

	#board,
	.dice,
	#loading-dice,
	.surface,
	.token,
	#turn-banner,
	#move-timer {
		transition-duration: 0.01s;
	}
}

/* keyboard focus (desktop Telegram) — invisible for touch, required for a11y */
.dice:focus-visible,
#layout-btn:focus-visible,
#view-btn:focus-visible,
#mute-btn:focus-visible,
#chat-btn:focus-visible {
	outline: 2px solid #ffd54f;
	outline-offset: 3px;
}

/* ─── misc ────────────────────────────────────────────────────── */

/* fatal-error screen (showError in app.js wipes <body> and injects .error-container) */
@keyframes pulse {

	0%,
	100% {
		transform: scale(1);
	}

	50% {
		transform: scale(1.2);
	}
}

@keyframes slideUp {
	from {
		transform: translateY(20px);
		opacity: 0;
	}

	to {
		transform: translateY(0);
		opacity: 1;
	}
}

.error-container {
	position: fixed;
	inset: 0;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	z-index: 9999;
}

.error-message {
	font-size: 17px;
	line-height: 1.9;
	padding: 22px 24px;
	margin: 0 16px;
	background: rgba(26, 15, 6, 0.96);
	border: 1.5px solid rgba(240, 193, 113, 0.35);
	border-radius: 14px;
	color: #fff;
	box-shadow: 0 12px 30px rgba(43, 18, 3, 0.5);
	text-align: center;
	direction: rtl;
	animation: slideUp 0.5s ease-out forwards;
	max-width: 400px;
}

.error-icon {
	font-size: 40px;
	margin-bottom: 10px;
	animation: pulse 2s infinite;
}

@keyframes shake {

	0%,
	100% {
		transform: translateX(0);
	}

	25% {
		transform: translateX(-4px);
	}

	75% {
		transform: translateX(4px);
	}
}

/* ─── lobby (color selection before game starts) ─────────────── */

#lobby {
	position: fixed;
	inset: 0;
	z-index: 10000;
	display: flex;
	align-items: center;
	justify-content: center;
	background: linear-gradient(155deg, #a5683f, #7d4a2a);
	padding: 20px;
	box-sizing: border-box;
}

.lobby-card {
	background: rgba(26, 15, 6, 0.88);
	border: 1.5px solid rgba(240, 193, 113, 0.35);
	border-radius: 16px;
	padding: 24px 20px;
	max-width: 320px;
	width: 100%;
	text-align: center;
	color: #fff;
	box-shadow: 0 12px 30px rgba(43, 18, 3, 0.5);
}

.lobby-title {
	font-size: 24px;
	font-weight: 700;
	margin-bottom: 2px;
}

.lobby-room {
	font-size: 12px;
	opacity: 0.65;
	margin-bottom: 18px;
}

.lobby-colors {
	display: grid;
	grid-template: repeat(2, 1fr) / repeat(2, 1fr);
	gap: 10px;
	margin-bottom: 14px;
}

.lobby-color {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 5px;
	padding: 12px 8px;
	border-radius: 12px;
	border: 2px solid transparent;
	background: rgba(255, 255, 255, 0.08);
	color: #fff;
	font-family: inherit;
	cursor: pointer;
	transition: opacity 0.2s, border-color 0.2s, background 0.2s, transform 0.2s var(--ease-out-strong);
}

.lobby-color:disabled,
.lobby-color.taken {
	opacity: 0.35;
	cursor: not-allowed;
}

.lobby-color.mine {
	border-color: #ffd54f;
	background: rgba(255, 213, 79, 0.15);
}

.lobby-color:not(:disabled):not(.mine):active {
	transform: scale(0.96);
}

.lc-dot {
	width: 30px;
	height: 30px;
	border-radius: 50%;
	border: 2px solid rgba(255, 255, 255, 0.6);
}

.lc-name {
	font-size: 14px;
	font-weight: 700;
}

.lc-status {
	font-size: 10px;
	opacity: 0.8;
	min-height: 13px;
}

.lobby-color.lc-red .lc-dot {
	background: radial-gradient(circle at 35% 30%, var(--red-light), var(--red) 60%, var(--red-dark));
}

.lobby-color.lc-green .lc-dot {
	background: radial-gradient(circle at 35% 30%, var(--green-light), var(--green) 60%, var(--green-dark));
}

.lobby-color.lc-purple .lc-dot {
	background: radial-gradient(circle at 35% 30%, var(--purple-light), var(--purple) 60%, var(--purple-dark));
}

.lobby-color.lc-blue .lc-dot {
	background: radial-gradient(circle at 35% 30%, var(--blue-light), var(--blue) 60%, var(--blue-dark));
}

.lobby-players {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	justify-content: center;
	margin-bottom: 14px;
	min-height: 26px;
}

.lp-item {
	font-size: 12px;
	padding: 3px 10px;
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.1);
}

.lp-red {
	border: 1px solid var(--red);
}

.lp-green {
	border: 1px solid var(--green);
}

.lp-purple {
	border: 1px solid var(--purple);
}

.lp-blue {
	border: 1px solid var(--blue);
}

.lobby-start {
	width: 100%;
	padding: 12px;
	border-radius: 12px;
	border: none;
	background: #a1683c;
	color: #fff;
	font-size: 15px;
	font-weight: 700;
	font-family: inherit;
	cursor: pointer;
	transition: opacity 0.2s;
}

.lobby-start:disabled {
	opacity: 0.4;
	cursor: not-allowed;
}

.lobby-start:not(:disabled):active {
	transform: scale(0.97);
}

/* lobby polish (dev/test overlay only — never ships to production): soft card
   entrance, clearer selected state, and pointer-device hover feedback. */
.lobby-card {
	animation: lobby-in 0.35s var(--ease-out-strong);
}

@keyframes lobby-in {
	from {
		opacity: 0;
		transform: translateY(14px) scale(0.98);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

.lobby-color.mine {
	transform: translateY(-2px);
}

.lobby-color.mine .lc-dot {
	box-shadow: 0 0 0 3px rgba(255, 213, 79, 0.35);
}

@media (hover: hover) and (pointer: fine) {

	.lobby-color:not(:disabled):not(.mine):hover {
		background: rgba(255, 255, 255, 0.12);
		border-color: rgba(240, 193, 113, 0.45);
	}

	.lobby-start:not(:disabled):hover {
		background: #b3743f;
	}
}