/* ===== tokens ===== */
:root {
  --ground: #fdf3f6;
  --panel: #fffafc;
  --panel-2: #f8e9ef;
  --ink: #3a2430;
  --ink-2: #7a5f6c;
  --ink-3: #7f6975;
  --line: #efd8e2;
  --grass: #5da838;
  --grass-soft: #eaf5e2;
  --blush: #ef7fa8;
  --lilac: #8f6fd0;
  --wood: #96684a;
  --locked: #b9a9b2;
  --shadow: 0 1px 0 rgba(58,36,48,.06), 0 10px 30px -18px rgba(58,36,48,.35);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground: #1d1620; --panel: #271d2b; --panel-2: #322436;
    --ink: #f6e9ef; --ink-2: #c3a9b6; --ink-3: #9a8290; --line: #3d2d42;
    --grass: #8fd268; --grass-soft: #2a3626; --blush: #ff9dbe; --lilac: #b79ce8;
    --wood: #c08a66; --locked: #574a52;
    --shadow: 0 1px 0 rgba(0,0,0,.2), 0 12px 34px -18px rgba(0,0,0,.8);
  }
}
:root[data-theme="dark"] {
  --ground: #1d1620; --panel: #271d2b; --panel-2: #322436;
  --ink: #f6e9ef; --ink-2: #c3a9b6; --ink-3: #8d7583; --line: #3d2d42;
  --grass: #8fd268; --grass-soft: #2a3626; --blush: #ff9dbe; --lilac: #b79ce8;
  --wood: #c08a66; --locked: #574a52;
  --shadow: 0 1px 0 rgba(0,0,0,.2), 0 12px 34px -18px rgba(0,0,0,.8);
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0; background: var(--ground); color: var(--ink);
  font-family: "Nunito Sans", ui-sans-serif, system-ui, sans-serif;
  overflow-x: hidden;
}
.px { font-family: "Silkscreen", ui-monospace, monospace; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
img { image-rendering: pixelated; }
:where(button, [tabindex]):focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* ===== persistent bar ===== */
.bar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 10px 18px; background: var(--panel); border-bottom: 2px solid var(--line);
}
.bar[hidden] { display: none; }
.bar-cd { display: flex; align-items: baseline; gap: 8px; }
.bar-cd .lbl { color: var(--grass); }
.cd { font-family: "Silkscreen", monospace; font-size: 14px; color: var(--blush); font-variant-numeric: tabular-nums; }
.bar-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.bar-clocks { display: flex; gap: 18px; }
.clock { display: flex; flex-direction: column; gap: 1px; text-align: right; }
.clock b { font-family: "Silkscreen", monospace; font-weight: 400; font-size: 15px; font-variant-numeric: tabular-nums; }
.clock .px { color: var(--ink-3); }

.bar-memory-btn {
  display: flex; align-items: center; gap: 6px;
  font-family: "Baloo 2", sans-serif; font-weight: 700; font-size: 13px;
  color: var(--ink); background: var(--panel-2); border: 2px solid var(--line);
  border-radius: 999px; padding: 7px 14px; white-space: nowrap;
  transition: border-color .15s ease, transform .15s ease;
}
.bar-memory-btn:hover { border-color: var(--blush); transform: translateY(-1px); }
.bar-memory-btn:active { transform: translateY(0); }
@media (max-width: 640px) {
  .bar { flex-wrap: wrap; row-gap: 8px; }
  .bar-actions { order: 3; flex-basis: 100%; justify-content: center; }
}

/* ===== memory generator card ===== */
.memory-card { display: flex; flex-direction: column; gap: 10px; }
.memory-card-tag {
  align-self: flex-start; font-family: "Silkscreen", monospace; font-size: 11px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px;
}
.memory-card-date { color: var(--ink-3); font-size: 12px; }
.memory-card p { margin: 0; line-height: 1.55; }
.memory-card-next {
  align-self: flex-start; margin-top: 4px; font-family: "Baloo 2", sans-serif; font-weight: 700;
  font-size: 13.5px; color: var(--ink); background: var(--panel-2); border: 2px solid var(--line);
  border-radius: 5px; padding: 8px 16px;
}
.memory-card-next:hover { border-color: var(--blush); }

/* ===== friendship passport ===== */
.panel.panel-passport { border-color: #b79ce8; }
.passport-cover { text-align: center; margin: -4px 0 16px; }
.passport-cover img {
  max-height: 150px; width: auto; border-radius: 8px; box-shadow: var(--shadow);
  animation: passport-cover-in .5s cubic-bezier(.2,.8,.2,1) both;
  image-rendering: auto;
}
@keyframes passport-cover-in { from { opacity: 0; transform: translateY(-8px) scale(.96); } to { opacity: 1; transform: translateY(0) scale(1); } }
@media (prefers-reduced-motion: reduce) { .passport-cover img { animation: none; } }
.passport-progress { color: var(--ink-2); margin: 0 0 14px; text-align: center; }
.passport-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px;
}
.stamp-card {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px;
  border: 2px solid var(--line); border-radius: 8px; padding: 14px 10px; background: var(--panel-2);
  font-family: "Nunito Sans", ui-sans-serif, system-ui, sans-serif;
}
.stamp-card.unlocked { border-color: #e8c94a; background: linear-gradient(180deg, #fff8e6, var(--panel-2)); }
.stamp-icon { width: 72px; height: 72px; object-fit: contain; image-rendering: auto; }
.stamp-icon.stamp-icon-locked { opacity: .8; filter: grayscale(1) contrast(.9); }
.stamp-icon-emoji {
  width: 72px; height: 72px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  font-size: 34px; background: linear-gradient(180deg, #fff8e6, var(--panel-2)); border: 3px solid #e8c94a;
}
.stamp-label { color: var(--ink-2); }
.stamp-card.unlocked .stamp-label { color: var(--blush); }
.stamp-text, .stamp-hint { margin: 0; font-size: 12px; line-height: 1.45; color: var(--ink-2); }
.stamp-hint { font-style: italic; color: var(--ink-3); }
button.stamp-card.claimable { cursor: pointer; transition: border-color .15s ease, transform .12s ease; }
button.stamp-card.claimable:hover { border-color: #e8c94a; transform: translateY(-2px); }
button.stamp-card.claimable:active { transform: translateY(0) scale(.98); }

/* the "ink stamp" slam: a freshly-unlocked card thumps down oversized and
   slightly rotated, like a rubber stamp hitting the page, then settles. */
.stamp-card.just-unlocked { animation: stamp-slam .5s cubic-bezier(.2,.9,.3,1.2) both; }
@keyframes stamp-slam {
  0% { transform: scale(2.4) rotate(-14deg); opacity: 0; }
  55% { transform: scale(.94) rotate(3deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .stamp-card.just-unlocked { animation: none; } }

.stamp-toast {
  position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 16px);
  display: flex; align-items: center; gap: 10px;
  background: var(--panel); border: 2px solid #e8c94a; border-radius: 999px;
  padding: 8px 20px 8px 10px; box-shadow: var(--shadow); z-index: 95;
  font-family: "Baloo 2", sans-serif; font-size: 13.5px; color: var(--ink);
  opacity: 0; transition: opacity .25s ease, transform .25s ease; pointer-events: none;
}
.stamp-toast.show { opacity: 1; transform: translate(-50%, 0); }
.stamp-toast-icon { width: 30px; height: 30px; object-fit: contain; image-rendering: auto; }
.stamp-toast-icon.stamp-icon-emoji { width: 26px; height: 26px; font-size: 15px; border-width: 2px; flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .stamp-toast { transition: opacity .01s linear; } }

/* a little extra bounce for the achievement-unlock toast specifically,
   so it reads as more of a celebration than a routine notice */
.achievement-toast {
  border-color: #e8c94a; background: linear-gradient(180deg, #fff8e6, var(--panel));
  transition: opacity .3s ease, transform .3s cubic-bezier(.3, 1.6, .4, 1);
}
.achievement-toast.show { transform: translate(-50%, 0) scale(1); }
.achievement-toast:not(.show) { transform: translate(-50%, 10px) scale(.85); }
/* one extra, restrained beat that a plain stamp/collectible toast doesn't
   get — a soft ring blooming behind the icon once, so an achievement (a
   milestone crossed, not just a thing found) reads as a step up without
   turning into a game-style celebration. */
.achievement-toast .stamp-toast-icon { position: relative; }
.achievement-toast .stamp-toast-icon::after {
  content: ""; position: absolute; inset: -5px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(232, 201, 74, .5), transparent 72%);
  animation: achievement-ring .9s ease .05s both;
}
@keyframes achievement-ring {
  0%   { opacity: 0; transform: scale(.5); }
  45%  { opacity: 1; transform: scale(1.15); }
  100% { opacity: 0; transform: scale(1.5); }
}
@media (prefers-reduced-motion: reduce) {
  .achievement-toast { transition: opacity .01s linear; }
  .achievement-toast .stamp-toast-icon::after { animation: none; }
}

/* ===== Noodle's status panel ===== */
.noodle-panel { display: flex; flex-direction: column; gap: 14px; text-align: center; }
.noodle-video {
  width: 100%; max-width: 200px; margin: 0 auto; border-radius: 14px; border: 3px solid var(--line);
  background: #fff; display: block; aspect-ratio: 5 / 6; object-fit: contain;
}
.noodle-stats { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }
.noodle-stat {
  display: flex; flex-direction: column; gap: 4px; min-width: 100px;
  border: 2px solid var(--line); border-radius: 8px; padding: 10px 14px; background: var(--panel-2);
}
.noodle-stat .px { color: var(--ink-3); }
.noodle-stat b { font-family: "Baloo 2", sans-serif; font-size: 14px; color: var(--ink); text-transform: capitalize; }
.noodle-activity { margin: 0; color: var(--ink-2); font-style: italic; }
.noodle-treat-btn {
  align-self: center; border: 2px solid var(--line); border-radius: 999px; padding: 10px 22px;
  background: var(--panel-2); color: var(--ink); font-family: "Baloo 2", sans-serif; font-weight: 700; font-size: 14.5px;
  transition: border-color .15s ease, transform .12s ease;
}
.noodle-treat-btn:hover { border-color: #e8c94a; transform: translateY(-2px); }
.noodle-treat-btn:active { transform: translateY(0) scale(.97); transition-duration: .08s; }
.noodle-response {
  margin: 0; min-height: 1.4em; color: var(--blush); font-weight: 600;
  opacity: 0; transform: translateY(4px); transition: opacity .25s ease, transform .25s ease;
}
.noodle-response.show { opacity: 1; transform: translateY(0); }
.noodle-warning { margin: 0; font-size: 12px; font-style: italic; color: var(--ink-3); }
.noodle-total { margin: 0; color: var(--ink-3); }
@media (prefers-reduced-motion: reduce) { .noodle-response { transition: opacity .01s linear; } }

/* ===== screens ===== */
.screen { min-height: calc(100vh - 52px); position: relative; opacity: 1; transition: opacity .22s ease, transform .22s ease; }
.screen[hidden] { display: none; }
.screen.entering { opacity: 0; transform: scale(.98); }

@media (prefers-reduced-motion: reduce) {
  .screen { transition: none; }
}

/* ===== map → room: a short camera move into the chosen memory ===== */
/* Every map card already shows the exact artwork its room paints as a
   background, so leaving the map can be a real shared element instead of a
   cross-fade: the thumbnail lifts off the card it was clicked on, travels
   into the room's scene box, and the real scene fades up underneath it once
   it lands. No new art and no library — only the artwork moves. */

/* the card dips first, so the world acknowledges the choice before anything
   travels. It has to be an animation, not a transform: .node already carries
   node-in with fill "both", and a filled animation outranks any plain
   declaration we could put here (including :hover's lift). */
.map .node.node-chosen {
  z-index: 2;
  /* it steadies as she commits to it: its drifting artwork stops, and it holds
     the frame while everything around it steps back */
  box-shadow: var(--shadow), 0 0 0 2px var(--blush), 0 26px 46px -18px rgba(239, 127, 168, .55);
  animation: node-choose .38s cubic-bezier(.3, 0, .2, 1) both;
}
@keyframes node-choose {
  0%   { transform: scale(1); }
  30%  { transform: scale(.965); }   /* the press answers the click */
  100% { transform: scale(1.035); }  /* then it becomes the thing you're going to */
}

/* the rest of the world recedes so the chosen place is unmistakably the
   anchor — depth and hierarchy, not everything fading out together */
.map.choosing .node:not(.node-chosen) {
  opacity: .3; transform: scale(.985); filter: saturate(.82);
  transition: opacity .36s ease, transform .36s cubic-bezier(.3, 0, .3, 1), filter .36s ease;
}
.map-screen.choosing .map-title,
.map-screen.choosing .minal-meter { opacity: .28; transition: opacity .36s ease; }

/* the map she's leaving, frozen exactly where she was looking so handing the
   document flow over to the room can't jump the page under the moving
   artwork. Below the bar, above the arriving room. */
.screen.screen-leaving {
  position: fixed; left: 0; right: 0; z-index: 10; pointer-events: none;
  transition: opacity .34s ease, transform .5s cubic-bezier(.4, 0, .2, 1);
}
.screen.screen-leaving.receding { opacity: 0; transform: scale(.975); }

/* the artwork in flight */
.room-flight {
  position: fixed; z-index: 45; pointer-events: none;
  border-radius: 6px; overflow: hidden;
  background-repeat: no-repeat; background-size: cover;
  box-shadow: 0 34px 70px -34px rgba(0, 0, 0, .6);
}
.room-flight.landed { opacity: 0; transition: opacity .2s ease; }

/* arrival: the scene is already sitting exactly under the flight layer, so it
   only has to fade up — then the room's own chrome follows a beat later, so
   the place reads as settling before its UI catches up with it. */
.room-screen.room-arriving .scene,
.room-screen.room-arriving .back,
.room-screen.room-arriving .room-info,
.room-screen.room-arriving .hotspots { opacity: 0; }
/* the place arrives first and is allowed a beat on its own — she looks around
   before the interface presents itself */
.room-screen.room-settling .scene { animation: room-scene-in .3s ease both; }
.room-screen.room-settling .back { animation: room-chrome-in .4s ease .18s both; }
.room-screen.room-settling .room-info { animation: room-chrome-in .4s ease .28s both; }
.room-screen.room-settling .hotspots { animation: room-chrome-in .4s ease .4s both; }
@keyframes room-scene-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes room-chrome-in { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: translateY(0); } }

/* stepping back out: the room recedes and the map returns as though the
   camera pulled back to it, rather than the two simply cross-fading */
.screen.leaving-back { opacity: 0; transform: scale(.965); }
.screen.entering-back { opacity: 0; transform: scale(1.025); }
.screen.back-move { transition: opacity .26s ease, transform .3s cubic-bezier(.2, .7, .2, 1); }

@media (prefers-reduced-motion: reduce) {
  .map .node.node-chosen { animation: none; }
  .map.choosing .node:not(.node-chosen),
  .map-screen.choosing .map-title, .map-screen.choosing .minal-meter { transition: none; }
  .screen.screen-leaving { transition: none; }
  .screen.back-move { transition: none; }
  .room-screen.room-settling .scene,
  .room-screen.room-settling .back,
  .room-screen.room-settling .room-info,
  .room-screen.room-settling .hotspots { animation: none; }
}

/* ===== opening screen ===== */
.intro-screen {
  min-height: 100vh; display: flex; align-items: center; justify-content: flex-end;
  position: relative; overflow: hidden; background: #8fb9d6; color: #f3e8ee;
  text-align: center; padding: 20px;
  /* right-anchored, vertically centered — clear of the hiker, who sits on
     the left of the photo; centers horizontally instead once the layout
     below switches to a narrow/portrait column */
  padding-right: clamp(24px, 9vw, 140px);
}
@media (max-aspect-ratio: 4/5) {
  .intro-screen { justify-content: center; padding-right: 20px; }
}

/* the trekking photo, letterboxed (never cropped/stretched) so the
   original art is shown at full quality and every overlay below lines
   up with it pixel-for-pixel regardless of viewport shape */
.intro-photo { position: absolute; inset: 0; overflow: hidden; background: linear-gradient(#bcdcef, #e7f1f6); }
.intro-photo-frame {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(1);
  width: min(100%, 177.68vh);
  height: min(100%, 56.28vw);
  aspect-ratio: 1672 / 941;
  animation: intro-kenburns 46s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes intro-kenburns {
  0% { transform: translate(-50%, -50%) scale(1); }
  100% { transform: translate(-52%, -49%) scale(1.07); }
}
/* on narrow/portrait screens, fill the viewport (cropping the sides)
   instead of letterboxing — same untouched image, just less empty
   space; the flag/cloud overlays stay aligned since they're positioned
   relative to this same frame either way */
@media (max-aspect-ratio: 4/5) {
  .intro-photo-frame { width: max(100%, 177.68vh); height: max(100%, 56.28vw); }
}
.intro-photo-img { display: block; width: 100%; height: 100%; object-fit: fill; }

/* prayer-flag strings: real pixels cropped from the photo (feathered
   edges so the patch is invisible when still), given a tiny fabric-like
   sway so they read as alive without touching image quality anywhere */
.intro-flag {
  position: absolute; background-repeat: no-repeat; background-size: 100% 100%;
  transform-origin: top center; animation-name: flag-sway; animation-timing-function: ease-in-out; animation-iteration-count: infinite;
  will-change: transform;
}
.intro-flag-left { animation-duration: 4.2s; }
.intro-flag-right { animation-duration: 5s; animation-delay: -1.4s; }
@keyframes flag-sway {
  0%, 100% { transform: skewX(-2.2deg) rotate(-0.6deg); }
  50% { transform: skewX(2.4deg) rotate(0.6deg); }
}

.intro-clouds, .intro-snow { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.intro-clouds i {
  position: absolute; display: block; border-radius: 50%;
  background: rgba(255,255,255,.55); filter: blur(6px);
  animation-name: cloud-drift; animation-timing-function: linear; animation-iteration-count: infinite;
  will-change: transform;
}
@keyframes cloud-drift {
  from { transform: translateX(0); }
  to { transform: translateX(var(--drift, 60px)); }
}
.intro-snow i {
  position: absolute; top: -4px; display: block; border-radius: 50%; background: #fff;
  opacity: 0; animation-name: snow-fall; animation-timing-function: linear; animation-iteration-count: infinite;
  will-change: transform, opacity;
}
@keyframes snow-fall {
  0% { opacity: 0; transform: translate(0, 0); }
  10% { opacity: .8; }
  90% { opacity: .5; }
  100% { opacity: 0; transform: translate(var(--drift, 10px), 340px); }
}

/* ---- the opening, as a sequence ----
   Every element below rests at its natural, fully visible style; the
   entrances use fill "backwards" so they only hold their *starting* frame
   during the delay, then hand the element back to plain CSS once they end.
   (A "both" fill would keep overriding it forever — which is what used to
   silently disable the button's hover lift.) It also means that if an
   animation never runs at all, nothing is ever left invisible.

     0.0s  dark, quiet
     0.4s  the veil starts to lift; the camera is already drifting in
     1.45s the reading surface settles into the scene
     1.8s  A LITTLE WORLD tracks in
     2.2s  for Minal pulls into focus
     3.0s  the heart arrives, a beat behind her name
     3.05s the three lines, one at a time, the last held back a little
     4.35s step in →
     ~5.5s the sequence ends; only slow, near-still life remains
   Enter / Space works from the first frame, and a click anywhere else
   hurries the rest along (see hurryIntro in main.js). */

/* beat 1 → 2: the dark that the world is revealed out of. A long, gentle
   curve rather than a fade, so the light seems to come up on the scene. */
.intro-veil {
  position: absolute; inset: 0; z-index: 4; background: #05060a; pointer-events: none;
  opacity: 0; animation: intro-veil-lift 1.9s cubic-bezier(.4, 0, .2, 1) .4s backwards;
}
@keyframes intro-veil-lift { from { opacity: 1; } to { opacity: 0; } }

/* beat 2: the camera travelling in. The push runs on the photo's container
   so it composes with the frame's own slow drift instead of fighting it for
   the same transform. The curve is deliberately gentle rather than front-
   loaded: most of the travel happens *after* the veil lifts, so she watches
   the camera arrive instead of only catching the last half-percent of it. */
.intro-photo {
  transform-origin: 46% 42%;
  animation: intro-camera-in 5.2s cubic-bezier(.3, .12, .2, 1) backwards;
}
@keyframes intro-camera-in { from { transform: scale(1.12); } to { transform: scale(1); } }

@keyframes intro-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

/* the title zone: a compact, quiet reading surface — not a website card.
   It lives on ::before so it can settle into the scene on its own beat,
   ahead of the words, without its opacity compounding into theirs. */
.intro-inner {
  position: relative; z-index: 3;
  width: min(360px, 84vw);
  display: flex; flex-direction: column; align-items: center;
  padding: 32px 26px;
  /* once the sequence is over: the words drift a few pixels against the
     photo's own slower drift — just enough parallax to feel like depth */
  animation: intro-float 16s ease-in-out 5.5s infinite alternate;
}
.intro-inner::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: rgba(10, 8, 15, .2);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 28px;
  box-shadow: 0 18px 40px -22px rgba(0, 0, 0, .45);
  animation: intro-surface-in 1.2s cubic-bezier(.22, 1, .36, 1) 1.45s backwards;
}
@keyframes intro-surface-in { from { opacity: 0; transform: scale(.965); } to { opacity: 1; transform: scale(1); } }
@keyframes intro-float { from { transform: translateY(0); } to { transform: translateY(-5px); } }

/* beat 3: small and precise — the letters close up from wide tracking to
   their normal spacing as they fade in, the way a film title card settles */
.intro-eyebrow {
  color: #f8ecf5; margin: 0 0 10px;
  text-shadow: 0 1px 2px rgba(0,0,0,.6);
  animation: intro-track-in 1.3s cubic-bezier(.22, 1, .36, 1) 1.8s backwards;
}
@keyframes intro-track-in { from { opacity: 0; letter-spacing: .34em; } to { opacity: 1; letter-spacing: .08em; } }

/* beat 4: the focal point — slower, and it arrives *into focus*: a small
   rise, a hair of scale and a light defocus that clears as it lands */
.intro-title {
  font-family: "Baloo 2", sans-serif; font-size: clamp(28px, 5.6vw, 38px); margin: 0 0 16px;
  text-shadow: 0 1px 3px rgba(0,0,0,.55);
  animation: intro-focus-in 1.5s cubic-bezier(.22, 1, .36, 1) 2.2s backwards;
}
@keyframes intro-focus-in {
  from { opacity: 0; transform: translateY(14px) scale(.985); filter: blur(5px); }
  60%  { filter: blur(0); }
  to   { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
}
/* the heart lands a beat after her name, then only ever breathes — once
   every eight seconds or so, far too slow to read as a pulse */
.intro-title .heart {
  color: #ff9dbe; display: inline-block; transform-origin: 50% 60%;
  animation:
    intro-heart-arrive 1s cubic-bezier(.22, 1, .36, 1) 3s backwards,
    intro-heart-breathe 4.2s ease-in-out 4.2s infinite alternate;
}
@keyframes intro-heart-arrive { from { opacity: 0; transform: translateY(4px) scale(.55); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes intro-heart-breathe { from { transform: scale(1); } to { transform: scale(1.07); } }

/* beat 5: the poem — a quieter, more literary voice than the rest of the
   site's rounded display type, told one line at a time. The last line is
   held back slightly longer than the first two; it's the one that matters. */
.intro-poem {
  font-family: "Fraunces", Georgia, serif; font-style: italic; font-weight: 400;
  font-size: clamp(15.5px, 3.2vw, 18px); line-height: 1.5; letter-spacing: .01em;
  color: #fbf6f9; margin: 0; text-wrap: balance;
  text-shadow: 0 1px 3px rgba(0,0,0,.55);
}
.intro-poem-line {
  display: block;
  animation: intro-line-in 1.2s cubic-bezier(.22, 1, .36, 1) backwards;
}
.intro-poem-line:nth-child(1) { animation-delay: 3.05s; }
.intro-poem-line:nth-child(2) { animation-delay: 3.4s; }
.intro-poem-line:nth-child(3) { animation-delay: 3.85s; }
@keyframes intro-line-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

/* beat 6: the invitation. Clickable the whole time; it simply becomes
   visible last, then its arrow leans forward once — a single nudge, not a
   loop — so the control announces itself without asking for attention. */
.intro-enter {
  position: relative; z-index: 3; margin-top: 22px;
  font-family: "Baloo 2", sans-serif; font-size: 14px; letter-spacing: .02em;
  padding: 10px 22px;
  border: 1px solid rgba(255,255,255,.5); border-radius: 10px; background: rgba(255,255,255,.08);
  color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.5);
  display: inline-flex; align-items: center; gap: 8px;
  animation: intro-rise .9s cubic-bezier(.22, 1, .36, 1) 4.35s backwards;
  transition: border-color .2s ease, background .2s ease, transform .2s ease;
}
.intro-enter span {
  display: inline-block; transition: transform .2s ease;
  animation: intro-arrow-nudge 1.1s ease-in-out 5.6s;
}
@keyframes intro-arrow-nudge { 0%, 100% { transform: translateX(0); } 45% { transform: translateX(5px); } }
.intro-enter:hover { border-color: #fff; background: rgba(255,255,255,.16); transform: translateY(-2px); }
.intro-enter:hover span { transform: translateX(4px); }
.intro-enter:active { transform: translateY(0) scale(.97); transition-duration: .08s; }

/* reduced motion: no veil, no camera, no stagger, no drift. Because every
   element's resting style is already its visible one, switching animation
   off is all it takes — nothing waits, nothing is left hidden. */
@media (prefers-reduced-motion: reduce) {
  .intro-veil { display: none; }
  .intro-photo, .intro-photo-frame,
  .intro-inner, .intro-inner::before,
  .intro-eyebrow, .intro-title, .intro-title .heart,
  .intro-poem-line, .intro-enter, .intro-enter span { animation: none; }
  .intro-flag, .intro-clouds i, .intro-snow i { animation: none; }
}

/* ===== arrival: crossing from the opening into the world =====
   Nothing here changes how the opening or the map look at rest — it only
   describes the moment between them. The camera keeps moving forward through
   the opening scene, the dark closes over it, and the world is already there
   when the dark lifts: a threshold, rather than one screen replacing another.
   The veil is a transient element created in enterFromIntro(); it lives on
   <body> so it can outlive the intro screen it started over. */
.world-veil { position: fixed; inset: 0; z-index: 70; background: #05060a; opacity: 0; pointer-events: none; }
.world-veil.closing { animation: world-veil-close .55s cubic-bezier(.45, 0, .7, .6) .25s both; }
.world-veil.opening { animation: world-veil-open .6s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes world-veil-close { from { opacity: 0; } to { opacity: 1; } }
@keyframes world-veil-open { from { opacity: 1; } to { opacity: 0; } }

/* the click itself: a small compression and release, so the button reads as
   having started something rather than having been merely registered */
.intro-enter.pressed { animation: world-press .24s cubic-bezier(.3, 0, .3, 1) both; }
@keyframes world-press { 0% { transform: scale(1); } 38% { transform: scale(.94); } 100% { transform: scale(1); } }

/* the opening answers: the camera accelerates forward into the scene while
   the words step out of the way first. The push starts from wherever the
   opening's own slow camera had got to (--depart-from, set on click), so it
   continues that move instead of snapping back to the start of it. */
.intro-screen.departing .intro-photo { animation: world-push .85s cubic-bezier(.5, 0, .75, .55) both; }
@keyframes world-push {
  from { transform: scale(var(--depart-from, 1)); }
  /* relative, never absolute: if she clicks while the opening's own camera is
     still wide, a fixed end scale would pull the camera backwards — the exact
     opposite of moving into the scene. Always push on from where we are. */
  to   { transform: scale(calc(var(--depart-from, 1) + .085)); }
}
.intro-screen.departing .intro-inner { animation: world-leave .42s cubic-bezier(.4, 0, 1, 1) both; }
@keyframes world-leave {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(-6px) scale(.985); }
}

/* the world settles out of the push — it is revealed by the veil lifting, so
   this only has to decelerate; fading it here too would muddy the children */
/* the settle scales a few pixels past the viewport; that can't be scrolled to
   (body carries no-scroll for the duration, and overflow-x: hidden after), so
   it needs no clipping of its own — clipping would only cut the card shadows */
.map-screen.arriving { transform-origin: 50% 42%; animation: world-settle .95s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes world-settle { from { transform: scale(1.035); } to { transform: scale(1); } }
.map-screen.arriving .map-title { animation: world-arrive-rise .7s cubic-bezier(.22, 1, .36, 1) .15s backwards; }
.map-screen.arriving .minal-meter { animation: world-arrive-rise .7s cubic-bezier(.22, 1, .36, 1) .3s backwards; }
@keyframes world-arrive-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
/* the destinations themselves keep the map's own node-in; the arrival only
   widens the gap between them (delays set in JS) so they arrive one at a time */

@media (prefers-reduced-motion: reduce) {
  .world-veil, .intro-enter.pressed,
  .intro-screen.departing .intro-photo, .intro-screen.departing .intro-inner,
  .map-screen.arriving, .map-screen.arriving .map-title, .map-screen.arriving .minal-meter { animation: none; }
}

/* ===== world map ===== */
.map-screen { display: flex; flex-direction: column; align-items: center; padding: 48px 20px 60px; }

/* ---- the map, quietly alive ----
   Three layers that answer the pointer at different rates (light least, the
   destinations most) so the place reads as having depth instead of being a
   list of cards. Nothing here changes the map's design: it's the existing
   artwork and the existing palette, only breathing. Ambient work is gated
   behind .map-alive, which JS adds once the arrival has settled — the world
   is allowed a moment of stillness first. */
.map-atmosphere {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: 0;
  /* the room's own lamplight, reaching the map: existing blush and lilac at
     very low alpha, plus a soft corner falloff to seat the world in its frame */
  background:
    radial-gradient(62% 46% at 50% 0%, rgba(239, 127, 168, .07), transparent 70%),
    radial-gradient(52% 42% at 14% 88%, rgba(143, 111, 208, .06), transparent 72%),
    radial-gradient(120% 92% at 50% 48%, transparent 56%, rgba(6, 4, 10, .17));
  /* furthest away, so it answers least and lags longest behind her */
  transform: translate3d(calc(var(--map-px, 0) * 2px), calc(var(--map-py, 0) * 1.5px), 0);
  transition: transform .9s cubic-bezier(.2, .7, .2, 1);
}
.map-screen.map-alive .map-atmosphere {
  animation: map-light-in 1.6s ease both, map-light-breathe 26s ease-in-out 1.6s infinite alternate;
}
@keyframes map-light-in { from { opacity: 0; } to { opacity: .82; } }
@keyframes map-light-breathe { from { opacity: .82; } to { opacity: 1; } }

/* the UI sits in front of the light, so it answers more than the light does */
.map-title, .minal-meter {
  position: relative; z-index: 1;
  transform: translate3d(calc(var(--map-px, 0) * 4px), calc(var(--map-py, 0) * 3px), 0);
  transition: transform .7s cubic-bezier(.2, .7, .2, 1);
}
/* the destinations are nearest, so they answer most — still only a few pixels */
.map {
  position: relative; z-index: 1;
  transform: translate3d(calc(var(--map-px, 0) * 7px), calc(var(--map-py, 0) * 5px), 0);
  transition: transform .7s cubic-bezier(.2, .7, .2, 1);
}

/* each destination's own artwork drifts, so a card is a window onto somewhere
   that is still happening rather than a thumbnail. Duration and a negative
   delay are set per card in renderMap(), so they are already out of step with
   each other on the first frame and never fall into sync. */
.map-alive .node-art {
  animation: map-art-drift var(--drift-dur, 34s) ease-in-out var(--drift-delay, 0s) infinite alternate;
}
@keyframes map-art-drift {
  from { transform: scale(1.06) translate3d(-.6%, -.4%, 0); }
  to   { transform: scale(1.06) translate3d(.6%, .5%, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .map-screen.map-alive .map-atmosphere { animation: none; opacity: .82; }
  .map-alive .node-art { animation: none; }
  .map-atmosphere, .map-title, .minal-meter, .map { transform: none; transition: none; }
}
.map-title { font-family: "Baloo 2", sans-serif; font-size: clamp(28px, 5vw, 40px); margin: 0 0 20px; text-wrap: balance; }
.heart { color: var(--blush); }
.minal-meter {
  width: 100%; max-width: 480px; margin: 0 0 32px; text-align: center;
  background: var(--panel-2); border: 2px solid var(--line); border-radius: 10px; padding: 14px 16px;
}
.meter-title { margin: 0 0 4px; color: var(--ink-3); }
.meter-quip { margin: 0 0 10px; font-style: italic; color: var(--ink-2); }
.meter-stats { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 16px; }
.meter-stat { font-size: 12.5px; color: var(--ink-2); }
.meter-stat b { color: var(--ink); }
.meter-fact-btn {
  margin-top: 12px; border: 2px solid var(--line); border-radius: 999px; padding: 6px 16px;
  background: var(--panel); font-family: "Baloo 2", sans-serif; font-size: 12.5px; color: var(--ink);
  cursor: pointer; transition: border-color .15s ease, transform .12s ease;
}
.meter-fact-btn:hover { border-color: var(--blush); transform: translateY(-1px); }
.meter-fact-btn:active { transform: translateY(0); }
.us-fact-text { font-size: 16px; text-align: center; margin: 8px 0 20px; color: var(--ink); }
.map {
  width: 100%; max-width: 920px;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px;
}
.node {
  background: var(--panel); border: 2px solid var(--line); border-radius: 6px;
  box-shadow: var(--shadow); overflow: hidden; text-align: left;
  display: flex; flex-direction: column;
  transition: transform .18s ease, box-shadow .18s ease;
  /* "backwards", not "both": a filled animation outranks every plain
     declaration, so holding node-in's end state here was silently cancelling
     the hover lift and the press below. Backwards still hides the card until
     its turn to arrive, then hands it back to normal styling. */
  animation: node-in .4s ease backwards;
}
.node:not(.locked):hover,
.node:not(.locked):focus-visible { transform: translateY(-4px); box-shadow: var(--shadow), 0 0 0 2px var(--blush), 0 14px 26px -12px rgba(239,127,168,.5); }
/* the artwork lifts with it — brightness rather than scale, because the card's
   own idle drift already owns that transform */
.node:not(.locked):hover .node-art,
.node:not(.locked):focus-visible .node-art { filter: brightness(1.07) saturate(1.04); }
.node:not(.locked):active { transform: translateY(-1px) scale(.98); transition-duration: .08s; }
@keyframes node-in { from { opacity: 0; transform: translateY(10px) scale(.97); } to { opacity: 1; transform: translateY(0) scale(1); } }
.node-art { height: 100px; transition: filter .22s ease; }
.node-body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 4px; }
.node-body h3 { margin: 0; font-family: "Baloo 2", sans-serif; font-size: 18px; }
.node-body p { margin: 0; color: var(--ink-2); font-size: 13.5px; }
.node.locked { cursor: default; }
.node.locked .node-body h3 { color: var(--ink-3); }
.node.locked .node-body p { color: var(--locked); }

/* the7th, the moment it unlocks: clearly different from every other node */
.node.node-the7th-open {
  border-color: #e8c94a;
  box-shadow: var(--shadow), 0 0 0 2px #e8c94a, 0 0 26px -6px rgba(232,201,74,.65);
  animation: node-in .4s ease backwards, the7th-glow 2.6s ease-in-out infinite;
}
.node.node-the7th-open .node-body h3 { color: var(--blush); }
@keyframes the7th-glow {
  0%, 100% { box-shadow: var(--shadow), 0 0 0 2px #e8c94a, 0 0 20px -6px rgba(232,201,74,.55); }
  50% { box-shadow: var(--shadow), 0 0 0 2px #e8c94a, 0 0 34px -4px rgba(232,201,74,.85); }
}
@media (prefers-reduced-motion: reduce) { .node.node-the7th-open { animation: node-in .4s ease backwards; } }

/* ===== the7th's house: its own little map of rooms =====
   The world map is somewhere she visits; this is somewhere that is theirs. So
   everything here runs slower, sits further apart and is lit warmer than the
   map — same palette, same type, a different rhythm. */
.house-screen {
  display: flex; flex-direction: column; align-items: center;
  padding: 60px 20px 76px; position: relative;
}

/* the room's own light: a lamp above, the floor holding some of it back.
   One static pair of gradients that only breathes — no particles, nothing
   per-frame. */
.house-atmosphere {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: 0;
  background:
    radial-gradient(ellipse 72% 48% at 50% 18%, rgba(255, 198, 142, .17) 0%, transparent 68%),
    radial-gradient(ellipse 96% 62% at 50% 104%, rgba(122, 74, 98, .22) 0%, transparent 72%);
  /* furthest layer, so it answers her least and lags longest */
  transform: translate3d(calc(var(--house-px, 0) * 1.5px), calc(var(--house-py, 0) * 1px), 0);
  transition: transform 1.5s cubic-bezier(.2, .7, .2, 1);
}
.house-screen.house-alive .house-atmosphere {
  animation: house-light-in 2.2s ease both, house-light-breathe 34s ease-in-out 2.2s infinite alternate;
}
@keyframes house-light-in { from { opacity: 0; } to { opacity: .9; } }
@keyframes house-light-breathe { from { opacity: .9; } to { opacity: 1; } }

/* everything else stands in front of that light */
.house-screen > *:not(.house-atmosphere) { position: relative; z-index: 1; }
/* nearer layers answer a little more — but half what the world map does, so
   the house reads as steadier than the world outside it */
.house-screen .map-title, .house-sub, .house-map {
  transform: translate3d(calc(var(--house-px, 0) * 3px), calc(var(--house-py, 0) * 2px), 0);
  transition: transform 1.2s cubic-bezier(.2, .7, .2, 1);
}

/* more air than the map gives anything: the whitespace is the point */
.house-screen .map-title { font-size: clamp(26px, 4.4vw, 36px); margin: 0 0 16px; }
.house-hero {
  width: 100%; max-width: 640px; height: auto; margin: -4px 0 26px;
  filter: drop-shadow(0 14px 26px rgba(20, 12, 16, .35));
}
/* the house itself breathes, very slightly — the only idle motion on the
   screen. The parallax offset is carried inside the keyframes so the breath
   composes with it instead of overriding it. */
.house-screen.house-alive .house-hero { animation: house-hero-breathe 24s ease-in-out infinite alternate; }
@keyframes house-hero-breathe {
  from { transform: translate3d(calc(var(--house-px, 0) * 3px), calc(var(--house-py, 0) * 2px), 0) scale(1); }
  to   { transform: translate3d(calc(var(--house-px, 0) * 3px), calc(var(--house-py, 0) * 2px), 0) scale(1.012); }
}
.house-sub {
  margin: 0 0 46px; color: var(--ink-2); font-size: 15px; text-align: center;
  font-style: italic; letter-spacing: .015em;
}

/* the rooms: parts of one building rather than eight destinations. Softer
   corners, warm edge, lit from the same lamp as the room they sit in. */
.house-map { max-width: 860px; gap: 18px; }
.house-node {
  border-radius: 12px; border-color: rgba(214, 166, 128, .34);
  background: linear-gradient(180deg, rgba(60, 42, 52, .55) 0%, var(--panel) 62%);
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 222, 190, .07);
  transition: transform .26s cubic-bezier(.22, 1, .36, 1), box-shadow .26s ease, border-color .26s ease;
  animation: house-node-in .62s cubic-bezier(.22, 1, .36, 1) backwards;
}
@keyframes house-node-in {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
/* :not(.locked) to match the map's own hover specificity — without it the
   map's pink lift outranks this and the house would glow the wrong colour */
.house-node:not(.locked):hover,
.house-node:not(.locked):focus-visible {
  transform: translateY(-3px); border-color: rgba(240, 196, 150, .6);
  box-shadow: var(--shadow), 0 0 0 2px rgba(240, 196, 150, .38), 0 18px 34px -16px rgba(255, 180, 120, .42);
}
.house-node:not(.locked):active { transform: translateY(-1px) scale(.988); transition-duration: .1s; }
.house-node .node-art { height: 112px; }
.house-node .node-body { padding: 13px 16px 15px; }
.house-node .node-body p { color: var(--ink-3); font-size: 12.5px; letter-spacing: .02em; }
.house-empty .node-body p { color: var(--ink-3); font-style: italic; }

/* ---- the threshold: arriving in the house ----
   Not a screen appearing. The house comes up first and alone, then the name
   of it, then what it is, and only then the rooms — one at a time, from
   delays set in renderHouseMap(). */
.house-screen.house-arriving .house-hero,
.house-screen.house-arriving .map-title,
.house-screen.house-arriving .house-sub { opacity: 0; }
.house-screen.house-settling .house-hero { animation: house-hero-in 1s cubic-bezier(.22, 1, .36, 1) both; }
.house-screen.house-settling .map-title  { animation: house-rise .78s cubic-bezier(.22, 1, .36, 1) .55s both; }
.house-screen.house-settling .house-sub  { animation: house-rise .78s cubic-bezier(.22, 1, .36, 1) .82s both; }
@keyframes house-hero-in { from { opacity: 0; transform: scale(1.04); } to { opacity: 1; transform: scale(1); } }
@keyframes house-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }

/* the veil that covers the handover from the ceremony into the house. Warm,
   not the arrival's near-black: this is a door opening onto a lit room. It
   lives on <body> so it outlives the overlay it started over. */
.house-veil {
  position: fixed; inset: 0; z-index: 95; pointer-events: none; opacity: 0;
  background: radial-gradient(ellipse 82% 70% at 50% 46%, #3c2836 0%, #190f16 72%);
}
.house-veil[hidden] { display: none; }
.house-veil.closing { animation: house-veil-close .56s cubic-bezier(.45, 0, .7, .6) both; }
.house-veil.opening { animation: house-veil-open .8s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes house-veil-close { from { opacity: 0; } to { opacity: 1; } }
@keyframes house-veil-open { from { opacity: 1; } to { opacity: 0; } }

/* ---- a room inside the house ----
   The same travel the map uses, but walked rather than flown: the scene barely
   scales, and the room's own furniture arrives further apart. She should feel
   the pacing drop without being able to say what changed. */
.room-screen.in-house.room-settling .scene { animation: house-room-scene-in .64s cubic-bezier(.22, 1, .36, 1) both; }
.room-screen.in-house.room-settling .back { animation: room-chrome-in .6s ease .3s both; }
.room-screen.in-house.room-settling .room-info { animation: room-chrome-in .6s ease .52s both; }
.room-screen.in-house.room-settling .hotspots { animation: room-chrome-in .6s ease .8s both; }
@keyframes house-room-scene-in { from { opacity: 0; transform: scale(1.012); } to { opacity: 1; transform: scale(1); } }

/* a leave-memory box with nothing in it yet: held open on purpose. Dashed and
   unfilled reads as a space being kept, where the filled card would read as a
   note that failed to load. The words are the ones already there. */
.room-note-card.note-waiting {
  background: rgba(255, 232, 206, .22);
  border: 2px dashed rgba(214, 166, 128, .5);
  box-shadow: none; color: #e8d8cc;
}
.room-note-card.note-waiting .note-pin { background: radial-gradient(circle at 35% 30%, #d8b49c, #8a6a58 70%); }

@media (prefers-reduced-motion: reduce) {
  .room-screen.in-house.room-settling .scene,
  .room-screen.in-house.room-settling .back,
  .room-screen.in-house.room-settling .room-info,
  .room-screen.in-house.room-settling .hotspots { animation: none; }
  .house-screen.house-alive .house-atmosphere { animation: none; opacity: .9; }
  .house-screen.house-alive .house-hero { animation: none; }
  .house-atmosphere, .house-screen .map-title, .house-sub, .house-map { transform: none; transition: none; }
  .house-node { animation: none; }
  .house-screen.house-arriving .house-hero,
  .house-screen.house-arriving .map-title,
  .house-screen.house-arriving .house-sub { opacity: 1; }
  .house-screen.house-settling .house-hero,
  .house-screen.house-settling .map-title,
  .house-screen.house-settling .house-sub { animation: none; }
  .house-veil, .house-veil.closing, .house-veil.opening { animation: none; display: none; }
}

/* ===== her, walking around the room ===== */
.room-character {
  position: absolute; height: 30%; transform: translate(-50%, 0);
  transform-origin: bottom center; z-index: 6; pointer-events: none;
  filter: drop-shadow(0 3px 3px rgba(0,0,0,.4));
}
.room-character img { display: block; height: 100%; width: auto; }
.room-character.facing-left img { transform: scaleX(-1); }

.sleep-btn {
  position: absolute; transform: translate(-50%, 0);
  z-index: 7; background: var(--panel); border: 2px solid var(--blush);
  border-radius: 999px; padding: 6px 14px; font-size: 13px; font-weight: 700;
  color: var(--ink); box-shadow: 0 6px 14px -6px rgba(239,127,168,.6);
  cursor: pointer; white-space: nowrap;
  animation: sleep-btn-in .2s ease both, sleep-btn-bob 1.6s ease-in-out .2s infinite;
}
.sleep-btn:hover { border-color: var(--ink); }
.sleep-btn:active { transform: translate(-50%, 0) scale(.94); }
@keyframes sleep-btn-in { from { opacity: 0; transform: translate(-50%, 6px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes sleep-btn-bob { 0%, 100% { margin-top: 0; } 50% { margin-top: -4px; } }
@media (prefers-reduced-motion: reduce) { .sleep-btn { animation: none; } }

/* ===== room ===== */
.room-screen { display: flex; flex-direction: column; align-items: center; padding: 20px 20px 60px; }
/* a 44px-tall touch target (was 22px). The padding grows the tappable area and
   the negative margins hand the same space back, so the text sits exactly where
   it did and nothing around it moves. */
.back {
  align-self: flex-start; color: var(--ink-2); font-size: 14px;
  min-height: 44px; padding: 0 12px; margin: -11px 0 1px -12px;
}
.back:hover { color: var(--ink); }
.scene {
  width: 100%; max-width: 900px; aspect-ratio: 16/9; position: relative;
  border: 2px solid var(--line); border-radius: 6px; overflow: hidden; box-shadow: var(--shadow);
}
/* a portrait scene (the7th's reveal photo) gets capped by height instead of
   stretched to the wide room frame the other rooms use */
.scene.portrait { width: auto; max-width: 100%; height: 78vh; max-height: 820px; margin: 0 auto; }
.scene i { position: absolute; display: block; }
.scene .obj { position: absolute; inset: 0; pointer-events: none; transition: filter .15s ease; }
.scene .obj.hl { filter: drop-shadow(0 0 3px var(--blush)) drop-shadow(0 0 8px var(--blush)); }
.room-info { max-width: 900px; width: 100%; margin-top: 16px; }
.room-info h2 { font-family: "Baloo 2", sans-serif; margin: 0 0 4px; }
.room-info p { margin: 0; color: var(--ink-2); }
.hotspots { list-style: none; margin: 18px 0 0; padding: 0; max-width: 900px; width: 100%;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.hotspot {
  display: block; width: 100%; text-align: left; padding: 10px 12px; border: 2px solid var(--line);
  border-radius: 5px; background: var(--panel); font-size: 14px;
  transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease;
  /* "backwards", not "both": a filled animation outranks every plain
     declaration, so holding hotspot-in's end state here was silently
     cancelling the hover lift and the press below — it had never worked. */
  animation: hotspot-in .35s ease backwards;
}
.hotspot:hover, .hotspot:focus-visible { border-color: var(--blush); transform: translateY(-2px); box-shadow: 0 6px 14px -8px rgba(239,127,168,.5); }
.hotspot:active { transform: translateY(0) scale(.97); transition-duration: .08s; }
/* a brief press response the instant she activates one — a small
   confirmation that the object answered, before whatever it opens appears */
.hotspot.confirming { animation: hotspot-confirm .26s ease; }
@keyframes hotspot-confirm { 0% { transform: scale(1); } 45% { transform: scale(.95); } 100% { transform: scale(1); } }
@keyframes hotspot-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .hotspot { animation: none; } .hotspot.confirming { animation: none; } }

/* block-by-block room entry */
@keyframes block-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.scene.building i, .scene.building img.obj { animation: block-in .35s ease both; }
.scene img.obj { image-rendering: pixelated; pointer-events: none; transition: filter .15s ease; }
/* these zones are invisible boxes over real painted furniture — there's no
   sprite of their own to lift, so "physical" has to come from the glow
   itself: it breathes in with a touch of scale, not just an instant ring */
.scene .obj.zone {
  border-radius: 10px; transform-origin: center;
  transition: box-shadow .18s ease, background-color .18s ease, transform .18s cubic-bezier(.3, 0, .2, 1);
}
.scene .obj.zone.hl { background: rgba(255,255,255,.10); box-shadow: 0 0 0 2px rgba(255,255,255,.6), 0 0 22px 6px var(--blush); transform: scale(1.015); }
/* the same brief confirmation the list button gets, so clicking the object
   itself in the scene reads identically to clicking its label below */
.scene .obj.zone.confirming { animation: zone-confirm .26s ease; }
@keyframes zone-confirm {
  0%   { box-shadow: 0 0 0 2px rgba(255,255,255,.6), 0 0 22px 6px var(--blush); }
  45%  { box-shadow: 0 0 0 2px rgba(255,255,255,.75), 0 0 30px 9px var(--blush); }
  100% { box-shadow: 0 0 0 2px rgba(255,255,255,.6), 0 0 22px 6px var(--blush); }
}
/* a soft one-time glow sweep across every interactive spot when a room first
   opens, so a new visitor's eye is drawn to what's clickable without a
   tutorial or a label. */
@keyframes zone-hint {
  0% { background: transparent; box-shadow: none; }
  35% { background: rgba(255,255,255,.10); box-shadow: 0 0 0 2px rgba(255,255,255,.55), 0 0 18px 5px var(--blush); }
  100% { background: transparent; box-shadow: none; }
}
.scene .obj.zone.hint { animation: zone-hint 1.15s ease both; }
@media (prefers-reduced-motion: reduce) { .scene .obj.zone.hint { animation: none; } .scene .obj.zone.confirming { animation: none; } .scene .obj.zone { transition: box-shadow .18s ease, background-color .18s ease; } }

/* ===== hidden collectibles: barely-there sparkles tucked in a scene ===== */
.collectible-spark {
  position: absolute; border: 0; background: transparent; padding: 0; cursor: pointer;
  border-radius: 50%; z-index: 3;
}
.collectible-spark::before {
  content: ""; position: absolute; inset: 15%; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,236,180,.55), rgba(255,236,180,0) 70%);
  opacity: 0; transition: opacity .2s ease; animation: collectible-pulse 2.6s ease-in-out infinite;
}
.collectible-spark:hover::before { opacity: 1; }
.collectible-spark.found::before { animation: none; opacity: .12; }
@keyframes collectible-pulse {
  0%, 100% { opacity: 0; transform: scale(.8); }
  50% { opacity: .28; transform: scale(1); }
}
/* the instant she finds one — a single bright breath that settles into the
   same quiet, permanently-found state (.found, above) it hands off to, so
   nothing jumps once the class is removed */
.collectible-spark.discovering::before {
  animation: collectible-discover .5s ease;
}
@keyframes collectible-discover {
  0%   { opacity: .35; transform: scale(.85); }
  40%  { opacity: 1; transform: scale(1.4); }
  100% { opacity: .12; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .collectible-spark::before, .collectible-spark.discovering::before { animation: none; } }

/* the room itself quiets for a breath around a discovery — attention narrows
   without anything glowing or exploding. Sits above the painted scene and
   its zones, below the spark and the roaming character, so what was just
   found stays the one bright thing in a slightly dimmed room. */
.scene.discovering::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: radial-gradient(ellipse 130% 110% at 50% 50%, transparent 55%, rgba(6, 4, 10, .16) 100%);
  animation: scene-focus .6s ease;
}
@keyframes scene-focus { 0%, 100% { opacity: 0; } 42% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .scene.discovering::after { display: none; } }

.collectible-toast { border-color: #a8d8c9; max-width: min(78vw, 380px); text-align: left; }

.collectibles-intro { margin: 0 0 14px; text-align: center; }
.collectibles-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
.collectible-card {
  display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
  border: 2px solid var(--line); border-radius: 12px; padding: 14px 10px; background: var(--panel-2);
}
.collectible-card.unlocked { border-color: #a8d8c9; }
/* the same "ink stamp" arrival the passport uses, reused rather than
   reinvented — one consistent language for "this just became yours" */
.collectible-card.just-unlocked { animation: stamp-slam .5s cubic-bezier(.2,.9,.3,1.2) both; }
@media (prefers-reduced-motion: reduce) { .collectible-card.just-unlocked { animation: none; } }
.collectible-icon {
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  font-size: 22px; border-radius: 50%; background: var(--panel); border: 2px solid #a8d8c9;
}
.collectible-icon-locked { border-color: var(--line); color: var(--ink-3); filter: grayscale(1); opacity: .7; }

/* ===== Minal's Inventory: RPG-style item codex ===== */
.inventory-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
/* a keepsake resting at a slight tilt rather than a flat app card — the same
   idiom the corkboards use elsewhere on the site. --tilt is set per card in
   JS (a short, fixed sequence, not random, so it's identical every visit).
   Hovering/focusing picks it up: it straightens and lifts a touch. */
.inventory-card {
  display: flex; flex-direction: column; align-items: center; gap: 5px; text-align: center;
  border: 2px solid var(--line); border-radius: 12px; padding: 14px 10px; background: var(--panel-2);
  box-shadow: 0 4px 12px -7px rgba(0,0,0,.32);
  transform: rotate(var(--tilt, 0deg));
  transition: transform .2s cubic-bezier(.3, 0, .2, 1), box-shadow .2s ease;
}
.inventory-card:hover, .inventory-card:focus-visible {
  transform: rotate(0deg) translateY(-4px);
  box-shadow: 0 12px 20px -9px rgba(0,0,0,.36);
}
@media (prefers-reduced-motion: reduce) { .inventory-card { transition: none; } }
.inventory-icon {
  width: 46px; height: 46px; display: flex; align-items: center; justify-content: center;
  font-size: 24px; border-radius: 50%; background: var(--panel); border: 2px solid var(--line);
}
.inventory-rarity { text-transform: uppercase; letter-spacing: .04em; font-size: 10px; font-weight: 700; }
.inventory-room { font-size: 11px; color: var(--ink-3); font-style: italic; }
.inventory-card.rarity-common .inventory-icon { border-color: var(--line); }
.inventory-card.rarity-common .inventory-rarity { color: var(--ink-3); }
.inventory-card.rarity-rare { border-color: #a9c7e8; }
.inventory-card.rarity-rare .inventory-icon { border-color: #a9c7e8; }
.inventory-card.rarity-rare .inventory-rarity { color: #5a86b8; }
.inventory-card.rarity-legendary { border-color: #e8c94a; background: linear-gradient(180deg, #fff8e6, var(--panel-2)); }
.inventory-card.rarity-legendary .inventory-icon { border-color: #e8c94a; }
.inventory-card.rarity-legendary .inventory-rarity { color: #b8901f; }

/* ===== Achievements: milestones computed from state tracked elsewhere ===== */
.achievements-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.achievement-card {
  display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
  border: 2px solid var(--line); border-radius: 12px; padding: 14px 10px; background: var(--panel-2);
}
.achievement-card.unlocked { border-color: #e8c94a; background: linear-gradient(180deg, #fff8e6, var(--panel-2)); }
.achievement-bar { width: 100%; height: 6px; border-radius: 4px; background: var(--panel); border: 1px solid var(--line); overflow: hidden; }
.achievement-bar-fill { width: 100%; height: 100%; background: #a8d8c9; transform: scaleX(0); transform-origin: left; transition: transform .25s ease; }
.achievement-card.unlocked .achievement-bar-fill { background: #e8c94a; }
.achievement-progress { color: var(--ink-3); font-size: 10px; }

/* ===== ambient life: sway + day/night, layered on top of the painted room ===== */
.scene .obj.sway-plant {
  position: absolute; inset: 0; pointer-events: none;
  background-repeat: no-repeat; background-position: center; background-size: cover;
  animation-name: plant-sway; animation-timing-function: ease-in-out; animation-iteration-count: infinite;
  will-change: transform;
}
@keyframes plant-sway {
  0%, 100% { transform: rotate(-1.1deg); }
  50% { transform: rotate(1.4deg); }
}
.scene .obj.amb-night {
  position: absolute; inset: 0; pointer-events: none;
  background: #0c1030; mix-blend-mode: multiply; opacity: 0;
  transition: opacity 1.2s ease;
}
.scene .obj.amb-warm {
  position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse at 50% 12%, rgba(255,170,90,.9), rgba(255,170,90,0) 60%);
  mix-blend-mode: soft-light; opacity: 0;
  transition: opacity 1.2s ease;
}
.scene .obj.amb-ceiling {
  position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse 70% 55% at 50% 38%, rgba(255,238,214,.85), rgba(255,238,214,0) 72%);
  mix-blend-mode: screen; opacity: 0;
  transition: opacity 1.2s ease;
}
.scene .obj.amb-lamp {
  position: absolute; inset: 0; pointer-events: none;
  mix-blend-mode: screen; opacity: .15;
  transition: opacity 1.2s ease;
}
/* a room's own quiet colour, independent of time of day — the same three
   tokens used everywhere else on the site, at a wash so faint it should
   never be consciously noticed, only felt. This is the difference between
   Kathmandu, Mumbai and Huddersfield when nothing else is telling them apart. */
.scene .obj.amb-mood { position: absolute; inset: 0; pointer-events: none; mix-blend-mode: soft-light; }
.scene-kathmandu .amb-mood { background: radial-gradient(ellipse 85% 65% at 50% 28%, rgba(143, 111, 208, .07), transparent 72%); }
.scene-mumbai .amb-mood { background: radial-gradient(ellipse 85% 65% at 50% 38%, rgba(239, 127, 168, .06), transparent 72%); }
.scene-huddersfield .amb-mood { background: radial-gradient(ellipse 85% 65% at 50% 45%, rgba(150, 104, 74, .07), transparent 72%); }

/* Kathmandu's roaming her: a small idle breathing loop while she's standing
   still, so the room reads as inhabited even between keypresses — only here,
   because only Kathmandu has a character to inhabit it. */
.room-character.roam-idle { animation: roam-breathe 4.4s ease-in-out infinite; }
@keyframes roam-breathe {
  0%, 100% { transform: translate(-50%, 0) scale(1); }
  50%      { transform: translate(-50%, -2%) scale(1.015); }
}
@media (prefers-reduced-motion: reduce) {
  .scene .obj.sway-plant { animation: none; }
  .room-character.roam-idle { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  .scene.building i { animation: none; }
  .node, .hotspot { transition: none; }
}

/* ===== object panel ===== */
.panel-backdrop {
  position: fixed; inset: 0; background: rgba(29,22,32,.5); z-index: 40;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  animation: backdrop-in .18s ease both;
}
.panel-backdrop[hidden] { display: none; }
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .panel-backdrop { animation: none; } }
.panel {
  background: var(--panel); border: 2px solid var(--line); border-radius: 8px;
  padding: 24px; max-width: 440px; width: 100%; position: relative; box-shadow: var(--shadow);
  animation: panel-in .18s ease both;
  max-height: 88vh; display: flex; flex-direction: column;
}
@keyframes panel-in { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .panel { animation: none; } }
/* 44x44 touch target (was about 23x27). The glyph is centred in it and the box
   is offset so the × stays exactly where it was. */
.panel-close {
  position: absolute; top: 2px; right: 2px; width: 44px; height: 44px;
  display: grid; place-items: center; border-radius: 50%;
  font-size: 20px; color: var(--ink-3); z-index: 2; background: transparent;
}
.panel-close:hover { color: var(--ink); }
.panel h3 { font-family: "Baloo 2", sans-serif; margin: 0 0 10px; flex: 0 0 auto; }
.panel p { margin: 0; color: var(--ink-2); line-height: 1.6; }
#panel-body { overflow-y: auto; min-height: 0; flex: 1 1 auto; }

/* ===== mirror: pixel avatar builder ===== */
.panel.panel-wide { max-width: 560px; }
.avatar-builder { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.avatar-frame {
  background: var(--panel-2); border: 2px solid var(--line); border-radius: 8px;
  padding: 18px; position: relative; overflow: hidden;
}
.avatar-frame::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(circle at 50% 35%, var(--blush) 0%, transparent 65%);
  opacity: .16; pointer-events: none;
}
.avatar-stage-img { display: block; height: 42vh; max-height: 340px; width: auto; max-width: 100%; image-rendering: pixelated; }
.cb-rows { width: 100%; max-width: 320px; margin-top: 16px; display: flex; flex-direction: column; gap: 10px; }
.cb-row { display: flex; align-items: center; gap: 12px; }
.cb-label { width: 52px; color: var(--ink-3); flex-shrink: 0; }
.cb-cycle-solo { margin-top: 16px; display: flex; align-items: center; gap: 14px; }
.cb-cycle { display: flex; align-items: center; gap: 10px; flex: 1; }
.cb-arrow {
  width: 30px; height: 30px; border: 2px solid var(--line); border-radius: 5px;
  background: var(--panel); font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.cb-arrow:hover { border-color: var(--blush); }
.cb-cycle-label { min-width: 130px; text-align: center; color: var(--ink-2); font-size: 10.5px; }
.cb-actions { margin-top: 16px; display: flex; align-items: center; gap: 12px; }
.cb-dice {
  border: 2px solid var(--line); border-radius: 5px; padding: 8px 14px; background: var(--panel);
  font-family: "Nunito Sans", sans-serif; font-size: 13.5px;
}
.cb-dice:hover { border-color: var(--blush); }
.cb-status { color: var(--grass); min-width: 60px; }
.cb-note { margin: 12px 0 0; color: var(--ink-2); font-size: 13px; text-align: center; }

/* ===== Mumbai shelf: Open When envelopes ===== */
.letters-intro { margin: 0 0 16px; color: var(--ink-2); font-size: 13.5px; }
.envelope-list { display: flex; flex-direction: column; gap: 10px; max-width: 420px; }
.envelope {
  border: 2px solid var(--line); border-radius: 6px; overflow: hidden; background: var(--panel-2);
  transition: box-shadow .3s ease;
}
/* the envelope lifts very slightly the moment it's open — a small physical
   acknowledgment that something changed, not just text appearing */
.envelope.expanded { box-shadow: 0 8px 16px -10px rgba(0,0,0,.3); }
.envelope-flap {
  width: 100%; display: flex; align-items: center; gap: 10px; padding: 12px 14px;
  text-align: left; background: none;
}
.envelope-seal { color: var(--ink-3); font-size: 14px; transition: color .2s ease, transform .2s ease; }
.envelope.opened .envelope-seal { color: var(--blush); }
.envelope.expanded .envelope-seal { transform: rotate(90deg); }
.envelope-label { color: var(--ink); font-size: 11.5px; }
/* the letter unfolds rather than appearing — a 0→1fr grid row is the only
   reliable way to animate toward "auto" height. The padding lives on the
   inner wrapper, not this row, so it doesn't show as leftover space while
   collapsed. JS still uses the hidden attribute for the true open/closed
   state (so a screen reader never meets it while it's closed); this only
   governs the transition while it's visible. */
.envelope-reveal {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows .36s cubic-bezier(.22, 1, .36, 1), opacity .3s ease;
}
.envelope-reveal[hidden] { display: none; }
.envelope.expanded .envelope-reveal { grid-template-rows: 1fr; opacity: 1; }
.envelope-reveal-inner { overflow: hidden; min-height: 0; padding: 0 16px 16px 40px; }
.envelope-reveal p { margin: 0; color: var(--ink-2); line-height: 1.6; font-style: italic; }
@media (prefers-reduced-motion: reduce) { .envelope-reveal, .envelope { transition: none; } }

/* ===== Mumbai: keepsake letter ===== */
.keepsake { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.keepsake-photo { max-width: 100%; max-height: 50vh; border-radius: 6px; box-shadow: var(--shadow); }
.keepsake-placeholder {
  width: 100%; padding: 40px 20px; border: 2px dashed var(--line); border-radius: 6px;
  display: flex; flex-direction: column; align-items: center; gap: 10px; color: var(--ink-3);
}
.keepsake-icon { font-size: 28px; }
.keepsake-caption { margin: 0; color: var(--ink-2); text-align: center; line-height: 1.6; }

/* ===== lightbox: any gallery photo opens big, like a photo viewer ===== */
.lightbox {
  position: fixed; inset: 0; background: rgba(10,7,12,.92); z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: 40px;
  opacity: 0; transition: opacity .22s ease;
}
.lightbox.show { opacity: 1; }
.lightbox[hidden] { display: none; }
.lightbox-img {
  max-width: min(92vw, 1100px); max-height: 86vh; object-fit: contain;
  border-radius: 6px; box-shadow: 0 10px 40px rgba(0,0,0,.5);
  transform: scale(.97); transition: transform .28s cubic-bezier(.22, 1, .36, 1);
}
.lightbox.show .lightbox-img { transform: scale(1); }
@media (prefers-reduced-motion: reduce) { .lightbox, .lightbox-img { transition: none; } }
.lightbox-close {
  position: absolute; top: 16px; right: 20px; font-size: 30px; line-height: 1;
  color: #fff; background: rgba(255,255,255,.12); border: 0; border-radius: 50%;
  width: 42px; height: 42px; cursor: pointer;
}
.lightbox-close:hover { background: rgba(255,255,255,.22); }
.lightbox-nav {
  position: absolute; top: 50%; transform: translateY(-50%); font-size: 34px; line-height: 1;
  color: #fff; background: rgba(255,255,255,.12); border: 0; border-radius: 50%;
  width: 52px; height: 52px; cursor: pointer;
}
.lightbox-nav:hover { background: rgba(255,255,255,.22); }
.lightbox-prev { left: 16px; }
.lightbox-next { right: 16px; }
.lightbox-count {
  position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
  color: rgba(255,255,255,.75); font-size: 13px; margin: 0;
}
@media (max-width: 640px) {
  .lightbox-nav { width: 42px; height: 42px; font-size: 26px; }
  .lightbox-close { width: 36px; height: 36px; font-size: 24px; }
}

/* ===== The Book of Us: the comic shelf on the Kobo stack ===== */
.book-of-us-btn { margin-top: 14px; }
.book-shelf {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 12px;
  margin: 10px 0 4px;
}
.book-chapter {
  display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center;
  border: 2px solid var(--line); border-radius: 12px; padding: 16px 10px;
  background: var(--panel-2); cursor: pointer; font-family: inherit;
  transition: transform .15s ease, border-color .15s ease;
}
.book-chapter.has-pages { border-color: var(--wood); background: linear-gradient(180deg, #fff8ef, var(--panel-2)); }
.book-chapter:not(.locked):hover,
.book-chapter:not(.locked):focus-visible { transform: translateY(-3px); border-color: var(--blush); }
.book-chapter.locked { opacity: .55; cursor: default; filter: grayscale(.5); }
.book-chapter-num { color: var(--ink-3); }
.book-chapter-title { font-family: "Baloo 2", sans-serif; font-weight: 700; color: var(--ink); }
.book-chapter-sub { color: var(--ink-2); font-size: 12px; }
.book-chapter-status { color: var(--ink-3); margin-top: 2px; }
.book-shelf-back { margin-top: 14px; }
.book-reader .motion-letter-img.book-page-img { max-height: 58vh; object-fit: contain; }

/* ===== motion letters: tap-to-open photographed sequence ===== */
.motion-letter { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.motion-letter-img {
  max-width: 100%; max-height: 62vh; border-radius: 6px; box-shadow: var(--shadow);
  background: var(--panel-2);
  /* a brief blank beat before the first frame, and a short settle on every
     frame after that — the photo arrives rather than snapping into place */
  opacity: 0; transform: scale(.97);
  transition: opacity .32s ease, transform .32s cubic-bezier(.22, 1, .36, 1);
}
.motion-letter-img.shown { opacity: 1; transform: scale(1); }
@media (prefers-reduced-motion: reduce) { .motion-letter-img { transition: none; opacity: 1; transform: none; } }
.motion-letter-hint { margin: 0; color: var(--ink-2); text-align: center; line-height: 1.6; min-height: 1.4em; }
.motion-letter-controls { display: flex; gap: 12px; align-items: center; justify-content: center; flex-wrap: wrap; }
.motion-letter-controls .arcade-btn {
  border: 2px solid var(--line); border-radius: 5px; padding: 8px 16px;
  background: var(--panel-2); color: var(--ink); font-family: "Baloo 2", sans-serif; font-size: 13.5px;
}
.motion-letter-controls .arcade-btn:hover { border-color: var(--blush); }
.motion-letter-controls .arcade-btn.primary { background: var(--blush); border-color: var(--blush); color: var(--ink); }

/* ===== Mumbai corkboard: notes wall ===== */
.note-board {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 22px 18px;
  max-width: 520px; padding: 10px 6px 6px;
}
.note-card {
  position: relative; padding: 18px 12px 14px; border-radius: 2px; color: #3a3020;
  box-shadow: 0 3px 8px rgba(0,0,0,.18); min-height: 96px; display: flex; flex-direction: column; gap: 6px;
}
.note-pin {
  position: absolute; top: -7px; left: 50%; transform: translateX(-50%);
  width: 11px; height: 11px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ff8fa8, #c23f5e 70%);
  box-shadow: 0 2px 3px rgba(0,0,0,.35);
}
.note-tag { color: rgba(58,48,32,.55); font-size: 9.5px; }
.note-card p { margin: 0; font-size: 13.5px; line-height: 1.45; }
.note-mine { outline: 1px dashed rgba(58,48,32,.3); outline-offset: 3px; }
.room-note-card { background: #ffe8ce; max-width: 420px; margin: 4px 0 4px; }
.note-heart { position: absolute; bottom: 8px; right: 10px; color: #c23f5e; font-size: 12px; }

/* ===== Huddersfield corkboard: photo pinboard ===== */
.photo-board {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 26px 20px;
  max-width: 620px; padding: 12px 6px 6px;
}
.photo-card {
  position: relative; padding: 10px 10px 14px; border-radius: 2px; background: #fbf9f4;
  box-shadow: 0 4px 10px rgba(0,0,0,.22); display: flex; flex-direction: column; gap: 8px;
  opacity: 0; transition: opacity .5s ease, filter .5s ease; filter: blur(2px);
}
.photo-card.in-view { opacity: 1; filter: blur(0); }
@media (prefers-reduced-motion: reduce) { .photo-card { opacity: 1; filter: none; transition: none; } }
.photo-card-img {
  display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 1px;
  background: var(--panel-2); cursor: zoom-in; transition: transform .15s ease;
}
.photo-card-img:hover { transform: scale(1.03); }
.photo-card-cap { margin: 0; font-size: 12.5px; line-height: 1.4; color: #5a4c38; text-align: center; }
.note-compose {
  max-width: 520px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 10px;
}
.note-textarea {
  width: 100%; min-height: 60px; resize: vertical; padding: 10px 12px; border: 2px solid var(--line);
  border-radius: 6px; background: var(--panel); color: var(--ink); font: inherit; font-size: 13.5px;
}
.note-textarea:focus { outline: none; border-color: var(--blush); }
.note-add {
  align-self: flex-end; border: 2px solid var(--line); border-radius: 5px; padding: 8px 16px;
  background: var(--panel-2); font-family: "Baloo 2", sans-serif; font-size: 13.5px;
}
.note-add:hover { border-color: var(--blush); }

/* ===== Huddersfield desk: the timeline ===== */
.timeline-wrap { display: flex; flex-direction: column; align-items: center; gap: 18px; max-width: 420px; margin: 0 auto; }
.timeline-page {
  width: 100%; min-height: 160px; padding: 22px 22px 18px; border: 2px solid var(--line);
  border-radius: 8px; background: var(--panel-2); box-shadow: var(--shadow);
}
.timeline-page.flip { animation: timeline-flip .32s ease both; }
@keyframes timeline-flip {
  from { opacity: 0; transform: rotateY(-8deg) translateX(10px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .timeline-page.flip { animation: none; } }
.timeline-date { margin: 0 0 6px; color: var(--blush); letter-spacing: .03em; }
.timeline-date[hidden] { display: none; }
.timeline-title { margin: 0 0 10px; font-family: "Baloo 2", sans-serif; color: var(--ink); }
.timeline-text { margin: 0; color: var(--ink-2); line-height: 1.6; }
.timeline-nav { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.timeline-btn {
  border: 2px solid var(--line); border-radius: 5px; padding: 7px 14px; background: var(--panel);
  font-family: "Baloo 2", sans-serif; font-size: 12.5px; color: var(--ink);
}
.timeline-btn:hover:not(:disabled) { border-color: var(--blush); }
.timeline-btn:disabled { opacity: .35; cursor: default; }
.timeline-dots { display: flex; align-items: center; gap: 7px; }
.timeline-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--line); cursor: pointer;
  transition: background .2s ease, transform .2s ease;
}
.timeline-dot:hover { transform: scale(1.2); }
.timeline-dot.active { background: var(--blush); transform: scale(1.25); }

/* ===== Huddersfield jigsaw: full-screen drag-and-connect puzzle ===== */
.jigsaw-overlay {
  position: fixed; inset: 0; z-index: 80; background: #17121a;
  display: flex; flex-direction: column; align-items: center;
  animation: backdrop-in .28s ease;
}
.jigsaw-overlay[hidden] { display: none; }
.jigsaw-overlay.closing { animation: arcade-fade-out .2s ease forwards; }
@media (prefers-reduced-motion: reduce) { .jigsaw-overlay, .jigsaw-overlay.closing { animation: none; } }
.jigsaw-bar {
  width: 100%; max-width: 900px; display: flex; align-items: center; gap: 16px;
  padding: 14px 18px; flex-shrink: 0;
}
.jigsaw-close {
  border: 2px solid var(--line); border-radius: 5px; padding: 6px 12px; background: var(--panel);
  font-family: "Baloo 2", sans-serif; font-size: 12.5px; color: var(--ink); flex-shrink: 0;
}
.jigsaw-close:hover { border-color: var(--blush); }
.jigsaw-progress { flex: 1; display: flex; flex-direction: column; gap: 4px; color: #f4e9ec; }
.jigsaw-progress-bar { width: 100%; height: 6px; border-radius: 3px; background: rgba(255,255,255,.18); overflow: hidden; }
.jigsaw-progress-fill {
  width: 100%; height: 100%; background: var(--blush);
  transform: scaleX(0); transform-origin: left center; transition: transform .3s ease;
}
@media (prefers-reduced-motion: reduce) { .jigsaw-progress-fill { transition: none; } }
.jigsaw-peek {
  border: 2px solid var(--line); border-radius: 5px; padding: 6px 12px; background: var(--panel);
  font-family: "Baloo 2", sans-serif; font-size: 12.5px; color: var(--ink); flex-shrink: 0;
}
.jigsaw-peek:hover { border-color: var(--blush); }
.jigsaw-hint {
  color: #d9c3cb; text-align: center; max-width: 640px; margin: 0 0 8px; padding: 0 16px;
}
.jigsaw-table {
  position: relative; overflow: auto; flex: 1; width: 100%; touch-action: pan-x pan-y;
  background: #cdbfa8;
}
.jigsaw-workspace { position: relative; }
.jigsaw-board {
  position: absolute; border: 2px dashed rgba(40,30,20,.4); border-radius: 4px;
  background-color: rgba(255,255,255,.18); pointer-events: none;
}
.jigsaw-piece-big {
  position: absolute; cursor: grab; filter: drop-shadow(0 3px 6px rgba(0,0,0,.45));
  touch-action: none; transition: transform .12s ease, filter .12s ease;
}
/* picked up off the table — a small lift, not just a bigger shadow */
.jigsaw-piece-big.dragging {
  cursor: grabbing; z-index: 5; filter: drop-shadow(0 8px 14px rgba(0,0,0,.55));
  transform: scale(1.035); transition: filter .12s ease;
}
.jigsaw-piece-big.solved { cursor: grab; z-index: 1; filter: drop-shadow(0 1px 2px rgba(0,0,0,.3)); }
.jigsaw-piece-big.just-solved { animation: jigsaw-pop .4s ease; }
@keyframes jigsaw-pop {
  0% { transform: scale(1.12); } 100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .jigsaw-piece-big.just-solved { animation: none; }
  .jigsaw-piece-big, .jigsaw-piece-big.dragging { transition: none; transform: none; }
}
.jigsaw-ref {
  position: fixed; right: 18px; bottom: 18px; z-index: 81; max-width: 220px;
  border: 3px solid var(--panel); border-radius: 8px; box-shadow: 0 6px 18px rgba(0,0,0,.5); overflow: hidden;
}
.jigsaw-ref img { display: block; width: 100%; height: auto; }
.jigsaw-ref[hidden] { display: none; }
.jigsaw-caption-big {
  position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%); z-index: 81;
  margin: 0; padding: 10px 22px; background: var(--panel); border: 2px solid var(--line);
  border-radius: 20px; color: var(--ink); font-style: italic; box-shadow: var(--shadow);
}
.jigsaw-caption-big[hidden] { display: none; }
.no-scroll { overflow: hidden; }

/* ===== gaming room: the arcade ===== */
/* stepping in: the room fades up, then the bar/title settle a beat after,
   then the game cards arrive one by one (their own stagger, below) — three
   small beats instead of the whole room appearing at once. Declared
   unconditionally (not behind a class) because a CSS animation restarts on
   its own whenever the element goes from display:none back to visible,
   which [hidden] does every time — the same mechanism .arcade-card already
   relied on, just applied to the room itself. */
.arcade-overlay {
  position: fixed; inset: 0; z-index: 80; background: #16121d;
  display: flex; flex-direction: column; align-items: center;
  font-family: "Nunito Sans", ui-sans-serif, system-ui, sans-serif;
  animation: backdrop-in .28s ease;
}
.arcade-overlay[hidden] { display: none; }
.arcade-overlay.closing { animation: arcade-fade-out .2s ease forwards; }
@keyframes arcade-fade-out { to { opacity: 0; } }
.arcade-bar {
  width: 100%; max-width: 980px; display: flex; align-items: center; gap: 14px;
  padding: 14px 18px; flex-shrink: 0;
  animation: room-chrome-in .32s ease .1s backwards;
}
@media (prefers-reduced-motion: reduce) {
  .arcade-overlay, .arcade-overlay.closing, .arcade-bar { animation: none; }
}
.arcade-back, .arcade-close {
  border: 2px solid var(--wood); border-radius: 5px; padding: 6px 12px;
  background: #241c2c; color: #f4ecf7; flex-shrink: 0; font-size: 11px;
}
.arcade-back:hover, .arcade-close:hover { border-color: var(--lilac); }
.arcade-title { flex: 1; text-align: center; color: var(--lilac); font-size: 13px; }
.arcade-menu {
  width: 100%; max-width: 980px; flex: 1; overflow: auto;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 18px;
  padding: 10px 18px 40px; align-content: start;
}
.arcade-card {
  background: #241c2c; border: 3px solid var(--wood); border-radius: 8px;
  padding: 20px 16px; display: flex; flex-direction: column; align-items: center; gap: 10px;
  text-align: center; color: #f4ecf7;
  transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
  /* "backwards", not "both": a filled animation outranks every plain
     declaration, so holding node-in's end state here was silently
     cancelling the hover lift and the press below — same bug already found
     and fixed on the world map's own cards, same fix. */
  animation: node-in .4s ease backwards;
}
.arcade-card:hover, .arcade-card:focus-visible { transform: translateY(-4px); border-color: var(--lilac); box-shadow: 0 14px 26px -14px rgba(183,156,232,.6); }
.arcade-card:active { transform: translateY(-1px) scale(.98); transition-duration: .08s; }
/* the machine answers before the room changes — a small press-and-settle
   the instant a game is chosen, held just long enough to register before
   the menu gives way to it */
.arcade-card.chosen { animation: arcade-card-choose .3s cubic-bezier(.3, 0, .2, 1); }
@keyframes arcade-card-choose {
  0%   { transform: translateY(-4px) scale(1); }
  40%  { transform: translateY(-1px) scale(.955); }
  100% { transform: translateY(-4px) scale(1.015); }
}
.arcade-card-icon { font-size: 34px; line-height: 1; transition: transform .15s ease; }
.arcade-card:hover .arcade-card-icon { transform: scale(1.12) rotate(-4deg); }
.arcade-card h4 { margin: 0; font-family: "Baloo 2", sans-serif; font-size: 17px; }
.arcade-card p { margin: 0; color: #c7b8d1; font-size: 12.5px; line-height: 1.5; }
/* re-plays every time the stage goes from hidden back to visible — a CSS
   animation restarts on its own when its element returns from display:none,
   so switching from the menu into any game (or back into a fresh one)
   always gets this same short settle with no extra JS to trigger it */
.arcade-stage {
  width: 100%; max-width: 980px; flex: 1; overflow: auto;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 6px 18px 40px; color: #f4ecf7;
  animation: room-chrome-in .3s ease;
}
.arcade-stage[hidden], .arcade-menu[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .arcade-card.chosen { animation: none; } .arcade-stage { animation: none; } }
.arcade-score-row {
  display: flex; gap: 18px; align-items: center; flex-wrap: wrap; justify-content: center;
}
.arcade-pill {
  background: #241c2c; border: 2px solid var(--wood); border-radius: 999px;
  padding: 6px 16px; font-size: 12px; color: var(--grass);
}
.arcade-btn {
  border: 2px solid var(--wood); border-radius: 6px; padding: 9px 18px;
  background: #241c2c; color: #f4ecf7; font-family: "Baloo 2", sans-serif; font-size: 14px;
  transition: border-color .15s ease, transform .12s ease;
}
.arcade-btn:hover { border-color: var(--lilac); transform: translateY(-2px); }
.arcade-btn:active { transform: translateY(0) scale(.95); transition-duration: .08s; }
.arcade-btn.primary { background: var(--lilac); border-color: var(--lilac); color: #221733; }

/* ===== the7th: the anniversary finale ===== */
.finale-overlay {
  position: fixed; inset: 0; z-index: 90;
  background: radial-gradient(ellipse 90% 70% at 50% 0%, #3a2130 0%, #1d1620 62%);
  color: #fbe9ef; display: flex; align-items: center; justify-content: center;
  padding: 20px; overflow-y: auto;
  /* opens slowly, and on nothing: the hold before the first question is what
     makes the three of them read as a ceremony rather than a form. Slow enough
     that the house is still visible falling away underneath it. */
  animation: backdrop-in .95s ease both;
}

/* the world going quiet behind it. The overlay takes almost a second to become
   opaque, so this recede is actually watched rather than merely performed. */
.screen.finale-receding {
  transition: transform 1.2s cubic-bezier(.22, 1, .36, 1), opacity 1.2s ease, filter 1.2s ease;
  transform: scale(.972); opacity: .45; filter: blur(3px);
}
.finale-overlay[hidden] { display: none; }
/* a little light gathering in the middle of the dark while she waits */
.finale-overlay::after {
  content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background: radial-gradient(ellipse 55% 46% at 50% 50%, rgba(255, 206, 150, .16) 0%, transparent 70%);
  opacity: 0; animation: finale-bloom 2.8s cubic-bezier(.22, 1, .36, 1) .3s both;
}
@keyframes finale-bloom { from { opacity: 0; transform: scale(.9); } to { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
  .finale-overlay { animation: none; }
  .finale-overlay::after { animation: none; opacity: 1; transform: none; }
}
.finale-close {
  position: fixed; top: 16px; right: 16px; z-index: 2;
  border: 2px solid rgba(255,255,255,.25); border-radius: 5px; padding: 6px 12px;
  background: rgba(0,0,0,.25); color: #fbe9ef; font-family: "Silkscreen", monospace; font-size: 11px;
}
.finale-close:hover { border-color: #e8c94a; }
/* a grid of one cell: a leaving page and an arriving one occupy the same place
   at the same time, so questions can overlap instead of one being deleted
   before the next is created */
.finale-stage {
  width: 100%; max-width: 560px; margin: auto; position: relative; z-index: 1;
  display: grid; justify-items: center;
}
/* min-width: 0 so a long unbreakable word in the copy can never push a page
   wider than the cell it sits in */
.finale-stage > .finale-step { grid-area: 1 / 1; width: 100%; min-width: 0; }

/* ---- turning a page ----
   The finished question leaves to one side while the next arrives from the
   other, through the same anchor. The arriving card only moves; its own
   children carry the fade, so the two never double up on opacity. */
.finale-step.finale-page-out { animation: finale-page-out .5s cubic-bezier(.4, 0, .6, 1) both; pointer-events: none; }
@keyframes finale-page-out {
  from { opacity: 1; transform: translateX(0) scale(1); }
  to   { opacity: 0; transform: translateX(-26px) scale(.985); }
}
.finale-step.finale-page-in { animation: finale-page-in .62s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes finale-page-in { from { transform: translateX(24px); } to { transform: translateX(0); } }

/* ---- the last answer ----
   Everything she is not looking at goes first, the answer she gave goes last,
   and the question itself is the final thing to leave. What follows is an
   empty screen, on purpose, for long enough to notice. */
.finale-step.finale-hush { pointer-events: none; }
/* a "to"-only keyframe so each element fades from wherever it already is —
   the dimmed options are at .3, not 1, and must not jump back up first */
@keyframes finale-hush-out { to { opacity: 0; } }
.finale-step.finale-hush .finale-option-btn:not(.chosen) { animation: finale-hush-out .45s ease both; }
.finale-step.finale-hush .finale-step-count { animation: finale-hush-out .5s ease .2s both; }
.finale-step.finale-hush .finale-option-btn.chosen { animation: finale-hush-out .6s ease .45s both; }
.finale-step.finale-hush .finale-question-text { animation: finale-hush-out .7s ease .75s both; }
.finale-step { display: flex; flex-direction: column; align-items: center; gap: 16px; text-align: center; overflow-wrap: break-word; animation: finale-step-in .55s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes finale-step-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* a step leaving: it recedes rather than being cut away, so the next one
   arrives into space the last one gave up */
.finale-step.finale-step-out { animation: finale-step-out .42s cubic-bezier(.4, 0, .6, 1) both; }
@keyframes finale-step-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(-10px) scale(.985); }
}

/* a question arrives in parts: what number it is, then the question itself,
   then — after a beat long enough to have actually read it — the answers */
.finale-question-card { animation: none; }
.finale-question-card .finale-step-count { animation: finale-rise .6s cubic-bezier(.22, 1, .36, 1) .1s backwards; }
.finale-question-card .finale-question-text { animation: finale-rise .75s cubic-bezier(.22, 1, .36, 1) .28s backwards; }
.finale-question-card .finale-options { animation: finale-rise .75s cubic-bezier(.22, 1, .36, 1) .72s backwards; }
@keyframes finale-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

/* the answer landing: the one she chose holds the light, the others step back.
   Nothing is "correct" here — this is the choice registering, not a score. */
.finale-options.answered .finale-option-btn { pointer-events: none; transition: opacity .34s ease, border-color .34s ease, background .34s ease; }
.finale-options.answered .finale-option-btn:not(.chosen) { opacity: .3; transform: none; }
.finale-option-btn.chosen {
  border-color: #e8c94a; background: rgba(232, 201, 74, .18); color: #fff3d6;
  animation: finale-chosen .34s cubic-bezier(.3, 0, .2, 1);
}
@keyframes finale-chosen {
  0%   { transform: translateY(-2px) scale(1); }
  40%  { transform: translateY(0) scale(.975); }
  100% { transform: translateY(-1px) scale(1.012); }
}

@media (prefers-reduced-motion: reduce) {
  .finale-step, .finale-step.finale-step-out,
  .finale-step.finale-page-out, .finale-step.finale-page-in,
  .finale-question-card .finale-step-count,
  .finale-question-card .finale-question-text,
  .finale-question-card .finale-options,
  .finale-option-btn.chosen,
  .finale-step.finale-hush .finale-option-btn:not(.chosen),
  .finale-step.finale-hush .finale-option-btn.chosen,
  .finale-step.finale-hush .finale-step-count,
  .finale-step.finale-hush .finale-question-text,
  .finale-letter,
  .finale-letter .finale-step-count,
  .finale-letter h2,
  .finale-voice,
  .finale-letter-para,
  .finale-letter .finale-continue-btn { animation: none; }
  .screen.finale-receding { transition: none; transform: none; opacity: 1; filter: none; }
}
.finale-step-count { color: #e0b8c8; }
.finale-question-text { font-family: "Baloo 2", sans-serif; font-size: clamp(20px, 4vw, 27px); margin: 0; line-height: 1.35; }
.finale-options { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 380px; }
.finale-option-btn {
  border: 2px solid rgba(255,255,255,.25); border-radius: 8px; padding: 13px 18px;
  background: rgba(255,255,255,.06); color: #fbe9ef; font-family: "Baloo 2", sans-serif; font-size: 15px;
  transition: border-color .15s ease, background .15s ease, transform .12s ease;
}
.finale-option-btn:hover { border-color: #e8c94a; background: rgba(255,255,255,.12); transform: translateY(-2px); }
.finale-option-btn:active { transform: translateY(0) scale(.98); transition-duration: .08s; }
/* ---- the letter ----
   Paper, not a card: it establishes itself first and empty, and only then does
   anything appear on it. The page is the dominant object on the screen. */
.finale-letter {
  /* dark ink, because this is the one surface on the site that is light. The
     shared --ink is #f6e9ef — near-white, for the dark rooms — so inheriting it
     here rendered the letter almost invisibly on its own paper. #3a2430 is the
     plum this section already uses for text on light (the continue button). */
  background: #fffafc; color: #3a2430; border-radius: 10px; padding: 46px 38px 40px;
  box-shadow: 0 28px 64px -22px rgba(0, 0, 0, .68), 0 2px 0 rgba(255, 255, 255, .5) inset;
  max-width: 520px; gap: 0;
  animation: letter-paper-in .95s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes letter-paper-in {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
/* every line waits its turn — delays are set per element in renderFinaleLetter().
   "backwards", never "both": a filled animation would outrank the button's own
   hover and press styles and quietly kill them. */
.finale-letter .finale-step-count,
.finale-letter h2,
.finale-letter-para,
.finale-letter .finale-continue-btn { animation: letter-line-in .62s cubic-bezier(.22, 1, .36, 1) backwards; }
@keyframes letter-line-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

.finale-letter h2 {
  /* the deep rose this project already uses on light paper (the note pins and
     hearts) — --blush is tuned for dark rooms and washes out on white */
  font-family: "Baloo 2", sans-serif; margin: 6px 0 26px; color: #c23f5e;
  font-size: clamp(21px, 3.6vw, 27px); line-height: 1.25; align-self: stretch; text-align: left;
}
.finale-letter .finale-step-count { color: rgba(58, 36, 48, .55); align-self: stretch; text-align: left; }
/* generous leading and a short measure: this is meant to read like a letter,
   not like a paragraph on a website */
/* break-word so a stray long token (a path, a link) wraps instead of stretching
   the paper past the screen on a narrow phone */
.finale-letter-body { text-align: left; line-height: 1.9; font-size: 16px; align-self: stretch; overflow-wrap: break-word; }
/* the voice note: one quiet line under the title. Same plum and rose the paper
   already uses, thin track, no chrome. Only ever transform/opacity. */
.finale-voice {
  display: flex; align-items: center; gap: 12px; align-self: stretch;
  margin: -8px 0 24px; animation: letter-line-in .62s cubic-bezier(.22, 1, .36, 1) backwards;
}
.finale-voice[hidden] { display: none; }
.finale-voice { flex-wrap: wrap; }
.finale-voice-label { flex-basis: 100%; text-align: left; color: rgba(58, 36, 48, .55); font-size: 10px; margin-bottom: -4px; }
.finale-voice + .finale-voice { margin-top: -8px; }
.finale-voice-btn {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  border: 2px solid #c23f5e; background: #fff; color: #c23f5e;
  display: grid; place-items: center; font-size: 14px; line-height: 1; padding: 0;
  transition: transform .15s ease, background .15s ease, color .15s ease;
}
.finale-voice-btn:hover { background: #c23f5e; color: #fffafc; transform: translateY(-1px); }
.finale-voice-btn:active { transform: scale(.94); transition-duration: .08s; }
.finale-voice-icon { display: block; }
.finale-voice-track {
  flex: 1; min-width: 0; height: 4px; border-radius: 2px; overflow: hidden;
  background: rgba(58, 36, 48, .14);
}
.finale-voice-fill {
  display: block; width: 100%; height: 100%; background: #c23f5e;
  transform-origin: left center; transform: scaleX(0);
}
.finale-voice-time { flex: none; color: rgba(58, 36, 48, .55); font-size: 10px; }
.finale-letter-para { margin: 0 0 1.15em; white-space: pre-line; }
.finale-letter-para:last-child { margin-bottom: 0; }
.finale-letter .finale-continue-btn { margin-top: 30px; align-self: center; }

/* on a phone the paper's generous margins cost more than they give: at 38px a
   side the measure drops to about 33 characters, which reads as choppy. Trading
   some margin back buys the line length, and the leading keeps it calm. */
@media (max-width: 640px) {
  .finale-letter { padding: 34px 20px 30px; }
  .finale-letter h2 { margin-bottom: 20px; }
  .finale-letter-body { line-height: 1.85; }
  .finale-letter .finale-continue-btn { margin-top: 26px; }
}

/* while she is reading, the way out stays reachable but stops asking for
   attention — the letter is first, the interface second */
.finale-overlay.reading .finale-close { opacity: .3; transition: opacity .6s ease .4s; }
.finale-overlay.reading .finale-close:hover,
.finale-overlay.reading .finale-close:focus-visible { opacity: 1; transition-delay: 0s; }
.finale-continue-btn {
  margin-top: 8px; border: 2px solid #e8c94a; border-radius: 999px; padding: 11px 26px;
  background: #e8c94a; color: #3a2430; font-family: "Baloo 2", sans-serif; font-weight: 700; font-size: 15px;
  transition: transform .15s ease, box-shadow .15s ease;
}
.finale-continue-btn:hover { transform: translateY(-2px); box-shadow: 0 10px 22px -10px rgba(232,201,74,.7); }
.finale-continue-btn:active { transform: translateY(0) scale(.97); transition-duration: .08s; }
.finale-flourish h2 { font-family: "Baloo 2", sans-serif; font-size: clamp(22px, 4.5vw, 30px); margin: 0; }
.finale-hearts { position: relative; width: 100%; height: 120px; pointer-events: none; }
.finale-hearts i {
  position: absolute; top: 100%; color: #e8c94a; font-style: normal;
  animation: finale-heart-rise linear forwards;
}
@keyframes finale-heart-rise {
  from { transform: translateY(0) rotate(0deg); opacity: 0; }
  10% { opacity: 1; }
  100% { transform: translateY(-160px) rotate(18deg); opacity: 0; }
}

/* -- chess -- */
.chess-board {
  display: grid; grid-template-columns: repeat(8, minmax(0, 48px)); grid-template-rows: repeat(8, minmax(0, 48px));
  border: 4px solid var(--wood); border-radius: 4px; overflow: hidden; touch-action: manipulation;
}
.chess-sq {
  width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;
  font-size: 30px; position: relative; user-select: none;
}
.chess-sq.light { background: #eadfc9; }
.chess-sq.dark { background: #8fa06a; }
.chess-sq.piece-w { color: #fbfbf5; text-shadow: 0 0 1px #221733, 0 1px 2px rgba(0,0,0,.6); }
.chess-sq.piece-b { color: #201829; text-shadow: 0 1px 0 rgba(255,255,255,.35); }
.chess-sq.selected { box-shadow: inset 0 0 0 3px var(--lilac); }
.chess-sq.legal::after {
  content: ""; position: absolute; width: 14px; height: 14px; border-radius: 50%;
  background: rgba(143, 111, 208, .55);
}
.chess-sq.legal.has-piece::after { width: 44px; height: 44px; border-radius: 8px; background: rgba(224, 90, 90, .35); }
.chess-sq.last-move { box-shadow: inset 0 0 0 3px rgba(255, 217, 102, .8); }
.chess-status { font-family: "Baloo 2", sans-serif; font-size: 15px; }
/* the board itself is rebuilt fresh on every move (see render() in main.js —
   nothing here changes that), so there's no single persistent DOM element
   for "a piece" to slide. Instead a short-lived clone travels from the
   origin square's last known position to the destination, over the real
   board, while the destination square's own glyph stays hidden until it
   arrives — the same shared-element technique already used for the map
   card → room scene travel. */
.chess-fly-piece {
  position: fixed; z-index: 50; display: flex; align-items: center; justify-content: center;
  font-size: 30px; pointer-events: none; user-select: none;
}
.chess-fly-piece.piece-w { color: #fbfbf5; text-shadow: 0 0 1px #221733, 0 1px 2px rgba(0,0,0,.6); }
.chess-fly-piece.piece-b { color: #201829; text-shadow: 0 1px 0 rgba(255,255,255,.35); }
/* a capture lands with a touch more emphasis than a quiet move */
.chess-fly-piece.capture-punch { animation: chess-capture-punch .22s ease; }
@keyframes chess-capture-punch { 0% { transform: scale(1); } 45% { transform: scale(1.3); } 100% { transform: scale(1); } }
.chess-sq.landing { color: transparent !important; }

/* -- connect 4 -- */
.connect4-frame {
  background: linear-gradient(180deg, #2f74d6, #1b4c9c);
  border-radius: 14px; padding: 10px 12px 14px;
  box-shadow: var(--shadow), inset 0 2px 3px rgba(255,255,255,.3), inset 0 -6px 10px rgba(0,0,0,.25);
}
.connect4-arrows { display: flex; gap: 6px; padding: 0 2px 2px; }
.connect4-arrow {
  width: 46px; height: 18px; display: flex; align-items: center; justify-content: center;
  font-size: 18px; line-height: 1; transition: color .12s ease;
}
.connect4-board { display: flex; gap: 6px; }
.connect4-col {
  display: flex; flex-direction: column; gap: 6px; padding: 4px; border-radius: 8px;
  cursor: pointer; transition: background .12s ease;
}
.connect4-col:hover { background: rgba(255,255,255,.10); }
.connect4-cell {
  width: 46px; height: 46px; border-radius: 50%; position: relative; overflow: visible;
  background: radial-gradient(circle at 35% 32%, #1b3f78 0%, #0b2140 75%);
  box-shadow: inset 0 4px 7px rgba(0,0,0,.65), inset 0 -2px 3px rgba(255,255,255,.06);
}
.connect4-disc {
  position: absolute; inset: 2px; border-radius: 50%;
  box-shadow: inset 0 -5px 7px rgba(0,0,0,.3), inset 0 3px 4px rgba(255,255,255,.45), 0 2px 3px rgba(0,0,0,.35);
}
.connect4-disc.vik { background: radial-gradient(circle at 35% 28%, #ff90ac 0%, #e0355f 55%, #9c1339 100%); }
.connect4-disc.minal { background: radial-gradient(circle at 35% 28%, #fff2ae 0%, #e8c94a 55%, #a5811a 100%); }
.connect4-disc.dropping {
  animation: connect4-fall .42s cubic-bezier(.4, 0, .2, 1.4);
}
@keyframes connect4-fall {
  from { transform: translateY(var(--drop-dist, -200px)); }
  to { transform: translateY(0); }
}
.connect4-disc.win {
  box-shadow: 0 0 0 3px #fff, 0 0 16px 5px rgba(255,255,255,.65);
  animation: connect4-pulse 1s ease-in-out infinite alternate;
}
@keyframes connect4-pulse { from { filter: brightness(1); } to { filter: brightness(1.3); } }
/* the winning disc is often also the one that was just dropped — without
   this, .win's own animation (added below, same specificity, later in the
   sheet) silently replaced .dropping's fall entirely, so the piece that
   actually won would snap straight into place instead of falling like
   every other piece before it lit up. Play the fall first, then hand off
   to the glow once it's landed. */
.connect4-disc.dropping.win {
  animation: connect4-fall .42s cubic-bezier(.4, 0, .2, 1.4), connect4-pulse 1s ease-in-out .42s infinite alternate;
}
@media (prefers-reduced-motion: reduce) {
  .connect4-disc.dropping, .connect4-disc.win, .connect4-disc.dropping.win { animation: none; }
}

/* the 46px cells (7 columns + gaps + padding) run wider than a phone
   screen, forcing an undiscoverable horizontal scroll to reach the last
   column — shrink everything down to fit narrow viewports without scrolling */
@media (max-width: 480px) {
  .connect4-cell { width: 32px; height: 32px; }
  .connect4-col { padding: 3px; gap: 4px; }
  .connect4-board { gap: 4px; }
  .connect4-arrow { width: 38px; }
  .connect4-arrows { gap: 4px; }
}

/* -- quiz -- */
.quiz-card {
  background: #241c2c; border: 3px solid var(--wood); border-radius: 8px;
  padding: 20px; max-width: 480px; width: 100%; display: flex; flex-direction: column; gap: 12px;
}
.quiz-q { font-family: "Baloo 2", sans-serif; font-size: 17px; margin: 0; animation: room-chrome-in .3s ease; }
.quiz-choice {
  border: 2px solid var(--wood); border-radius: 6px; padding: 10px 14px; text-align: left;
  background: #2c2236; color: #f4ecf7;
  transition: border-color .2s ease, background .2s ease, opacity .2s ease, transform .12s ease;
}
.quiz-choice:hover, .quiz-choice:focus-visible { border-color: var(--lilac); }
/* the choice itself answers first — a quick settle, not a jump straight to
   green or red — then the correct/wrong colour eases in on top of it */
.quiz-choice.picked { animation: quiz-pick .22s ease; }
@keyframes quiz-pick { 0% { transform: scale(1); } 45% { transform: scale(.96); } 100% { transform: scale(1); } }
.quiz-choice.correct { border-color: var(--grass); background: rgba(143, 210, 104, .18); }
.quiz-choice.wrong { border-color: #e05a5a; background: rgba(224, 90, 90, .18); }
/* once answered, the choices that weren't picked and weren't the right
   answer quiet down, so the eye goes to what mattered rather than a wall of
   equally-loud buttons */
.quiz-card.answered .quiz-choice:not(.correct):not(.wrong) { opacity: .45; }
@media (prefers-reduced-motion: reduce) {
  .quiz-q { animation: none; }
  .quiz-choice { transition: border-color .2s ease, background .2s ease, opacity .2s ease; }
  .quiz-choice.picked { animation: none; }
}
.quiz-form { display: flex; flex-direction: column; gap: 8px; width: 100%; max-width: 480px; }
.quiz-form input {
  border: 2px solid var(--wood); border-radius: 6px; padding: 8px 10px; background: #2c2236; color: #f4ecf7;
  font-family: "Nunito Sans", sans-serif; font-size: 13.5px;
}
.quiz-form input::placeholder { color: #8d7f99; }
.quiz-list-item {
  border: 2px solid var(--wood); border-radius: 6px; padding: 10px 12px; width: 100%; max-width: 480px;
  font-size: 12.5px; color: #d7cbe0; display: flex; justify-content: space-between; gap: 10px; align-items: center;
}
.quiz-del { color: #e05a5a; font-size: 11px; flex-shrink: 0; }

/* -- scoreboard -- */
.scoreboard-tally {
  display: flex; align-items: center; gap: 16px; font-family: "Baloo 2", sans-serif;
}
.scoreboard-tally .scoreboard-side { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.scoreboard-tally .scoreboard-num { font-size: 40px; color: var(--lilac); line-height: 1; }
.scoreboard-tally .scoreboard-name { font-size: 13px; color: #c7b8d1; font-family: "Nunito Sans", sans-serif; }
.scoreboard-tally .scoreboard-vs { font-size: 16px; color: #8d7f99; }
.scoreboard-log-empty { color: #8d7f99; font-size: 12.5px; }
.scoreboard-quicklog { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 4px; }
/* the confirmation eases in rather than snapping the buttons straight to
   text, so logging a win reads as a small settle connected to the tap
   rather than the row's content just being replaced */
.scoreboard-logged { color: var(--grass); font-size: 12.5px; animation: room-chrome-in .28s ease; }
@media (prefers-reduced-motion: reduce) { .scoreboard-logged { animation: none; } }
