/* ==========================================================================
   css/scene.css — the overworld scene (docs/ARCHITECTURE.md 11.4)
   --------------------------------------------------------------------------
   Owner: overworld-frontend-agent. Loaded by index.html (or injected by
   js/ui/location.js on first mount). Builds on the tokens in css/base.css —
   the accent is always `var(--accent)`, never a hard-coded colour.

   Structure (js/ui/location.js + js/scene/*.js):
     .scene-panel
       .scene-head        compact header: type icon, name, tags, count, buttons
       .scene-wrap        5:3 box that scales with the width (max-height capped)
         .scene           background SVG (1000x600 viewBox), position: relative
           .scene-actors  .actor.player / .other / .npc / .pokemon (follower) / .object
           .exit-hint     chevron buttons on the exit edges
           .bubble        speech bubble (one at a time)
           .excl          "!" pop above a head
       .scene-strip       exit buttons + controls hint
     body > .scene-ballwipe  Poké Ball wipe on an encounter (survives the view switch)
     body > .scene-flash     white flash fallback when the ball image is missing

   Sprites: trainers are the 32x32 overworld stills of the DS games (best.json,
   DECISIONS.md 53) drawn at 1.5x with nearest-neighbour scaling, followers the
   32x32 menu icons at 40 px (DECISIONS.md 54). The battle-sprite fallback
   (DECISIONS.md 43) uses the same height, so a missing still changes nothing else.
   ========================================================================== */

:root {
  --actor-h: 48px;          /* trainer sprite height: 32 px still x 1.5 (DECISIONS.md 53) */
  --follower-h: 40px;       /* follower icon height: 32 px icon x 1.25 */
  --scene-radius: 12px;
}

/* --------------------------------------------------------------------------
   Panel + header
   -------------------------------------------------------------------------- */
.scene-panel { padding: 10px 12px 12px; }
.scene-panel > * + * { margin-top: 10px; }
/* On #location the panel sits flush against the red bar and both menus (owner, 27 September): no
   outer margin left / top / right and square corners, so the overworld gets the scroller's 2 x 8 px
   of width back - and, being a 5:3 box, grows in height with it. Nothing inside the panel changes;
   the bottom keeps the scroller's padding. The id outweighs `.view-scroll > *:first-child`. */
#view-scroll[data-view="location"] > .scene-panel {
  margin: 0 calc(-1 * var(--main-pad, 8px));
  border-radius: 0;
  /* And the map 3 px nearer the top and left edge (owner, 27 September: "nog 3 pixels breder en
     hoger ... nog grotere map"; was 10 / 12). Right stays 12: the wild column stands there. */
  padding: 7px 12px 12px 9px;
}

.scene-head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
}
.scene-type-icon {
  flex: none;
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 9px;
  background: var(--panel-3);
  color: var(--accent);
}
.scene-head.type-town .scene-type-icon { background: rgba(255, 181, 71, 0.18); color: #ffcf7a; }
.scene-head.type-forest .scene-type-icon { background: rgba(76, 175, 80, 0.2); color: #9be29f; }
.scene-head.type-cave .scene-type-icon { background: rgba(158, 158, 158, 0.2); color: #d0d0d0; }
.scene-head.type-water .scene-type-icon { background: rgba(90, 169, 255, 0.2); color: #a7cdff; }
.scene-title { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.scene-title h2 { margin: 0; font-size: 1.15rem; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.scene-meta { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.scene-meta .tag { font-size: 0.7rem; }
.scene-meta .tag.gym { background: rgba(186, 104, 200, 0.25); color: #e6c3ec; }
.scene-count { color: var(--muted); font-size: 0.8rem; white-space: nowrap; }
.scene-count::before { content: "·"; margin-right: 6px; opacity: 0.6; }
.scene-actions { display: flex; gap: 6px; flex: none; }

/* --------------------------------------------------------------------------
   The 5:3 box
   -------------------------------------------------------------------------- */
/* The map's cell (js/ui/location.js renderScene): as wide as the map may be, centred; the action bar
   under the map lives in it too, so its buttons are exactly the map's width (owner, 26 September). */
/* The height budget: the window minus what stands above and below the map, so the page never scrolls
   (DECISIONS 442). 217, was 220: the 3 px the flush panel's top padding gave back go to the map's
   height (owner, 27 September) - the same number in .scene-wrap and .wild-col below. */
.scene-main {
  width: min(100%, calc((100vh - 217px) * 5 / 3));
  width: min(100%, calc((100dvh - 217px) * 5 / 3));   /* phones: the real viewport */
  margin: 0 auto;
}
.scene-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 5 / 3;
  max-height: calc(100vh - 217px);
  max-height: calc(100dvh - 217px);
  min-height: 180px;
  margin: 0 auto;
  border-radius: var(--scene-radius);
  border: var(--panel-border-strong);
  overflow: hidden;
  background: #274b2e;
  box-shadow: var(--shadow-sm), inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.scene {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background-color: #3c7a3f;
  background-repeat: no-repeat;
  background-size: 100% 100%;     /* the SVG viewBox is exactly 5:3 -> units map 1:1 */
  background-position: center;
  image-rendering: auto;          /* vector background: keep it smooth */
  cursor: crosshair;
  outline: none;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}
.scene:focus-visible { box-shadow: inset 0 0 0 3px var(--accent); }
/* Placeholder "meadow" while a location has no SVG yet: a field with a sand path across it (the
   path is an element, .path-band below). A little detail since DECISIONS 380: two layers of grass
   blades on uneven tiles (so no grid shows), a shadowed patch bottom right, a sunlit one left of
   the middle and a shadow top right, over the mowing stripes and the daylight from the top. */
.scene.no-bg {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.10), transparent 40%),
    radial-gradient(ellipse 60% 35% at 20% 30%, rgba(255, 255, 255, 0.08), transparent 70%),
    radial-gradient(1.5px 3px at 6px 9px, rgba(30, 80, 35, 0.09) 0 60%, transparent 100%) 0 0 / 23px 17px repeat,   /* dark blades: 0.24 was too heavy, a dotted grid (owner, 25 September) */
    radial-gradient(1.5px 3px at 17px 4px, rgba(190, 230, 160, 0.14) 0 60%, transparent 100%) 0 0 / 31px 19px repeat,
    radial-gradient(ellipse 34% 22% at 72% 82%, rgba(0, 0, 0, 0.08), transparent 70%),
    radial-gradient(ellipse 28% 18% at 14% 64%, rgba(255, 255, 255, 0.07), transparent 70%),
    radial-gradient(ellipse 30% 18% at 86% 26%, rgba(0, 0, 0, 0.06), transparent 70%),
    repeating-linear-gradient(45deg, rgba(0, 0, 0, 0.04) 0 6px, transparent 6px 12px),
    linear-gradient(180deg, #4d9a4f 0%, #3f8a43 100%) #3c7a3f;
}
/* The sand of the placeholder meadow, one look for every surface (owner, 25 September, DECISIONS
   380): the band across the field (.path-band, 58-74 % of the height), a strip from it to a gate
   on the top or bottom edge (.path-strip, DECISIONS 371/379: 10 % wide, centred on the gate's
   chevron) and the thin walks from the service buildings to the band (.path-spur, DECISIONS 376,
   3 % wide). A soft darker rim of 4 px where grass meets sand, a lighter trodden middle, a fine
   diagonal grain and two layers of pebbles on uneven tiles (light 41x29, dark 53x37, so no grid
   shows). The band has its rims top and bottom, strips and spurs left and right; strips and spurs
   run 4 px into the band so its rim does not cross the junction as a seam. Real elements, hung on
   the scene by js/scene/scene.js before the actor layer so they stay under the sprites; the
   placeholder meadow only - SVG and tile scenes paint their own ground. */
.scene .path-band, .scene .path-strip, .scene .path-spur { display: none; }
.scene.no-bg .path-band, .scene.no-bg .path-strip, .scene.no-bg .path-spur {
  --sand: rgba(214, 190, 120, 0.55);
  --sand-worn: rgba(232, 212, 150, 0.62);
  --sand-rim: rgba(120, 95, 50, 0.38);
  display: block;
  position: absolute;
  pointer-events: none;
  background:
    radial-gradient(circle at 9px 11px, rgba(255, 250, 225, 0.55) 0 1.2px, transparent 2px) 0 0 / 41px 29px repeat,
    radial-gradient(circle at 27px 19px, rgba(110, 85, 45, 0.45) 0 1.3px, transparent 2.1px) 0 0 / 53px 37px repeat,
    repeating-linear-gradient(115deg, rgba(110, 90, 50, 0.12) 0 1px, transparent 1px 7px),
    var(--sand-shape);
}
.scene.no-bg .path-band {
  --sand-shape: linear-gradient(180deg, var(--sand-rim), var(--sand) 4px, var(--sand-worn) 50%, var(--sand) calc(100% - 4px), var(--sand-rim));
  left: 0;
  right: 0;
  top: 58%;
  height: 16%;
}
.scene.no-bg .path-strip, .scene.no-bg .path-spur {
  --sand-shape: linear-gradient(90deg, var(--sand-rim), var(--sand) 4px, var(--sand-worn) 50%, var(--sand) calc(100% - 4px), var(--sand-rim));
}
.scene.no-bg .path-strip { width: 10%; }                                   /* left comes from scene.js */
.scene.no-bg .path-strip.side-bottom { top: calc(74% - 4px); bottom: 0; }
.scene.no-bg .path-strip.side-top { top: 0; height: calc(58% + 4px); }
/* Tile layer (docs/TILES.md): the 640x384 canvas of the location's tile map, drawn by
   js/tiles under the actors and scaled into the 5:3 box with hard pixels like the GBA
   games; clicks go through it to the scene. `has-tiles` beats the SVG and the placeholder. */
.scene-tiles {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  image-rendering: crisp-edges;
  image-rendering: pixelated;
  pointer-events: none;
}
.scene.has-tiles { background-image: none !important; }
.scene.has-tiles .actor.object { background: transparent; }   /* the buildings are in the picture */
.scene-actors { position: absolute; inset: 0; }
/* The HD look (js/tiles/hd.js; the admin's Route 2 test, owner 29 September, DECISIONS 524): a 1920 x 1152 canvas of the owner's own
   sprites, scaled down smoothly - hard pixels would chop his detailed art at 1.67 screen pixels per map pixel. Its trees
   are pictures in the actor layer: feet on the tile, z-index by the feet (inline) like every trainer, so a trainer north
   of a tree walks behind its crown. A click goes through them to the map. */
.scene-tiles.hd { image-rendering: auto; }
.hd-tree { position: absolute; max-width: none; pointer-events: none; user-select: none; }
/* The admin's switch in the red bar (js/ui/location.js hdLookSwitch): Meadow = what players see, New sprites = the test.
   The pressed one wears the red bar's .btn.active (css/layout.css). */
.hd-switch { display: inline-flex; flex: none; }
.hd-switch .btn { border-radius: 0; }
.hd-switch .btn:first-child { border-radius: 6px 0 0 6px; }
.hd-switch .btn:last-child { border-radius: 0 6px 6px 0; margin-left: -1px; }
@media (max-width: 700px) {
  .hd-switch .btn { padding: 4px 6px; font-size: 0.7rem; }
}

/* --------------------------------------------------------------------------
   Actors
   -------------------------------------------------------------------------- */
.actor {
  position: absolute;
  transform: translate(-50%, -100%);  /* feet on the (x, y) point */
  width: 0;                           /* children centre on the point */
  height: 0;
  pointer-events: none;
}
.actor.other, .actor.npc, .actor.object { pointer-events: auto; cursor: pointer; }
.actor.other:focus-visible, .actor.npc:focus-visible, .actor.object:focus-visible { outline: none; }
.actor.other:focus-visible .actor-body, .actor.npc:focus-visible .actor-body { box-shadow: 0 0 0 2px var(--accent); border-radius: 6px; }

.actor-body {
  position: absolute;
  left: 0; bottom: 0;
  transform: translateX(-50%);
  height: var(--actor-h);
  line-height: 0;
  animation: sceneBreathe 2.8s ease-in-out infinite;
  transform-origin: 50% 100%;
}
.actor-body img,
.actor .actor-sprite {
  height: var(--actor-h);
  width: auto;
  max-width: none;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  object-fit: contain;
  object-position: center bottom;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.35));
  transition: transform 0.08s linear;
}
.actor.face-left .actor-body img { transform: scaleX(-1); }
.actor.moving .actor-body { animation: sceneBob 0.36s ease-in-out infinite alternate; }
/* The run to a clicked exit (js/scene/scene.js dashToExit, DECISIONS 342): quicker steps. */
.actor.moving.dashing .actor-body { animation-duration: 0.12s; }

/* Follower Pokémon (HeartGold/SoulSilver walking Pokémon): menu icon, no name, small shadow.
   --grow is its size step (DECISIONS 304, js/scene/actors.js setSize): +3 px per step on the
   40 px icon, the phone's 32 px in the same proportion. Unset = 1 = the normal size. */
.actor.pokemon { pointer-events: none; }
.actor.pokemon .actor-body { height: calc(var(--follower-h) * var(--grow, 1)); }
.actor.pokemon .actor-sprite {
  height: calc(var(--follower-h) * var(--grow, 1));
  width: calc(var(--follower-h) * var(--grow, 1));
  object-fit: contain;
  object-position: center bottom;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.3));
}
.actor.pokemon .actor-shadow { width: calc(18px * var(--grow, 1)); height: calc(6px * var(--grow, 1)); bottom: -2px; background: rgba(0, 0, 0, 0.26); }
/* Variant recolours: same recipes as .variant-golden / .variant-dark in css/game.css, applied
   to the body because the img already carries the drop-shadow filter above. */
.actor.pokemon.variant-golden .actor-body { filter: sepia(1) saturate(3) hue-rotate(-12deg) brightness(1.02) contrast(1.22); }
/* Owner (22 September): the battle recipe (brightness 0.45) turns a 32 px overworld still
   into a black blob. Lighter here, with a faint violet glow so it still reads as a dark form. */
.actor.pokemon.variant-dark .actor-body { filter: brightness(0.74) contrast(1.15) saturate(0.85) hue-rotate(250deg) drop-shadow(0 0 3px rgba(167, 139, 250, 0.55)); }
.actor.pokemon.variant-frozen .actor-body { filter: sepia(1) saturate(2.2) hue-rotate(160deg) brightness(1.18) contrast(1.05) drop-shadow(0 0 4px rgba(120, 220, 255, 0.7)); }   /* DECISIONS 149 */
/* Shadow (DECISIONS 330): black and white inside a dark smoky edge, a little lighter than in battle for the small follower. */
.actor.pokemon.variant-shadow .actor-body { filter: grayscale(1) contrast(1.15) drop-shadow(0 0 2px rgba(18, 8, 30, 0.9)) drop-shadow(0 0 5px rgba(72, 34, 110, 0.55)); }
/* Shiny icons do not exist (the icons are always the normal palette): a twinkling star instead. */
.actor.pokemon.variant-shiny .actor-body::after {
  content: "✦";
  position: absolute;
  top: -6px; right: -8px;
  font-size: 11px;
  line-height: 1;
  color: #fff3b0;
  text-shadow: 0 0 4px rgba(255, 230, 120, 0.9);
  animation: sceneTwinkle 1.6s ease-in-out infinite;
  pointer-events: none;
}

.actor-shadow {
  position: absolute;
  left: 0; bottom: -3px;
  width: 26px; height: 8px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.32);
  filter: blur(1px);
  z-index: -1;
}

.actor-name {
  position: absolute;
  left: 0;
  bottom: calc(var(--actor-h) + 4px);
  transform: translateX(-50%);
  padding: 1px 7px;
  border-radius: 999px;
  background: rgba(12, 18, 32, 0.78);
  color: var(--text);
  font-size: 11px;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}
.actor.player .actor-name { background: var(--accent); color: var(--accent-text); }
.actor.npc .actor-name { background: rgba(12, 18, 32, 0.7); color: #dfe5f2; }
.actor.near .actor-name { box-shadow: 0 0 0 2px var(--accent), 0 1px 3px rgba(0, 0, 0, 0.35); }
.actor.near .actor-name::after { content: "  E"; font-weight: 800; opacity: 0.85; letter-spacing: 0.02em; }
.actor.player.near .actor-name::after { content: none; }

.actor-badge {
  position: absolute;
  left: 0;
  bottom: calc(var(--actor-h) + 22px);
  transform: translateX(-50%);
  width: 16px; height: 16px;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  color: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}
.actor-badge[hidden] { display: none; }
.actor-badge svg { width: 10px; height: 10px; }
.actor-badge.defeated { background: var(--success); color: #0f2b14; }
.actor-badge.battle { background: var(--accent); color: var(--accent-text); animation: sceneBadgePulse 1.4s ease-in-out infinite; }

/* Objects: the SVG draws the building; this is the invisible click target. */
.actor.object {
  transform: none;
  width: auto; height: auto;
  border-radius: 8px;
  outline: 2px dashed transparent;
  outline-offset: -2px;
  transition: outline-color 0.15s ease, background-color 0.15s ease;
}
.actor.object:hover, .actor.object:focus-visible, .actor.object.near {
  outline-color: rgba(255, 255, 255, 0.75);
  background: rgba(255, 255, 255, 0.06);
}
.scene.no-bg .actor.object { background: rgba(255, 255, 255, 0.14); outline-color: rgba(255, 255, 255, 0.35); }
.object-label {
  position: absolute;
  left: 50%; bottom: -8px;
  transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px;
  border-radius: 999px;
  background: rgba(12, 18, 32, 0.72);
  color: #e8edf7;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  transition: background-color 0.15s ease, transform 0.15s ease;
}
.object-label .object-icon { display: inline-flex; color: var(--accent); }
.object-label .object-icon svg { width: 11px; height: 11px; }
.actor.object:hover .object-label, .actor.object.near .object-label { background: rgba(12, 18, 32, 0.92); transform: translateX(-50%) translateY(-1px); }
.actor.object.near .object-label::after { content: "E"; font-weight: 800; margin-left: 2px; opacity: 0.85; }

/* The Center, the Mart and the Gym on the placeholder meadow as real buildings (owner, 27 September,
   DECISIONS 447; the owner's own pictures since DECISIONS 455: "je zet alleen de sprite neer met hover effect
   of als spelers in de buurt staan"). The picture stands on the rect's bottom edge, so the roof rises above
   the rect the way a front-view building does, and it is drawn wider than the rect, so the trees of the
   picture stand beside the building: the Center and the Mart 118.8 % (first 132 %, then 10 % smaller -
   owner: "ietsje kleiner zeg ongeveer 10 %"), the Gym 128 %. The rect stays the wall and the
   click target (js/scene/scene.js defaultBuildings), so walking, Heal, Shop and the Gym work exactly as
   before. A trainer behind the building is drawn under it (z-index = feet y). Only .no-bg: the tile maps
   draw their own buildings. These pictures are detailed art (487-631 px) shown smaller than they are, so
   smooth scaling keeps them sharp at every map size; the whole-pixel rule of DECISIONS 451 was for the
   128 px pixel sprites they replace. */
.scene:is(.no-bg, .has-tiles) .actor.object:is(.obj-pokecenter, .obj-shop, .obj-gym, .obj-ferry) { background: transparent; outline-color: transparent; }
.scene:is(.no-bg, .has-tiles) .actor.object:is(.obj-pokecenter, .obj-shop, .obj-gym)::before {
  content: "";
  position: absolute;
  left: -9.4%; right: -9.4%;   /* 118.8 % of the rect = 132 % less 10 % (owner, DECISIONS 455) */
  bottom: var(--foot, 0%);     /* on a tile map: the footprint's foot, above the door row (js/scene/scene.js, DECISIONS 464) */
  aspect-ratio: 487 / 419;   /* pokecenter-hd.png */
  background: url("../assets/scenes/buildings/pokecenter-hd.webp") center bottom / contain no-repeat;
  filter: drop-shadow(0 3px 2px rgba(0, 0, 0, 0.3));
  pointer-events: none;
  transform-origin: 50% 100%;
  transition: filter 0.15s ease, transform 0.15s ease;
}
.scene:is(.no-bg, .has-tiles) .actor.object.obj-shop::before {
  aspect-ratio: 494 / 419;   /* pokemart-hd.png */
  background-image: url("../assets/scenes/buildings/pokemart-hd.webp");
}
.scene:is(.no-bg, .has-tiles) .actor.object.obj-gym::before {
  left: -14%; right: -14%;   /* 128 %, kept when the other two shrank; not 132 %: its roof would pass the map's top edge (rect at y 60) */
  aspect-ratio: 631 / 545;   /* gym-hd.png */
  background-image: url("../assets/scenes/buildings/gym-hd.webp");
}
/* The same pictures on the tile maps of Kanto..Unova (owner, 27 September, DECISIONS 464: "als ik in Kanto Pallet Town
   ben of andere regio's ... die moeten wel dezelfde sprites hebben"): the tile renderer leaves those three buildings
   out in the game and these stand on their footprint. A tile building's footprint is small (Center and Mart 3 tiles =
   75 units, the Gym 4 = 100), so the picture is 130 % of it, trees beside the building like on the meadow. */
.scene.has-tiles .actor.object:is(.obj-pokecenter, .obj-shop, .obj-gym)::before { left: -15%; right: -15%; }
/* Hover, keyboard focus or standing next to it (E): a soft white glow and a 1 px lift, the foot stays put.
   Toned down (owner: "de hover (popup) van de sprite moet minder"): was a 4 px glow at 90 %, 2 px up and 2 % larger. */
.scene:is(.no-bg, .has-tiles) .actor.object:is(.obj-pokecenter, .obj-shop, .obj-gym, .obj-ferry):is(:hover, :focus-visible, .near)::before {
  filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.75)) drop-shadow(0 3px 2px rgba(0, 0, 0, 0.3));
  transform: translateY(-1px);
}
@media (prefers-reduced-motion: reduce) {
  .scene:is(.no-bg, .has-tiles) .actor.object:is(.obj-pokecenter, .obj-shop, .obj-gym, .obj-ferry):is(:hover, :focus-visible, .near)::before { transform: none; }
}

/* Tidecrest Harbor's harbour over the meadow (owner, 27 September, DECISIONS 459: "er moet ook water en een
   Ferry komen (kloppend volgens de Halcyon region map)"): the stone quay under the road, the sea south and east,
   the river mouth and the wooden jetty (assets/scenes/tidecrest-harbor-water.svg, in scene units, stretched
   like the meadow). The scene's ::before is its first box, so the sand paths and the trainers stay on top; the
   road band ends at 74 %, where the quay begins. The water is walled by the scene block of data/locations.json,
   on the server and in the client alike. */
.scene.no-bg[data-loc="tidecrest-harbor"]::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../assets/scenes/tidecrest-harbor-water.svg") center / 100% 100% no-repeat;
  pointer-events: none;
}
/* The ferry (the owner's picture, assets/scenes/buildings/ferry-hd.webp) is an object like the buildings: E,
   click, hover and "near" glow the same way. Its rect (x 730-880, y 450-575) is the click target in the water;
   the picture lies at x 675-875, y 440-600, its gangway on the jetty's planks. The same ship at the jetty of each
   region's port (owner, 27 September, DECISIONS 467: "in pallet town moet de ferry er ook staan"): a rect of the
   same size at x 320-470, y 392-517 (js/tiles/generate.js), so the picture lies at x 265-465, y 382-542.
   In front of the ship or behind it by where you stand, like every actor (z-index = 100 + feet y): the object's
   `waterline` (the quay edge) sets its z-index in js/scene/scene.js - on the jetty you stand in front of it, on
   the quay behind it. It was a fixed 599 here while Tidecrest had the only ship. */
.scene:is(.no-bg, .has-tiles) .actor.object.obj-ferry::before {
  content: "";
  position: absolute;
  left: -36.7%; right: 3.4%; bottom: -20%;
  aspect-ratio: 1019 / 813;   /* ferry-hd.png */
  background: url("../assets/scenes/buildings/ferry-hd.webp") center bottom / contain no-repeat;
  filter: drop-shadow(0 3px 2px rgba(0, 0, 0, 0.3));
  pointer-events: none;
  transform-origin: 50% 100%;
  transition: filter 0.15s ease, transform 0.15s ease;
  animation: ferryBob 3.6s ease-in-out infinite;
}
/* The ferry rocks at its moorings and the water rings round its hull (owner, 27 September, DECISIONS 464: "er mag
   een kleine ferry water animatie zijn"). The bob moves the `translate` and `rotate` properties, so the hover lift
   (`transform`) still adds to it. The ring lies round the waterline (x 670-890, y ~548-596 of the scene; in a port
   x 260-480, y ~490-538), under the ship: z-index -1 inside the ferry's own layer. Still with "reduce motion". */
.scene:is(.no-bg, .has-tiles) .actor.object.obj-ferry::after {
  content: "";
  position: absolute;
  z-index: -1;
  left: -40%; right: -7%; top: 78%; bottom: -17%;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.5);
  pointer-events: none;
  animation: ferryRipple 3.6s ease-out infinite;
}
@keyframes ferryBob { 0%, 100% { translate: 0 0; rotate: 0deg; } 50% { translate: 0 -2px; rotate: -0.5deg; } }
@keyframes ferryRipple { 0% { transform: scale(0.88); opacity: 0.8; } 100% { transform: scale(1.12); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .scene:is(.no-bg, .has-tiles) .actor.object.obj-ferry::before { animation: none; }
  .scene:is(.no-bg, .has-tiles) .actor.object.obj-ferry::after { animation: none; opacity: 0; }
}

/* --------------------------------------------------------------------------
   Exit chevrons on the edges
   -------------------------------------------------------------------------- */
.exit-hint {
  position: absolute;
  z-index: 600;
  width: 26px; height: 46px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0;
  background: rgba(12, 18, 32, 0.55);
  color: #fff;
  cursor: pointer;
  opacity: 0.85;
  transition: opacity 0.15s ease, background-color 0.15s ease;
  animation: sceneNudge 1.6s ease-in-out infinite;
}
.exit-hint:hover, .exit-hint:focus-visible { opacity: 1; background: var(--accent); color: var(--accent-text); outline: none; }
.exit-hint svg { width: 16px; height: 16px; }
.exit-hint.side-left { left: 0; transform: translateY(-50%); border-radius: 0 8px 8px 0; }
.exit-hint.side-right { right: 0; transform: translateY(-50%); border-radius: 8px 0 0 8px; }
.exit-hint.side-top { top: 0; transform: translateX(-50%) rotate(180deg); width: 46px; height: 26px; border-radius: 0 0 8px 8px; }
.exit-hint.side-bottom { bottom: 0; transform: translateX(-50%); width: 46px; height: 26px; border-radius: 8px 8px 0 0; }

/* --------------------------------------------------------------------------
   Action bar under the map (every screen since DECISIONS 270)
   --------------------------------------------------------------------------
   Owner (21 September): on a phone you saw "E to interact" but the bubble button fell
   outside the screen. This bar repeats that action as one big button under the map, the
   way Continue sits under the battle screen. Coarse pointers get it too, so a tablet keeps
   it on a wide screen. DECISIONS 118.
   Owner (24 September, DECISIONS 270): on a PC too - there you had to click the trainer to see
   anything, while a smaller window showed buttons with words. The buttons used to stop growing at
   280 px; since 26 September (owner, DECISIONS 419) the one, two or three of them share the whole
   width of the map ("that is easier too") - the bar sits in the map's cell (.scene-main). */
/* The row under the map is always there, one fixed height, never empty (owner, 27 September, DECISIONS 442:
   "de map moet gewoon op dezelfde plek blijven staan", "de scrollbalk moet daar weg"). The bar came and went
   with every step past a trainer, a building or another player: everything under it jumped 23 to 50 px
   (measured at 1366x650, 1536x750 and 1920x950; hovering the wild column moved nothing), and where the exits
   wrapped to a second row the page grew a scrollbar that took 10 px off the map. Now the controls hint fills
   the row while nothing is near and the buttons take its place when something is; they stretch to the row
   (alone they were 40.2 px). */
.scene-actrow { display: flex; align-items: center; justify-content: center; height: 41px; margin-top: 10px; }
.scene-actrow > .scene-touchbar { flex: 1 1 auto; align-self: stretch; }
.scene-touchbar { display: flex; justify-content: center; gap: 8px; }   /* under the map (DECISIONS 271) */
.scene-touchbar[hidden] { display: none; }
.scene-touchbar:not([hidden]) + .scene-hint { display: none; }   /* a button takes the hint's place */
/* Tidecrest's ferry lists nine boats since Gen 6-9 (27 September): ten copies squeezed into this one row (DECISIONS
   442) are unreadable, 28 px each on a phone. From seven buttons on the bar stays out and the bubble - docked full
   width on a phone - carries them; the hint keeps the row, so nothing moves. Six (five boats + Close) still fit. */
.scene-touchbar:has(> .btn:nth-child(7)) { display: none; }
@media (min-width: 701px) { .scene-touchbar:has(> .btn:nth-child(7)) + .scene-hint { display: block; } }
.scene-touchbar .btn {
  flex: 1 1 0; min-width: 0; min-height: 40px;
  justify-content: center; font-weight: 700;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
@media (max-width: 899px), (pointer: coarse) {
  .scene-actrow { height: 46px; }   /* the row of the bigger touch buttons (DECISIONS 442) */
  .scene-touchbar .btn { max-width: none; min-height: 46px; }
  /* Prestige (DECISIONS 238): three buttons share a phone's width, so this one is just the
     coloured tag - "★1 Bronze" - without the words and the star icon. */
  .scene-touchbar .btn-prestige-go .lbl-long,
  .scene-touchbar .btn-prestige-go > svg { display: none; }
  .scene-touchbar .btn-prestige-go .prestige-tag { margin-left: 0; padding: 3px 10px; font-size: 0.82rem; }
}

/* --------------------------------------------------------------------------
   Speech bubble
   -------------------------------------------------------------------------- */
.bubble {
  --shift: 0px;
  --lift: 78px;
  position: absolute;
  z-index: 700;
  width: max-content;
  max-width: min(280px, 80%);
  transform: translate(calc(-50% + var(--shift)), calc(-100% - var(--lift)));
  display: flex;
  gap: 10px;
  padding: 10px 12px 10px;
  border-radius: 12px;
  background: rgba(14, 20, 36, 0.95);
  border: var(--panel-border-strong);
  box-shadow: var(--shadow);
  color: var(--text);
  cursor: default;
  animation: sceneBubbleIn 0.14s ease-out;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.bubble::after {
  content: "";
  position: absolute;
  left: calc(50% - var(--shift));
  bottom: -7px;
  width: 12px; height: 12px;
  transform: translateX(-50%) rotate(45deg);
  background: rgba(14, 20, 36, 0.95);
  border-right: 1px solid rgba(255, 255, 255, 0.16);
  border-bottom: 1px solid rgba(255, 255, 255, 0.16);
}
.bubble.below { transform: translate(calc(-50% + var(--shift)), 12px); }
.bubble.below::after { bottom: auto; top: -7px; transform: translateX(-50%) rotate(-135deg); }
/* Docked (DECISIONS 152): when the bubble cannot fit above or below its speaker - on a
   phone the scene box is only a few bubble-heights tall - it sits against the edge of the
   box away from the speaker, full width, like the text box at the bottom of the games. */
.bubble.docked {
  left: 8px; right: 8px; width: auto; max-width: none;
  transform: none; animation-name: sceneBubbleDock;
}
.bubble.docked.dock-bottom { top: auto; bottom: 8px; }
.bubble.docked.dock-top { top: 8px; bottom: auto; }
.bubble.docked::after { display: none; }
@keyframes sceneBubbleDock { from { opacity: 0; } to { opacity: 1; } }
.bubble-close {
  position: absolute; top: 4px; right: 4px;
  width: 20px; height: 20px;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--muted);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.bubble-close:hover { background: rgba(255, 255, 255, 0.1); color: var(--text); }
.bubble-sprite { flex: none; width: 56px; height: 64px; display: flex; align-items: flex-end; justify-content: center; }
.bubble-sprite img { height: 64px; width: auto; image-rendering: pixelated; }
/* Gym Leader bubble: the B2W2 VS strip (128×64) shown 1:1; falls back to the battle sprite. */
.bubble-sprite.vs { width: 128px; }
.bubble-sprite.vs img { height: 64px; width: auto; max-width: 128px; object-fit: contain; object-position: right bottom; }
.bubble-body { min-width: 0; flex: 1 1 auto; padding-right: 14px; }
.bubble-title { font-weight: 700; font-size: 0.9rem; line-height: 1.2; }
.bubble-sub { color: var(--muted); font-size: 0.72rem; margin-top: 1px; }
.bubble-text { font-size: 0.85rem; line-height: 1.4; margin-top: 6px; }
.bubble-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.bubble-actions .btn { min-height: 26px; }

/* --------------------------------------------------------------------------
   "!" and the encounter flash
   -------------------------------------------------------------------------- */
.excl {
  --excl-lift: 70px;              /* above a trainer's head (sprite + name label) */
  position: absolute;
  z-index: 650;
  transform: translate(-50%, calc(-100% - var(--excl-lift)));
  width: 26px; height: 26px;
  border-radius: 6px;
  background: #fff;
  color: #101522;
  font-weight: 900;
  font-size: 20px;
  line-height: 26px;
  text-align: center;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
  animation: sceneExcl 0.9s ease-out forwards;
  pointer-events: none;
}
.excl.on-object { --excl-lift: 6px; }

/* Emote balloons (Trainer Shop, DECISIONS 489, js/scene/actors.js showEmote): the white box of the "!" pop above with
   a small tail, a child of the actor so it walks along; up for 4 s. Over the name label (sprite + label + 6 px). */
.emote-bubble {
  position: absolute; left: 0; bottom: calc(var(--actor-h) + 26px); z-index: 2;
  display: flex; align-items: center; justify-content: center; width: 30px; height: 28px;
  transform: translateX(-50%); border-radius: 8px; background: #fff; color: #101522;
  font-weight: 900; font-size: 18px; line-height: 1; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
  animation: sceneEmote 4s ease-out forwards; pointer-events: none;
}
.emote-bubble::after {
  content: ""; position: absolute; left: 50%; bottom: -4px; width: 9px; height: 9px;
  background: #fff; transform: translateX(-50%) rotate(45deg); border-radius: 0 0 2px 0;
}
.emote-face { position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; }
.emote-face svg { width: 17px; height: 17px; }
.emote-face.emote-heart, .emote-face.emote-heart svg { color: #e5484d; }
.emote-face.emote-note { color: #2f7fe0; }
.emote-face.emote-sleep { color: #6b7a99; font-size: 0.8em; letter-spacing: -0.5px; }
.emote-face.emote-gg { font-size: 0.82em; letter-spacing: -0.5px; }
@keyframes sceneEmote {
  0% { opacity: 0; transform: translate(-50%, 6px) scale(0.6); }
  8% { opacity: 1; transform: translate(-50%, 0) scale(1.08); }
  14%, 88% { opacity: 1; transform: translate(-50%, 0) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -4px) scale(1); }
}
/* The row of emote buttons at the left of the row under the map (js/ui/location.js emoteRow): 30 px white keys like
   the balloons; the pack ones dimmed with a lock until it is bought. */
.scene-emotes { display: flex; align-items: center; gap: 4px; flex: none; margin-right: 10px; }
.scene-emotes ~ .scene-hint { flex: 1 1 auto; min-width: 0; text-align: center; }   /* the hint centred in the rest */
.emote-btn {
  position: relative; display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px;
  padding: 0; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.2); background: #fff; color: #101522;
  font: inherit; font-weight: 900; font-size: 16px; line-height: 1; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.emote-btn:hover, .emote-btn:focus-visible { transform: translateY(-1px); box-shadow: 0 0 0 2px var(--accent), 0 2px 4px rgba(0, 0, 0, 0.3); outline: none; }
.emote-btn.locked { background: rgba(255, 255, 255, 0.28); color: rgba(16, 21, 34, 0.6); }
.emote-btn.locked::after {
  content: ""; position: absolute; right: -3px; bottom: -3px; width: 12px; height: 12px; border-radius: 50%;
  background: #1a2338 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f6c945' stroke-width='3' stroke-linecap='round'%3E%3Crect x='5' y='11' width='14' height='10' rx='2'/%3E%3Cpath d='M8 11V8a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E") center / 8px no-repeat;
}

.scene-flash {
  position: fixed;
  inset: 0;
  z-index: 3000;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  animation: sceneFlash 0.45s ease-out forwards;
}

/* Poké Ball wipe (Black/White wild-battle transition): the ball grows from the player
   (--wipe-x / --wipe-y, viewport px, set by js/scene/scene.js) until it covers the screen
   at 40% of the animation (= BALL_WIPE_MS), holds while the battle view mounts, fades. */
.scene-ballwipe {
  position: fixed;
  inset: 0;
  z-index: 3000;
  overflow: hidden;
  pointer-events: none;
}
.scene-ballwipe img {
  position: absolute;
  left: var(--wipe-x, 50vw);
  top: var(--wipe-y, 50vh);
  width: 600px;                 /* BALL_BASE_PX in scene.js; the final size comes from --wipe-scale */
  height: 600px;
  max-width: none;
  transform: translate(-50%, -50%) scale(0) rotate(-40deg);
  transform-origin: 50% 50%;
  image-rendering: auto;        /* scaled up ~10x: smooth beats blocky here */
  will-change: transform, opacity;
  animation: sceneBallWipe 0.8s cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}

/* --------------------------------------------------------------------------
   Strip under the scene: the exits, and in a port town the boats
   -------------------------------------------------------------------------- */
.scene-strip { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
/* One row, always (owner, 27 September, DECISIONS 442: "de scrollbalk moet daar weg"). With the hint in the
   middle (DECISIONS 425) four exits wrapped to a second row, and in a port town the boats had a strip of their
   own: one row too many for a laptop, so the page scrolled and the map lost 10 px. Now the hint lives in the
   row under the map, the exits have the whole width, and the boats stand on the right of the same row. More
   than fits in a small window? The buttons get narrower, never a second row. */
.scene-exit-strip { flex-wrap: nowrap; }
.scene-exits, .scene-ferries { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; min-width: 0; }
.scene-exits .btn, .scene-ferries .btn { flex: 0 1 auto; min-width: 0; overflow: hidden; }
.scene-exit.locked { opacity: 0.6; }
.scene-hint { white-space: nowrap; text-align: center; }
/* The boats (owner, 22 September): "⛴ Kanto", the harbour they land in in the tooltip, like the pier of the
   S.S. Anne at Vermilion City. A boat into a region that is not open yet (DECISIONS 250): lock, muted, the
   reason in the tooltip. */
.scene-ferry.locked { opacity: 0.65; cursor: help; }
.scene-ferry.locked svg { vertical-align: -1px; }
.scene-ferry { border-color: rgba(79, 195, 247, 0.6); }
.scene-ferry:hover { border-color: #4fc3f7; }
/* The wild list under the map (DECISIONS 335) glides in instead of popping up, and its cards
   follow one after the other (`--i`, set by js/ui/location.js), also after a rarity filter
   (DECISIONS 342). `backwards`: once done, the animation leaves nothing pinned on them. */
.loc-wild { animation: locWildIn 0.4s ease-out 0.1s backwards; }
.loc-wild .sfa-mon { animation: locWildCard 0.3s ease-out backwards; animation-delay: calc(var(--i, 0) * 35ms); }

/* --------------------------------------------------------------------------
   Wild Pokémon in view: the column beside the map (owner, 26 September; js/ui/location.js wildColumn)
   The list of 335 always fell below the fold. Of three layouts built side by side (DECISIONS 416) the
   owner picked the column (419), "a bit more compact and narrower so more map is left": 200 px, one
   32 px row per species, rarity in words, a Poké Ball on what you have caught. The column scrolls
   inside itself when taller than the map; the page never does. On a phone it drops under the map.
   188 px since 27 September (owner, DECISIONS 442: "10-15 pixels te breed"): the 12 px come out of the gap
   between the name and "Not caught", and go to the map, which fills its cell. Where nothing wild lives
   the column lists the Pokémon Center, Poké Mart and Gym instead (placesColumn), so every place has it
   and the map stands on the same spot on every route and in every town.
   -------------------------------------------------------------------------- */
.scene-stage.cols { display: grid; grid-template-columns: minmax(0, 1fr) 188px; gap: 10px; align-items: start; }
.wild-col {
  max-height: calc(100vh - 217px); max-height: calc(100dvh - 217px); overflow: auto;
  padding: 6px 7px 7px; border-radius: var(--radius-sm, 8px);
  background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08);
}
.wild-head { margin-bottom: 6px; }
.wild-head h4 { margin: 0; font-size: 0.86rem; }
.wild-col .wild-head { flex-direction: column; align-items: flex-start; gap: 1px; }
/* The count line is always two lines tall (owner, 27 September, DECISIONS 442: "pixel precisie"): the wild one
   ("8 species · Lv. 16-22 · 0 caught · 3 undiscovered") wraps to two, the town's "No wild Pokémon here" is
   one, and the rows under them started 13 px apart. A longer translation stops at two lines with "…". */
.wild-col .wild-head .small {
  font-size: 0.66rem; line-height: 1.3; height: 2.6em;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
.wild-rows { display: flex; flex-direction: column; gap: 2px; }
.wild-row {
  display: flex; align-items: center; gap: 6px; min-height: 38px; padding: 1px 4px;
  border-radius: 6px; border: 1px solid transparent;
  animation: locWildCard 0.3s ease-out backwards; animation-delay: calc(var(--i, 0) * 35ms);
}
/* A known species opens its Pokédex window (the summary's "Show in Pokédex" does the same). */
.wild-row.is-link { cursor: pointer; }
.wild-row.is-link:hover, .wild-row.is-link:focus-visible { border-color: var(--accent-light, #f08c8c); background: rgba(214, 69, 69, 0.14); outline: none; }
/* The front sprite (the gif of the battle, not the menu icon - owner), fitted in a 40 x 36 box; smooth
   scaling, a pixelated shrink of a ~96 px gif looks torn. The "?" of an undiscovered species sits in
   the same box. */
.wild-row .wild-row-icon { width: 40px; height: 36px; flex: none; object-fit: contain; image-rendering: auto; }
.wild-row .sfa-mon-q.wild-row-icon { width: 30px; height: 30px; margin: 0 5px; font-size: 0.95rem; }
.wild-row-text { min-width: 0; flex: 1 1 auto; }
/* Rarity as a small line ABOVE the name (owner: calmer), coloured like the Pokédex's dots: green
   common, yellow uncommon, gold rare; then the name, then the levels. */
.wild-rar { font-size: 0.52rem; font-weight: 800; letter-spacing: 0.02em; line-height: 1.1; }   /* no capitals, one size smaller (owner) */
.wild-rar.common { color: #62d26f; }
.wild-rar.uncommon { color: #ffd86b; }
.wild-rar.rare { color: #f6c945; }
.wild-row-name { font-size: 0.76rem; font-weight: 700; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wild-row-sub { font-size: 0.62rem; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }   /* a long translation never runs under the status */
/* Caught: the Bag's coloured Poké Ball (assets/items/poke-ball.png) that opens the PC on the Pokémon
   you have of this species (the row's own click opens the Pokédex, so the button stops the event).
   The image is a block in a flex box, so the ball sits dead centre. Seen only: "Not caught". */
.wild-row-ball {
  flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 0; line-height: 0;
  width: 28px; height: 28px; min-width: 0; min-height: 0; padding: 0; border-radius: 50%;
}
.wild-row-ball img { display: block; width: 22px; height: 22px; object-fit: contain; }
.wild-row-ball:hover img, .wild-row-ball:focus-visible img { transform: scale(1.12); }
.wild-row-ball:hover, .wild-row-ball:focus-visible { background: rgba(255, 255, 255, 0.12); }
.wild-row-status { flex: none; font-size: 0.6rem; font-weight: 700; }
.wild-row-status.miss { color: #ff8f8f; }
/* In your Pokédex as caught, but none in your PC or party right now (evolved, traded, released): the
   word instead of the ball, since there is nothing to open (owner, 26 September). */
.wild-row-status.gone { color: var(--safari, #9db43f); opacity: 0.75; }
/* Only a species you caught before but hold none of now steps back (js/ui/location.js .is-gone); one
   you own stays in full colour (owner, 26 September: the Safari's "caught = done, dimmed" rule read
   as "my Pikachu is not coloured"). A rare one you have seen glows gold. */
.wild-row.is-gone .wild-row-icon { opacity: 0.6; filter: saturate(0.6); }
.wild-row.is-gone .wild-row-name { color: var(--muted, #9aa3b8); }
.wild-row.r-rare.st-seen { border-color: rgba(246, 201, 69, 0.55); background: rgba(246, 201, 69, 0.08); }
.wild-row.r-rare.st-unknown .sfa-mon-q { border-color: rgba(246, 201, 69, 0.6); color: #f6c945; }
/* The same column where nothing wild lives (owner, 27 September, DECISIONS 442; js/ui/location.js
   placesColumn): Pokémon Center, Poké Mart and Gym, only to look at - no hover, no click. Where a Pokémon has
   its sprite, the Center and the Mart show their own building (assets/scenes/buildings, WebP q90 copies of the PNG sources - DECISIONS 582,
   in the same 40 x 36 box through .wild-row-icon) and the Gym its leader's sprite. The small word above the
   name takes the roof's colour - pink for the Center, blue for the Mart - the way rarity does for a wild one. */
.wild-rar.heal { color: #f7a1bf; }
.wild-rar.shop { color: #a7cdff; }
.wild-rar.gym { color: #e6c3ec; }   /* the Gym tag of the red bar */
.wild-row-status.won { color: #62d26f; }   /* "✓ Badge": green like a common one, "No badge" red like "Not caught" */

/* --------------------------------------------------------------------------
   Keyframes
   -------------------------------------------------------------------------- */
@keyframes sceneBob { from { transform: translateX(-50%) translateY(0); } to { transform: translateX(-50%) translateY(-3px); } }
@keyframes sceneBreathe { 0%, 100% { transform: translateX(-50%) scaleY(1); } 50% { transform: translateX(-50%) scaleY(1.025); } }
@keyframes sceneBadgePulse { 0%, 100% { transform: translateX(-50%) scale(1); } 50% { transform: translateX(-50%) scale(1.18); } }
@keyframes sceneNudge { 0%, 100% { opacity: 0.7; } 50% { opacity: 1; } }
@keyframes sceneBubbleIn { from { opacity: 0; transform: translate(calc(-50% + var(--shift)), calc(-100% - var(--lift) + 6px)) scale(0.96); } }
@keyframes sceneExcl {
  0%   { opacity: 0; transform: translate(-50%, calc(-100% - var(--excl-lift) + 14px)) scale(0.4); }
  18%  { opacity: 1; transform: translate(-50%, calc(-100% - var(--excl-lift) - 6px)) scale(1.15); }
  30%  { transform: translate(-50%, calc(-100% - var(--excl-lift))) scale(1); }
  80%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, calc(-100% - var(--excl-lift))) scale(1); }
}
@keyframes sceneFlash { 0% { opacity: 0; } 30% { opacity: 1; } 55% { opacity: 1; } 100% { opacity: 0; } }
/* --wipe-scale (set per encounter by scene.js) is the scale at which the ball covers the
   farthest viewport corner. 45 % = BALL_WIPE_MS: fully covered, the battle view mounts
   underneath; it stays covered until 70 %, then fades over the new screen. */
@keyframes sceneBallWipe {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(0) rotate(-40deg); }
  45%  { opacity: 1; transform: translate(-50%, -50%) scale(var(--wipe-scale, 8)) rotate(0deg); }
  70%  { opacity: 1; transform: translate(-50%, -50%) scale(var(--wipe-scale, 8)) rotate(0deg); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(var(--wipe-scale, 8)) rotate(0deg); }
}
@keyframes sceneBallWipeStill { 0%, 70% { opacity: 1; transform: translate(-50%, -50%) scale(var(--wipe-scale, 8)); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(var(--wipe-scale, 8)); } }
@keyframes sceneTwinkle { 0%, 100% { opacity: 0.55; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.15); } }
@keyframes locWildIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes locWildCard { from { opacity: 0; transform: translateY(8px) scale(0.96); } to { opacity: 1; transform: none; } }

/* --------------------------------------------------------------------------
   Responsive + reduced motion
   -------------------------------------------------------------------------- */
@media (max-width: 700px) {
  .scene-panel { padding: 8px; }
  /* Phones: the map spans the panel, so 3 px nearer every side but the bottom (owner, 27 September). */
  #view-scroll[data-view="location"] > .scene-panel { padding: 5px 5px 8px; }
  .scene-head { gap: 8px; min-height: 34px; }
  .scene-type-icon { width: 28px; height: 28px; }
  .scene-title h2 { font-size: 1rem; }
  .scene-meta .tag:not(:first-child) { display: none; }
  .scene-count { display: none; }
  .scene-actions .btn .lbl { display: none; }
  .scene-actions .btn { padding: 4px 7px; min-width: 30px; }
  .scene-hint { display: none; }
  .scene-main { width: 100%; }
  .scene-wrap { width: 100%; max-height: none; border-radius: 10px; }
  .actor-name { font-size: 10px; max-width: 110px; }
  .object-label { font-size: 9.5px; }
  .bubble { max-width: min(240px, 90%); padding: 8px 10px; }
  .bubble-sprite { width: 44px; height: 52px; }
  .bubble-sprite img { height: 52px; }
  .exit-hint { width: 22px; height: 40px; }
  .exit-hint.side-top, .exit-hint.side-bottom { width: 40px; height: 22px; }
  /* The wild column drops under the map (owner: "op mobiel moet hij onder de map vallen"): one grid
     column, full width, and it lists everything - the page scrolls there anyway. */
  .scene-stage.cols { grid-template-columns: minmax(0, 1fr); gap: 8px; }
  .wild-col { max-height: none; }
  /* The page scrolls here anyway, so phones keep the old way (DECISIONS 442): no row under the map while
     nothing is near (the hint is hidden above), and the exits may wrap. */
  .scene-actrow { height: auto; margin-top: 0; }
  .scene-actrow > .scene-touchbar { height: 46px; margin-top: 10px; }
  .scene-actrow { flex-wrap: wrap; }
  .scene-emotes { flex: 1 1 100%; justify-content: center; margin: 10px 0 0; gap: 6px; }
  .emote-btn { width: 34px; height: 34px; }
  .scene-exit-strip, .scene-exits, .scene-ferries { flex-wrap: wrap; }
}
/* MASTER WR-09 (bugronde 28-09): a phone turned sideways is wider than 700 px, so the rule above did not apply and
   emotes, hint and Heal/Battle/Ferry had to share one row; the buttons on the right shrank to a sliver. Same row
   for the emotes as on an upright phone. */
@media (max-height: 480px) and (pointer: coarse) {
  .scene-actrow { flex-wrap: wrap; }
  .scene-emotes { flex: 1 1 100%; justify-content: center; margin: 10px 0 0; gap: 6px; }
}
@media (max-width: 420px) {
  :root { --actor-h: 40px; --follower-h: 32px; }
  .scene-exits .btn, .scene-ferries .btn { font-size: 0.72rem; padding: 3px 7px; }
}

@media (prefers-reduced-motion: reduce) {
  .actor-body, .actor.moving .actor-body, .actor-badge.battle, .exit-hint, .bubble,
  .actor.pokemon.variant-shiny .actor-body::after, .loc-wild, .loc-wild .sfa-mon, .wild-row, .emote-bubble { animation: none !important; }
  .excl { animation: sceneExclStill 0.9s linear forwards; }
  .scene-flash { animation-duration: 0.2s; }
  /* No growing ball: it simply covers the screen and fades (still a cut, not a zoom). */
  .scene-ballwipe img { animation: sceneBallWipeStill 0.5s linear forwards; filter: none; }
  .actor-body img { transition: none; }
}
@keyframes sceneExclStill { 0%, 85% { opacity: 1; } 100% { opacity: 0; } }

/* --------------------------------------------------------------------------
   Safari Zone page (js/ui/safari.js): the "Safari Zone" row of the Challenges menu
   --------------------------------------------------------------------------
   What the park is, the rules and the "Go to the Safari Zone" button (owner, 21 September).
   The khaki tokens come from css/safari.css (--safari, --safari-soft); the fallbacks below are
   the same colours, so the page also reads right when that sheet is missing. */
/* Full width like every other view (owner, 22 September: the old 820 px cap left a bare
   strip on the right). The hero reads as the sign at the gate: khaki band, icon, the rules
   in two columns; under it the roster as sprites, then the status and the button. */
.safari-page { max-width: none; padding: 0; overflow: hidden; }
.safari-page-hero {
  display: flex; gap: 18px; align-items: flex-start;
  padding: 18px 20px 16px;
  background:
    radial-gradient(90% 140% at 0% 0%, rgba(157, 180, 63, 0.22), transparent 60%),
    linear-gradient(180deg, rgba(157, 180, 63, 0.08), transparent);
  border-bottom: 1px solid rgba(157, 180, 63, 0.25);
}
.safari-page-icon {
  flex: none;
  width: 64px; height: 64px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 16px;
  background: var(--safari-soft, rgba(157, 180, 63, 0.16));
  color: var(--safari, #9db43f);
  box-shadow: inset 0 0 0 1px rgba(157, 180, 63, 0.35);
}
.safari-page-icon svg { width: 36px; height: 36px; }
.safari-page h3 { margin: 0 0 6px; }
.safari-page p { line-height: 1.5; margin: 0; }
.safari-rules {
  list-style: none;
  padding: 0;
  margin: 10px 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 6px 14px;
  font-size: 0.86rem;
  color: var(--muted);
}
.safari-rules li { display: flex; align-items: flex-start; gap: 7px; line-height: 1.4; }
.safari-rules li svg { flex: none; width: 14px; height: 14px; margin-top: 3px; color: var(--safari, #9db43f); }
/* The sign at the gate: what lives in each half of the park, as menu icons (DECISIONS 165). */
.safari-roster { padding: 14px 20px 4px; display: flex; flex-direction: column; gap: 10px; }
.safari-roster-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.safari-roster-title { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; font-size: 0.92rem; }
.safari-roster-title svg { width: 14px; height: 14px; color: var(--safari, #9db43f); }
.safari-roster-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(78px, 1fr)); gap: 6px; margin-bottom: 6px; }
.safari-roster-mon {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 8px 4px 7px; border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.045); border: 1px solid rgba(255, 255, 255, 0.08);
}
.safari-roster-mon.rare { border-color: rgba(246, 201, 69, 0.45); background: rgba(246, 201, 69, 0.07); }
.safari-roster-icon { width: 40px; height: 40px; image-rendering: pixelated; }
.safari-roster-name { font-size: 0.7rem; font-weight: 600; line-height: 1.15; text-align: center; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.safari-roster-rare { position: absolute; top: 4px; right: 5px; font-size: 0.58rem; font-weight: 800; color: #f6c945; letter-spacing: 0.02em; }
/* Directly under the hero, above the roster: the gate is always in view without scrolling. */
.safari-page-status {
  display: flex; flex-direction: column; gap: 8px;
  margin: 0; padding: 12px 20px 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.025);
}
.safari-roster { padding-bottom: 14px; }
.safari-page-line { margin: 0; }
.safari-page-go { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-top: 4px; }
@media (max-width: 600px) {
  .safari-page-hero { flex-direction: column; padding: 14px; }
  .safari-roster { padding: 12px 14px 4px; }
  .safari-roster-grid { grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); }
  .safari-page-status { padding: 10px 14px 12px; }
  .safari-page-go .btn { width: 100%; }
}

/* A mass outbreak on this place (DECISIONS 532, player idea #36): an orange card above "Wild Pokémon here" and the
   species' row on top with an "Outbreak · 1 in 2" chip instead of its rarity word. */
/* Since DECISIONS 537 one quiet line (owner: "mag iets minder aanwezig ... dikke letters ... 2 regels"): a small icon, the
   name in normal weight that cuts off with "…" rather than wrap, the time left on the right; the text is the tooltip. */
.outbreak-card {
  display: flex; align-items: center; gap: 6px; padding: 3px 8px 3px 4px; margin-bottom: 6px; border-radius: 8px;
  background: rgba(255, 159, 67, 0.09); border: 1px solid rgba(255, 159, 67, 0.32); cursor: help;
}
.outbreak-card .outbreak-card-img { width: 26px; height: 26px; flex: none; object-fit: contain; }
.outbreak-title { flex: 1 1 auto; min-width: 0; font-weight: 600; font-size: 0.72rem; color: #ffd0a1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.outbreak-left { flex: none; font-size: 0.64rem; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; white-space: nowrap; }
.outbreak-chip { display: inline-block; padding: 0 6px; border-radius: 999px; background: #ff9f43; color: #2a1400; font-size: 0.56rem; font-weight: 800; line-height: 1.55; white-space: nowrap; }
.wild-row.is-outbreak { border-color: rgba(255, 159, 67, 0.65); background: rgba(255, 159, 67, 0.1); }
