/*
 * Board styling.
 *
 * Ported from bga_ternary/ternary.css, reduced to what the scene renderer emits. The layer
 * structure and the 1100px authored size are inherited from the BGA template so the existing
 * artwork drops in without re-cutting.
 *
 * Pieces are positioned by their CENTRE (translate(-50%,-50%)), because the layout module
 * computes centroids — offsetting by half a sprite in CSS instead would put the two
 * definitions out of sync the moment a sprite size changed.
 */

.t-board {
  position: relative;
  width: 1100px;
  height: 1100px;
  background-size: cover;
  background-position: center;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}

.t-layer,
.t-path-overlay {
  position: absolute;
  inset: 0;
  width: 1100px;
  height: 1100px;
  pointer-events: none;
}

/* The overlay carries the route line AND the closed-road scrim, so it has to sit above the
   tiles (which the layer order already gives it) but below the workers. */
.t-path-overlay { z-index: 20; }
#layer_workers { z-index: 30; }

/* Only pieces the player can act on take pointer events; the board handles the rest. */
.t-worker,
.t-dest,
.t-face {
  pointer-events: auto;
}

/* --- tokens (reef: pearls) ---
 *
 * Silvery and COOL, deliberately. The earlier cream pearl sat in the same warm family as the
 * sand it rests on, so a pearl and an empty node read as almost the same mark. A cool highlight
 * plus a faint blue halo separates them at a glance without adding another colour to a board
 * that already uses colour to mean something.
 */
.t-token {
  position: absolute;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 34% 27%,
      #ffffff 0%, #f4f9fd 18%, #d8e7f2 46%, #a9c0d2 74%, #7d95a9 100%);
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.8),
    0 2px 6px rgba(2, 18, 28, 0.55),
    0 0 13px rgba(150, 215, 255, 0.55),
    inset 0 -3px 5px rgba(38, 68, 90, 0.28);
  animation: pearlLuster 4s ease-in-out infinite;
}

/* A slow four-point glint. Offset per pearl so the field shimmers rather than pulsing in
   unison, which reads as a UI blink rather than as light on water. */
.t-token::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 13px;
  height: 13px;
  margin: -6.5px 0 0 -6.5px;
  background:
    linear-gradient(to bottom, transparent 44%, #fff 50%, transparent 56%),
    linear-gradient(to right, transparent 44%, #fff 50%, transparent 56%);
  opacity: 0;
  animation: pearlSparkle 3.6s ease-in-out infinite;
}

.t-token:nth-child(3n)   ::after,
.t-token:nth-child(3n)   { animation-delay: -1.2s; }
.t-token:nth-child(3n+1) ::after,
.t-token:nth-child(3n+1) { animation-delay: -2.4s; }

@keyframes pearlLuster {
  0%, 100% { filter: brightness(1); }
  50% { filter: brightness(1.15); }
}

@keyframes pearlSparkle {
  0%, 72%, 100% { opacity: 0; transform: rotate(0deg) scale(0.6); }
  82%           { opacity: 0.95; transform: rotate(45deg) scale(1.15); }
  92%           { opacity: 0; transform: rotate(90deg) scale(0.7); }
}

/*
 * A pearl with a worker standing on it: nudged clear of the piece and drawn above it.
 *
 * Both need to be legible at once — the worker to show where you are, the pearl to show the
 * square is still worth visiting. It lives in the workers layer (see render.ts), so a z-index
 * here lifts it over the piece.
 */
.t-token-shared {
  z-index: 5;
  transform: translate(11px, -11px) scale(0.86);
  box-shadow:
    0 0 0 1.5px rgba(255, 255, 255, 0.95),
    0 2px 7px rgba(2, 18, 28, 0.7),
    0 0 14px rgba(150, 215, 255, 0.75);
}

/*
 * --- pearls this move would claim ---
 *
 * Set while a worker is selected and a destination is hovered or staged, on exactly the pearls
 * the engine says the move collects (see ViewState.claiming).
 *
 * It has to differ from the ambient shimmer IN KIND, not just in degree: every pearl is already
 * slowly lustring and glinting, so a brighter or faster version of the same idea reads as noise.
 * Hence a warm amber ring against the field's cool silver, a fast beat an order of magnitude
 * quicker than the 4s luster, and a size change — three channels the resting state does not use.
 *
 * The ambient sparkle is suppressed underneath so the two do not interleave into a stutter.
 */
.t-token-claiming {
  z-index: 6;
  animation: pearlClaiming 720ms ease-in-out infinite;
}
.t-token-claiming::after { animation: none; opacity: 0; }

/*
 * NO `filter: brightness()` here, deliberately. A filter applies to the element's box-shadows
 * as well as its body, so brightening the pearl also washed the amber ring out to white — the
 * highlight still read as "bigger", but lost the warm colour that distinguishes it from the
 * cool destination rings. Contrast comes from the ring and the scale instead.
 *
 * A dark inner rim sits under the amber so the ring holds up against the pale sand as well as
 * against the dark water; the board has both, and amber alone vanishes on sand.
 */
@keyframes pearlClaiming {
  0%, 100% {
    transform: scale(1.05);
    box-shadow:
      0 0 0 1.5px rgba(120, 74, 8, 0.55),
      0 0 0 3px rgba(240, 194, 78, 0.85),
      0 2px 6px rgba(2, 18, 28, 0.55),
      0 0 12px 3px rgba(240, 168, 40, 0.5);
  }
  50% {
    transform: scale(1.36);
    box-shadow:
      0 0 0 2px rgba(120, 74, 8, 0.7),
      0 0 0 5px rgba(247, 205, 92, 1),
      0 2px 8px rgba(2, 18, 28, 0.6),
      0 0 24px 8px rgba(240, 168, 40, 0.85);
  }
}

/* A claimed pearl under a worker keeps its nudge, so it stays clear of the piece. */
.t-token-shared.t-token-claiming {
  animation: pearlClaimingShared 720ms ease-in-out infinite;
}
@keyframes pearlClaimingShared {
  0%, 100% {
    transform: translate(11px, -11px) scale(0.92);
    box-shadow:
      0 0 0 1.5px rgba(120, 74, 8, 0.55), 0 0 0 3px rgba(240, 194, 78, 0.85),
      0 0 12px 3px rgba(240, 168, 40, 0.5);
  }
  50% {
    transform: translate(11px, -11px) scale(1.18);
    box-shadow:
      0 0 0 2px rgba(120, 74, 8, 0.7), 0 0 0 5px rgba(247, 205, 92, 1),
      0 0 24px 8px rgba(240, 168, 40, 0.85);
  }
}

/* --- tiles --- */
.t-tile,
.t-ghost {
  position: absolute;
  width: 150px;
  height: 150px;
  /* Authored at 300px cells, drawn at 150px: the sheet is 6x9 cells. */
  background-size: 900px 1350px;
  background-repeat: no-repeat;
}

/*
 * Two sheets, deliberately. Placed tiles use the BORDERLESS art so neighbouring tiles read as
 * one continuous reef; the ghost uses the BORDERED art, whose foam outline is what makes a
 * preview legible as "not yet placed". BGA's stylesheet makes the same split.
 */
.t-tile { background-image: var(--t-spritesheet-noborder, var(--t-spritesheet)); }
.t-ghost { background-image: var(--t-spritesheet); }

.t-scaler { transform-origin: top left; }

.t-glow {
  filter: drop-shadow(0 0 10px rgba(255, 236, 150, 0.9));
}

.t-ghost {
  opacity: 0.55;
  pointer-events: none;
}

.t-ghost.t-illegal {
  opacity: 0.3;
  filter: grayscale(1) brightness(0.7);
}

/*
 * --- placement affordances ---
 *
 * The hit target stays large (a triangle is a small tap target on a phone), but the RESTING
 * mark is deliberately tiny. Face centroids are only ~89px apart, so anything approaching the
 * hit-target size tiles the board into overlapping discs — and since every empty face is legal,
 * that paints 54 of them at once and communicates nothing. A small dot says "you may place
 * here" without competing with the artwork; hover and staging carry the real emphasis.
 */
.t-face {
  position: absolute;
  width: 84px;
  height: 84px;
  margin: -42px 0 0 -42px;
  border-radius: 50%;
  cursor: pointer;
  display: grid;
  place-items: center;
}

/*
 * No resting mark on a placeable face.
 *
 * Every empty face is legal, so marking them all painted 50-odd dots across the board to say
 * something the player already knows. The ghost tile follows the cursor and lands exactly on
 * the face it would occupy, which answers "can I place here, and how would it look" far better
 * than a dot ever did. The hit target stays full size — only the paint is gone.
 */
.t-face::after {
  content: '';
  width: 0;
  height: 0;
  border-radius: 50%;
  transition: width 90ms ease, height 90ms ease, background-color 90ms ease;
}

/*
 * Contrast is set against the LAGOON SAND, which is bright and warm — white at low alpha
 * simply disappears on it. A dark translucent fill with a light rim reads on both the sand
 * and the darker water beyond it.
 */
/* Nothing at rest. Hover still gives a light ring, so it is clear which triangle a click
   would land on when the ghost sits between two of them. */
.t-face-placeable:hover::after {
  width: 58px;
  height: 58px;
  background: transparent;
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.55);
}

.t-face-staged::after {
  width: 74px;
  height: 74px;
  background: rgba(255, 214, 80, 0.22);
  box-shadow: inset 0 0 0 3px rgba(255, 226, 120, 1), 0 0 16px rgba(255, 214, 80, 0.65);
}

/* --- movement affordances --- */
.t-dest {
  position: absolute;
  width: 46px;
  height: 46px;
  margin: -23px 0 0 -23px;
  border-radius: 50%;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.t-dest-reachable {
  background: rgba(0, 150, 190, 0.35);
  box-shadow: inset 0 0 0 3px rgba(150, 240, 255, 0.95), 0 0 10px rgba(0, 184, 212, 0.5);
}

/*
 * The destination the player has CHOSEN, pending confirmation.
 *
 * This used to differ from an offered destination only by hue — mint instead of cyan — which
 * on a board showing a dozen cyan rings is not a difference you notice, especially on a phone
 * in daylight. It now separates on three channels at once: it is larger, it pulses, and it
 * throws a coloured glow outward instead of sitting inside its own outline. Motion is the one
 * of the three that survives being glanced at.
 */
.t-dest-staged {
  background: rgba(120, 255, 180, 0.34);
  box-shadow:
    inset 0 0 0 3px rgba(150, 255, 200, 1),
    0 0 14px 4px rgba(90, 255, 170, 0.55);
  animation: destChosen 1000ms ease-in-out infinite;
  z-index: 4;
}

@keyframes destChosen {
  0%, 100% {
    transform: scale(1);
    box-shadow:
      inset 0 0 0 3px rgba(150, 255, 200, 1),
      0 0 12px 3px rgba(90, 255, 170, 0.5);
  }
  50% {
    transform: scale(1.22);
    box-shadow:
      inset 0 0 0 4px rgba(190, 255, 220, 1),
      0 0 26px 9px rgba(90, 255, 170, 0.85);
  }
}

/*
 * "Send this worker home" — offered as the worker's OWN square during a bonus move. Warm and
 * dashed so it does not read as another destination; without a distinct treatment it is just
 * an unexplained ring underneath the piece.
 */
.t-dest-recall {
  background: rgba(255, 170, 80, 0.18);
  box-shadow: inset 0 0 0 3px rgba(255, 190, 110, 0.9);
  border: 2px dashed rgba(255, 210, 150, 0.75);
}

/*
 * The pearls-collected badge, shown only for the destination under the pointer.
 *
 * Annotating every reachable square at once produced a board full of competing numbers; the
 * question a player is actually asking is "what do I get if I stop HERE", one square at a time.
 */
.t-gain {
  font: 700 17px/1 system-ui, sans-serif;
  color: #08202e;
  background: rgba(255, 255, 255, 0.94);
  border-radius: 9px;
  padding: 2px 7px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
  transform: translateY(-26px);
  white-space: nowrap;
  animation: gainIn 120ms ease-out;
  /*
   * Above the pearls. The destination is itself usually a claimed pearl, and its pulsing halo
   * washed straight over the number — the two strongest pieces of information in the preview
   * were fighting each other. Neither #layer_nodes nor #layer_tokens sets a z-index, so
   * neither opens a stacking context and this can lift out of its own layer; it stays under
   * the workers (z-index 30) so a piece is never hidden behind a badge.
   */
  position: relative;
  z-index: 25;
}

@keyframes gainIn {
  from { opacity: 0; transform: translateY(-18px) scale(0.9); }
  to   { opacity: 1; transform: translateY(-26px) scale(1); }
}

/* --- workers --- */
.t-worker {
  position: absolute;
  width: 58px;
  height: 58px;
  margin: -29px 0 0 -29px;
  cursor: pointer;
  transition: transform 120ms ease;
}

.t-worker.t-selected {
  transform: scale(1.15);
  filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.9));
}

/*
 * Workers waiting in the den. Dimmed and smaller so the lagoon reads as "in play" and the
 * margins as "in reserve" — but still fully visible and clickable, because selecting one is
 * the only way to bring a piece onto the board.
 */
.t-worker.t-reserve {
  opacity: 0.62;
  transform: scale(0.82);
}

.t-worker.t-reserve.t-selected {
  opacity: 1;
  transform: scale(1);
}

/* Only the side to move can act, so only its workers invite a click. */
.t-worker:not(.t-actionable) {
  cursor: default;
}

.t-worker.t-actionable:hover {
  filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.75));
}

.t-worker .octo {
  width: 100%;
  height: 100%;
  overflow: visible;
}

/*
 * Tentacle motion.
 *
 * Each arm is shoulder > mid > tip, nested, each rotating about its own joint. Small rotations
 * COMPOUND down the limb, so the tip travels several times as far as the shoulder and the arm
 * curls instead of waggling rigidly. Segments run at different periods so the curl never
 * repeats cleanly, which is what stops it reading as a mechanism.
 */
.octo-seg { transform-box: view-box; }
.octo-rear { transform-box: view-box; }

/*
 * `--dir` MIRRORS THE LEFT AND RIGHT ARMS. Without it every arm rotates the same way, so the
 * left limb swings out while the right limb swings in and the whole creature reads as leaning
 * sideways rather than as flexing. With it, +1 and −1 curl the outer arms toward each other and
 * the motion reads as the body breathing. The centre arm gets a fraction so it doesn't mirror
 * anything exactly.
 */
.octo-arm-0 { --dir: 1; }
.octo-arm-1 { --dir: -0.45; }
.octo-arm-2 { --dir: -1; }

.octo-seg-1 { animation: segSway 3.4s ease-in-out infinite; }
.octo-seg-2 { animation: segCurl 2.6s ease-in-out infinite; }
.octo-seg-3 { animation: segFlick 1.9s ease-in-out infinite; }

/* Out of phase per arm, so a group of workers never pulses in lockstep. */
.octo-arm-0 .octo-seg-1 { animation-delay: -0.7s; }
.octo-arm-0 .octo-seg-2 { animation-delay: -1.5s; }
.octo-arm-1 .octo-seg-1 { animation-delay: -2.1s; }
.octo-arm-1 .octo-seg-2 { animation-delay: -0.4s; }
.octo-arm-1 .octo-seg-3 { animation-delay: -1.1s; }
.octo-arm-2 .octo-seg-1 { animation-delay: -1.6s; }
.octo-arm-2 .octo-seg-2 { animation-delay: -2.3s; }
.octo-arm-2 .octo-seg-3 { animation-delay: -0.6s; }

/* The rear pair drift slower and wider — background bulk, not focus. */
.octo-rear-0 { animation: rearDrift 5.2s ease-in-out infinite; }
.octo-rear-1 { animation: rearDrift 5.2s ease-in-out infinite reverse; animation-delay: -2.6s; }

/*
 * Amplitudes are per-JOINT and compound: the tip carries shoulder + mid + tip, so a ~45° total
 * swing comes out of three modest rotations. Raising any one of these alone stiffens the limb;
 * the curl comes from the ratio between them.
 */
@keyframes segSway {
  0%, 100% { transform: rotate(calc(var(--dir) * -9deg)); }
  50%      { transform: rotate(calc(var(--dir) * 9deg)); }
}
@keyframes segCurl {
  0%, 100% { transform: rotate(calc(var(--dir) * 13deg)); }
  50%      { transform: rotate(calc(var(--dir) * -14deg)); }
}
@keyframes segFlick {
  0%, 100% { transform: rotate(calc(var(--dir) * -19deg)); }
  50%      { transform: rotate(calc(var(--dir) * 22deg)); }
}
@keyframes rearDrift { 0%, 100% { transform: rotate(-7deg); } 50% { transform: rotate(9deg); } }

/* The beak stays shut except when feeding. */
.octo-maw { transform-box: view-box; transform: scaleY(0.35); opacity: 0.7; }

/*
 * FEEDING. Applied for the moment a pearl is taken (see render.ts): the mantle lunges forward
 * and squashes, every arm snaps inward as if seizing something, and the beak gapes. Short and
 * sharp — a claim is worth several pearls, so this fires repeatedly along a route and must not
 * outstay its welcome.
 */
.t-worker.t-gobbling .octo-body { animation: krakenLunge 380ms cubic-bezier(0.3, 1.4, 0.4, 1); }
.t-worker.t-gobbling .octo-seg-1 { animation: armSeize 380ms ease-in-out; }
.t-worker.t-gobbling .octo-seg-2 { animation: armSeize 380ms ease-in-out; }
.t-worker.t-gobbling .octo-seg-3 { animation: armSeizeTip 380ms ease-in-out; }
.t-worker.t-gobbling .octo-maw { animation: mawGape 380ms ease-out; }

@keyframes krakenLunge {
  0%   { transform: scale(1, 1); }
  30%  { transform: scale(1.16, 0.86) translateY(2px); }
  60%  { transform: scale(0.93, 1.1) translateY(-2px); }
  100% { transform: scale(1, 1); }
}
/*
 * Mirrored via --dir, and in two beats: a brief outward FLARE (anticipation) and then a clutch
 * inward past neutral. A single inward sweep reads as the arms merely drooping; the flare is
 * what makes the grab register in under 400ms. Positive --dir opens the arms outward here —
 * the shoulder strokes run down-and-out, so a clockwise rotation lifts them away from centre.
 */
@keyframes armSeize {
  0%, 100% { transform: rotate(0deg); }
  20%      { transform: rotate(calc(var(--dir) * 11deg)); }
  45%      { transform: rotate(calc(var(--dir) * -30deg)); }
}
@keyframes armSeizeTip {
  0%, 100% { transform: rotate(0deg); }
  20%      { transform: rotate(calc(var(--dir) * 16deg)); }
  45%      { transform: rotate(calc(var(--dir) * -48deg)); }
}
@keyframes mawGape {
  0%   { transform: scaleY(0.35); }
  30%  { transform: scaleY(1.5); }
  100% { transform: scaleY(0.35); }
}

@media (prefers-reduced-motion: reduce) {
  .octo-seg, .octo-rear, .octo-maw, .t-token, .t-token::after { animation: none; }
  /*
   * The claim highlight must SURVIVE reduced motion — it carries information ("these are the
   * pearls you would take"), not decoration. Blanket `animation: none` above would erase it,
   * so it is restated here as a static ring at the pulse's bright extreme.
   */
  .t-token-claiming {
    transform: scale(1.3);
    box-shadow:
      0 0 0 2px rgba(120, 74, 8, 0.7),
      0 0 0 5px rgba(247, 205, 92, 1),
      0 0 22px 7px rgba(240, 168, 40, 0.8);
  }
  .t-token-shared.t-token-claiming { transform: translate(11px, -11px) scale(1.12); }
  .t-worker.t-gobbling .octo-body { animation: none; }
}

/* Abstract theme: workers are plain discs, no SVG. */
.t-theme-abstract .t-worker {
  border-radius: 50%;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 -3px 6px rgba(0, 0, 0, 0.25);
}

/* --- path overlay --- */
/*
 * The route the worker would take.
 *
 * Drawn as TWO strokes: a dark casing, then the travelling dashes over it. A single white
 * dashed line is nearly invisible against the pale sand that covers most of the board — which
 * is most of why players reported not being able to tell where a move was going. The casing
 * gives it an edge on sand without making it heavy over the darker water.
 */
.t-path-casing {
  fill: none;
  stroke: rgba(4, 22, 34, 0.55);
  stroke-width: 11;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.t-path {
  fill: none;
  stroke: #ffffff;
  stroke-width: 6;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 11 11;
  animation: pathFlow 900ms linear infinite;
}

@keyframes pathFlow {
  to { stroke-dashoffset: -22; }
}

@media (prefers-reduced-motion: reduce) {
  .t-path { animation: none; }
  .t-dest-staged { animation: none; transform: scale(1.15); }
}

/*
 * Closed roads.
 *
 * A dark scrim laid over the painted ribbon, plus a dashed bar pattern on top so it reads as
 * "barred" rather than merely "dim" — dimming alone is easy to mistake for the art. The
 * geometry mirrors the sprite generator's beziers (see layout.ts roadSegments), so the scrim
 * follows the current rather than cutting across it.
 */
/*
 * Closed roads: the colour is drained OUT OF THE SPRITE, not painted over it.
 *
 * The roads live inside the tile spritesheet, so nothing drawn on top can soften them — a
 * stroke just covers them up. Two earlier attempts did exactly that: an opaque scrim (obvious
 * paint), then `mix-blend-mode: saturation` (which silently did nothing, because the SVG
 * overlay is its own stacking context and so blended against a transparent backdrop, leaving
 * a flat grey stroke — "grey goo" with no road underneath).
 *
 * `backdrop-filter` genuinely filters the pixels already painted behind the element, across
 * stacking contexts. Clipping it to the road ribbons therefore desaturates the artwork itself:
 * the current keeps its shape, its foam highlights and its chevrons, and only loses its hue.
 */
.t-closed-mask {
  position: absolute;
  inset: 0;
  width: 1100px;
  height: 1100px;
  pointer-events: none;
  backdrop-filter: var(--t-closed-filter);
  -webkit-backdrop-filter: var(--t-closed-filter);
}

/*
 * How hard to drain a closed road. One place to tune.
 *
 * PARTIAL, not full: at grayscale(1) a closed road loses its colour entirely, and since the
 * colour IS the rule ("roads of the closed colour cannot be travelled"), that throws away the
 * very thing the player needs to reason about. Measured against the open roads, this setting
 * keeps roughly 60% of the original colourfulness — enough to read the hue at a glance, little
 * enough that the road plainly looks inactive.
 */
:root {
  --t-closed-filter: grayscale(0.55) brightness(0.8) contrast(0.95);
}

/* Without backdrop-filter there is no way to alter the sprite, so fall back to a translucent
   scrim: visibly worse, but still unambiguous about which roads are shut. */
@supports not ((backdrop-filter: grayscale(1)) or (-webkit-backdrop-filter: grayscale(1))) {
  .t-closed-mask { background: rgba(10, 26, 36, 0.55); }
}

/* --- move animation ---
 * The worker is moved by writing left/top directly rather than by re-rendering the scene each
 * frame: a full re-render replaces the element mid-flight, which cancels the transition and
 * (as the click handling already taught us) detaches the node under the pointer.
 */
.t-worker.t-moving {
  z-index: 40;
  filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.8));
}

/* A pearl being collected: it should feel taken, not just deleted. */
.t-token.t-collected {
  animation: pearlCollect 420ms cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
  pointer-events: none;
}

/* Drawn INTO the creature rather than drifting off: it is being eaten, not evaporating. */
@keyframes pearlCollect {
  0%   { transform: scale(1);    opacity: 1; filter: brightness(1); }
  25%  { transform: scale(1.75); opacity: 1; filter: brightness(2.3); }
  100% { transform: scale(0.08); opacity: 0; filter: brightness(0.8); }
}

/* Score flash on the player who just claimed. */
@keyframes scorePop {
  0%, 100% { transform: scale(1); }
  40%      { transform: scale(1.35); }
}

@media (prefers-reduced-motion: reduce) {
  .t-token.t-collected { animation-duration: 1ms; }
}

/*
 * The den drop target: the selected worker's own, currently-empty slot.
 *
 * Recall is expressed in the rules as "move to your own square", which is underneath the piece
 * and so cannot be clicked. Offering the den slot instead is both clickable and self-evident —
 * you are putting the piece back where it came from. It is only ever drawn for a worker that
 * may legally return, and each worker keeps a fixed slot, so the space is guaranteed free.
 */
.t-den-target {
  position: absolute;
  width: 58px;
  height: 58px;
  margin: -29px 0 0 -29px;
  border-radius: 50%;
  cursor: pointer;
  pointer-events: auto;
  opacity: 0.34;
  box-shadow: inset 0 0 0 2px rgba(255, 226, 120, 0.85), 0 0 14px rgba(255, 214, 80, 0.35);
  transition: opacity 120ms ease, transform 120ms ease;
  animation: denBeckon 2.2s ease-in-out infinite;
}

.t-den-target .octo { width: 100%; height: 100%; overflow: visible; }

.t-den-target:hover {
  opacity: 0.72;
  transform: scale(1.08);
  animation: none;
}

.t-den-target.t-den-staged {
  opacity: 0.95;
  animation: none;
  box-shadow: inset 0 0 0 3px rgba(255, 226, 120, 1), 0 0 18px rgba(255, 214, 80, 0.75);
}

/* A slow pulse so an empty slot reads as "available" rather than as leftover art. */
@keyframes denBeckon {
  0%, 100% { opacity: 0.28; }
  50%      { opacity: 0.46; }
}

@media (prefers-reduced-motion: reduce) {
  .t-den-target { animation: none; }
}
