/* ============================================================
   SANTA OLIVER — North Pole adventure map
   The map replaces the activity card grid. It always shows in
   full and scales to the screen, so nothing is ever parked off
   the side of a phone. The trade-off is that the name plaques
   baked into the artwork get small on a narrow screen, which is
   what the activity list underneath is for.
   ============================================================ */

/* The activity cards stay in the DOM because the fullscreen launcher moves
   those nodes into its window. Hiding the GRID (not the cards) keeps that
   working: once a card is moved out of this container it is no longer a
   descendant of a hidden element, so it shows normally in the window. */
.toy-grid.is-mapped { display: none; }

/* ---------------- the map owns the page ----------------
   There is no hero and no nav any more: the map IS the page. The section is a
   full-viewport flex column with the map centred in it. Browser history is the
   way back to the page that launched the game.

   These rules live in THIS file rather than north-pole.css or global.css, and
   the reason is not taste. `.np-page` is also reindeer-hunt.html's body class,
   and `.nav` is on every page of the site, so neither selector isolates this
   page on its own. np-map.css is the only stylesheet north-pole.html loads
   alone, which makes it the only safe home for a rule that must not leak.

   The nav is hidden, not deleted: magic.js looks up .nav, .nav-burger and
   .mmenu on every page, and display:none keeps those lookups valid while
   taking the whole bar out of the layout and out of the accessibility tree. */
.np-page .nav,
.np-page .nav-burger,
.np-page .mmenu { display: none; }

.np-section {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Respect the device notch; bottom is breathing room under the list. */
  padding: max(12px, env(safe-area-inset-top)) 0 28px;
}
/* .wrap carries `margin: 0 auto` from global.css. Inside a column flex
   container those become AUTO CROSS-AXIS MARGINS, which suppress the default
   stretch and shrink-wrap the box to its content — 764px instead of 1240px on
   a 1440px screen, which shrank the map to little over half its size. An
   explicit width restores the stretch; the auto margins then only centre it. */
.np-section > .wrap { width: 100%; }
.np-head { display: none; }
.parent-note { margin-top: 26px; }

@media (max-width: 620px) {
  .np-section { padding: max(8px, env(safe-area-inset-top)) 0 20px; }
  .parent-note { margin-top: 18px; }
}

.np-map {
  position: relative;
  margin: 0 auto;
  /* The map is the page now, so it is sized to FIT THE SCREEN rather than
     merely capped. --np-chrome is the vertical room the activity toggle and
     parent note need below it. Whatever
     height is left is converted into a width through the artwork's 1376/768
     ratio, so the map is always as large as it can be while staying whole.

     Stated as max-width, never width: the map can only ever shrink to fit its
     container, so it cannot outgrow .np-map-scroll and get silently clipped —
     the failure the landscape rule at the foot of this file was written for. */
  --np-chrome: 190px;
  width: 100%;
  max-width: min(1180px, calc((100dvh - var(--np-chrome)) * 1376 / 768));

  /* A CONTAINER, so the score chips, tool buttons and toast can size themselves
     against the map instead of against the viewport. This is not a refinement,
     it is a bug fix: every phone rule in this file used to be gated on
     `max-width: 899px`, and a modern Android phone in landscape is about 915
     CSS px wide. None of them fired, so a 233px-wide map was being dressed in
     full desktop chrome — the HUD alone came to 95% of the map's width. What
     the chrome actually cares about is how big the MAP is, which is exactly
     what a container query asks. */
  container-type: inline-size;
  container-name: npmap;
}

.np-map-scroll {
  overflow: hidden;
  border-radius: 18px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 16px 44px rgba(0, 0, 0, 0.38);
}

/* The whole map is always visible, at every width. It scales down rather than
   panning, so nothing is ever hidden off the side of a phone screen. */
.np-map-inner {
  position: relative;
  width: 100%;
  aspect-ratio: 1376 / 768;
  /* The adventure layer's scenery is sized in cqw so that a chimney puff or a
     window glow keeps its proportion to the building it sits on at every map
     size. inline-size (not size) on purpose: the height comes from the aspect
     ratio, so an inline-axis container is all that is needed and it never
     depends on the box's own contents. Every rule that sizes this element
     therefore states a width explicitly — see the landscape and fullscreen
     overrides below. */
  container-type: inline-size;
}
/* Fills the frame as a block. NOT display:contents - the img would then be
   laid out directly by .np-map-inner, which is a size container. */
.np-map-inner picture { display: block; width: 100%; height: 100%; }
.np-map-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-drag: none;
}

/* ---------------- portrait: the tall cut ----------------
   A second illustration, 768x1376, so the frame flips ratio with it and the map
   is sized from the WIDTH. At 375px that is a 375x672 map filling 83% of the
   viewport height, against 187px before.

   It cannot fill the height completely: the artwork is 0.558 wide-to-tall and a
   phone is nearer 0.46, so filling vertically would crop the START cabin and the
   Joke Show off the sides. Whole map beats full bleed.

   Placed AFTER .np-map-inner's base rule on purpose. Before it, the base
   aspect-ratio won on source order at equal specificity and the frame stayed
   landscape-shaped - the same cascade trap as the explore pill. */
@media (orientation: portrait) {
  .np-map-inner { aspect-ratio: 688 / 1536; }
  /* The chrome allowance drops hard here. At 0.448 the map is height-bound, so
     every pixel the chrome reserves comes straight off the map's WIDTH: at the
     190px used elsewhere this cut came out 279px wide, narrower than the
     squarer artwork it replaces. At 60px it reaches the full 335px and 748px
     tall — 92% of the viewport, against 73% before. The activity toggle and
     the note simply sit below the fold, which on a map that finally fills the
     screen is the right trade. */
  .np-map { --np-chrome: 60px; max-width: min(100%, calc((100dvh - var(--np-chrome)) * 688 / 1536)); }
  .np-section { justify-content: flex-start; }
}

/* ---------------- portrait, naturally full viewport ----------------
   Portrait phones open directly into the map as the page itself. This is CSS
   viewport coverage, not the browser Fullscreen API, so it needs no user
   gesture and keeps the browser's own Back navigation available.

   The artwork box retains its exact 1376/3072 ratio so every percentage-based
   hotspot and animation stays registered. max() scales it like background-size:
   cover; .np-map-scroll owns any small crop and remains natively scrollable so
   every building can still be reached on unusually squat or tall phones. */
@media (orientation: portrait) {
  .np-section { min-height: 100dvh; padding: 0; justify-content: flex-start; }
  .np-section > .wrap { padding: 0; }
  .np-map {
    --np-chrome: 0px;
    width: 100%;
    height: 100vh;
    height: 100dvh;
    max-width: none;
    margin: 0;
    background: #0b1626;
  }
  .np-map-scroll {
    width: 100%;
    height: 100vh;
    height: 100dvh;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }
  .np-map-inner {
    width: max(100%, 44.7916667dvh);
  }
  .parent-note { margin-top: 22px; }
}

/* ---------------- hotspots ---------------- */
.np-spot {
  position: absolute;
  z-index: 10;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 14px;
  cursor: pointer;
  /* Percentage boxes shrink with the map; on a small phone the smallest
     hotspot fell to 30x35. Children aim less precisely than adults, not more. */
  min-width: 44px;
  min-height: 44px;
  -webkit-tap-highlight-color: transparent;
  transition: background .16s ease, transform .16s ease;
}
@media (hover: hover) and (pointer: fine) {
  .np-spot:hover {
    background: rgba(255, 255, 255, 0.16);
    box-shadow: inset 0 0 0 2px rgba(255, 246, 214, 0.85), 0 6px 18px rgba(0, 0, 0, 0.3);
    outline: none;
  }
}
.np-spot:focus-visible {
  background: rgba(255, 255, 255, 0.16);
  box-shadow: inset 0 0 0 2px rgba(255, 246, 214, 0.85), 0 6px 18px rgba(0, 0, 0, 0.3);
  outline: none;
}
.np-spot:active { transform: scale(0.98); background: rgba(255, 255, 255, 0.16); }

/* Name repeated on hover/focus: the plaque is baked into the artwork and can
   be small once the map is scaled down. */
.np-spot-tip {
  position: absolute;
  left: 50%;
  bottom: -6px;
  transform: translate(-50%, 100%);
  padding: 5px 10px;
  border-radius: 8px;
  background: rgba(12, 24, 38, 0.94);
  color: #fff6de;
  font-family: var(--body, system-ui, sans-serif);
  font-size: 12.5px;
  font-weight: 700;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .16s ease;
  z-index: 3;
}
@media (hover: hover) and (pointer: fine) {
  .np-spot:hover .np-spot-tip { opacity: 1; }
}
.np-spot:focus-visible .np-spot-tip { opacity: 1; }

/* Below this width the map has shrunk enough that a nowrap tooltip no longer
   fits inside it and gets clipped by the frame. Touch devices never hover, so
   nothing is lost; the labelled activity list is the path on small screens.
   Focused hotspots still announce themselves through their aria-label. */
@media (max-width: 899px) {
  .np-spot-tip { display: none; }
}

/* Outline every hotspot for tuning: append ?maphot=1 to the URL. */
.np-map--debug .np-spot { background: rgba(255, 90, 90, 0.28); box-shadow: inset 0 0 0 2px #ff4d4d; }
.np-map--debug .np-spot-tip { opacity: 1; }

/* ---------------- list fallback ---------------- */
.np-list { max-width: 1180px; margin: 12px auto 0; text-align: center; }
.np-list-toggle {
  padding: 12px 18px;
  min-height: 44px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--gold-200, #e3c878);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .np-list-toggle:hover { background: rgba(255, 255, 255, 0.12); }
}
.np-list-toggle:active { background: rgba(255, 255, 255, 0.12); transform: scale(0.97); }
.np-list-toggle:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(200, 162, 74, .5); }

.np-list-panel {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 9px;
}
.np-list-panel[hidden] { display: none; }
.np-list-panel button {
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  font: inherit;
  font-size: 14.5px;
  font-weight: 600;
  text-align: left;
  color: var(--snow, #fbfaf6);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 12px;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .np-list-panel button:hover { background: rgba(255, 255, 255, 0.14); }
}
.np-list-panel button:active { background: rgba(255, 255, 255, 0.14); }
.np-list-panel button:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(200, 162, 74, .5); }

@media (prefers-reduced-motion: reduce) {
  .np-spot, .np-spot-tip { transition: none; }
}

/* ---------------- short screens (landscape phones) ----------------
   Height, not width, is the scarce dimension here. A 16:9 map at full width
   is ~450px tall in a 390px-tall viewport, so it was pushed entirely below
   the fold. Size the map by height instead and trim the hero further.

   The orientation guard is load-bearing, not decoration. Keyed on height
   alone this fired in PORTRAIT too: a 320dp phone with browser chrome top and
   bottom leaves ~418px of viewport, well under 520. The map was then sized by
   height, its width resolved through the 1.79:1 image to ~390dp against ~290dp
   of room, and .np-map-scroll's overflow:hidden silently clipped the right
   third — "NORTH POLE ADVENTURE", "SANTA'S VILLAGE" and "WRITE A LETTER TO
   SANTA" all lost their ends, with no way to pan to them since np-map.js has
   no drag. That directly contradicted the promise eight rules above, that the
   whole map is visible at every width. */
/* A short viewport, which in practice means a phone turned sideways. NOTE THE
   MISSING WIDTH GUARD: this rule used to read `and (max-width: 899px)`, and a
   modern Android phone in landscape is about 915 CSS px wide, so it never
   matched. --np-chrome stayed at its 190px desktop value against a ~320px
   viewport and the map came out 233px wide — a quarter of the screen.

   Height is the only scarce dimension here, so the chrome allowance drops to
   almost nothing and the map takes the whole of it. At 915x320 that is a
   545x304 map: 95% of the available height, against 131px before. The activity
   toggle and the note fall below the fold, which is the right trade — the map
   IS the page, and a landscape phone is where it can finally be seen properly.

   The map stays height-bound at any real phone size, so it remains fully visible. */
@media (max-height: 560px) and (orientation: landscape) {
  .np-section {
    /* flex-start, not centre: with the activity list open the content is taller
       than the viewport, and centring an overflowing column pushes its top out
       of reach in both directions rather than just the bottom. */
    justify-content: flex-start;
    padding: max(8px, env(safe-area-inset-top)) 0 10px;
  }
  .np-map { --np-chrome: 20px; }
  .np-list { margin-top: 8px; }
  .parent-note { display: none; }
}

/* Short AND narrow — not a phone, a small desktop window. Here the map is
   width-bound instead of height-bound, so it reaches the edges and the back
   link would land on its top-left corner, on top of the score chips. Give the
   section back enough head room to push the map clear. */
@media (max-height: 560px) and (max-width: 700px) and (orientation: landscape) {
  .np-section { padding-top: max(52px, calc(env(safe-area-inset-top) + 46px)); }
}

/* ---------------- explore fullscreen ----------------
   Inline, the entire illustration remains visible. Explore mode instead fills
   the dynamic viewport edge to edge: the artwork scales like background-size:
   cover, while .np-map-scroll takes the small crop so percentage-positioned
   hotspots and scenery stay attached to the illustration. Native scrolling
   lets a child reach any cropped edge without custom drag code. */
.np-map-explore {
  position: absolute; right: 10px; bottom: 10px; z-index: 3;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 36px; padding: 7px 13px;
  border: 0; border-radius: 999px; cursor: pointer;
  font-family: var(--body); font-size: 13px; font-weight: 700;
  color: #10233a; background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.np-map-explore:active { transform: scale(0.97); }
.np-map.is-fs .np-map-explore { display: none; }

/* Sized in fixed px, so on a phone — where the map is only ~335px wide — the
   pill grew until it covered the Letter to Santa house. Scaled back to the
   corner it belongs in. This has to sit AFTER the base rule above: an earlier
   media query loses the cascade to it at equal specificity, which is exactly
   how the first attempt at this failed. Still a 32px target, and the whole map
   is tappable for the same action anyway. */
@media (max-width: 620px) {
  .np-map-explore {
    right: 6px; bottom: 6px;
    min-height: 32px; padding: 5px 11px;
    font-size: 11.5px; gap: 4px;
  }
}
.np-map-explore[hidden] { display: none; }
@media (orientation: portrait) {
  /* Portrait is already a natural full-viewport page. */
  .np-map > .np-map-explore { display: none; }
}

body.np-map-fs { overflow: hidden; }

/* Below .np-win (z-index 100000) on purpose: a hotspot tapped in here opens
   its activity ON TOP, and closing that returns to the map still open. */
.np-map.is-fs {
  position: fixed; inset: 0; z-index: 120;
  /* Explicit dynamic-viewport sizing follows mobile browser chrome as it opens
     and closes. width:auto still defeats the short-landscape fit-content rule. */
  margin: 0; width: auto; height: 100vh; height: 100dvh;
  max-width: none; background: #0b1626;
}
.np-map.is-fs .np-map-scroll {
  width: 100%; height: 100vh; height: 100dvh;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  border: 0; border-radius: 0; box-shadow: none;
}
/* Cover the viewport without changing the artwork coordinate system. Landscape
   needs at least viewport-height × 1376/768; portrait uses its own ratio below.
   max(100%, ...) guarantees there can be no uncovered side or bottom edge. */
.np-map.is-fs .np-map-inner {
  width: max(100%, calc(100dvh * 1376 / 768));
  height: auto;
}
@media (orientation: portrait) {
  .np-map.is-fs .np-map-inner {
    width: max(100%, calc(100dvh * 1376 / 3072));
  }
}

.np-map-close {
  position: fixed; top: max(10px, env(safe-area-inset-top)); right: max(10px, env(safe-area-inset-right)); z-index: 4;
  min-width: 44px; min-height: 44px;
  display: none; align-items: center; justify-content: center;
  border: 0; border-radius: 999px; cursor: pointer;
  background: rgba(12, 27, 46, 0.86); color: #fff;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
}
.np-map.is-fs .np-map-close { display: inline-flex; }

/* ============================================================
   NORTH POLE ADVENTURE — the living map layer

   Everything below paints on top of the artwork and keeps score.
   Two rules govern it:

   1. SCENERY scales with the map. Aurora, stars, window glows,
      chimney smoke, snow and the elf live inside .np-map-inner
      and are sized in cqw, so a chimney puff stays the size of a
      chimney puff whether the map is 340px or 1180px wide, and
      pans with the artwork in fullscreen.
   2. CHROME does not. The score chips, the tool buttons and the
      toast live on .np-map and are sized in px. A puff that gets
      small is weather; a score that gets small is a bug. They
      also stay pinned to the frame's corners rather than
      scrolling away when the map is explored fullscreen.

   The frame's aspect ratio is locked at 1376/768, so vertical
   distances are expressible in cqw: 100cqh == 55.8cqw. That is
   why the snowfall travels 58cqw rather than a cqh figure — it
   keeps .np-map-inner an inline-size container, which cannot
   deadlock against the widths set on it elsewhere in this file.
   ============================================================ */

/* Scenery never takes a click: the hotspots, the canes and the
   tap-to-explore handler underneath must all still get through. */
.npa-aurora, .npa-sky, .npa-lights, .npa-smoke, .npa-ice,
.npa-snow, .npa-vignette, .npa-elf, .npa-target {
  position: absolute;
  pointer-events: none;
}

/* ---------------- aurora ----------------
   Three drifting curtains over the top third of the artwork, where the
   painted sky is. Screen blending lets the painted aurora show through
   rather than being covered by a second one. */
.npa-aurora {
  inset: 0 0 68% 0;
  z-index: 2;
  filter: blur(16px);
  mix-blend-mode: screen;
}
.npa-curtain {
  position: absolute;
  border-radius: 50%;
  animation: npa-aurora 18s ease-in-out infinite;
}
.npa-curtain--1 {
  left: 12%; top: -20%; width: 30%; height: 120%;
  background: linear-gradient(180deg, rgba(96,240,180,0) 0%, rgba(96,240,180,.5) 44%, rgba(80,200,255,0) 100%);
}
.npa-curtain--2 {
  left: 38%; top: -24%; width: 34%; height: 130%;
  background: linear-gradient(180deg, rgba(140,246,200,0) 0%, rgba(140,246,200,.4) 46%, rgba(150,140,225,0) 100%);
  animation-duration: 25s; animation-delay: -7s;
}
.npa-curtain--3 {
  left: 64%; top: -18%; width: 30%; height: 118%;
  background: linear-gradient(180deg, rgba(170,130,240,0) 0%, rgba(170,130,240,.44) 42%, rgba(96,240,180,0) 100%);
  animation-duration: 21s; animation-delay: -13s;
}
@keyframes npa-aurora {
  0%   { transform: translateX(-5%) skewX(-9deg) scaleY(1);    opacity: .35; }
  50%  { transform: translateX(6%)  skewX(7deg)  scaleY(1.18); opacity: .75; }
  100% { transform: translateX(-5%) skewX(-9deg) scaleY(1);    opacity: .35; }
}

/* ---------------- stars ----------------
   Full-frame container, not a band. Each star carries a `top` computed in
   np-map.js against the sky ceiling measured at its own column, because the
   painted skyline swings between 9% and 33% across the frame — no single
   rectangle is all sky. */
.npa-sky { inset: 0; z-index: 2; }
.npa-star {
  position: absolute;
  border-radius: 50%;
  background: #f4f8ff;
  animation: npa-twinkle ease-in-out infinite;
}
@keyframes npa-twinkle { 0%, 100% { opacity: .2; } 50% { opacity: 1; } }

/* ---------------- lit windows and lamps ----------------
   Screen blending again: these sit exactly on lights already painted into
   the artwork and make them breathe rather than adding new ones. */
.npa-lights { inset: 0; z-index: 3; mix-blend-mode: screen; }
.npa-glow {
  position: absolute;
  border-radius: 50%;
  filter: blur(6px);
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(255,206,128,.85) 0%, rgba(255,186,96,.4) 45%, rgba(255,186,96,0) 72%);
  animation: npa-glow ease-in-out infinite;
}
@keyframes npa-glow {
  0%, 100% { opacity: .45; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: .95; transform: translate(-50%, -50%) scale(1.08); }
}

/* ---------------- chimney smoke ---------------- */
.npa-smoke { inset: 0; z-index: 4; }
.npa-puff {
  position: absolute;
  border-radius: 50%;
  background: rgba(228,236,250,.4);
  filter: blur(4px);
  animation: npa-smoke linear infinite;
}
@keyframes npa-smoke {
  0%   { transform: translate(0, 0) scale(.5);            opacity: .5; }
  100% { transform: translate(-1.4cqw, -6cqw) scale(2.2); opacity: 0; }
}

/* ---------------- the frozen lake ---------------- */
.npa-ice {
  left: 24%; top: 93%; width: 34%; height: 6%;
  z-index: 4;
  background: linear-gradient(90deg, rgba(255,255,255,0), rgba(214,236,255,.9), rgba(255,255,255,0));
  filter: blur(7px);
  animation: npa-shimmer 8s ease-in-out infinite;
}
@keyframes npa-shimmer {
  0%, 100% { opacity: .12; transform: translateX(-2%); }
  50%      { opacity: .38; transform: translateX(2%); }
}

/* ---------------- candy canes ----------------
   Above the hotspots (z 10), not below. The hotspot boxes are deliberately
   oversized — they cover building and name plaque both — so two of the five
   canes fall inside one. Stacking the cane on top means it takes a small
   bite out of a large target instead of being swallowed by it.

   The button is only as big as the drawn cane; the ::before is the touch
   target, expanded to 44px around it because the players here are children. */
.npa-cane {
  position: absolute;
  z-index: 12;
  width: max(11px, 1.6cqw);
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  animation: npa-bob 3.4s ease-in-out infinite;
  filter: drop-shadow(0 .2cqw .3cqw rgba(20,40,70,.45));
}
.npa-cane::before {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: max(44px, 100%); height: max(44px, 100%);
  transform: translate(-50%, -50%);
  border-radius: 50%;
}
.npa-cane svg { width: 100%; display: block; }
.npa-cane.is-taken { display: none; }
.npa-cane:focus-visible {
  outline: none;
  filter: drop-shadow(0 0 3px var(--gold-glow, #f2dca0)) drop-shadow(0 0 7px var(--gold, #c8a24a));
}
@media (hover: hover) and (pointer: fine) {
  .npa-cane:hover { filter: drop-shadow(0 0 4px rgba(255,246,214,.9)); }
}
.npa-cane:active { transform: scale(.9); }
@keyframes npa-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-.4cqw); } }

/* ---------------- the elf ----------------
   The illustrated elf, lifted out of the map artwork into its own 62x86 sprite
   (assets/img/np-elf.webp) and walked around on top of it. 62/1376 = 4.51cqw,
   so at full map width it draws at its native size with no resampling; the
   floor keeps it visible on a phone, where the whole map is only ~330px wide. */
.npa-elf {
  z-index: 11;
  width: max(18px, 4.51cqw);
  transform: translate(-50%, -100%);
  transition-property: left, top;
  transition-timing-function: linear;
  transition-duration: 0ms;
  filter: drop-shadow(0 .3cqw .4cqw rgba(20,40,70,.5));
}
.npa-elf-body {
  position: relative;
  width: 100%;
  aspect-ratio: 62 / 86;
  animation: npa-step .46s ease-in-out infinite;
  animation-play-state: paused;
}
.npa-elf-img {
  display: block;
  width: 100%;
  height: 100%;
  user-select: none;
  -webkit-user-drag: none;
}
.npa-elf.is-walking .npa-elf-body { animation-play-state: running; }
@keyframes npa-step {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  25%      { transform: translateY(-.5cqw) rotate(-3deg); }
  75%      { transform: translateY(-.18cqw) rotate(3deg); }
}
.npa-elf-shadow {
  width: 130%;
  height: max(2px, .45cqw);
  margin-left: -15%;
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 50%, rgba(46,72,110,.5), rgba(46,72,110,0) 72%);
  filter: blur(2px);
}

/* ---------------- snow ----------------
   The frame's aspect ratio is fixed, so the fall distance is written in cqw:
   104cqh of a 1376x768 box is 58cqw. */
.npa-snow { inset: 0; z-index: 13; }
.npa-flake {
  position: absolute;
  top: 0;
  border-radius: 50%;
  background: #fff;
  animation: npa-fall linear infinite;
}
@keyframes npa-fall {
  0%   { transform: translate3d(0, -8%, 0);      opacity: 0; }
  8%   { opacity: 1; }
  100% { transform: translate3d(3cqw, 58cqw, 0); opacity: .85; }
}

.npa-vignette {
  inset: 0;
  z-index: 14;
  background: radial-gradient(122% 88% at 50% 44%, rgba(0,0,0,0) 54%, rgba(8,12,26,.44) 100%);
}

/* ---------------- earned-star badge ----------------
   Anchored to the bottom-right of the hotspot box, which puts it on the end
   of the building's painted name plaque rather than floating in the sky
   above it — the boxes here run from roof to plaque, not plaque only. */
.npa-badge {
  position: absolute;
  right: -3px;
  bottom: -3px;
  z-index: 2;
  display: none;
  place-items: center;
  width: clamp(15px, 2cqw, 26px);
  height: clamp(15px, 2cqw, 26px);
  border-radius: 50%;
  background: var(--forest-900, #0f1a14);
  border: 1px solid var(--gold, #c8a24a);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .55);
  animation: npa-rise .3s ease-out;
}
.np-spot.is-starred .npa-badge { display: grid; }
.npa-badge svg { width: 62%; height: 62%; color: var(--gold-200, #e3c878); }
@keyframes npa-rise {
  from { transform: translateY(6px) scale(.8); opacity: 0; }
  to   { transform: translateY(0)   scale(1);  opacity: 1; }
}

/* ============================================================
   CHROME — score, tools, toast

   On .np-map, not .np-map-inner, so these stay pinned to the frame's corners
   instead of scrolling off with the artwork in fullscreen. Painted from
   tokens.css so the game HUD reads as part of this site rather than a panel
   borrowed from somewhere else.
   ============================================================ */
.npa-hud,
.npa-tools {
  position: absolute;
  top: 12px;
  z-index: 16;
  display: flex;
  align-items: center;
  gap: 7px;
}
.npa-hud { left: 12px; flex-wrap: wrap; }
.npa-tools { right: 12px; }

.npa-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border-radius: 999px;
  background: rgba(9, 17, 12, .72);
  border: 1px solid var(--hairline, rgba(200, 162, 74, .28));
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--cream, #f6efdf);
  font-family: var(--body, system-ui, sans-serif);
  line-height: 1;
}
.npa-chip-ico { display: inline-flex; width: 15px; height: 15px; }
.npa-chip-ico svg { width: 100%; height: 100%; }
.npa-chip-ico--star { color: var(--gold-200, #e3c878); }
.npa-chip-ico--cane { color: #ff8898; margin-left: 4px; }
.npa-num {
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 13px;
  font-weight: 600;
  color: var(--snow, #fbfaf6);
  font-variant-numeric: tabular-nums;
}

.npa-tool {
  position: relative;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 10px;
  background: rgba(9, 17, 12, .72);
  border: 1px solid var(--hairline, rgba(200, 162, 74, .28));
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--gold-200, #e3c878);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background .16s ease, color .16s ease;
}
.npa-tool svg { width: 17px; height: 17px; }
@media (hover: hover) and (pointer: fine) {
  .npa-tool:hover { background: rgba(200, 162, 74, .22); color: var(--snow, #fbfaf6); }
}
.npa-tool:active { background: rgba(200, 162, 74, .22); transform: scale(.95); }
.npa-tool:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(200, 162, 74, .5); }

/* The toast sits bottom-centre and the explore pill bottom-right. At the
   map's full width they never meet; on a narrow screen they would, so the
   toast lifts clear above it. */
.npa-toast {
  position: absolute;
  left: 50%;
  bottom: 4%;
  z-index: 18;
  transform: translate(-50%, 8px);
  max-width: min(78%, 460px);
  padding: 8px 16px;
  border-radius: 999px;
  background: rgba(9, 17, 12, .9);
  border: 1px solid var(--hairline, rgba(200, 162, 74, .28));
  box-shadow: 0 10px 26px rgba(0, 0, 0, .55);
  color: var(--cream, #f6efdf);
  font-family: var(--body, system-ui, sans-serif);
  font-size: 13px;
  line-height: 1.35;
  text-align: center;
  text-wrap: pretty;
  opacity: 0;
  pointer-events: none;
  transition: opacity .24s ease, transform .24s ease;
}
.npa-toast.is-on { opacity: 1; transform: translate(-50%, 0); }

@container npmap (max-width: 620px) {
  .npa-hud, .npa-tools { top: 8px; gap: 5px; }
  .npa-hud { left: 8px; }
  .npa-tools { right: 8px; }
  .npa-chip { padding: 5px 9px; gap: 5px; }
  .npa-chip-ico { width: 13px; height: 13px; }
  .npa-num { font-size: 11.5px; }
  /* No width cap: with one score chip there is nothing to stack, and capping
     it would squeeze the score itself. */
  .npa-tool { width: 30px; height: 30px; border-radius: 9px; }
  .npa-tool svg { width: 15px; height: 15px; }
  .npa-toast { bottom: 15%; font-size: 12px; padding: 7px 13px; max-width: 84%; }
  /* Half the snow on a phone: the flakes are sub-pixel at this size and the
     count is a battery cost with nothing to show for it. */
  .npa-flake:nth-child(n+21) { display: none; }
}

/* One more notch down for a frame around 330px: keep the single score chip and
   tools compact enough to leave the map artwork visible. */
@container npmap (max-width: 400px) {
  .npa-chip { padding: 4px 7px; gap: 4px; }
  .npa-chip-ico { width: 11px; height: 11px; }
  .npa-chip-ico--cane { margin-left: 2px; }
  .npa-num { font-size: 10.5px; }
  .npa-tool { width: 28px; height: 28px; }
  .npa-tool svg { width: 14px; height: 14px; }
}

/* Touch targets. Keep the visible tools at 40px so the pair clears the painted
   banner, then expand each button's invisible hit area to 44px. The 8px layout
   gap leaves 4px between the expanded targets, so they never swallow an edge. */
@media (pointer: coarse) {
  .npa-tools { gap: 8px; }
  .npa-tool { width: 40px; height: 40px; }
  .npa-tool::before {
    content: "";
    position: absolute;
    inset: -2px;
    border-radius: 12px;
  }
  .npa-tool svg { width: 18px; height: 18px; }
}

/* Full-viewport frames respect notches. The explicit fullscreen overlay also
   reserves room for its close button; natural portrait has no extra control. */
@media (orientation: portrait) {
  .np-map:not(.is-fs) .npa-hud {
    top: max(10px, env(safe-area-inset-top));
    left: max(10px, env(safe-area-inset-left));
  }
  .np-map:not(.is-fs) .npa-tools {
    top: max(10px, env(safe-area-inset-top));
    right: max(10px, env(safe-area-inset-right));
  }
}
.np-map.is-fs .npa-hud {
  top: max(10px, env(safe-area-inset-top));
  left: max(10px, env(safe-area-inset-left));
}
.np-map.is-fs .npa-tools {
  top: max(10px, env(safe-area-inset-top));
  right: calc(max(10px, env(safe-area-inset-right)) + 52px);
}

/* Outline the canes and mark the elf's walking targets alongside the
   hotspots: append ?maphot=1 to the URL. */
.np-map--debug .npa-cane { outline: 2px solid #ff4d4d; outline-offset: 3px; }
.npa-target {
  z-index: 15;
  width: 10px; height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: 50%;
  background: #00e5ff;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, .6);
}

/* ---------------- reduced motion ----------------
   Everything that moves stops. The map keeps every bit of its function: the
   elf still marks where you are, it just steps there instead of strolling
   (np-map.js skips the walk and opens the activity straight away), and the
   snow is dropped rather than frozen mid-air. */
@media (prefers-reduced-motion: reduce) {
  .npa-curtain, .npa-star, .npa-glow, .npa-puff, .npa-ice,
  .npa-cane, .npa-elf-body, .npa-badge {
    animation: none;
  }
  .npa-snow { display: none; }
  .npa-elf { transition: none; }
  .npa-toast { transition: opacity .01s linear; }
  .npa-curtain { opacity: .5; }
  .npa-star { opacity: .7; }
  .npa-glow { opacity: .7; }
  .npa-puff { opacity: .28; }
  .npa-ice { opacity: .25; }
}
