/* ============================ Goblins! — styles ===========================

   A lava swamp: dark sludge at the edges, molten rivers creeping through the
   middle, gray stepping stones lit orange from underneath, and volcanoes along
   the back. Everything on the board is placed in percentages by the game, so
   this file only ever says how big things are and what they look like — never
   where they go.
   ========================================================================= */

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* 100dvh is the *visible* viewport. Plain 100% includes the strip hiding behind
   a phone's address bar, which pushes the bottom of the game off the screen. */
html, body {
  margin: 0;
  height: 100%;
  height: 100dvh;
  overflow: hidden;
  font-family: "Baloo 2", system-ui, "Segoe UI", sans-serif;
  user-select: none;
  -webkit-user-select: none;
  /* The viewport meta says user-scalable=no, but phones have ignored that for
     years. touch-action is what actually stops double-tap-to-zoom. */
  touch-action: manipulation;
  background: #150c0d;
}

#game {
  position: fixed;
  inset: 0;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: #150c0d;
}

/* --------------------------------- HUD ----------------------------------- */

#hud {
  flex: 0 0 auto;
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: clamp(6px, 2.2vw, 20px);
  flex-wrap: wrap;
  padding:
    calc(clamp(6px, 2vw, 14px) + env(safe-area-inset-top, 0px))
    calc(clamp(8px, 2vw, 16px) + env(safe-area-inset-right, 0px))
    clamp(6px, 2vw, 14px)
    calc(clamp(8px, 2vw, 16px) + env(safe-area-inset-left, 0px));
  background: linear-gradient(180deg, rgba(10, 5, 6, 0.85), rgba(10, 5, 6, 0));
}

.hud-item { flex: 0 0 auto; text-align: center; }

#timer-num {
  font-size: clamp(28px, 7vw, 46px);
  font-weight: 800;
  line-height: 1;
  color: #ffb347;
  text-shadow: 0 0 12px rgba(255, 120, 20, 0.6);
}

/* The last few seconds go red and pulse. */
#timer-num.low { color: #ff5061; animation: pulse 0.6s ease-in-out infinite; }
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.18); }
}

.mini-bar {
  width: clamp(64px, 18vw, 110px);
  height: 10px;
  margin-top: 4px;
  border-radius: 99px;
  background: rgba(255, 180, 90, 0.2);
  overflow: hidden;
}

.mini-bar > div {
  width: 100%;
  height: 100%;
  border-radius: 99px;
  background: linear-gradient(90deg, #ffd23f, #e0402f);
  transform-origin: left center;
  transition: transform 0.15s linear;
}

#timer-num.low ~ .mini-bar > div { background: #ff5061; }

.stat-label {
  font-size: clamp(13px, 3.2vw, 19px);
  font-weight: 700;
  color: #f0c99a;
  white-space: nowrap;
}

.stat-label b { color: #ffb347; font-size: 1.15em; }
#goblin-goal { color: #c99b70; font-size: 0.95em; }

/* A tiny mud blob for the HUD label, drawn the same way the real splotches
   are — no emoji, so it can't turn into an empty box on anybody's phone. */
.mud-chip {
  display: inline-block;
  width: 0.95em;
  height: 0.95em;
  vertical-align: -0.12em;
  border-radius: 62% 38% 47% 53% / 55% 44% 56% 45%;
  background: radial-gradient(58% 58% at 38% 32%, #8a5f38 0%, #56361c 55%, #33200f 100%);
}

/* ------------------------------ The baby wipe ----------------------------- */

/* Your baby wipe. It lives in the top bar so it is always on the screen, and
   the little you holds one exactly like it down in the swamp — same class, two
   sizes, so they can never drift apart.

   Drawn in CSS for the same reason as the mud blob above it: the wipe and
   sponge emoji come out as empty boxes on a lot of devices, and this is the one
   thing in the game you're never allowed to lose sight of.

   Everything is in em, so it's the font-size of whatever it's sitting in that
   decides how big it is. */
.wipe {
  display: inline-block;
  position: relative;
  width: 1.2em;
  height: 0.95em;
  vertical-align: -0.16em;
  border-radius: 5px 5px 6px 6px / 5px 5px 8px 8px;
  background: linear-gradient(180deg, #ffffff 0%, #eaf6ff 55%, #cbe4fa 100%);
  box-shadow:
    inset 0 -2px 3px rgba(110, 165, 205, 0.5),
    0 1px 3px rgba(0, 0, 0, 0.45);
}

/* The damp stripe across the middle. */
.wipe::before {
  content: "";
  position: absolute;
  left: 14%;
  right: 14%;
  top: 36%;
  height: 15%;
  border-radius: 99px;
  background: rgba(88, 172, 228, 0.5);
}

/* One corner turned up, so it reads as a soft cloth and not a playing card. */
.wipe::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: 34%;
  height: 36%;
  border-radius: 0 5px 0 70%;
  background: linear-gradient(135deg, #ffffff, #b6d8f4);
  box-shadow: -1px 1px 2px rgba(0, 0, 0, 0.22);
}

/* A flick of the wrist every time a splotch comes off, so the wipe is visibly
   the thing doing the cleaning. */
#wipe-chip.flick { animation: wipeFlick 340ms ease; }
@keyframes wipeFlick {
  0% { transform: rotate(0deg) scale(1); }
  28% { transform: rotate(-24deg) scale(1.3); }
  62% { transform: rotate(16deg) scale(1.12); }
  100% { transform: rotate(0deg) scale(1); }
}

.bump { display: inline-block; animation: bump 0.3s ease; }
@keyframes bump {
  0% { transform: scale(1); }
  50% { transform: scale(1.6); color: #fff36b; }
  100% { transform: scale(1); }
}

#quit-btn {
  flex: 0 0 auto;
  margin-left: auto;
  font-size: clamp(18px, 4.6vw, 28px);
  line-height: 1;
  background: rgba(60, 30, 20, 0.92);
  border: 3px solid #e0802f;
  border-radius: 50%;
  width: clamp(42px, 11vw, 56px);
  height: clamp(42px, 11vw, 56px);
  cursor: pointer;
  box-shadow: 0 4px 0 rgba(120, 50, 20, 0.6);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
  touch-action: manipulation;
}

#quit-btn:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 rgba(120, 50, 20, 0.6);
}

/* ------------------------------ The swamp -------------------------------- */

/* min-height:0 is required, or this refuses to shrink inside the flex column
   and the doctor's bank gets pushed off the bottom of a short screen. */
#swamp {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  overflow: hidden;
  touch-action: manipulation;
}

/* The lava. Three molten pools drifting slowly under a dark sludgy swamp, so
   the ground is always moving a little without anything ever jumping. */
#lava {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(78% 30% at 20% 55%, rgba(255, 132, 30, 0.55) 0%, rgba(255, 60, 10, 0.24) 45%, transparent 72%),
    radial-gradient(66% 26% at 78% 34%, rgba(255, 168, 44, 0.5) 0%, rgba(220, 50, 10, 0.2) 48%, transparent 74%),
    radial-gradient(92% 34% at 52% 90%, rgba(255, 110, 20, 0.5) 0%, rgba(190, 40, 10, 0.2) 50%, transparent 76%),
    radial-gradient(120% 85% at 50% 0%, #3b2a17 0%, #24180f 42%, #120b0a 100%);
  background-size: 200% 200%, 185% 185%, 220% 220%, 100% 100%;
  background-position: 0% 50%, 100% 40%, 50% 100%, center;
  animation: lavaCreep 26s ease-in-out infinite alternate;
}

@keyframes lavaCreep {
  to { background-position: 34% 62%, 68% 58%, 38% 84%, center; }
}

/* A slow heat shimmer over the top, plus a dark vignette so the edges of the
   swamp fade off into somewhere you don't want to go. */
#lava::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 100% at 50% 55%, transparent 40%, rgba(8, 3, 4, 0.75) 100%);
  animation: heat 5s ease-in-out infinite alternate;
}

@keyframes heat {
  from { opacity: 0.75; }
  to { opacity: 1; }
}

/* ------------------------------ Volcanoes -------------------------------- */

/* The wrapper is what you touch and what the bubbles float out of. It doesn't
   move — only the mountain inside it does — so an eruption never drags the
   bubbles around with it. */
.volcano {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: clamp(28px, 7.5vmin, 60px);
  line-height: 1;
  cursor: pointer;
}

.volcano-face {
  display: block;
  filter: drop-shadow(0 0 10px rgba(255, 120, 20, 0.55));
}

.volcano-face.erupting { animation: erupt 900ms ease-out; }
@keyframes erupt {
  0% { transform: scale(1); }
  14% {
    transform: translateY(-8%) scale(1.4);
    filter: drop-shadow(0 0 30px rgba(255, 210, 63, 1)) brightness(1.7);
  }
  45% { transform: scale(1.12); }
  100% { transform: scale(1); }
}

/* ------------------------------ Warm bubbles ------------------------------ */

/* The volcanoes bubble. Always, quietly, three at a time on a stagger — and a
   whole crowd of them when one goes off or when you put a finger on it. Warm
   gold with a highlight, so they read as something you'd want to touch rather
   than something that would burn you. */
.bubble {
  position: absolute;
  left: 50%;
  top: 14%;
  width: 0.3em;
  height: 0.3em;
  margin-left: -0.15em;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle at 32% 28%,
    rgba(255, 255, 226, 0.95) 0%,
    rgba(255, 200, 92, 0.8) 44%,
    rgba(255, 122, 32, 0.4) 100%);
  box-shadow: 0 0 9px rgba(255, 184, 64, 0.85);
  opacity: 0;
  animation: bubbleUp 2.7s ease-out infinite;
}

.bubble.burst { animation-iteration-count: 1; animation-fill-mode: forwards; }

@keyframes bubbleUp {
  0% { transform: translate(0, 0) scale(calc(var(--bs, 1) * 0.35)); opacity: 0; }
  16% { opacity: 0.95; }
  70% { opacity: 0.8; }
  100% {
    transform: translate(var(--drift, 0px), -2.5em) scale(calc(var(--bs, 1) * 1.3));
    opacity: 0;
  }
}

/* The ground jolt that comes with an eruption. */
#swamp.shake { animation: swampShake 600ms ease; }
@keyframes swampShake {
  0%, 100% { transform: translate(0, 0); }
  12% { transform: translate(-7px, 4px); }
  28% { transform: translate(6px, -5px); }
  44% { transform: translate(-5px, -2px); }
  62% { transform: translate(4px, 4px); }
  82% { transform: translate(-2px, 1px); }
}

/* --------------------------- Stepping stones ------------------------------ */

/* The only dry land in the whole place, and the only spots a goblin will
   stand on. Lit orange along the bottom by the lava underneath. */
.rock {
  position: absolute;
  width: clamp(46px, 13vmin, 96px);
  height: clamp(46px, 13vmin, 96px);
  transform: translate(-50%, -50%) rotate(var(--tilt, 0deg)) scale(var(--sc, 1)) scaleY(var(--sq, 0.6));
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(62% 62% at 38% 28%, #918d88 0%, #66625f 45%, #3b3836 100%);
  box-shadow:
    0 0 0 3px rgba(28, 18, 14, 0.55),
    0 10px 20px rgba(0, 0, 0, 0.55),
    0 0 26px rgba(255, 110, 30, 0.35);
}

/* -------------------------------- The mud -------------------------------- */

/* Drawn in CSS rather than as an emoji on purpose: the mud-splotch emoji is a
   very new one and shows up as an empty box on a lot of devices. A blob is a
   blob everywhere, and it reads better against lava anyway. Every splotch gets
   its own random rotation and size from the game. */
.splotch {
  position: absolute;
  width: calc(clamp(42px, 11.5vmin, 80px) * var(--tscale, 1));
  height: calc(clamp(42px, 11.5vmin, 80px) * var(--tscale, 1));
  transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(var(--sz, 1));
  border-radius: 62% 38% 47% 53% / 55% 44% 56% 45%;
  cursor: pointer;
  background: radial-gradient(58% 58% at 38% 30%, #7d5432 0%, #56361c 44%, #31200f 100%);
  box-shadow:
    inset 0 -7px 12px rgba(0, 0, 0, 0.45),
    inset 0 5px 9px rgba(198, 148, 96, 0.3),
    0 5px 12px rgba(0, 0, 0, 0.55);
}

/* Splashes around the edge, so a splotch looks thrown rather than placed. */
.splotch::before,
.splotch::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.4);
}

.splotch::before {
  width: 30%;
  height: 30%;
  right: -11%;
  top: 7%;
  background: #56361c;
}

.splotch::after {
  width: 20%;
  height: 20%;
  left: -8%;
  bottom: 13%;
  background: #4a2f16;
}

/* Mud still in the air: it exists and it counts, but you can't see or tap it
   until it lands. */
.splotch.hidden { opacity: 0; pointer-events: none; }

/* Landing. `backwards` matters — the staggered mud at the start of a game has
   an animation-delay, and without it every splotch would sit there fully
   visible waiting for its turn to pop. */
.splotch.pop {
  animation: splotchPop 380ms cubic-bezier(0.34, 1.6, 0.64, 1) backwards;
}

@keyframes splotchPop {
  0% { transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(0.05); opacity: 0; }
  60% { transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(calc(var(--sz, 1) * 1.3)); opacity: 1; }
  100% { transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(var(--sz, 1)); opacity: 1; }
}

/* Wiped. pointer-events go first so a fast second tap can't land on something
   that's already been counted. Keep the timing in step with timings.splatMs. */
.splotch.clean {
  pointer-events: none;
  animation: splotchClean 320ms ease forwards;
}

@keyframes splotchClean {
  0% { transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(var(--sz, 1)); opacity: 1; }
  35% { transform: translate(-50%, -50%) rotate(var(--rot, 0deg)) scale(calc(var(--sz, 1) * 1.4)); opacity: 0.9; }
  100% { transform: translate(-50%, -50%) rotate(calc(var(--rot, 0deg) + 50deg)) scale(0.05); opacity: 0; }
}

/* ------------------------------- The goblins ------------------------------ */

/* A hop is just left/top changing — the transition does the moving. The easing
   on top overshoots a little so it reads as a jump rather than a slide. */
.critter {
  position: absolute;
  font-size: calc(clamp(34px, 9vmin, 62px) * var(--tscale, 1));
  line-height: 1;
  cursor: pointer;
  transform: translate(-50%, -60%) scaleX(var(--flip, 1));
  filter: drop-shadow(0 5px 6px rgba(0, 0, 0, 0.65));
  transition:
    left var(--hop, 900ms) cubic-bezier(0.36, -0.2, 0.5, 1.25),
    top var(--hop, 900ms) cubic-bezier(0.34, 1.35, 0.64, 1);
  animation: bob 1.1s ease-in-out infinite;
}

.critter.facing-left { --flip: -1; }

@keyframes bob {
  0%, 100% { transform: translate(-50%, -60%) scaleX(var(--flip, 1)); }
  50% { transform: translate(-50%, -67%) scaleX(var(--flip, 1)) rotate(3deg); }
}

/* Everything below replaces the bob while its class is on — an element only
   ever runs the last animation declared for it, which is exactly what we want
   here. Order in this file is what decides that, so don't move these up. */

.critter.arriving { animation: critterIn 380ms cubic-bezier(0.34, 1.6, 0.64, 1); }
@keyframes critterIn {
  0% { transform: translate(-50%, -140%) scaleX(var(--flip, 1)) scale(0.3); opacity: 0; }
  70% { transform: translate(-50%, -54%) scaleX(var(--flip, 1)) scale(1.15); opacity: 1; }
  100% { transform: translate(-50%, -60%) scaleX(var(--flip, 1)) scale(1); opacity: 1; }
}

/* The Goblin King. The crown says boss; the size says it too. */
.critter.king {
  font-size: calc(clamp(42px, 11vmin, 76px) * var(--tscale, 1));
  filter: drop-shadow(0 5px 8px rgba(0, 0, 0, 0.7)) drop-shadow(0 0 14px rgba(255, 60, 40, 0.5));
}

.critter.king::after {
  content: "👑";
  position: absolute;
  top: -0.42em;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.5em;
}

/* The Goblin Princess is the same goblin turned pink — the tint is on HER, not
   on the swamp around her. The bow gets the tint spun back off it so it stays
   a bow-colored bow. */
.critter.princess {
  font-size: calc(clamp(40px, 10.5vmin, 72px) * var(--tscale, 1));
  filter:
    drop-shadow(0 5px 8px rgba(0, 0, 0, 0.7))
    hue-rotate(292deg) saturate(1.6) brightness(1.12);
}

.critter.princess::after {
  content: "🎀";
  position: absolute;
  top: -0.34em;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.46em;
  filter: hue-rotate(-292deg) saturate(0.62);
}

/* The doctor's rescue. He glows so you spot him instantly in all that brown,
   and carries a bomb so it's obvious what tapping him is going to do. */
.critter.doctor {
  filter:
    drop-shadow(0 5px 6px rgba(0, 0, 0, 0.65))
    drop-shadow(0 0 16px rgba(120, 255, 220, 0.95));
  animation: docPulse 0.66s ease-in-out infinite;
}

.critter.doctor::after {
  content: "💣";
  position: absolute;
  bottom: -0.12em;
  right: -0.3em;
  font-size: 0.52em;
}

@keyframes docPulse {
  0%, 100% { transform: translate(-50%, -60%) scaleX(var(--flip, 1)) scale(1); }
  50% { transform: translate(-50%, -66%) scaleX(var(--flip, 1)) scale(1.14); }
}

/* A royal taking a hit that isn't the last one. */
.critter.hit { animation: critterHit 380ms ease; }
@keyframes critterHit {
  0%, 100% { transform: translate(-50%, -60%) scaleX(var(--flip, 1)); filter: none; }
  20% { transform: translate(-52%, -58%) scaleX(var(--flip, 1)) rotate(-16deg); filter: brightness(2.4); }
  55% { transform: translate(-48%, -62%) scaleX(var(--flip, 1)) rotate(14deg); filter: brightness(1.1); }
  80% { transform: translate(-51%, -60%) scaleX(var(--flip, 1)) rotate(-7deg); }
}

/* Bombed and waiting for it to land — no more taps, and no more hops either,
   because the game has already taken them off the board. */
.critter.doomed { pointer-events: none; }

.critter.boom { animation: critterBoom 480ms ease forwards; }
@keyframes critterBoom {
  0% { transform: translate(-50%, -60%) scale(1); opacity: 1; }
  25% { transform: translate(-50%, -80%) scale(1.5) rotate(20deg); opacity: 1; filter: brightness(2.6); }
  100% { transform: translate(-50%, -160%) scale(0.2) rotate(160deg); opacity: 0; }
}

.critter.leaving {
  pointer-events: none;
  animation: critterLeave 500ms ease forwards;
}
@keyframes critterLeave {
  0% { transform: translate(-50%, -60%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -30%) scale(0.4); opacity: 0; }
}

.critter.fleeing {
  pointer-events: none;
  animation: critterFlee 500ms ease-in forwards;
}
@keyframes critterFlee {
  0% { transform: translate(-50%, -60%) scale(1); opacity: 1; }
  30% { transform: translate(-50%, -95%) scale(1.15) rotate(-14deg); opacity: 1; }
  100% { transform: translate(-50%, -420%) scale(0.5) rotate(-40deg); opacity: 0; }
}

/* -------------------------------- The little you -------------------------- */

/* You, in the swamp. It moves the way the goblins move — left and top change,
   a transition carries it — but a good bit quicker than a hop, because it is
   chasing your finger and a finger doesn't wait.

   pointer-events: none is not a detail. Without it the little you would eat the
   taps meant for the mud it is standing on, and the game would feel broken in a
   way that would be very hard to work out. */
#me {
  position: absolute;
  left: 50%;
  top: 84%;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  font-size: calc(clamp(30px, 8vmin, 54px) * var(--tscale, 1));
  line-height: 1;
  transform: translate(-50%, -62%) scaleX(var(--flip, 1));
  filter: drop-shadow(0 5px 6px rgba(0, 0, 0, 0.6));
  transition:
    left 360ms cubic-bezier(0.3, 0.85, 0.4, 1),
    top 360ms cubic-bezier(0.3, 0.85, 0.4, 1),
    opacity 250ms ease;
}

#me.in { opacity: 1; }
#me.facing-left { --flip: -1; }
#me .me-body { display: block; }

/* The wipe in your hand. Same .wipe as the one in the top bar, sized and pinned
   to the side of you. */
#me .me-wipe {
  position: absolute;
  right: -0.3em;
  bottom: 0.02em;
  width: 0.48em;
  height: 0.38em;
  transform-origin: 25% 80%;
}

#me .me-wipe.swipe { animation: wipeSwipe 400ms ease; }
@keyframes wipeSwipe {
  0% { transform: rotate(0deg) translateY(0) scale(1); }
  25% { transform: rotate(-50deg) translateY(-0.18em) scale(1.3); }
  58% { transform: rotate(40deg) translateY(0.06em) scale(1.15); }
  100% { transform: rotate(0deg) translateY(0) scale(1); }
}

/* Jogging on the spot because YOU are jogging on the spot. --jog comes from
   me.js and is shorter the harder you go, so the legs really do speed up. */
#me.jogging { animation: meJog var(--jog, 320ms) linear infinite; }
@keyframes meJog {
  0%, 100% { transform: translate(-50%, -62%) scaleX(var(--flip, 1)) rotate(-5deg); }
  50% { transform: translate(-50%, -73%) scaleX(var(--flip, 1)) rotate(5deg); }
}

/* Declared after .jogging on purpose — an element only runs the last animation
   declared for it, and a cheer should always beat a jog. */
#me.cheer { animation: meCheer 900ms ease; }
@keyframes meCheer {
  0%, 100% { transform: translate(-50%, -62%) scaleX(var(--flip, 1)) scale(1); }
  20% { transform: translate(-50%, -98%) scaleX(var(--flip, 1)) scale(1.22) rotate(-9deg); }
  45% { transform: translate(-50%, -62%) scaleX(var(--flip, 1)) scale(1); }
  68% { transform: translate(-50%, -94%) scaleX(var(--flip, 1)) scale(1.18) rotate(9deg); }
}

/* The doctor has decided you're a hero, and a hero gets a cape. It sits behind
   you rather than on you: #me already has a transform on it, which makes it a
   stacking context of its own, so a negative z-index here goes behind the
   emoji and no further. */
#me.hero .me-body { filter: drop-shadow(0 0 14px rgba(255, 210, 63, 0.95)); }

#me.hero::after {
  content: "";
  position: absolute;
  left: 0.1em;
  top: 0.26em;
  z-index: -1;
  width: 0.78em;
  height: 0.9em;
  background: linear-gradient(180deg, #ff5061 0%, #b3122a 100%);
  border-radius: 42% 42% 55% 55% / 18% 18% 82% 82%;
  transform-origin: 50% 0;
  animation: capeFlap 900ms ease-in-out infinite;
}

@keyframes capeFlap {
  0%, 100% { transform: rotate(-5deg) skewX(4deg); }
  50% { transform: rotate(5deg) skewX(-6deg) scaleY(1.06); }
}

/* ---------------------------- Things in the air --------------------------- */

/* Mud on its way down from a volcano, or off a royal's hand. Pinned to the
   page rather than to the swamp, so nothing on the board ever reflows. */
.mud-flier {
  position: fixed;
  z-index: 50;
  font-size: clamp(24px, 6.5vmin, 44px);
  width: 1em;
  height: 1em;
  margin-left: -0.5em;
  margin-top: -0.5em;
  pointer-events: none;
  border-radius: 60% 40% 55% 45% / 50% 55% 45% 50%;
  background: radial-gradient(58% 58% at 38% 30%, #7d5432 0%, #4a2e14 62%, #2a190b 100%);
  box-shadow: 0 5px 12px rgba(0, 0, 0, 0.6);
  transform: translate(0, 0);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.35, -0.45, 0.6, 1);
}

/* The doctor's bomb, lobbed from the bank. */
.bomb-flier {
  position: fixed;
  z-index: 52;
  font-size: clamp(26px, 7vmin, 46px);
  line-height: 1;
  margin-left: -0.5em;
  margin-top: -0.5em;
  pointer-events: none;
  transform: translate(0, 0);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.3, -0.6, 0.6, 1);
  filter: drop-shadow(0 4px 4px rgba(0, 0, 0, 0.5));
}

.blast {
  position: fixed;
  z-index: 55;
  font-size: clamp(42px, 12vmin, 84px);
  line-height: 1;
  margin-left: -0.5em;
  margin-top: -0.5em;
  pointer-events: none;
  animation: blastPop 480ms ease-out forwards;
}

@keyframes blastPop {
  0% { transform: scale(0.25); opacity: 0; }
  28% { transform: scale(1.35); opacity: 1; }
  100% { transform: scale(1.8); opacity: 0; }
}

/* ---------------------------- The doctor's bank --------------------------- */

#doctor-bay {
  flex: 0 0 auto;
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(8px, 3vw, 22px);
  height: clamp(50px, 11vh, 84px);
  padding: 0 12px calc(4px + env(safe-area-inset-bottom, 0px));
  pointer-events: none;
  background: linear-gradient(0deg, rgba(10, 5, 6, 0.9), rgba(10, 5, 6, 0));
}

#doc {
  font-size: clamp(32px, 8.5vw, 56px);
  line-height: 1;
  filter: drop-shadow(0 4px 5px rgba(0, 0, 0, 0.6)) drop-shadow(0 0 10px rgba(120, 255, 220, 0.4));
}

#doc.throw { animation: docThrow 340ms ease; }
@keyframes docThrow {
  0%, 100% { transform: rotate(0deg) translateY(0); }
  30% { transform: rotate(-22deg) translateY(-6px) scale(1.1); }
  60% { transform: rotate(16deg) translateY(2px); }
}

/* You won, and he is beside himself. Arms up, glowing gold, and he stays that
   way until the next round starts. */
#doc.proud {
  filter:
    drop-shadow(0 4px 5px rgba(0, 0, 0, 0.6))
    drop-shadow(0 0 18px rgba(255, 210, 63, 0.95));
  animation: docProud 1.1s ease-in-out infinite;
}

@keyframes docProud {
  0%, 100% { transform: translateY(0) rotate(-7deg) scale(1); }
  50% { transform: translateY(-9px) rotate(7deg) scale(1.18); }
}

/* This used to be one line with nowrap and an ellipsis on it, which is exactly
   how the wipe in "Tap the mud! 🧽" came to be invisible on a phone: the
   emoji was on the end, so the emoji was the first thing chopped off. It wraps
   now, and the thing you clean with lives in the top bar where nothing can cut
   it off at all. */
#doc-hint {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 24ch;
  font-size: clamp(13px, 3.4vw, 20px);
  font-weight: 800;
  line-height: 1.15;
  color: #ffd9a8;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8);
  overflow-wrap: break-word;
}

/* ------------------------------- The banner ------------------------------- */

#banner {
  position: fixed;
  left: 50%;
  top: 20%;
  z-index: 50;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.4);
  padding: clamp(10px, 2.6vw, 18px) clamp(16px, 5vw, 34px);
  max-width: 92vw;
  text-align: center;
  font-size: clamp(16px, 4.4vw, 29px);
  font-weight: 800;
  color: #fff;
  background: linear-gradient(180deg, #ff7a4a, #e0402f);
  border: 4px solid #fff;
  border-radius: 22px;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.55);
}

#banner.show {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
  animation: bannerIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}

#banner.show.win { background: linear-gradient(180deg, #7be06a, #2f9d4e); }
#banner.show.lose { background: linear-gradient(180deg, #8a4a6a, #4a1f38); }

@keyframes bannerIn {
  0% { transform: translate(-50%, -50%) scale(0.3) rotate(-6deg); opacity: 0; }
  60% { transform: translate(-50%, -50%) scale(1.12) rotate(2deg); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; }
}

/* -------------------------- The race to Goblin City ----------------------- */

/* Above the banner (50) and below the cards (60): a race is always finished and
   gone before a win card can appear, and nothing else should ever be on top of
   it while it's open. */
#race {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: none;
  align-items: center;
  justify-content: center;
  padding: clamp(12px, 3vw, 24px);
  background: radial-gradient(72% 72% at 50% 42%, rgba(70, 20, 12, 0.88), rgba(12, 5, 6, 0.97));
  touch-action: manipulation;
}

#race.show { display: flex; }

.race-card {
  width: min(680px, 100%);
  text-align: center;
  animation: cardIn 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.race-title {
  font-size: clamp(19px, 5.2vw, 36px);
  font-weight: 800;
  color: #ffd23f;
  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.5);
}

.race-how {
  margin-top: 4px;
  font-size: clamp(12px, 3.2vw, 19px);
  font-weight: 700;
  color: #ffd9a8;
}

.track {
  display: flex;
  flex-direction: column;
  gap: clamp(14px, 3vw, 22px);
  margin: clamp(16px, 4vw, 28px) 0 clamp(10px, 2.4vw, 16px);
}

/* A lane is a strip with a castle parked at the end of it. The runner inside is
   placed by percentage, exactly like everything on the swamp, so the track is
   the same track on a phone and on a big screen. */
.lane {
  position: relative;
  height: clamp(54px, 13vmin, 92px);
  border-radius: 18px;
  border: 3px dashed rgba(255, 190, 120, 0.45);
  background: linear-gradient(180deg, rgba(96, 48, 26, 0.75), rgba(40, 18, 12, 0.75));
}

.lane-who {
  position: absolute;
  left: 12px;
  top: -0.8em;
  padding: 0 8px;
  border-radius: 99px;
  font-size: clamp(10px, 2.6vw, 14px);
  font-weight: 800;
  letter-spacing: 1px;
  color: #ffb347;
  background: #2a1109;
}

.lane-city {
  position: absolute;
  right: 2%;
  top: 50%;
  transform: translateY(-50%);
  font-size: clamp(28px, 8vmin, 54px);
  line-height: 1;
  filter: drop-shadow(0 0 12px rgba(255, 180, 80, 0.65));
}

/* left is set by race.js, 0..100 across the lane. */
.runner {
  position: absolute;
  left: 3%;
  top: 50%;
  font-size: clamp(26px, 7.5vmin, 50px);
  line-height: 1;
  transform: translate(-50%, -50%);
  filter: drop-shadow(0 4px 5px rgba(0, 0, 0, 0.6));
}

.runner.you { animation: runBob 280ms linear infinite; }
.runner.gob { animation: runBob 340ms linear infinite; }

@keyframes runBob {
  0%, 100% { transform: translate(-50%, -50%) rotate(-6deg); }
  50% { transform: translate(-50%, -62%) rotate(6deg); }
}

/* A tap gives your runner a visible kick, so tapping is worth something to look
   at and not only something on the meter. Declared after the bob so it wins. */
.runner.kick { animation: runKick 220ms ease; }
@keyframes runKick {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  40% { transform: translate(-50%, -78%) scale(1.28) rotate(-10deg); }
}

.runner.winner { animation: runWin 520ms ease-in-out infinite; }
@keyframes runWin {
  0%, 100% { transform: translate(-50%, -50%) scale(1.15); }
  50% { transform: translate(-50%, -78%) scale(1.4) rotate(10deg); }
}

/* How fast you are going, right now. Without this, running on the spot and
   standing still look identical until somebody crosses the line. */
.race-meter {
  height: clamp(14px, 3.4vw, 22px);
  border-radius: 99px;
  border: 2px solid rgba(255, 180, 90, 0.35);
  background: rgba(255, 180, 90, 0.16);
  overflow: hidden;
}

.race-meter > i {
  display: block;
  width: 100%;
  height: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, #ffd23f, #ff8b3d);
  transition: transform 90ms linear;
}

.race-meter > i.hot { background: linear-gradient(90deg, #7be06a, #2f9d4e); }

/* Somebody has crossed the line. Nothing you do matters any more, so the
   instructions and the speed meter step back and leave the result on its own. */
.race-card.done .race-how,
.race-card.done .race-meter { opacity: 0.32; transition: opacity 250ms ease; }

.race-call {
  margin-top: clamp(8px, 2vw, 14px);
  min-height: 1.15em;
  font-size: clamp(26px, 8vw, 54px);
  font-weight: 800;
  line-height: 1.1;
  color: #fff;
  text-shadow: 0 4px 0 rgba(0, 0, 0, 0.45);
}

.race-call.count { color: #ffd23f; animation: countPop 520ms ease; }
.race-call.go { color: #7be06a; animation: countPop 380ms ease; }
.race-call.won { color: #7be06a; animation: countPop 420ms ease; }
.race-call.lost { color: #ff8b8b; animation: countPop 420ms ease; }

@keyframes countPop {
  0% { transform: scale(0.4); opacity: 0; }
  50% { transform: scale(1.25); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

/* ------------------------------- FX canvas -------------------------------- */

#fx {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 40;
}

/* -------------------------------- Overlays -------------------------------- */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(30, 8, 10, 0.62);
  backdrop-filter: blur(3px);
  overflow-y: auto;
}

.overlay.show { display: flex; }

/* The card stays bright and warm even though the game is dark — it's the bit
   with the words on, and words need to be easy. */
.card-box {
  background: #fff6e9;
  border-radius: 28px;
  border: 6px solid #e0802f;
  box-shadow: 0 16px 0 rgba(120, 50, 20, 0.4), 0 24px 40px rgba(0, 0, 0, 0.5);
  padding: clamp(20px, 5vw, 40px);
  max-width: 470px;
  width: 100%;
  text-align: center;
  animation: cardIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes cardIn {
  from { transform: scale(0.6) translateY(30px); opacity: 0; }
}

.card-emoji {
  font-size: clamp(46px, 12vw, 72px);
  line-height: 1;
  margin-bottom: 6px;
}

.card-box h1 {
  margin: 6px 0 12px;
  font-size: clamp(25px, 6.8vw, 42px);
  font-weight: 800;
  color: #e0402f;
  letter-spacing: 1px;
}

.overlay.win .card-box h1 { color: #2f9d4e; }
.overlay.lose .card-box h1 { color: #7a3a5a; }

.card-box p {
  margin: 8px 0;
  font-size: clamp(15px, 3.6vw, 19px);
  font-weight: 500;
  color: #5a3a2a;
}

.card-box p.small { font-size: clamp(13px, 3vw, 16px); color: #8a7060; }

.card-box .new-best {
  font-weight: 800;
  color: #ff9500;
  font-size: clamp(17px, 4.2vw, 22px);
}

/* ------------------------------- The hero card ---------------------------- */

/* Winning doesn't just win — the doctor decides you're a hero and pins a medal
   on you, and this is that moment. It's the plain win card underneath, with a
   little scene at the top of it. */
.overlay.win.hero .card-box h1 { color: #ff9500; }

.hero-sky {
  font-size: clamp(32px, 9vw, 54px);
  line-height: 1;
  animation: heroSky 3.2s ease-in-out infinite;
}

@keyframes heroSky {
  0%, 100% { transform: translateY(0) scale(1); }
  50% { transform: translateY(-5px) scale(1.06); }
}

.hero-stage {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: clamp(4px, 2vw, 14px);
  margin: 2px 0 4px;
  font-size: clamp(40px, 11vw, 68px);
  line-height: 1;
}

.hero-doc {
  animation: docProud 1.4s ease-in-out infinite;
  filter: drop-shadow(0 0 14px rgba(255, 210, 63, 0.8));
}

/* isolate rather than leaning on the animation's transform to make the stacking
   context: the cape below is z-index -1 and has to stay behind YOU and in front
   of the card, and that has to be true on the very first frame too. */
.hero-me {
  position: relative;
  isolation: isolate;
  animation: heroHop 1.2s ease-in-out infinite;
}

@keyframes heroHop {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50% { transform: translateY(-10px) rotate(3deg); }
}

/* The cape, behind you. Same shape as the one out on the swamp. */
.hero-cape {
  position: absolute;
  left: 0.08em;
  top: 0.24em;
  z-index: -1;
  width: 0.8em;
  height: 0.92em;
  background: linear-gradient(180deg, #ff5061 0%, #b3122a 100%);
  border-radius: 42% 42% 55% 55% / 18% 18% 82% 82%;
  transform-origin: 50% 0;
  animation: capeFlap 900ms ease-in-out infinite;
}

/* The medal drops in a beat after the card lands, so you see it arrive rather
   than finding it already there. `backwards` is what keeps it out of sight
   during that beat. */
/* The resting transform has to match the last keyframe exactly, or the medal
   snaps half a width sideways the moment the animation lets go of it. */
.hero-medal {
  position: absolute;
  left: 50%;
  top: 38%;
  font-size: 0.52em;
  transform: translate(-50%, 0);
  animation: medalDrop 800ms cubic-bezier(0.34, 1.5, 0.64, 1) 450ms backwards;
}

@keyframes medalDrop {
  0% { transform: translate(-50%, -420%) scale(2.2) rotate(-40deg); opacity: 0; }
  60% { transform: translate(-50%, 6%) scale(1.15) rotate(8deg); opacity: 1; }
  100% { transform: translate(-50%, 0) scale(1) rotate(0deg); opacity: 1; }
}

.card-box .hero-says { font-style: italic; }

.big-btn {
  margin-top: 18px;
  font-family: inherit;
  font-size: clamp(19px, 4.8vw, 27px);
  font-weight: 800;
  color: #fff;
  background: linear-gradient(180deg, #ff7a4a, #e0402f);
  border: none;
  border-radius: 99px;
  padding: 14px 32px;
  cursor: pointer;
  box-shadow: 0 6px 0 #a82a1c, 0 10px 18px rgba(0, 0, 0, 0.35);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
  touch-action: manipulation;
}

.big-btn:active {
  transform: translateY(4px);
  box-shadow: 0 2px 0 #a82a1c, 0 4px 10px rgba(0, 0, 0, 0.3);
}

.change-btn {
  display: block;
  margin: 14px auto 0;
  font-family: inherit;
  font-size: clamp(14px, 3.4vw, 18px);
  font-weight: 700;
  color: #8a4a2a;
  background: #fff;
  border: 3px solid #f0c98c;
  border-radius: 99px;
  padding: 8px 22px;
  cursor: pointer;
  touch-action: manipulation;
}

.change-btn:active { transform: translateY(2px); }

/* --------------------------- Picking a difficulty ------------------------- */

/* These all have to be scoped to .card-box: the plain `.card-box p` and
   `.card-box p.small` rules above are more specific than a bare class would be,
   and would win on margin, size and color. */
.card-box .pick-title {
  margin: 22px 0 0;
  font-size: clamp(17px, 4.2vw, 22px);
  font-weight: 800;
  color: #b8341f;
}

.mode-pick { margin-top: 10px; }
.mode-pick .big-btn { margin-top: 8px; }

/* Easy is the plain .big-btn. Goblin Hard is the same button in a darker, more
   ominous coat — still big and still easy to hit, just clearly the other one. */
.big-btn.hard-btn {
  background: linear-gradient(180deg, #7a4a6a, #43203a);
  box-shadow: 0 6px 0 #2b1226, 0 10px 18px rgba(0, 0, 0, 0.35);
}

.big-btn.hard-btn:active {
  box-shadow: 0 2px 0 #2b1226, 0 4px 10px rgba(0, 0, 0, 0.3);
}

.card-box .mode-blurb { margin: 6px 0 0; }

.card-box .mode-note { margin: 14px 0 0; font-style: italic; }

/* ---------------------------- Easy Peasy targets -------------------------- */

/* Fingers are blunt and the goblins were too jumpy, so on the easy game the mud
   and everybody standing on the swamp are 15% bigger.

   It's a variable rather than a pile of `#game.easy .splotch { width: ... }`
   overrides for one specific reason: an ID selector outranks anything in a
   media query, so overrides would have beaten the smaller sizes further down
   this file and left a short landscape phone covered in giant goblins. Every
   size rule multiplies by this instead, so the media queries keep working and
   easy mode stays 15% bigger than whatever the right size for that screen is. */
#game { --tscale: 1; }
#game.easy { --tscale: 1.15; }

/* ============================== Small phones ============================== */

@media (max-width: 430px) {
  #hud { gap: 5px; }
  #timer-num { font-size: clamp(24px, 6.5vw, 34px); }
  .mini-bar { width: clamp(52px, 15vw, 78px); height: 8px; }
  .stat-label { font-size: 12px; }
  /* Four stats plus a button is a lot of phone. The numbers say enough on
     their own once the words come off, and the wipe is a picture already. */
  .hud-word { display: none; }
  #wipe-chip { font-size: 15px; }
  #quit-btn { width: 40px; height: 40px; font-size: 18px; border-width: 2px; }
  #doc-hint { font-size: 13px; max-width: 18ch; }
}

/* Phone held sideways: plenty of width, almost no height. The swamp keeps as
   much of it as it can and everything else gives way. */
@media (max-height: 560px) {
  #hud {
    padding-top: calc(4px + env(safe-area-inset-top, 0px));
    padding-bottom: 4px;
    gap: 8px;
  }
  #timer-num { font-size: 26px; }
  .mini-bar { height: 7px; margin-top: 2px; }
  .hud-word { display: none; }
  #doctor-bay { height: clamp(38px, 9vh, 56px); }
  #doc { font-size: clamp(26px, 8vh, 44px); }
  #doc-hint { font-size: 12px; max-width: 30ch; }
  .rock { width: clamp(38px, 15vmin, 70px); height: clamp(38px, 15vmin, 70px); }
  /* The track has to fit above the meter and the countdown with no scrolling —
     a race you have to scroll is a race you lose. */
  .lane { height: clamp(40px, 16vh, 62px); }
  .track { gap: 12px; margin: 14px 0 8px; }
  .race-call { font-size: clamp(22px, 7vh, 40px); }
  #me { font-size: calc(clamp(26px, 10vmin, 44px) * var(--tscale, 1)); }
  /* Same * --tscale trick as the full-size rules, so easy mode is still 15%
     bigger than the sideways-phone size rather than ignoring it. */
  .splotch {
    width: calc(clamp(38px, 13vmin, 62px) * var(--tscale, 1));
    height: calc(clamp(38px, 13vmin, 62px) * var(--tscale, 1));
  }
  .critter { font-size: calc(clamp(30px, 11vmin, 50px) * var(--tscale, 1)); }
}

/* ========================= Gentler on the eyes ============================ */

/* Everything that loops or jolts stops. The game still plays exactly the same:
   mud still appears, goblins still hop (the transitions carry them), and every
   state you need to see is a color or a position, never only a motion. */
@media (prefers-reduced-motion: reduce) {
  #lava,
  #lava::after,
  #swamp.shake,
  .volcano-face.erupting,
  .bubble,
  #timer-num.low,
  .bump,
  .critter,
  .critter.doctor,
  .critter.arriving,
  .critter.hit,
  #doc.throw,
  #doc.proud,
  #banner.show,
  #wipe-chip.flick,
  #me.jogging,
  #me.cheer,
  #me.hero::after,
  #me .me-wipe.swipe,
  .runner.you,
  .runner.gob,
  .runner.kick,
  .runner.winner,
  .race-call.count,
  .race-call.go,
  .race-call.won,
  .race-call.lost,
  .hero-sky,
  .hero-doc,
  .hero-me,
  .hero-cape,
  .hero-medal,
  .card-box { animation: none; }

  .splotch.pop { animation-duration: 1ms; }
  .mini-bar > div { transition: none; }
  .mud-flier, .bomb-flier { transition: none; }
  /* The little you still gets where it's going — it just gets there without
     the run. Everything the race needs to say is a position or a color. */
  #me { transition: opacity 250ms ease; }
  .race-meter > i { transition: none; }
}
