/* Layout and type follow onlyphucnguyensiavn.com (Cormorant Infant, gradient headings,
   glass panels); the palette is taken from the sky lanterns themselves. */
:root {
  /* Page background — top of the dreamy periwinkle→lavender→mauve gradient
     defined on #sky. Kept as an HSL triplet so the same value can be used
     directly (body fallback) and via hsl() elsewhere without a colour cast
     if hsl() is used raw. */
  --bg: 231 46% 27%;
  --foreground: 40 40% 96%;
  --muted-foreground: 36 22% 68%;
  --border: 32 40% 26%;
  --input: 260 20% 10%;
  --destructive: 6 82% 66%;

  /* lantern palette */
  --l-core: 46 100% 95%;
  --l-hot: 40 100% 78%;
  --l-mid: 32 100% 60%;
  --l-deep: 22 92% 50%;
  --l-dark: 16 82% 36%;

  --gradient-primary: linear-gradient(135deg, hsl(46 100% 90%), hsl(36 100% 68%), hsl(22 92% 52%));
  --glass-bg: hsl(28 40% 10% / .45);
  --glass-border: hsl(36 100% 62% / .22);
  --shadow-card: 0 14px 44px -12px hsl(22 92% 30% / .55);
  --transition-smooth: all .35s cubic-bezier(.4, 0, .2, 1);
  --radius: 1rem;

  /* the ocean now occupies roughly the lower ~42% of the viewport so the boat
     has real explorable water; the sky above still hosts the header, moon,
     lanterns and stars. */
  --sea-h: clamp(280px, 42vh, 520px);
  --boat-scale: 1;
  /* the artwork is square; the lamp sits at 40.5% across and 26.2% down it.
     The lighthouse now lives on the RIGHT side of the scene, so the lamp — via
     .headland-art scaleX(-1) — reads at (100 - 40.5)% = 59.5% across. */
  --lh-w: clamp(240px, 34vw, 460px);
  --lh-right: -1.5rem;
  /* the boat's run in from the open sea, and the lighthouse's growth, share it */
  --voyage: 26s;

  /* world "camera": ONE transform on .scene (sea + islands + boat +
     lighthouse + moon together), written by the journey in app.js with a
     Web Animation — see JOURNEY › CAMERA. Nothing else moves the world. */

  --font: "Cormorant Infant", Georgia, "Times New Roman", serif;
}

* { box-sizing: border-box; }

/* The hidden attribute must always win — class selectors that set `display`
   (e.g. .reader-amount { display:flex }) would otherwise defeat it and leave
   the element visible even when [hidden] is set. */
[hidden] { display: none !important; }

html, body { margin: 0; min-height: 100%; }

body {
  min-height: 100svh;
  background: hsl(var(--bg));
  color: hsl(var(--foreground));
  font-family: var(--font);
  font-size: 17.5px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- sky ----------
   Dreamy pastel night-sky gradient, top → bottom, sampled from the reference
   coming-soon poster:
     top    #5368B5  hsl(227 40% 52%)   deep periwinkle / cornflower
     middle #7779B9  hsl(238 32% 60%)   soft muted lavender-blue
     bottom #C9A5B8  hsl(328 25% 72%)   warm dusty lavender / mauve
   Intermediate interpolated stops smooth the periwinkle↔lavender↔mauve
   transitions so there's no visible band where hues change families.
   The zenith is deepened to a night indigo (was hsl(227 40% 52%)) so the
   upper sky reads as night and the lanterns/text glow against it; the
   poster's lavender→mauve horizon is unchanged. */
#sky {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  display: block;
  background:
    linear-gradient(180deg,
      hsl(231 46% 27%) 0%,
      hsl(232 40% 38%) 20%,
      hsl(238 33% 53%) 48%,
      hsl(268 28% 63%) 72%,
      hsl(310 26% 68%) 88%,
      hsl(328 25% 72%) 100%);
}


/* ---------- shooting stars ---------- */
.sky-fx {
  position: fixed;
  inset: 0;
  z-index: 1;         /* in the sky, behind the boat/lighthouse/lanterns */
  overflow: hidden;
  pointer-events: none;
}

.shooting-star {
  position: absolute;
  left: var(--sx);
  top: var(--sy);
  width: var(--len, 120px);
  height: 2px;
  border-radius: 2px;
  opacity: 0;
  /* a soft glowing trail, brightest at the leading (right) end */
  background: linear-gradient(90deg,
    transparent 0%,
    hsl(42 100% 88% / .12) 38%,
    hsl(45 100% 94% / .7) 82%,
    hsl(46 100% 97% / .95) 100%);
  filter: drop-shadow(0 0 4px hsl(43 100% 82% / .6));
  animation: shoot var(--dur, 1.1s) ease-in forwards;
}

/* the bright head */
.shooting-star::after {
  content: "";
  position: absolute;
  right: -1px;
  top: 50%;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  transform: translateY(-50%);
  background: hsl(46 100% 97%);
  box-shadow: 0 0 8px 2px hsl(44 100% 88% / .9);
}

@keyframes shoot {
  0%   { transform: rotate(var(--ang)) translateX(0); opacity: 0; }
  12%  { opacity: 1; }
  84%  { opacity: 1; }
  100% { transform: rotate(var(--ang)) translateX(var(--dist)); opacity: 0; }
}

/* ---------- the flying wishes ---------- */
.field {
  position: fixed;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

.lantern {
  position: absolute;
  top: 0;
  left: var(--x);
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  pointer-events: auto;
  will-change: transform;
  animation: rise var(--dur) linear var(--delay) infinite;
}

.lantern-inner {
  display: block;
  transform: scale(var(--scale));
  transform-origin: 50% 0;
}

.lantern-sway {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.45rem;
  animation: sway var(--sway) ease-in-out infinite alternate;
}

.lantern-body {
  position: relative;
  width: 44px;
  height: 58px;
  border-radius: 46% 46% 40% 40% / 24% 24% 50% 50%;
  background:
    radial-gradient(56% 40% at 50% 84%, hsl(var(--l-core)) 0%, hsl(var(--l-hot)) 22%,
                    hsl(var(--l-mid)) 48%, hsl(var(--l-deep)) 74%, hsl(var(--l-dark)) 100%);
  box-shadow:
    0 0 22px hsl(var(--l-mid) / .55),
    0 0 54px hsl(var(--l-deep) / .3),
    inset 0 -6px 12px hsl(var(--l-dark) / .45);
}

.lantern-body::before {
  content: "";
  position: absolute;
  left: 16%; right: 16%;
  top: -2px;
  height: 9px;
  border-radius: 50%;
  background: linear-gradient(180deg, hsl(var(--l-dark) / .9), hsl(var(--l-deep) / .35));
}

.lantern-body::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -3px;
  width: 10px;
  height: 13px;
  transform: translateX(-50%);
  border-radius: 50% 50% 46% 46% / 62% 62% 38% 38%;
  background: radial-gradient(circle at 50% 70%, hsl(var(--l-core)) 0%, hsl(var(--l-hot)) 42%, hsl(var(--l-deep) / .5) 78%, transparent 100%);
  filter: blur(0.4px);
}

.lantern-name {
  max-width: 11ch;
  font-size: 0.792rem;
  color: hsl(var(--l-hot) / .82);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-shadow: 0 1px 8px hsl(260 30% 4% / .9);
}

.lantern:hover .lantern-body,
.lantern:focus-visible .lantern-body {
  box-shadow:
    0 0 30px hsl(var(--l-hot) / .8),
    0 0 80px hsl(var(--l-mid) / .45),
    inset 0 -6px 12px hsl(var(--l-dark) / .45);
}

.lantern:focus-visible {
  outline: 2px solid hsl(var(--l-hot));
  outline-offset: 8px;
  border-radius: 14px;
}

@keyframes rise {
  from { transform: translate3d(0, 108vh, 0); }
  to   { transform: translate3d(var(--drift), -24vh, 0); }
}

/* a wish launched from the boat: a glowing lantern that rises into the sky */
.sky-launch {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 10;
  width: 40px;
  height: 52px;
  margin: -26px 0 0 -20px;
  border-radius: 46% 46% 40% 40% / 24% 24% 50% 50%;
  pointer-events: none;
  background:
    radial-gradient(56% 40% at 50% 84%, hsl(var(--l-core)) 0%, hsl(var(--l-hot)) 22%,
                    hsl(var(--l-mid)) 48%, hsl(var(--l-deep)) 74%, hsl(var(--l-dark)) 100%);
  box-shadow: 0 0 26px hsl(var(--l-mid) / .7), 0 0 70px hsl(var(--l-deep) / .4);
}

/* a sparkle tail streaming below the launched lantern */
.sky-launch::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 90%;
  width: 12px;
  height: 120px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: linear-gradient(180deg, hsl(var(--l-hot) / .7) 0%, hsl(var(--l-mid) / .28) 36%, transparent 88%);
  filter: blur(3px);
}

/* the Wish Guide lighting a wish (app.js › releaseFromBoat): the light sits
   at the lantern in the character's hand, inside the guide's own box, so it
   moves, scales and bobs with the character; the held lantern steps aside
   while it is the wish, and is back in hand a little later */
.wg-light {
  position: absolute;
  z-index: 3;
  width: 0;
  height: 0;
  pointer-events: none;
}
.wish-guide-isle .wg-light { animation: caller-bob 5s ease-in-out infinite; }
.sky-launch.is-held { position: absolute; z-index: auto; transform-origin: 50% 50%; }
.sky-launch.is-held::after { opacity: 0; }
.wp-lantern, .wp-string { transition: opacity .9s ease; }
.is-lighting .wp-lantern, .is-lighting .wp-string { opacity: 0; transition-duration: .25s; }
.is-lighting .wish-person-glow { fill: hsl(38 100% 64% / .42); transition: fill 1.2s ease; }

/* a freshly released wish trails light as it climbs */
.lantern .trail { display: none; }

.lantern.releasing .trail {
  display: block;
  position: absolute;
  left: 50%;
  bottom: -8px;
  width: 12px;
  height: 130px;
  transform: translateX(-50%);
  pointer-events: none;
  border-radius: 50%;
  background: linear-gradient(180deg,
    hsl(var(--l-hot) / .75) 0%,
    hsl(var(--l-mid) / .3) 34%,
    hsl(var(--l-deep) / .08) 66%,
    transparent 92%);
  filter: blur(3px);
  animation: trail-fade 6s ease-out forwards;
}

/* a scatter of sparks streaming off the tail */
.lantern.releasing .trail::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    radial-gradient(2px 2px at 50% 20%, hsl(var(--l-core)), transparent 60%),
    radial-gradient(1.6px 1.6px at 35% 42%, hsl(var(--l-hot)), transparent 60%),
    radial-gradient(1.6px 1.6px at 66% 55%, hsl(var(--l-hot)), transparent 60%),
    radial-gradient(1.4px 1.4px at 45% 74%, hsl(var(--l-mid)), transparent 60%);
  animation: sparks 0.8s linear infinite;
}

@keyframes trail-fade {
  0%   { opacity: 0.95; }
  70%  { opacity: 0.55; }
  100% { opacity: 0; }
}

@keyframes sparks {
  from { transform: translateY(0); opacity: 1; }
  to   { transform: translateY(14px); opacity: 0.3; }
}

@keyframes sway {
  from { transform: translateX(-6px) rotate(-2.5deg); }
  to   { transform: translateX(6px) rotate(2.5deg); }
}

/* ================= the scene ================= */
.scene {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 2;
  height: calc(var(--sea-h) + 40vh);
  pointer-events: none;
  /* the world camera scales about the scene's bottom-left corner; the
     journey writes translate(x, y) scale(z) here (never below 1, and never
     letting an edge of the sea into view) */
  transform-origin: 0 100%;
}

/* ---------- sea ---------- */
.sea {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: var(--sea-h);
  z-index: 1;
  overflow: hidden;
  /* Blue-purple night-ocean gradient, top → bottom. Targets sampled to match
     the illustrated look the reference (and the current design brief) calls
     for — a softer, luminous blue-purple, NOT near-black.
       top    #46548A  hsl(228 33% 41%)  dusty indigo (far water)
       middle #344A78  hsl(221 40% 34%)  medium blue with purple tint
       bottom #172B50  hsl(219 55% 20%)  deep but not black (foreground)
     Intermediate stops interpolate to avoid any visible banding across the
     ~130–300px sea height, and the whole stack stays independent of --bg. */
  background:
    linear-gradient(180deg,
      hsl(228 33% 41%) 0%,
      hsl(226 36% 38%) 25%,
      hsl(221 40% 34%) 52%,
      hsl(220 47% 27%) 78%,
      hsl(219 55% 20%) 100%);
}

/* the horizon catching the last of the light */
.sea::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 2px;
  z-index: 5;
  background: linear-gradient(90deg, transparent, hsl(var(--l-mid) / .5) 20%, hsl(var(--l-hot) / .7) 52%, hsl(var(--l-mid) / .5) 84%, transparent);
}

/* the lighthouse laid out on the water; inside .headland so it shortens
   with distance along with the tower */
.reflect-lh {
  position: absolute;
  left: 40.5%;
  top: 99%;
  width: 9%;
  height: 46%;
  margin-left: -4.5%;
  background: linear-gradient(180deg, hsl(46 100% 74% / .55), hsl(38 100% 60% / .1) 72%, transparent);
  -webkit-mask-image: repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 9px);
  mask-image: repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 9px);
  filter: blur(1.2px);
  animation: reflect 6s ease-in-out infinite alternate;
}

@keyframes reflect {
  from { transform: translateX(-4px) scaleX(0.86); opacity: 0.75; }
  to   { transform: translateX(4px) scaleX(1.16); opacity: 1; }
}

/* ---------- moon behind the lighthouse ----------
   Large full moon anchored behind the lighthouse tower, sitting low enough to
   overlap the tower's upper mid-section. Sits BELOW the headland (z-index 3)
   and above the sky/lanterns, so the tower silhouette reads cleanly in front.
   The lamp beam (.lh-beam) lives inside the headland, above the moon. */
.moon {
  position: absolute;
  /* pin behind the RIGHT-side lighthouse, sized so the tower silhouette
     visibly overlaps its upper third and the moon never dominates the scene. */
  right: 8vw;
  left: auto;
  bottom: calc(var(--sea-h) + 6vw);
  width: clamp(120px, 14vw, 210px);
  height: clamp(120px, 14vw, 210px);
  pointer-events: none;
  z-index: 2;
}
.moon-body {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle at 28% 34%, hsl(30 32% 60% / .28) 0 2.6%, transparent 3.6%),
    radial-gradient(circle at 62% 28%, hsl(30 32% 60% / .22) 0 1.8%, transparent 2.6%),
    radial-gradient(circle at 46% 62%, hsl(28 30% 58% / .24) 0 3.4%, transparent 4.4%),
    radial-gradient(circle at 70% 56%, hsl(28 30% 58% / .18) 0 2.2%, transparent 3.0%),
    radial-gradient(circle at 36% 76%, hsl(28 30% 58% / .18) 0 2.6%, transparent 3.6%),
    radial-gradient(circle at 78% 74%, hsl(30 32% 60% / .16) 0 1.6%, transparent 2.4%),
    radial-gradient(circle at 42% 40%,
      hsl(44 64% 92%) 0%,
      hsl(40 55% 86%) 40%,
      hsl(34 44% 76%) 72%,
      hsl(30 32% 66%) 100%);
  opacity: .82;
  filter: blur(.4px);
}
/* soft warm haze around the moon — atmospheric, integrated with the night sky */
.moon-haze {
  position: absolute;
  inset: -55%;
  border-radius: 50%;
  background: radial-gradient(circle,
    hsl(42 60% 84% / .22) 0%,
    hsl(38 55% 78% / .13) 34%,
    hsl(34 50% 72% / .06) 58%,
    transparent 78%);
  pointer-events: none;
}
@media (max-width: 720px) {
  .moon { right: 4vw; bottom: calc(var(--sea-h) + 8vw); width: clamp(96px, 20vw, 150px); height: clamp(96px, 20vw, 150px); }
}
@media (prefers-reduced-motion: reduce) { .moon { transition: none; } }

/* ---- moon reflection on the water ----
   NOT a second moon: a soft, vertically stretched, heavily blurred glow that
   sits directly under the moon and fades away toward the foreground. The
   waves drift over the top of it (they sit above in z-order), so it reads as
   moonlight broken by the swell rather than a solid disc. */
.moon-reflect {
  position: absolute;
  /* horizontally centred under the moon (moon: right 8vw, width ~14vw) */
  right: calc(8vw + clamp(120px, 14vw, 210px) / 2);
  bottom: 0;
  transform: translateX(50%);
  width: clamp(70px, 9vw, 130px);
  height: calc(var(--sea-h) * 0.85);
  pointer-events: none;
  z-index: 1; /* behind the wave layers so the crests break the reflection */
  opacity: .38;
  filter: blur(8px);
  mix-blend-mode: screen;
  /* the elongated glow itself: a soft radial cone that fades downward, with
     faint horizontal streaks laid on top to suggest wavelets catching light */
  background:
    repeating-linear-gradient(180deg,
      hsl(42 60% 84% / .18) 0px, hsl(42 60% 84% / .18) 2px,
      transparent 4px, transparent 11px),
    radial-gradient(ellipse 70% 100% at 50% 0%,
      hsl(42 60% 86% / .34) 0%,
      hsl(40 55% 80% / .18) 32%,
      hsl(36 46% 72% / .07) 62%,
      transparent 100%);
  /* mask so the whole thing softly fades to nothing at the bottom */
  -webkit-mask-image: linear-gradient(180deg, black 0%, black 30%, transparent 100%);
          mask-image: linear-gradient(180deg, black 0%, black 30%, transparent 100%);
}
@media (max-width: 720px) {
  .moon-reflect { right: calc(4vw + clamp(96px, 20vw, 150px) / 2); width: clamp(40px, 8vw, 80px); opacity: .30; }
}

/* ---------- headland and lighthouse ---------- */
.headland {
  position: absolute;
  right: var(--lh-right);
  left: auto;
  bottom: calc(var(--sea-h) - 14px);
  width: var(--lh-w);
  z-index: 3;
  /* grow from the tower's base (mirror of 40.5%) so it stays planted at the
     waterline as it scales up when its milestone is reached */
  transform-origin: 59.5% 100%;
  /* final destination on the FAR RIGHT: always visible as a dark silhouette,
     planted at the horizon. Fully lit only once milestone 4 is reached. (The
     world camera lives on .scene, so this is only its own planted pose.) */
  transform: translate(2vw, calc(var(--sea-h) * 0.03)) scale(0.92);
  transition: transform 2.2s cubic-bezier(.33, 0, .2, 1), filter 1.6s ease;
}

/* reached: the lighthouse settles fully into the foreground, fully lit */
.headland.lit {
  transform: translate(2vw, calc(var(--sea-h) * 0.05)) scale(1);
}

/* the SVG is authored lamp-on-the-left; flip it so the tower faces the ocean
   the boat approaches from (the whole art is horizontally mirrored) */
.headland-art { display: block; width: 100%; height: auto; overflow: visible; transform: scaleX(-1); transform-origin: 50% 50%; }

.cliff, .tower, .stair, .gallery, .dome { fill: hsl(252 34% 7%); }
.rail { stroke: hsl(252 34% 7%); stroke-width: 2.6; }

/* the ridge and the tower's lit edge, so the silhouette reads against the sky */
.rim {
  fill: none;
  stroke: hsl(38 90% 70% / .3);
  stroke-width: 2;
  stroke-linecap: round;
}
.finial { fill: hsl(256 40% 10%); }
.lamp-room { fill: hsl(30 40% 12%); }

.pane {
  fill: hsl(48 100% 68%);
  animation: lamp-pulse 5s ease-in-out infinite;
}

.lamp-halo {
  fill: hsl(46 100% 70% / .16);
  filter: blur(10px);
  animation: lamp-pulse 5s ease-in-out infinite;
}

@keyframes lamp-pulse {
  0%, 100% { opacity: 0.78; }
  50%      { opacity: 1; }
}

/* a zero-size marker at the lamp; JS reads its screen position. Mirrored to
   59.5% because .headland-art is scaleX(-1) (lamp on the right side now). */
.beacon {
  position: absolute;
  left: 59.5%;
  top: 26.2%;
  width: 0;
  height: 0;
}



/* ---------- boat ---------- */
.boat-lane {
  position: absolute;
  left: 0; right: 0;
  bottom: 0;
  height: var(--sea-h);
  /* not a stacking context: the boat is depth-sorted with the islands (its
     z-index ≥ 1000 always keeps it in front of the headland, z 3) */
  z-index: auto;
}

.boat {
  position: absolute;
  bottom: 0;
  left: 0;
  /* sized for the world, not the viewport: clearly smaller than an island
     (they are ~180–240px) so it fits the water between the island rows and
     can moor in front of each one — verified by the journey's water graph at
     375 / 390 / 430 / 875 / 1440px. Its exclusion box is measured from this. */
  --bw: clamp(96px, 11vw, 150px);
  width: var(--bw);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  pointer-events: auto;
  /* scale about the waterline so the hull stays in the water */
  transform-origin: 50% 100%;
  /* position is driven from JS (journey between islands); it eases between
     milestones rather than looping. Set on load before the first paint. */
  /* starts lower-LEFT now; JS overwrites this on init but keep the pre-paint
     value close to where the journey begins to avoid a flash from the old
     right-hand start position. */
  transform: translate(2vw, calc(var(--sea-h) * -0.05)) scale(0.86);
  /* NO transition: the journey controller in app.js is the boat's only
     mover (one Web Animation per curved leg). A transition here would
     tween every committed pose in a straight line and fight it. */
  transition: none;
  /* (no will-change: the legs are Web Animations, composited while they run;
     at rest the boat is redrawn at its real on-screen size — the camera's
     1.42× push-in and the near-water scale would otherwise stretch a bitmap
     drawn at its far-sea size) */
}

.boat:focus-visible {
  outline: 2px solid hsl(var(--l-hot));
  outline-offset: 6px;
  border-radius: 12px;
}

.boat:hover .boat-art { filter: drop-shadow(0 0 7px hsl(var(--l-hot) / .65)) drop-shadow(0 3px 6px hsl(222 60% 8% / .35)); }

/* riding the swell: a slow heave up and down, a quicker roll on top of it */
.boat-heave { animation: heave 6.5s ease-in-out infinite alternate; }
.boat-roll {
  transform-origin: 50% 92%;
  animation: roll 3.1s ease-in-out infinite alternate;
}

.boat-art {
  display: block;
  width: 100%;
  height: auto;
  /* the swell / rock / settle move the drawing INSIDE this svg (.boat-heave-g
     lifts it up to 33 units) while the rider's hair sits ~8 units under the
     viewBox top: an svg clips to its viewBox by default, so the head was cut
     on most swell frames. Let the drawing paint past its own box. */
  overflow: visible;
  /* FIXED, STABLE orientation. The bow always faces RIGHT — the SVG itself is
     internally flipped (see index.html: <g transform="translate(200,0)
     scale(-1,1)">). NO JS-driven rotation is applied here: movement lives on
     the outer .boat (translate + scale), and this element only carries the
     boat's visual scale. Never add rotate/rotateX/rotateY/perspective/scaleX
     here — the boat must not turn, flip, mirror or tumble as it moves.
     Subtle rocking comes from the .boat-heave / .boat-roll wrappers, both
     capped to a few degrees, and does NOT change the boat's readable pose. */
  transform: scale(var(--boat-scale));
  transform-origin: 50% 100%;
  /* the lantern's warmth as a TIGHT rim, plus a little shade beneath for
     separation from the sea. (It was a 22px + 30px orange haze — a fifth to a
     third of the boat's width — laid around every edge, the hair and the
     hull included: the whole silhouette read soft and blurry.) The wide
     warm light lives on the water instead (.boat-glow). Kept faint: a bright
     rim round the hull's foot outlined it against the water it sits in. */
  filter: drop-shadow(0 0 4px hsl(34 100% 62% / .14))
          drop-shadow(0 3px 6px hsl(222 60% 8% / .35));
}

/* top-down boat colours. .hull is the dark wooden outer rim; .hull-inside
   is the lighter wooden deck plank surface set inside the rim. */
.hull { fill: hsl(24 46% 22%); }
.hull-rim { fill: hsl(32 58% 34%); }
.hull-inside { fill: hsl(30 48% 42%); }
.plank { stroke: hsl(24 38% 24%); stroke-width: 1.1; fill: none; stroke-linecap: round; }
.stern-transom { stroke: hsl(22 40% 16%); stroke-width: 2; fill: none; stroke-linecap: round; }

/* Oars — pale wooden shaft with a slightly darker blade tip. Sit UNDER the
   hull rim in stacking order (they're drawn first in the SVG). */
.oar-shaft { stroke: hsl(32 40% 44%); stroke-width: 2.4; stroke-linecap: round; }
.oar-blade { fill: hsl(28 34% 30%); }
.lamp-post { stroke: hsl(28 40% 24%); stroke-width: 2.4; fill: none; stroke-linecap: round; }
.bow-lamp {
  fill: hsl(46 100% 90%);
  filter: drop-shadow(0 0 8px hsl(42 100% 74%))
          drop-shadow(0 0 20px hsl(36 100% 62%))
          drop-shadow(0 0 40px hsl(30 100% 56% / .7));
  /* the lantern starts unlit and slowly kindles, then flickers gently */
  animation: lamp-ignite 6s ease-in both,
             lamp-flicker 3s ease-in-out 6s infinite alternate;
}

@keyframes lamp-ignite {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

@keyframes lamp-flicker {
  0%   { opacity: 0.9; }
  100% { opacity: 1; }
}
.mast { fill: hsl(30 30% 24%); }
/* the open wooden hull and the sliver of interior we see over the gunwale */
.hull-inside { fill: hsl(20 45% 11%); }

/* a soft warm glow pooling under the clustered wishes */
.wish-halo {
  fill: hsl(38 100% 64% / .38);
  filter: blur(6px);
  animation: wish-twinkle 3.6s ease-in-out infinite alternate;
}

/* several warm wishes glowing inside the boat */
.wish-light {
  fill: hsl(44 100% 88%);
  filter: drop-shadow(0 0 5px hsl(40 100% 70%)) drop-shadow(0 0 12px hsl(32 100% 58% / .9));
  animation: wish-twinkle 2.8s ease-in-out var(--tw, 0s) infinite alternate;
}

@keyframes wish-twinkle {
  from { opacity: 0.55; }
  to   { opacity: 1; }
}
.flag {
  transform-box: view-box;
  transform-origin: 82px 19px;
  animation: flutter 1.9s ease-in-out infinite alternate;
}

.flag-cloth {
  fill: hsl(var(--l-deep));
  stroke: hsl(38 90% 72% / .5);
  stroke-width: 1;
}

.flag-text {
  fill: hsl(46 100% 94%);
  font: 800 14.5px/1 var(--font);
  letter-spacing: 0.06em;
  text-anchor: middle;
}

/* the crew turns to keep the lighthouse in view as the boat passes it, and
   paces the deck meanwhile; JS toggles .face-left on the boat */
.crew {
  transform-box: view-box;
  transform-origin: 139px 78px;
  transition: transform 0.7s cubic-bezier(.4, 0, .2, 1);
}

/* the boat always heads toward the lighthouse, so the crew never flips */

.crew-move {
  transform-box: view-box;
  transform-origin: 139px 78px;
  animation: pace 9s ease-in-out infinite alternate;
}

@keyframes pace {
  from { transform: translateX(-11px); }
  to   { transform: translateX(13px); }
}

.figure {
  fill: hsl(18 30% 9%);
  filter: drop-shadow(0 0 5px hsl(var(--l-hot) / .75))
          drop-shadow(0 0 14px hsl(var(--l-mid) / .5));
}

.arm { transform-origin: 137px 68px; animation: wave 2.6s ease-in-out infinite alternate; }


/* TOP-DOWN VIEW WAKE:
   The boat travels RIGHT toward the lighthouse. Bow wave breaks at the RIGHT
   (front) of the boat; the wake trails behind to the LEFT. */
.bow-wave {
  position: absolute;
  left: -7%;                      /* small foam blob at the BOW: the drawing's bow is LEFT (mirrored with it) */
  top: 45%;
  width: 30%;
  height: 13px;
  border-radius: 50%;
  background: radial-gradient(65% 100% at 72% 50%, hsl(196 70% 94% / .55), hsl(200 60% 86% / .18) 55%, transparent 78%);
  filter: blur(1.6px);
  animation: bow-wave 1.5s ease-in-out infinite alternate;
  transform: translateY(-50%);
}

/* the boat, turned to face its heading: the journey writes scaleX (1 = bow
   right, −1 = bow left, narrower while it points into / out of the sea) */
.boat-yaw { display: block; position: relative; transform-origin: 50% 80%; }
/* the water it disturbs, pivoting at the hull's waterline: the journey turns
   this to the boat's ACTUAL direction of travel on screen, so in its frame
   the bow is always +x and everything here trails astern (−x) */
.boat-water {
  position: absolute;
  left: 50%;
  top: 84%;
  width: 0;
  height: 0;
  pointer-events: none;
}
.wake {
  position: absolute;
  right: calc(var(--bw) * .16);   /* from just aft of the midline, trailing astern */
  top: 0;
  width: calc(var(--bw) * 1.65);
  height: 20px;
  overflow: hidden;
  /* a wedge OPENING to the LEFT (away from the stern) — narrow at the boat,
     spreading out into the water behind */
  clip-path: polygon(0 0, 0 100%, 100% 62%, 100% 38%);
  background: linear-gradient(270deg,
    hsl(198 65% 92% / .4) 0%,
    hsl(200 60% 86% / .16) 34%,
    hsl(200 55% 80% / .05) 66%,
    transparent 92%);
  filter: blur(1.8px);
  transform: translateY(-50%);
}

/* (the wake is the soft wedge alone: its old foam was a row of hard dashes) */
.wake::before { content: none; }

.ripple {
  position: absolute;
  right: calc(var(--bw) * .1);    /* ripples shed astern */
  top: 0;
  width: calc(var(--bw) * .26);
  height: 11px;
  border-radius: 50%;
  /* a soft swell of light, not an outlined ring */
  background: radial-gradient(closest-side, transparent 55%, hsl(199 60% 86% / .22) 80%, transparent 100%);
  opacity: 0;
  animation: ripple 3.4s linear infinite;
  transform: translateY(-50%);
}

.ripple-2 { animation-delay: 1.13s; }
.ripple-3 { animation-delay: 2.26s; }

@keyframes bow-wave {
  from { transform: scaleX(0.86) scaleY(0.8); opacity: 0.6; }
  to   { transform: scaleX(1.1) scaleY(1.15); opacity: 1; }
}

@keyframes foam {
  from { background-position-x: 0; }
  to   { background-position-x: 27px; }
}

@keyframes ripple {
  0%   { transform: translate(0, -50%) scale(0.35); opacity: 0.6; }
  100% { transform: translate(-90px, -50%) scale(2.1); opacity: 0; }
}

/* the paddles pushing water: a small ring either side of the hull, drawn
   back astern as it spreads (in the boat's own frame, so it turns with it) */
.paddle-ripple {
  position: absolute;
  left: calc(var(--bw) * -.08);
  width: calc(var(--bw) * .16);
  height: 7px;
  margin-left: calc(var(--bw) * -.08);
  border: 1px solid hsl(199 70% 90% / .4);
  border-radius: 50%;
  opacity: 0;
  animation: paddle-ripple 2.6s ease-out infinite;
}
.pr-a { top: -11px; }
.pr-b { top: 5px; animation-delay: 1.3s; }
@keyframes paddle-ripple {
  0%   { transform: translateX(0) scale(.3); opacity: 0; }
  12%  { opacity: .75; }
  100% { transform: translateX(calc(var(--bw) * -.35)) scale(1.8, 1.4); opacity: 0; }
}

/* moored at the lighthouse: the water settles */
.boat.moored .bow-wave,
.boat.moored .wake,
.boat.moored .ripple,
.boat.moored .paddle-ripple { opacity: 0; transition: opacity 1.4s ease; }

/* (the boat's position is driven entirely from JS — the journey state machine
   in app.js sets boat.style.transform for each leg. There is no CSS keyframe
   animating the boat, so nothing competes with the state machine.) */

@keyframes heave {
  from { transform: translateY(4px); }
  to   { transform: translateY(-9px); }
}

@keyframes roll {
  from { transform: rotate(-3.4deg); }
  to   { transform: rotate(3.4deg); }
}

@keyframes luff {
  from { transform: skewX(-2deg) scaleX(0.98); }
  to   { transform: skewX(2deg) scaleX(1.02); }
}

@keyframes flutter {
  from { transform: skewY(-5deg) scaleX(0.94); }
  to   { transform: skewY(5deg) scaleX(1.04); }
}

@keyframes wave {
  from { transform: rotate(-12deg); }
  to   { transform: rotate(14deg); }
}


/* (the lotus lanterns on the water are gone: their box stays, empty — the
   ride view hangs its glints after it) */
.water-lanterns {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: var(--sea-h);
  z-index: 3;
  pointer-events: none;
  overflow: hidden;
}

/* ---------- foreground ---------- */
/* Only the controls capture clicks, so lanterns stay reachable behind the copy. */
.shell {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  min-height: 100svh;
  pointer-events: none;
}

.shell a, .shell button, .shell input, .shell textarea { pointer-events: auto; }

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem clamp(1rem, 4vw, 2rem);
}

.brand {
  font-size: 1.65rem;
  font-weight: 700;
  text-decoration: none;
  background: var(--gradient-primary);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.lang-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.95rem;
  font: 600 0.935rem/1 var(--font);
  letter-spacing: 0.04em;
  color: hsl(var(--foreground));
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  cursor: pointer;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: var(--transition-smooth);
}

.lang-toggle svg { width: 16px; height: 16px; fill: none; stroke: hsl(var(--l-mid)); stroke-width: 1.6; }

.lang-toggle:hover { border-color: hsl(var(--l-mid) / .6); box-shadow: 0 0 24px hsl(var(--l-mid) / .25); }

.topnav {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
}

.site-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.95rem;
  font: 600 0.935rem/1 var(--font);
  letter-spacing: 0.04em;
  color: hsl(var(--foreground));
  text-decoration: none;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: var(--transition-smooth);
}

.site-link svg { width: 15px; height: 15px; fill: none; stroke: hsl(var(--l-mid)); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

.site-link:hover { border-color: hsl(var(--l-mid) / .6); box-shadow: 0 0 24px hsl(var(--l-mid) / .25); }

@media (max-width: 480px) {
  .site-link { padding: 0.5rem 0.7rem; font-size: 0.858rem; }
}

.lang-toggle:focus-visible,
.site-link:focus-visible,
.write-cta:focus-visible,
.reader-close:focus-visible,
.sheet-close:focus-visible,
button[type="submit"]:focus-visible {
  outline: 2px solid hsl(var(--l-hot));
  outline-offset: 3px;
}

/* ---------- headline ---------- */
.intro { padding: clamp(0.5rem, 3vh, 2rem) 1.25rem 0; text-align: center; }

.eyebrow {
  margin: 0 0 0.5rem;
  font-size: 0.792rem;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: hsl(var(--l-hot) / .9);
  text-shadow: 0 0 20px hsl(var(--l-deep) / .6);
}

.gradient-text {
  background: var(--gradient-primary);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

h1 {
  margin: 0;
  font-size: clamp(2.31rem, 7vw, 3.96rem);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.02em;
  /* the gradient clips a text-shadow, so cast the glow from behind */
  filter: drop-shadow(0 0 34px hsl(32 100% 50% / .4));
}

.lede {
  margin: 0.75rem auto 0;
  max-width: 34ch;
  font-size: 1.045rem;
  color: hsl(var(--muted-foreground));
  text-shadow: 0 1px 10px hsl(260 30% 4% / .8);
}

.count {
  margin: 0.5rem 0 0;
  min-height: 1.4em;
  font-size: 0.935rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: hsl(var(--l-mid));
}

.hint { margin: 0.15rem 0 0; font-size: 0.858rem; color: hsl(var(--muted-foreground) / .75); }

/* ---------- the call to action ---------- */
/* the call to action, centred just below the hint line */
.dock {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin: 0.9rem auto 0;
}

.toast {
  margin: 0;
  min-height: 1.3em;
  padding: 0 0.9rem;
  font-size: 0.902rem;
  font-weight: 500;
  text-align: center;
  color: hsl(var(--l-hot));
  text-shadow: 0 1px 12px hsl(260 30% 4% / .95);
  opacity: 0;
  transition: opacity 0.35s ease;
}
.toast.show { opacity: 1; }
.toast.error { color: hsl(var(--destructive)); }



/* ---------- the wish-caller: a figure on the shore by the lighthouse ---------- */
.wish-caller {
  position: fixed;
  /* lighthouse moved to the far RIGHT, so the wish-caller now stands on the
     shore at its base. Anchored from the right and mirrored to sit slightly
     inside the headland (mirroring the previous "on the left shore" position). */
  right: clamp(4rem, 16vw, 11rem);
  left: auto;
  bottom: calc(var(--sea-h) + 4px);
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  animation: caller-bob 5s ease-in-out infinite;
}

/* the soft-glowing speech bubble */
.wish-bubble {
  position: relative;
  padding: 0.32rem 0.7rem;
  font: 600 0.814rem/1 var(--font);
  white-space: nowrap;
  color: hsl(var(--l-hot));
  background: hsl(28 40% 10% / .66);
  border: 1px solid hsl(36 100% 62% / .35);
  border-radius: 999px;
  box-shadow: 0 0 14px hsl(36 100% 60% / .28), inset 0 0 10px hsl(38 100% 60% / .1);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: bubble-glow 2.8s ease-in-out infinite;
}

/* a little pointer down toward the person */
.wish-bubble::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  width: 7px;
  height: 7px;
  transform: translate(-50%, -4px) rotate(45deg);
  background: hsl(28 40% 10% / .66);
  border-right: 1px solid hsl(36 100% 62% / .35);
  border-bottom: 1px solid hsl(36 100% 62% / .35);
}

.wish-spark { filter: drop-shadow(0 0 4px hsl(44 100% 70%)); }

.wish-person { width: clamp(38px, 4.8vw, 56px); height: auto; display: block; }
.wp-skin { fill: hsl(30 58% 78%); }
.wp-hair { fill: hsl(20 42% 24%); }
.wp-coat { fill: hsl(14 62% 52%); }
.wp-arm  { fill: hsl(14 58% 46%); }
.wp-face { fill: hsl(22 40% 22%); }
.wp-cheek { fill: hsl(6 72% 74% / .55); }
.wp-smile { stroke: hsl(12 45% 34%); stroke-width: 1.4; stroke-linecap: round; }
.wp-string { stroke: hsl(30 30% 42%); stroke-width: 1; }
.wp-lantern {
  fill: hsl(44 100% 84%);
  filter: drop-shadow(0 0 5px hsl(40 100% 70%)) drop-shadow(0 0 12px hsl(30 100% 56% / .85));
}
/* a soft warm halo behind the friendly figure */
.wish-person-glow { fill: hsl(38 100% 62% / .16); filter: blur(7px); }
.wish-person { filter: drop-shadow(0 0 7px hsl(36 100% 58% / .3)); }

.wish-caller:hover .wish-bubble,
.wish-caller:focus-visible .wish-bubble {
  color: hsl(var(--l-core));
  border-color: hsl(var(--l-mid) / .7);
  box-shadow: 0 0 24px hsl(var(--l-mid) / .5), inset 0 0 12px hsl(38 100% 60% / .18);
}
.wish-caller:hover .wish-person-glow,
.wish-caller:focus-visible .wish-person-glow { fill: hsl(38 100% 64% / .45); }

.wish-caller:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 6px; border-radius: 12px; }

@keyframes caller-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}

/* ---- the WISH GUIDE's two anchors (JOURNEY › WISH_GUIDE in app.js) ----
   Home: the figure above, on the lighthouse shore — shown in the settled
   wide shot only. It never moves: while the camera frames a destination it
   simply steps out (.is-away), and returns once the camera is home. */
.wish-caller { transition: opacity .7s ease, visibility 0s; }
.wish-caller.is-away {
  opacity: 0 !important;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .35s ease, visibility 0s .35s;
}
/* Away: the SAME character projected onto the focused island, standing at
   its wishGuidePosition inside the island's focus layer (world-anchored;
   counter-scaled by --gs so it reads at the guide's own size). */
.island > .isle-focus { position: relative; }
.wish-guide-isle {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .3rem;
  min-width: 44px;                /* touch target */
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  pointer-events: none;
  transform: translateX(-50%) scale(var(--gs, 1));
  transform-origin: 50% 100%;
  opacity: 0;
  visibility: hidden;
  filter: blur(3px);
  transition: opacity .45s ease, filter .45s ease, visibility 0s .45s;
}
.wish-guide-isle.is-on {
  opacity: 1;
  visibility: visible;
  filter: none;
  pointer-events: auto;
  /* it gathers out of the island's light while the camera pushes in */
  transition: opacity 1.1s ease .7s, filter 1.1s ease .7s, visibility 0s;
}
.wish-guide-isle .wish-person { animation: caller-bob 5s ease-in-out infinite; }
/* a warm pool of light at its feet + a few rising motes: a projection,
   not a stranger — same figure, same bubble */
.wish-guide-isle::after {
  content: "";
  position: absolute;
  left: 50%; bottom: -6px;
  width: 140%; height: 22px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, hsl(42 100% 68% / .45), transparent);
  filter: blur(3px);
  pointer-events: none;
}
.wg-motes { position: absolute; inset: 20% 0 0; pointer-events: none; }
.wg-motes i {
  position: absolute;
  bottom: 10%;
  width: 3px; height: 3px;
  border-radius: 50%;
  background: hsl(46 100% 82%);
  box-shadow: 0 0 6px hsl(42 100% 66%);
  opacity: 0;
  animation: wg-mote 3.6s ease-out infinite;
}
.wg-motes i:nth-child(1) { left: 18%; animation-delay: .2s; }
.wg-motes i:nth-child(2) { left: 74%; animation-delay: 1.4s; }
.wg-motes i:nth-child(3) { left: 46%; animation-delay: 2.5s; }
@keyframes wg-mote {
  0%   { opacity: 0; transform: translateY(0); }
  25%  { opacity: .9; }
  100% { opacity: 0; transform: translateY(-34px); }
}
.wish-guide-isle:hover .wish-bubble,
.wish-guide-isle:focus-visible .wish-bubble {
  color: hsl(var(--l-core));
  border-color: hsl(var(--l-mid) / .7);
}
.wish-guide-isle:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 4px; border-radius: 12px; }
body.modal-open .wish-guide-isle { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .wish-guide-isle .wish-person, .wg-motes i { animation: none; }
  .wish-guide-isle, .wish-guide-isle.is-on, .wish-caller, .wish-caller.is-away { transition: none; }
}

@keyframes bubble-glow {
  0%, 100% { box-shadow: 0 0 12px hsl(36 100% 60% / .22), inset 0 0 10px hsl(38 100% 60% / .08); }
  50%      { box-shadow: 0 0 22px hsl(38 100% 62% / .45), inset 0 0 12px hsl(40 100% 62% / .18); }
}


/* ---------- countdown ---------- */
.countdown {
  margin: 0.75rem auto 0;
  text-align: center;
}
.countdown.done { display: none; }

.cd-lead {
  margin: 0 0 0.35rem;
  font-size: 0.748rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: hsl(var(--l-hot) / .8);
}

.cd-row {
  display: inline-flex;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.5rem 0.9rem;
  border-radius: 14px;
  background: hsl(28 40% 10% / .4);
  border: 1px solid hsl(36 100% 62% / .2);
  box-shadow: 0 0 22px hsl(36 100% 60% / .12);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.cd-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  min-width: 2.4ch;
}

.cd-num {
  font-size: 1.43rem;
  font-weight: 800;
  line-height: 1;
  color: hsl(46 100% 90%);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 16px hsl(36 100% 60% / .5);
}

.cd-lbl {
  font-size: 0.638rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground) / .8);
}

.cd-sep {
  font-size: 1.32rem;
  font-weight: 700;
  line-height: 1.1;
  color: hsl(var(--l-mid) / .55);
}

/* ---------- fireworks ---------- */
#fireworks {
  position: fixed;
  inset: 0;
  z-index: 14;         /* over the scene, under the controls (15) and dialogs (20) */
  width: 100%;
  height: 100%;
  display: none;
  pointer-events: none;
}
#fireworks.on { display: block; }

/* the big birthday banner, sitting just under the countdown during the party */
.party-banner {
  position: fixed;
  left: 50%;
  top: 30vh;                 /* JS sets the real top, just below the countdown */
  z-index: 16;               /* above the fireworks (14), below dialogs (20) */
  transform: translateX(-50%);
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity .5s ease;
}
.party-banner.show { opacity: 1; }

.party-text {
  display: inline-block;
  font: 900 clamp(1.76rem, 6vw, 3.41rem)/1.12 var(--font);
  letter-spacing: -0.01em;
  background: linear-gradient(100deg,
    hsl(46 100% 90%), hsl(38 100% 68%), hsl(22 92% 56%),
    hsl(42 100% 74%), hsl(46 100% 90%));
  background-size: 250% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation:
    party-pop .7s cubic-bezier(.2, 1.5, .4, 1) both,
    party-shimmer 4.5s linear .7s infinite,
    party-glow 2.4s ease-in-out .7s infinite alternate,
    party-float 3.2s ease-in-out .7s infinite alternate;
}

@keyframes party-pop {
  from { transform: scale(.8); }
  to   { transform: scale(1); }
}
@keyframes party-shimmer {
  from { background-position: 0% center; }
  to   { background-position: 250% center; }
}
@keyframes party-glow {
  from { filter: drop-shadow(0 0 14px hsl(36 100% 60% / .4)); }
  to   { filter: drop-shadow(0 0 40px hsl(42 100% 66% / .85)); }
}
@keyframes party-float {
  from { transform: translateY(0); }
  to   { transform: translateY(-6px); }
}

@media (max-width: 480px) {
  .cd-num { font-size: 1.21rem; }
  .cd-row { gap: 0.35rem; padding: 0.45rem 0.7rem; }
}

/* ---------- dialogs ---------- */
/* `display` below would otherwise defeat the [hidden] attribute. */
.reader[hidden], .sheet[hidden] { display: none; }

.reader, .sheet {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: hsl(260 30% 2% / .82);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}

.sheet-card {
  position: relative;
  max-width: 27rem;
  width: 100%;
  padding: 1.6rem 1.5rem 1.25rem;
  background: linear-gradient(180deg, hsl(28 40% 12% / .96), hsl(265 30% 8% / .96));
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card), 0 0 90px hsl(var(--l-deep) / .22);
}

/* ---------- the wish, read off a lit lantern ---------- */
.lantern-dialog {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.3rem;
  width: 100%;
  max-width: 17rem;
}
.lantern-dialog:focus { outline: none; }

.lantern-shape {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 21rem;
  max-height: 74vh;
  padding: 3.4rem 1.6rem;
  text-align: center;
  /* same proportions and profile as the lanterns flying in the sky */
  border-radius: 46% 46% 40% 40% / 24% 24% 50% 50%;
  background:
    linear-gradient(90deg, hsl(24 82% 50% / .38) 0%, transparent 17%,
                    transparent 83%, hsl(24 82% 50% / .38) 100%),
    radial-gradient(78% 55% at 50% 96%, hsl(46 100% 92%) 0%, hsl(41 97% 83%) 32%,
                    hsl(33 93% 73%) 68%, hsl(26 88% 64%) 100%);
  box-shadow:
    0 0 60px hsl(var(--l-mid) / .5),
    0 0 150px hsl(var(--l-deep) / .32),
    inset 0 -20px 44px hsl(24 82% 52% / .45);
  animation: lantern-breathe 5.5s ease-in-out infinite alternate;
}

/* the paper ribs */
.lantern-shape::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: repeating-linear-gradient(90deg, transparent 0 15px, hsl(26 68% 42% / .09) 15px 16px);
  pointer-events: none;
}

@keyframes lantern-breathe {
  from { box-shadow: 0 0 54px hsl(var(--l-mid) / .42), 0 0 130px hsl(var(--l-deep) / .26), inset 0 -20px 44px hsl(24 82% 52% / .45); }
  to   { box-shadow: 0 0 74px hsl(var(--l-mid) / .62), 0 0 180px hsl(var(--l-deep) / .4), inset 0 -20px 44px hsl(24 82% 52% / .45); }
}

.lc-cord {
  position: absolute;
  left: 50%;
  top: -26px;
  width: 2px;
  height: 26px;
  transform: translateX(-50%);
  background: linear-gradient(180deg, transparent, hsl(30 40% 40%));
}

.lc-cap {
  position: absolute;
  left: 34%;
  right: 34%;
  top: -5px;
  height: 12px;
  border-radius: 6px;
  background: linear-gradient(180deg, hsl(28 35% 26%), hsl(24 45% 38%));
  box-shadow: 0 2px 10px hsl(20 60% 10% / .5);
}

.lc-base {
  position: absolute;
  left: 37%;
  right: 37%;
  bottom: -4px;
  height: 10px;
  border-radius: 5px;
  background: linear-gradient(180deg, hsl(24 45% 34%), hsl(22 40% 22%));
}

.lc-flame {
  position: absolute;
  left: 50%;
  bottom: -17px;
  width: 20px;
  height: 24px;
  transform: translateX(-50%);
  border-radius: 50% 50% 46% 46% / 62% 62% 38% 38%;
  background: radial-gradient(circle at 50% 72%, hsl(48 100% 96%) 0%, hsl(42 100% 78%) 40%, hsl(28 96% 58% / .55) 74%, transparent 100%);
  filter: blur(1px);
  animation: flame 1.6s ease-in-out infinite alternate;
}

@keyframes flame {
  from { transform: translateX(-50%) scale(0.88) translateY(1px); opacity: 0.85; }
  to   { transform: translateX(-50%) scale(1.1) translateY(-1px); opacity: 1; }
}

.lc-paper {
  position: relative;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
}

.sheet-card h2 {
  margin: 0 0 1.1rem;
  font-size: 1.54rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  text-align: center;
}

.field-row { position: relative; margin-bottom: 1rem; }

.label {
  display: block;
  margin-bottom: 0.4rem;
  font-size: 0.836rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: hsl(var(--muted-foreground) / .85);
}

.optional { text-transform: none; letter-spacing: 0; font-weight: 400; opacity: 0.72; }

textarea, input[type="text"], input[type="tel"], input[type="url"] {
  width: 100%;
  padding: 0.7rem 0.85rem;
  font: 400 1.045rem/1.5 var(--font);
  color: hsl(var(--foreground));
  background: hsl(var(--input) / .85);
  border: 1px solid hsl(var(--border) / .8);
  border-radius: 12px;
  resize: none;
  transition: var(--transition-smooth);
}

textarea { min-height: 5rem; padding-bottom: 1.4rem; }

textarea::placeholder, input::placeholder { color: hsl(var(--muted-foreground) / .55); }

textarea:focus, input:focus {
  outline: none;
  border-color: hsl(var(--l-mid) / .75);
  box-shadow: 0 0 0 3px hsl(var(--l-mid) / .16);
}

.counter {
  position: absolute;
  right: 0.6rem;
  bottom: 0.45rem;
  font-size: 0.748rem;
  color: hsl(var(--muted-foreground) / .65);
  pointer-events: none;
}

button[type="submit"] {
  width: 100%;
  padding: 0.8rem 1.3rem;
  font: 700 1.045rem/1 var(--font);
  color: hsl(20 60% 12%);
  background: var(--gradient-primary);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 0 26px hsl(var(--l-mid) / .35);
  transition: var(--transition-smooth);
}

button[type="submit"]:hover { transform: translateY(-1px); box-shadow: 0 0 42px hsl(var(--l-hot) / .55); }
button[type="submit"]:active { transform: translateY(0); }
button[type="submit"]:disabled { opacity: 0.55; cursor: default; transform: none; box-shadow: none; }

.status {
  margin: 0.7rem 0 0;
  min-height: 1.2em;
  font-size: 0.88rem;
  text-align: center;
  color: hsl(var(--muted-foreground));
}
.status.error { color: hsl(var(--destructive)); }
.status.ok { color: hsl(var(--l-hot)); }

.reader-msg {
  margin: 0;
  font-size: 1.265rem;
  line-height: 1.55;
  color: hsl(20 68% 17%);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.reader-from {
  margin: 1rem 0 0;
  font-size: 0.935rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: hsl(18 80% 32%);
}

.reader-amount {
  margin: 0.5rem 0 0;
  display: inline-block;
  padding: 0.28rem 0.7rem;
  font-size: 0.99rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: hsl(22 90% 30%);
  background: hsl(42 100% 62% / .22);
  border: 1px solid hsl(36 100% 50% / .38);
  border-radius: 999px;
}

.reader-time {
  margin: 0.35rem 0 0;
  font-size: 0.792rem;
  letter-spacing: 0.04em;
  color: hsl(20 40% 40%);
}

.reader-close, .sheet-close {
  padding: 0.5rem 1.5rem;
  font: 600 0.902rem/1 var(--font);
  color: hsl(var(--muted-foreground));
  background: transparent;
  border: 1px solid hsl(var(--border));
  border-radius: 999px;
  cursor: pointer;
  transition: var(--transition-smooth);
}

.reader-close { margin-top: 0; }
.sheet-close { display: block; margin: 0.9rem auto 0; }

.reader-close:hover, .sheet-close:hover {
  color: hsl(var(--foreground));
  border-color: hsl(var(--l-mid) / .6);
}

/* ---------- narrow screens ---------- */
@media (max-width: 640px) {
  :root {
    --sea-h: clamp(128px, 22vh, 190px);
    --boat-scale: 0.72;
    --lh-w: clamp(190px, 48vw, 260px);
    --lh-left: -0.75rem;
  }
}

/* ---------- reduced motion: everything holds still ---------- */
@media (prefers-reduced-motion: reduce) {
  .lantern { animation: none; transform: translateY(var(--y)); }
  .lantern-sway,
  .reflect-lh, .pane, .lamp-halo,
  .boat, .boat-heave, .boat-roll, .flag, .arm, .wake, .wish-light,
  .crew-move, .lantern-shape, .lc-flame, .boat-hint, .lantern .trail,
  .wish-light, .wish-halo, .sky-launch, .water-lantern, .wl-reflect,
  .bow-wave, .ripple, .paddle-ripple, .wake::before { animation: none; }

  /* the boat is placed at its final reached island from JS (inline transform);
     the lighthouse keeps its static silhouette transform. */

  * { transition-duration: 0.01ms !important; }
}

/* Background-music YouTube player: a valid, real-sized embed kept offscreen
   (not display:none, which can block playback on mobile). Audio only — the
   user drives playback from #music-toggle. */
#yt-music {
  position: fixed;
  left: -9999px;
  top: 0;
  width: 200px;
  height: 200px;
  opacity: 0;
  pointer-events: none;
}

/* ---------- floating controls, mirroring the main site's player ---------- */
.controls {
  position: fixed;
  right: clamp(0.7rem, 2.5vw, 1.4rem);
  bottom: clamp(0.7rem, 2.5vw, 1.4rem);
  z-index: 15;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.ctrl {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.3rem;
  height: 2.3rem;
  padding: 0;
  border-radius: 10px;
  cursor: pointer;
  background: hsl(28 40% 8% / .45);
  border: 1px solid hsl(36 100% 62% / .16);
  box-shadow: none;
  opacity: 0.5;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: var(--transition-smooth);
}

.ctrl:hover, .ctrl:focus-visible {
  opacity: 1;
  border-color: hsl(var(--l-mid) / .5);
  background: hsl(28 40% 10% / .75);
}

/* the active track should still read at a glance */
#music-toggle.playing { opacity: 0.85; }

.ctrl:focus-visible {
  outline: 2px solid hsl(var(--l-hot));
  outline-offset: 3px;
}

.ctrl svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: hsl(var(--l-hot));
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* the play triangle and the muted speaker body read better filled */
.ctrl .ico-play path,
.ctrl .ico-sound-on path:first-child,
.ctrl .ico-sound-off path:first-child { fill: hsl(var(--l-hot)); }

/* one icon at a time */
.ico-pause, .ico-sound-off { display: none; }
#music-toggle.playing .ico-play { display: none; }
#music-toggle.playing .ico-pause { display: block; }
#sound-toggle.muted .ico-sound-on { display: none; }
#sound-toggle.muted .ico-sound-off { display: block; }
/* pressed, the music still starting: the play icon breathes until it sounds */
#music-toggle.is-loading .ico-play { animation: music-wait 1.1s ease-in-out infinite; }
@keyframes music-wait { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) {
  #music-toggle.is-loading .ico-play { animation: none; opacity: .55; }
}

/* keep the controls clear of the wish button on small screens */
@media (max-width: 640px) {
  .ctrl { width: 2.1rem; height: 2.1rem; }
  .ctrl svg { width: 15px; height: 15px; }
}

@media (prefers-reduced-motion: reduce) {
  .ctrl:hover { transform: none; }
}

/* ---------- boat hint ---------- */
.boat-hint {
  position: fixed;
  left: 0;
  top: 0;
  margin: 0;
  padding: 0.4rem 0.85rem;
  transform: translate(-50%, -100%);
  white-space: nowrap;
  font-size: 0.858rem;
  font-weight: 600;
  color: hsl(var(--l-hot));
  background: hsl(28 40% 8% / .78);
  border: 1px solid hsl(36 100% 62% / .28);
  border-radius: 999px;
  box-shadow: 0 6px 20px -8px hsl(24 80% 20% / .8);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
  z-index: 6;
  opacity: 0;
  animation: hint-breathe 3.4s ease-in-out 1.2s infinite;
}

/* a small pointer down toward the boat */
.boat-hint::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  width: 8px;
  height: 8px;
  transform: translateX(-50%) translateY(-5px) rotate(45deg);
  background: hsl(28 40% 8% / .72);
  border-right: 1px solid hsl(36 100% 62% / .28);
  border-bottom: 1px solid hsl(36 100% 62% / .28);
}

/* fades out for good once the boat has been opened */
.boat-lane.explored .boat-hint { animation: none; opacity: 0; }

@keyframes hint-breathe {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}

/* ---------- the wishes the boat is carrying ---------- */
/* ---------- topbar "Hướng dẫn" pill ---------- */
.guide-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.95rem;
  font: 600 0.902rem/1 var(--font);
  letter-spacing: 0.02em;
  color: hsl(var(--foreground));
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  cursor: pointer;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: var(--transition-smooth);
}
.guide-toggle svg {
  width: 16px; height: 16px;
  fill: none; stroke: hsl(var(--l-hot));
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.guide-toggle:hover {
  border-color: hsl(var(--l-hot) / .55);
  box-shadow: 0 0 22px hsl(var(--l-hot) / .28);
}
.guide-toggle:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }
@media (max-width: 640px) {
  /* On narrow screens: keep only the icon + spark so the pill doesn't push
     the language toggle off-screen. The full label stays in the aria-label. */
  .guide-toggle span { display: none; }
  .guide-toggle { padding: 0.5rem 0.6rem; }
}

/* ---------- How-it-works guide: a swipeable little story ----------
   Horizontal scroll-snap track, one "scene" per step. Ambient motion is
   declared only under .gd-slide.is-active, so exactly one small SVG scene
   animates at a time, using transform/opacity only (no filters, no blur). */
.guide[hidden] { display: none; }
.guide {
  position: fixed;
  inset: 0;
  z-index: 22; /* above logbook (20) — opened from topbar, sits on top */
  display: grid;
  place-items: center;
  padding: max(0.75rem, env(safe-area-inset-top)) 0.75rem max(0.75rem, env(safe-area-inset-bottom));
  background: hsl(260 30% 2% / .82);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}
.gd-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.guide-card {
  --gd-progress: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 27rem;
  height: min(100%, 44rem);
  overflow: hidden;
  background:
    radial-gradient(120% 60% at 50% 0%, hsl(36 100% 60% / .10), transparent 60%),
    linear-gradient(180deg, hsl(252 38% 10%), hsl(236 40% 12%) 55%, hsl(228 44% 15%));
  border: 1px solid var(--glass-border);
  border-radius: 24px;
  box-shadow: var(--shadow-card), 0 0 80px hsl(var(--l-deep) / .2);
  outline: none;
}

.gd-top {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 0.6rem 0.6rem 0.2rem 1.1rem;
}
.gd-count {
  font: 600 0.88rem/1 var(--font);
  letter-spacing: 0.08em;
  color: hsl(var(--muted-foreground));
}
.gd-count b { color: hsl(var(--l-hot)); font-weight: 800; }
.gd-title {
  margin: 0;
  font: 600 0.88rem/1 var(--font);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: hsl(var(--l-hot) / .75);
  white-space: nowrap;
}
.gd-x {
  justify-self: end;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  font-size: 1.1rem;
  color: hsl(var(--foreground) / .75);
  background: transparent;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}
.gd-x:hover, .gd-x:focus-visible { color: hsl(var(--l-hot)); background: hsl(var(--l-hot) / .08); outline: none; }

.gd-track {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.gd-track::-webkit-scrollbar { display: none; }
.gd-slide {
  flex: 0 0 100%;
  min-width: 0;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  padding: 0.35rem 1.1rem 0.5rem;
  overflow-y: auto;
  scrollbar-width: none;
}
.gd-slide::-webkit-scrollbar { display: none; }

.gd-stage {
  flex: 0 0 auto;
  height: clamp(140px, 30dvh, 230px);
  border-radius: 18px;
  overflow: hidden;
  cursor: pointer;
  background:
    radial-gradient(90% 70% at 50% 110%, hsl(222 55% 30% / .7), transparent 70%),
    linear-gradient(180deg, hsl(250 40% 9%), hsl(236 42% 15%) 70%, hsl(226 46% 20%));
  box-shadow: inset 0 0 0 1px hsl(var(--l-hot) / .10);
  -webkit-tap-highlight-color: transparent;
}
.guide-card.is-last .gd-slide.is-active .gd-stage { cursor: default; }
.gd-dense .gd-stage { height: clamp(112px, 21dvh, 170px); }
.gd-dense { gap: 0.7rem; }
.gd-stage svg { display: block; width: 100%; height: 100%; }
.gd-stars circle { fill: #fff6dc; opacity: .7; }

.gd-copy { text-align: center; }
.gd-copy h3 {
  margin: 0 0 0.55rem;
  font: 800 1.485rem/1.2 var(--font);
}
.gd-copy p {
  margin: 0 auto 0.45rem;
  max-width: 22rem;
  font: 500 1.078rem/1.5 var(--font);
  color: hsl(var(--foreground) / .94);
}
.gd-copy .gd-soft {
  font-size: 0.968rem;
  font-weight: 400;
  color: hsl(var(--muted-foreground));
}
/* a note worth stopping for (the QR step: a transfer shown before yours is someone else's) */
.gd-copy .gd-note {
  margin-top: 0.7rem;
  padding: 0.55rem 0.8rem;
  font-size: 0.924rem;
  font-weight: 500;
  line-height: 1.45;
  color: hsl(var(--l-core) / .92);
  background: hsl(36 100% 60% / .08);
  border: 1px dashed hsl(var(--l-hot) / .4);
  border-radius: 12px;
}
.gd-chips {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.7rem 0;
}
.gd-chip, .gd-fields li {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 0.8rem;
  font: 600 0.924rem/1.2 var(--font);
  color: hsl(var(--l-core));
  background: hsl(36 100% 60% / .08);
  border: 1px solid hsl(var(--l-hot) / .28);
  border-radius: 999px;
}
.gd-fields {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
  margin: 0.7rem 0;
  padding: 0;
}
.gd-fields li { font-size: 0.88rem; padding: 0.38rem 0.7rem; }
.gd-copy .gd-lead {
  margin-top: 0.55rem;
  margin-bottom: 0.3rem;
  font-size: 0.99rem;
}
/* the explorer's switch, step by step: each row is the button as it looks on
   the page (a pill: amber to go, glass to pause) and what it does */
.gd-steps {
  list-style: none;
  display: grid;
  grid-template-columns: max-content 1fr;   /* one column of buttons, one of words */
  align-items: center;
  gap: 0.45rem 0.6rem;
  max-width: 22rem;
  margin: 0.4rem auto 0;
  padding: 0;
  text-align: left;
}
.gd-steps li { display: contents; }
.gd-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  padding: 0.42rem 0.75rem;
  border-radius: 999px;
  white-space: nowrap;
  font: 600 0.858rem/1.2 var(--font);
  color: hsl(40 70% 88%);
  background: hsl(250 34% 10% / .55);
  border: 1px solid hsl(36 100% 70% / .22);
}
.gd-btn.is-go {
  color: hsl(30 60% 12%);
  background: linear-gradient(180deg, hsl(40 100% 74%), hsl(32 95% 60%));
  border-color: hsl(40 100% 82% / .7);
}
.gd-step-t { font: 400 0.924rem/1.4 var(--font); color: hsl(var(--muted-foreground)); }
@media (max-width: 380px) {
  .gd-btn { font-size: 0.814rem; padding: 0.38rem 0.6rem; }
  .gd-step-t { font-size: 0.88rem; }
}
.gd-fields .gd-opt { color: hsl(var(--muted-foreground)); border-style: dashed; }

/* constellation progress: seven dots joined by a line that fills as you go */
.gd-path {
  position: relative;
  display: flex;
  justify-content: space-between;
  margin: 0.2rem 1rem 0;
}
.gd-path-line {
  position: absolute;
  left: 21px;
  right: 21px;
  top: 50%;
  height: 1px;
  background: hsl(var(--l-hot) / .18);
}
.gd-path-fill {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, hsl(var(--l-hot) / .5), hsl(var(--l-hot)));
  transform: scaleX(var(--gd-progress));
  transform-origin: left center;
  transition: transform .5s cubic-bezier(.4, 0, .2, 1);
}
.gd-dot {
  position: relative;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.gd-dot span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: hsl(250 30% 22%);
  box-shadow: 0 0 0 1px hsl(var(--l-hot) / .35);
  transition: transform .3s ease, background-color .3s ease, box-shadow .3s ease;
}
.gd-dot.is-lit span { background: hsl(var(--l-hot)); box-shadow: 0 0 8px hsl(var(--l-mid) / .7); }
.gd-dot[aria-current="step"] span { transform: scale(1.6); background: hsl(var(--l-core)); }
.gd-dot:focus-visible { outline: none; }
.gd-dot:focus-visible span { box-shadow: 0 0 0 3px hsl(var(--l-hot) / .6); }

.gd-nav {
  display: flex;
  gap: 0.7rem;
  padding: 0.5rem 1.1rem 1.1rem;
}
.gd-prev, .gd-next {
  height: 54px;
  border-radius: 999px;
  cursor: pointer;
  font: 700 1.1rem/1 var(--font);
  -webkit-tap-highlight-color: transparent;
  transition: transform .15s ease, opacity .2s ease, box-shadow .2s ease;
}
.gd-prev {
  flex: 0 0 54px;
  color: hsl(var(--l-hot));
  background: transparent;
  border: 1px solid hsl(var(--l-hot) / .4);
  font-size: 1.32rem;
}
.gd-prev:disabled { opacity: .25; cursor: default; }
.gd-next {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  color: hsl(24 60% 12%);
  background: var(--gradient-primary);
  border: 0;
  box-shadow: 0 0 24px hsl(var(--l-mid) / .35);
}
.gd-prev:active:not(:disabled), .gd-next:active { transform: scale(.97); }
.gd-prev:focus-visible, .gd-next:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }
.gd-start-t { display: none; }
.guide-card.is-last .gd-next-t { display: none; }
.guide-card.is-last .gd-start-t { display: inline; }

/* ---- scene entrance + ambient motion (active scene only) ---- */
.gd-float, .gd-ring, .gd-rock, .gd-key, .gd-blink, .gd-rise, .gd-born {
  transform-box: fill-box;
  transform-origin: center;
}
.gd-rock { transform-origin: 50% 100%; }
.gd-key { transform-origin: 12% 50%; }
.gd-ring { opacity: 0; }
.gd-born, .gd-rise { opacity: 0; }
.gd-travel { stroke-dasharray: 10 100; stroke-dashoffset: 10; opacity: 0; }

.gd-slide.is-active .gd-stage svg { animation: gd-stage-in .6s cubic-bezier(.2, .7, .2, 1) both; }
.gd-slide.is-active .gd-copy > * { animation: gd-in .5s cubic-bezier(.2, .7, .2, 1) both; }
.gd-slide.is-active .gd-copy > :nth-child(2) { animation-delay: .06s; }
.gd-slide.is-active .gd-copy > :nth-child(3) { animation-delay: .12s; }
.gd-slide.is-active .gd-copy > :nth-child(4) { animation-delay: .18s; }

.gd-slide.is-active .gd-float { animation: gd-float 3.6s ease-in-out infinite alternate; }
.gd-slide.is-active .gd-float.d2 { animation-duration: 4.2s; animation-delay: -1.4s; }
.gd-slide.is-active .gd-float.d3 { animation-duration: 3.9s; animation-delay: -2.6s; }
.gd-slide.is-active .gd-ring { animation: gd-ring 2.4s ease-out infinite; }
.gd-slide.is-active .gd-ring.d2 { animation-delay: 1.2s; }
.gd-slide.is-active .gd-rock { animation: gd-rock 3.2s ease-in-out infinite alternate; }
.gd-slide.is-active .gd-tw { animation: gd-tw 2.4s ease-in-out infinite alternate; }
.gd-slide.is-active .gd-tw.d2 { animation-delay: -0.8s; }
.gd-slide.is-active .gd-tw.d3 { animation-delay: -1.6s; }
.gd-slide.is-active .gd-travel { animation: gd-travel 3.2s ease-in-out infinite; }
.gd-slide.is-active .gd-born { animation: gd-born 3.2s ease-out infinite; }
.gd-slide.is-active .gd-key { animation: gd-key 2.6s ease-in-out infinite alternate; }
.gd-slide.is-active .gd-blink { animation: gd-blink 3.6s ease-in-out infinite; }
.gd-slide.is-active .gd-rise { animation: gd-rise 7s ease-in-out infinite; }
.gd-slide.is-active .gd-rise.d2 { animation-delay: -3.5s; }

@keyframes gd-stage-in { from { opacity: .3; transform: scale(.96); } to { opacity: 1; transform: none; } }
@keyframes gd-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes gd-float { from { transform: translateY(3px); } to { transform: translateY(-5px); } }
@keyframes gd-ring { 0% { opacity: .8; transform: scale(.7); } 100% { opacity: 0; transform: scale(1.7); } }
@keyframes gd-rock { from { transform: rotate(-2.5deg) translateY(0); } to { transform: rotate(2.5deg) translateY(-2px); } }
@keyframes gd-tw { from { opacity: .25; } to { opacity: 1; } }
@keyframes gd-travel {
  0%   { stroke-dashoffset: 10; opacity: 0; }
  10%  { opacity: 1; }
  60%  { stroke-dashoffset: -100; opacity: 1; }
  61%, 100% { stroke-dashoffset: -100; opacity: 0; }
}
@keyframes gd-born {
  0%, 55% { opacity: 0; transform: translateY(14px) scale(.5); }
  72%     { opacity: 1; transform: translateY(0) scale(1); }
  100%    { opacity: 1; transform: translateY(-6px) scale(1); }
}
@keyframes gd-key { from { transform: rotate(-14deg); } to { transform: rotate(10deg); } }
@keyframes gd-blink { 0%, 86%, 100% { transform: scaleY(1); } 92% { transform: scaleY(.1); } }
@keyframes gd-rise {
  0%   { opacity: 0; transform: translate(0, 18px) scale(.7); }
  12%  { opacity: 1; }
  80%  { opacity: .9; }
  100% { opacity: 0; transform: translate(-150px, -90px) scale(.35); }
}

@media (prefers-reduced-motion: reduce) {
  .guide *, .guide *::before, .guide *::after { animation: none !important; transition: none !important; }
  .gd-born, .gd-rise { opacity: 1; }
}

.logbook[hidden] { display: none; }

.logbook {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: hsl(260 30% 2% / .82);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}

.logbook-card {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 30rem;
  height: min(88dvh, 46rem);
  padding: 1.4rem 1.25rem 1.1rem;
  background: linear-gradient(180deg, hsl(28 40% 12% / .96), hsl(265 30% 8% / .96));
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card), 0 0 90px hsl(var(--l-deep) / .22);
}

.logbook-card h2 {
  margin: 0;
  font-size: 1.54rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  text-align: center;
}

.logbook-sub {
  margin: 0.3rem 0 1.1rem;
  font-size: 0.902rem;
  text-align: center;
  color: hsl(var(--muted-foreground) / .85);
}

/* --- Wishes / Top Donors tab bar inside the logbook --- */
.logbook-tabs {
  display: flex;
  justify-content: center;
  gap: 0.35rem;
  margin: 0.4rem auto 0.2rem;
  padding: 0.25rem;
  background: hsl(var(--card) / .35);
  border: 1px solid hsl(var(--l-hot) / .18);
  border-radius: 999px;
  width: max-content;
  max-width: 100%;
}
.logbook-tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: hsl(var(--muted-foreground) / .85);
  font: inherit;
  font-size: 0.902rem;
  letter-spacing: 0.02em;
  padding: 0.4rem 0.95rem;
  border-radius: 999px;
  cursor: pointer;
  transition: background .18s ease, color .18s ease, box-shadow .18s ease;
}
.logbook-tab:hover { color: hsl(var(--l-hot) / .95); }
.logbook-tab:focus-visible {
  outline: 2px solid hsl(var(--l-hot));
  outline-offset: 2px;
}
.logbook-tab.is-active {
  background: linear-gradient(180deg, hsl(var(--l-hot) / .28), hsl(var(--l-hot) / .12));
  color: hsl(var(--l-hot));
  box-shadow: 0 0 14px hsl(var(--l-hot) / .35), inset 0 0 0 1px hsl(var(--l-hot) / .45);
}

/* --- Top Donors list — subtle, warm, matches the lantern feel --- */
.top-donors {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0.6rem 0.25rem 0.4rem;
  margin: 0.4rem 0 0;
}
.top-donors[hidden] { display: none; }
.top-donors-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: end;
  column-gap: .5rem;
}
/* ranks 4–10: a quiet, lantern-lit list under the podium */
.top-donor {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: .85rem;
  min-height: 48px;
  padding: .55rem .6rem;
  color: hsl(var(--foreground) / .92);
  border-bottom: 1px solid hsl(var(--l-hot) / .08);
}
.top-donor:last-child { border-bottom: 0; }
.top-donor-rank {
  flex: 0 0 auto;
  min-width: 1.8rem;
  text-align: center;
  font-size: 0.99rem;
  font-weight: 700;
  color: hsl(var(--muted-foreground) / .8);
  font-variant-numeric: tabular-nums;
}
.top-donor-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: 1.056rem;
  letter-spacing: .02em;
  color: hsl(var(--l-hot) / .95);
}

/* the podium: three hanging lanterns, 2 · 1 · 3, first one tallest */
.top-donor-podium {
  grid-column: auto;
  grid-row: 1;
  flex-direction: column;
  justify-content: flex-end;
  gap: .35rem;
  min-height: 0;
  padding: .4rem .2rem 1.1rem;
  margin-bottom: .6rem;
  text-align: center;
  border-bottom: 0;
}
.top-donor-rank-1 { grid-column: 2; }
.top-donor-rank-2 { grid-column: 1; }
.top-donor-rank-3 { grid-column: 3; }
.top-donor-podium .top-donor-rank { min-width: 0; font-size: 1.265rem; line-height: 1; }
.top-donor-podium .top-donor-name {
  flex: 0 0 auto;
  font-size: 1.012rem;
  font-weight: 700;
  line-height: 1.25;
  text-shadow: 0 0 12px hsl(var(--l-hot) / .3);
}
.top-donor-rank-1 .top-donor-name { font-size: 1.122rem; color: hsl(var(--l-core)); }
.td-lamp {
  position: relative;
  width: 40px;
  height: 52px;
  margin-bottom: .25rem;
  border-radius: 46% 46% 40% 40% / 24% 24% 50% 50%;
  background: radial-gradient(56% 40% at 50% 84%, hsl(var(--l-core)) 0%, hsl(var(--l-hot)) 22%,
              hsl(var(--l-mid)) 48%, hsl(var(--l-deep)) 76%, hsl(var(--l-dark)) 100%);
  box-shadow: 0 0 22px hsl(var(--l-mid) / .5), 0 0 54px hsl(var(--l-deep) / .22);
}
/* a thread up to the night */
.td-lamp::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 1px;
  height: 18px;
  background: linear-gradient(transparent, hsl(var(--l-hot) / .45));
}
.top-donor-rank-1 .td-lamp { width: 52px; height: 68px; box-shadow: 0 0 30px hsl(var(--l-hot) / .7), 0 0 80px hsl(var(--l-mid) / .35); }
.top-donor-rank-1 .td-lamp::before { height: 8px; }
.top-donor-rank-3 .td-lamp { width: 36px; height: 47px; }
.top-donor-rank-2 .td-lamp, .top-donor-rank-3 .td-lamp { opacity: .9; }

.top-donors-empty {
  text-align: center;
  color: hsl(var(--muted-foreground) / .8);
  font-size: 0.935rem;
  padding: 1.4rem .5rem;
}
/* loading: a single dim lantern breathing while the list is fetched */
.top-donors-empty.is-loading::before {
  content: "";
  display: block;
  width: 30px;
  height: 39px;
  margin: 0 auto .8rem;
  border-radius: 46% 46% 40% 40% / 24% 24% 50% 50%;
  background: radial-gradient(56% 40% at 50% 84%, hsl(var(--l-hot)) 0%, hsl(var(--l-mid)) 50%, hsl(var(--l-deep)) 100%);
  animation: td-breathe 1.6s ease-in-out infinite alternate;
}
@keyframes td-breathe { from { opacity: .35; } to { opacity: .9; } }
@media (prefers-reduced-motion: reduce) { .top-donors-empty.is-loading::before { animation: none; opacity: .7; } }

/* a wall of lanterns, one per wish, names hidden until opened */
.logbook-close { flex: 0 0 auto; }

/* ---------- wish explorer: search + filter + virtual list ---------- */
.wl {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin-top: 0.75rem;
}
.wl[hidden], .logbook-sub[hidden] { display: none; }

.wl-search {
  position: relative;
  flex: 0 0 auto;
}
.wl-search-ico {
  position: absolute;
  left: 0.95rem;
  top: 50%;
  width: 18px;
  height: 18px;
  transform: translateY(-50%);
  color: hsl(var(--l-hot) / .75);
  pointer-events: none;
}
.wl-search input {
  width: 100%;
  height: 48px;
  padding: 0 2.9rem 0 2.75rem;
  font: 500 1.1rem/1 var(--font);   /* ≥16px so iOS doesn't zoom on focus */
  color: hsl(var(--foreground));
  background: hsl(250 32% 8% / .7);
  border: 1px solid hsl(var(--l-hot) / .25);
  border-radius: 999px;
  outline: none;
  -webkit-appearance: none;
  appearance: none;
  transition: border-color .2s ease, box-shadow .2s ease;
}
.wl-search input::placeholder { color: hsl(var(--muted-foreground) / .8); }
.wl-search input::-webkit-search-cancel-button { display: none; }
.wl-search input:focus {
  border-color: hsl(var(--l-hot) / .65);
  box-shadow: 0 0 0 3px hsl(var(--l-mid) / .18), 0 0 18px hsl(var(--l-mid) / .15);
}
.wl-clear {
  position: absolute;
  right: 2px;
  top: 50%;
  width: 44px;
  height: 44px;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  font-size: 0.99rem;
  color: hsl(var(--muted-foreground));
  background: transparent;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}
.wl-clear[hidden] { display: none; }
.wl-clear:hover, .wl-clear:focus-visible { color: hsl(var(--l-hot)); outline: none; }

.wl-filters {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr 1.35fr;
  gap: 0.4rem;
  margin: 0.65rem 0 0;
}
.wl-chip {
  min-height: 40px;
  padding: 0 0.5rem;
  font: 600 0.924rem/1.1 var(--font);
  color: hsl(var(--foreground) / .8);
  background: hsl(var(--card) / .3);
  border: 1px solid hsl(var(--l-hot) / .18);
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  transition: background-color .2s ease, border-color .2s ease, color .2s ease;
}
.wl-chip.is-on {
  color: hsl(24 60% 12%);
  background: var(--gradient-primary);
  border-color: transparent;
  box-shadow: 0 0 14px hsl(var(--l-mid) / .3);
}
.wl-chip:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 2px; }

.wl-count {
  flex: 0 0 auto;
  margin: 0.7rem 0.2rem 0.35rem;
  font: 600 0.88rem/1.35 var(--font);
  letter-spacing: 0.02em;
  color: hsl(var(--l-hot) / .85);
}
.wl-partial {
  display: block;
  font-weight: 400;
  color: hsl(var(--muted-foreground) / .85);
}

/* ============================================================
   Milestone islands — the boat's fundraising journey
   Four islands sit far -> near along the voyage. Each is a dark
   silhouette until its milestone is reached, then it lights up
   with a warm glow. The 4th island is the lighthouse (above).
   ============================================================ */
.islands {
  position: absolute;
  inset: 0;
  /* NOT a stacking context: each island and the boat are depth-sorted in
     the scene (JS writes z-index 1000–2000 from their distance out to sea),
     so a boat sailing farther out than an island passes behind it */
  z-index: auto;
  pointer-events: none;
}

.island {
  position: absolute;
  left: var(--ix);
  /* WATERLINE anchor: --iy is a fraction of the OCEAN height (0 = shore, 1 =
     horizon). The bottom edge of the island is placed at that height above the
     viewport bottom, which is the same as .sea's bottom edge, so islands live
     inside the ocean's coordinate system even though .islands is a sibling
     overlay (not a child of .sea) — .scene is fixed to the viewport bottom
     too, so the two frames share their bottom edge. */
  bottom: calc(var(--sea-h) * var(--iy));
  /* Island width scales with the OCEAN, not the raw viewport. This is the fix
     for the mobile bug: on mobile --sea-h shrinks to ~180px, and a fixed 200px
     island would stand taller than the ocean and poke into the sky. Tying the
     island's width (and therefore its height, via the SVG's 240x90 viewBox)
     to --sea-h keeps islands proportional to their sea at every viewport.
     Desktop cap 240px preserves the existing composition; mobile floor 88px
     keeps them readable at 375–430px viewports. */
  width: calc(clamp(88px, var(--sea-h) * 0.6, 240px) * var(--iscale));
  transform: translateX(-50%);
  /* Horizon safety cap. The island SVG has aspect 240x90 (H = W * 0.375). The
     space between the waterline and the horizon is `sea-h * (1 - iy)`. If the
     scaled width would render taller than that, we cap the width so the top
     of the silhouette can NEVER cross the horizon into the sky — even if a
     future --iy is set close to 1.0 or the viewport shrinks unexpectedly. */
  max-width: calc((var(--sea-h) * (1 - var(--iy))) / 0.375);
}

.island-art {
  position: relative;
  width: 100%;
  /* farther (smaller) islands sit in a little more haze */
  filter: blur(calc((1 - var(--iscale)) * 1.7px));
}

/* atmospheric veil: farther islands (higher --idim) fade into the night,
   so distance reads as darker + less detail as well as smaller */
.island-art::after {
  content: "";
  position: absolute;
  left: -8%; right: -8%; top: -16%; bottom: -4%;
  background: radial-gradient(62% 82% at 50% 58%,
    hsl(224 46% 8% / calc(var(--idim, 0) * .72)), transparent 80%);
  pointer-events: none;
}

/* Arrival animation is driven by boat state (`.approaching`), not milestone
   state. The `.next` class only sets stacking order + subtle glow — no scale
   animation lives here, so an active island stays quiet in the sea until the
   boat is actually sailing toward it. */
.island.next {
  /* (stacking is by depth — see .islands) */
}
.island.next .island-art {
  filter: brightness(1.08) drop-shadow(0 0 14px hsl(42 100% 60% / .30));
}

.island .island-art {
  /* all three scale states (base / lit / approaching) transition through the
     same declaration — no `animation:` fights with the JS-driven class */
  transform-origin: 50% 100%;
  transform: scale(1);
  transition: transform 1.6s cubic-bezier(.4, 0, .2, 1),
              filter    1.6s ease,
              opacity   .9s ease;
}
.island.lit .island-art { transform: scale(1.06); }   /* STATE_K in app.js */
/* locked (not-yet-reached, not-active) islands recede: a little smaller + dimmer */
.island.locked:not(.focused):not(.lit) .island-art { transform: scale(.9); opacity: .82; }

/* ---- ISLAND FOCUS (the discovery layer) ----
   .isle-focus wraps the art INSIDE the island's position box: the journey
   grows it (Web Animations, transform only) as the boat closes in and
   settles it back as the boat leaves. Position, collision and the label
   never move. .focused = the current destination (glow), .arrived = the boat
   is moored beside it (lights up fully, shoreline + reflection strongest). */
.isle-focus { transform-origin: 50% 100%; }
.headland > .isle-focus { position: relative; transform-origin: 59.5% 100%; }
.island.focused .island-art {
  filter: brightness(1.22) drop-shadow(0 0 22px hsl(42 100% 60% / .45));
}
.island.arrived .island-art {
  filter: brightness(1.34) drop-shadow(0 0 32px hsl(42 100% 62% / .6));
}
.island.focused .isle-halo { opacity: .8; }
.island.arrived .isle-halo { opacity: 1; }
.island.arrived .isle-reflect { opacity: 1; transform: translateX(-50%) scaleY(1.25); transform-origin: 50% 0; }
.island.arrived .isle-rim { stroke: hsl(44 100% 78% / .95); }
.island.arrived .isle-window { fill: hsl(48 100% 82%); filter: drop-shadow(0 0 4px hsl(42 100% 66%)); }
.island.arrived .isle-flame { fill: hsl(48 100% 92%); filter: drop-shadow(0 0 7px hsl(42 100% 74%)) drop-shadow(0 0 18px hsl(34 100% 60%)); }
.headland.focused .lamp-halo { opacity: 1; }
.headland.arrived .reflect-lh { opacity: 1; }
.headland.arrived .headland-art { filter: drop-shadow(0 0 24px hsl(42 100% 62% / .35)); }

/* the destination sheds its distance haze: sharper, clearer, more detail */
.island-art::after { transition: opacity 1.4s ease; }
.island.focused .island-art::after { opacity: 0; }
.island.focused .isle-tree { opacity: 1; }
.island.focused .island-label,
.headland.arrived .island-label { opacity: 0 !important; transition: opacity .6s ease; }

/* ---- DISCOVERY: while the journey frames one destination (.scene.discover),
   everything else in the world steps back — darker, flatter, softly out of
   focus — so the destination is the only thing lit. Nothing is removed or
   moved; the camera (one transform on .scene) does the framing. */
.isle-focus { transition: opacity 1.4s ease, filter 1.4s ease; }
.scene.discover .island:not(.focused) .isle-focus,
.scene.discover .headland:not(.focused) > .isle-focus {
  opacity: .34;
  filter: brightness(.5) saturate(.55) blur(1.3px);
}
.scene.discover .island:not(.focused) .island-label { opacity: .1 !important; }
.water-lanterns, .moon, .moon-reflect { transition: opacity 1.4s ease; }
.scene.discover .water-lanterns { opacity: .55; }
.scene.discover .moon, .scene.discover .moon-reflect { opacity: .45; }
.scene.discover .sea-route { opacity: 0; }

/* a soft night vignette closes in around the framed destination (its
   centre is written once per arrival; only opacity animates) */
.cam-vignette {
  position: fixed;
  inset: 0;
  z-index: 3;            /* over the world (2), under the title + UI (6+) */
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.6s ease;
  background: radial-gradient(ellipse 58% 62% at var(--vx, 50%) var(--vy, 70%),
    transparent 38%, hsl(232 60% 4% / .38) 72%, hsl(232 60% 3% / .62) 100%);
}
.cam-vignette.is-on { opacity: 1; }
body.modal-open .cam-vignette { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .isle-focus, .cam-vignette, .island-art::after { transition: none; }
}

.isle-svg { display: block; width: 100%; height: auto; overflow: visible; }

.isle-land { fill: hsl(252 34% 7%); }
.isle-rim {
  fill: none;
  stroke: hsl(38 90% 70% / calc(.28 * (1 - var(--idim, 0) * .6)));
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 1.2s ease;
}
/* foliage detail thins out with distance */
.isle-tree {
  fill: none;
  stroke: hsl(252 34% 7%);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: calc(1 - var(--idim, 0) * .85);
}
.isle-pole { stroke: hsl(28 40% 24%); stroke-width: 2; stroke-linecap: round; }

/* small buildings and shore rocks — cheap detail that makes each island read
   as a real place rather than a black blob. Warm window glow lights up when
   the island's milestone is reached. */
.isle-hut  { fill: hsl(28 22% 16%); }
.isle-window {
  fill: hsl(46 100% 68% / .38);
  transition: fill .9s ease, filter .9s ease;
}
.island.lit .isle-window {
  fill: hsl(46 100% 76%);
  filter: drop-shadow(0 0 3px hsl(42 100% 66%));
}
.isle-rock { fill: hsl(250 26% 10%); }
.isle-flame { fill: hsl(220 16% 26%); transition: fill 1s ease; }

/* soft radial glow around a lit island, and its reflection on the water */
.isle-halo {
  position: absolute;
  left: 50%; bottom: 6%;
  width: 155%; aspect-ratio: 1;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle, hsl(40 100% 66% / .5), hsl(32 100% 56% / .16) 44%, transparent 70%);
  opacity: 0;
  transition: opacity 1.6s ease;
  pointer-events: none;
}
.isle-reflect {
  position: absolute;
  left: 50%; top: 96%;
  width: 30%; height: 66%;
  transform: translateX(-50%);
  background: linear-gradient(180deg, hsl(42 100% 66% / .5), hsl(34 100% 56% / .1) 60%, transparent);
  filter: blur(2px);
  -webkit-mask-image: repeating-linear-gradient(180deg, #000 0 2px, transparent 2px 6px);
  mask-image: repeating-linear-gradient(180deg, #000 0 2px, transparent 2px 6px);
  opacity: 0;
  transition: opacity 1.6s ease;
}

/* lit state — a warm, magical glow */
.island.lit .isle-flame {
  fill: hsl(46 100% 90%);
  filter: drop-shadow(0 0 6px hsl(42 100% 74%)) drop-shadow(0 0 16px hsl(34 100% 60%));
  animation: lamp-flicker 3s ease-in-out infinite alternate;
}
.island.lit .isle-rim { stroke: hsl(42 100% 72% / .8); }
.island.lit .isle-halo { opacity: 1; animation: lamp-pulse 5s ease-in-out infinite; }
.island.lit .isle-reflect { opacity: 1; }

/* the active destination glows warm and inviting (the current goal) */
.island.next .isle-halo {
  opacity: .45;
  background: radial-gradient(circle, hsl(40 100% 66% / .55), hsl(32 100% 56% / .2) 44%, transparent 70%);
  animation: isle-next 2.8s ease-in-out infinite;
}
@keyframes isle-next { 0%, 100% { opacity: .3; } 50% { opacity: .55; } }

/* ---- island label (milestone amount) ---- */
.island-label {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 8px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  min-width: 104px;
  padding: .32rem .55rem;
  font: 600 0.792rem/1 var(--font);
  white-space: nowrap;
  color: hsl(var(--muted-foreground));
  background: hsl(28 40% 10% / .5);
  border: 1px solid hsl(36 100% 62% / .16);
  border-radius: 12px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: calc(.62 + .38 * var(--iscale));
  transition: color .9s ease, border-color .9s ease, box-shadow .9s ease, opacity .9s ease;
}
.isle-head { display: flex; align-items: center; gap: .34rem; }
.isle-progress {
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: hsl(var(--muted-foreground) / .85);
}
.isle-bar {
  height: 4px;
  border-radius: 999px;
  background: hsl(220 22% 32% / .55);
  overflow: hidden;
}
.isle-bar-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: linear-gradient(90deg, hsl(42 100% 64%), hsl(30 100% 56%));
  transition: width 1.2s cubic-bezier(.4, 0, .2, 1);
}
/* completed / active(next destination) / locked milestone states */
.island.done .isle-progress, .headland.done .isle-progress { color: hsl(var(--l-hot)); }
.island.active .island-label, .headland.active .island-label {
  opacity: 1;
  border-color: hsl(42 100% 60% / .6);
  box-shadow: 0 0 16px hsl(38 100% 55% / .32);
}
.island.locked .island-label, .headland.locked .island-label { opacity: calc(.5 + .3 * var(--iscale)); }
.isle-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: hsl(220 14% 40%);
  transition: background .9s ease, box-shadow .9s ease;
}
.island.lit .island-label { color: hsl(var(--l-hot)); border-color: hsl(36 100% 62% / .5); }
.island.lit .isle-dot { background: hsl(46 100% 72%); box-shadow: 0 0 8px hsl(42 100% 66%); }
.island.next .island-label { border-color: hsl(205 55% 66% / .4); }

/* the lighthouse's own milestone label, pinned above the tower */
.lh-label {
  position: absolute;
  left: 40.5%;
  top: 4%;
  transform: translateX(-50%);
  bottom: auto;
  margin: 0;
}

/* ---- lighthouse: the TOP LAMP is always on (an operating lighthouse) ----
   Decoupled from the donation milestone: the pane fill + halo + beam glow
   whenever the page is open, regardless of whether island 4 is reached.
   MILESTONE STATE STILL DRIVES the tower's water reflection, the label
   badge, and the strength of the lit-up state — see .headland.lit rules
   below. Only the top-lamp visuals were separated.

   Uses a DEDICATED wide-amplitude pulse (lighthouse-lamp-pulse) — the
   pre-existing `lamp-pulse` is a barely-perceptible 0.78→1 swing, which
   we deliberately reserve for the intensified .lit state. The always-on
   halo needs a much bigger, clearly-visible breathing pulse. */
@keyframes lighthouse-lamp-pulse {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1;    }
}
.headland .pane {
  fill: hsl(46 100% 64%);              /* warm gold baseline (dimmer than .lit) */
  animation: lamp-pulse 5s ease-in-out infinite;
}
.headland .lamp-halo {
  /* Boost the halo's own colour alpha so the glow actually reads on-screen —
     the underlying rule (line ~540) sets .16 alpha which is nearly invisible
     against the sky. Also give it a soft blur so it looks like light spill. */
  fill: hsl(46 100% 70% / .38);
  filter: blur(14px);
  opacity: .7;                         /* midpoint of the animation range */
  animation: lighthouse-lamp-pulse 4.2s ease-in-out infinite;
}
/* water reflection stays gated by the milestone — a strong "reached" cue */
.headland:not(.lit) .reflect-lh { opacity: 0; animation: none; }

/* MILESTONE REACHED (`lit` added by JS): intensify the top lamp and turn on
   the water reflection. This is additive on top of the always-on baseline. */
.headland.lit .pane { fill: hsl(48 100% 72%); }         /* brighter gold */
.headland.lit .lamp-halo {
  fill: hsl(46 100% 74% / .55);
  opacity: 1;                          /* full intensity + still pulsing */
}
.headland.lit .reflect-lh { opacity: 1; }

/* `.next` (lighthouse is the boat's current target) — keep the badge/label
   hints elsewhere, but do NOT recolour the top halo cool-blue any more:
   the lamp is warm-lit at all times, so a cool tint would clash. Force the
   baseline warm halo through even when `.next` is active. */
.headland.next .lamp-halo {
  opacity: .7;
  background: none;
  fill: hsl(46 100% 70% / .38);
  animation: lighthouse-lamp-pulse 4.2s ease-in-out infinite;
}

@media (max-width: 640px) {
  .island { width: calc(130px * var(--iscale)); }
  .island-label { font-size: 0.704rem; padding: .18rem .42rem; }
}

@media (prefers-reduced-motion: reduce) {
  .island.next .island-art,
  .island.lit .isle-flame,
  .island.lit .isle-halo,
  .island.next .isle-halo,
  .headland.next .lamp-halo { animation: none; }
}

/* the lighthouse (island 4) shares the island-label look for its state */
.headland.lit .island-label { color: hsl(var(--l-hot)); border-color: hsl(36 100% 62% / .5); }
.headland.lit .isle-dot { background: hsl(46 100% 72%); box-shadow: 0 0 8px hsl(42 100% 66%); }
.headland.next .island-label { border-color: hsl(205 55% 66% / .4); }

/* ---------- masked amount + full-name verification ---------- */
/* sender name now sits at the top of the lantern detail */
.reader-from { margin: 0 0 .3rem; font-size: 1.1rem; font-weight: 800; color: hsl(18 80% 30%); }

.reader-amount {
  display: flex;
  align-items: center;
  gap: .45rem;
  margin: 0 0 .2rem;
}
.reader-amount-val {
  padding: .28rem .7rem;
  font-size: 1.045rem;
  font-weight: 800;
  letter-spacing: .12em;
  color: hsl(22 90% 30%);
  background: hsl(42 100% 62% / .22);
  border: 1px solid hsl(36 100% 50% / .38);
  border-radius: 999px;
}
.reader-eye {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px; height: 30px;
  padding: 0;
  font-size: 1.045rem;
  line-height: 1;
  cursor: pointer;
  background: hsl(28 40% 96%);
  border: 1px solid hsl(30 40% 70%);
  border-radius: 50%;
  transition: var(--transition-smooth);
}
.reader-eye:hover { background: hsl(42 100% 88%); border-color: hsl(36 100% 55%); }
.reader-eye svg {
  width: 16px; height: 16px;
  fill: none;
  stroke: hsl(22 55% 30%);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.reader-label {
  margin: .6rem 0 .1rem;
  font-size: 0.748rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: hsl(20 45% 42%);
}

/* the masked chip in the wish list */

/* verify dialog */
.reveal[hidden] { display: none; }
.reveal {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: hsl(260 30% 2% / .82);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}
.reveal-card {
  width: min(92vw, 380px);
  padding: 1.5rem 1.4rem;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  text-align: center;
}
.reveal-card h3 { margin: 0 0 .4rem; font-size: 1.265rem; }
.reveal-sub { margin: 0 0 1rem; font-size: 0.935rem; color: hsl(var(--muted-foreground)); }
#reveal-name {
  width: 100%;
  padding: .7rem .9rem;
  font: 600 1.1rem/1.2 var(--font);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: hsl(var(--foreground));
  background: hsl(var(--input));
  border: 1px solid hsl(var(--border));
  border-radius: .7rem;
  outline: none;
}
#reveal-name:focus-visible { border-color: hsl(var(--l-mid)); box-shadow: 0 0 0 2px hsl(var(--l-mid) / .3); }
.reveal-error { margin: .6rem 0 0; font-size: 0.88rem; color: hsl(var(--destructive)); }
.reveal-actions { display: flex; gap: .6rem; justify-content: center; margin-top: 1rem; }
.reveal-cancel, .reveal-verify {
  padding: .55rem 1.2rem;
  font: 700 0.935rem/1 var(--font);
  border-radius: 999px;
  cursor: pointer;
  transition: var(--transition-smooth);
}
.reveal-cancel {
  color: hsl(var(--muted-foreground));
  background: transparent;
  border: 1px solid hsl(var(--border));
}
.reveal-verify {
  color: hsl(24 60% 12%);
  background: var(--gradient-primary);
  border: 0;
}
.reveal-verify:disabled { opacity: .6; cursor: default; }

/* ---------- "Send a wish" chooser + bank-transfer QR + donor form ---------- */
/* `display` here would defeat the [hidden] attribute, so guard those too. */
.chooser[hidden], .pay[hidden] { display: none; }

.chooser, .pay {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: hsl(260 30% 2% / .82);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}

.chooser-card, .pay-card {
  position: relative;
  width: 100%;
  max-width: 27rem;
  max-height: 88vh;
  overflow-y: auto;
  padding: 1.6rem 1.5rem 1.25rem;
  text-align: center;
  background: linear-gradient(180deg, hsl(28 40% 12% / .96), hsl(265 30% 8% / .96));
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card), 0 0 90px hsl(var(--l-deep) / .22);
}

.chooser-card h2, .pay-card h2 { margin: 0 0 .35rem; font-size: 1.54rem; font-weight: 800; }
.chooser-sub, .pay-sub { margin: 0 0 1.15rem; font-size: 0.968rem; color: hsl(var(--muted-foreground)); }

.chooser-opts { display: flex; flex-direction: column; gap: .8rem; }

.chooser-opt {
  display: flex;
  align-items: center;
  gap: .9rem;
  width: 100%;
  padding: 1rem 1.1rem;
  text-align: left;
  color: hsl(var(--foreground));
  background: hsl(var(--input) / .7);
  border: 1px solid hsl(var(--border) / .8);
  border-radius: 14px;
  cursor: pointer;
  transition: var(--transition-smooth);
}
.chooser-opt:hover, .chooser-opt:focus-visible {
  outline: none;
  border-color: hsl(var(--l-mid) / .75);
  box-shadow: 0 0 0 3px hsl(var(--l-mid) / .16), 0 0 26px hsl(var(--l-mid) / .2);
  transform: translateY(-1px);
}
.chooser-emoji { font-size: 1.87rem; line-height: 1; flex: 0 0 auto; }
.chooser-opt-body { display: flex; flex-direction: column; gap: .18rem; }
.chooser-opt-t { font-size: 1.1rem; font-weight: 700; }
.chooser-opt-d { font-size: 0.88rem; color: hsl(var(--muted-foreground)); }

/* QR */
.pay-qr[hidden] { display: none; }
.pay-qr {
  display: inline-flex;
  padding: .7rem;
  margin: 0 auto .3rem;
  background: #fff;
  border-radius: 14px;
  box-shadow: 0 0 30px hsl(var(--l-mid) / .28);
}
.pay-qr img { display: block; width: 240px; height: auto; border-radius: 6px; }

.pay-info {
  margin: .9rem 0 .3rem;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .3rem .8rem;
  text-align: left;
  font-size: 0.946rem;
}
.pay-info dt { color: hsl(var(--muted-foreground)); }
.pay-info dd { margin: 0; font-weight: 700; color: hsl(var(--foreground)); overflow-wrap: anywhere; }

.pay-note {
  margin: .6rem 0 0;
  font-size: 0.902rem;
  line-height: 1.5;
  color: hsl(var(--l-hot));
  background: hsl(42 100% 60% / .12);
  border: 1px dashed hsl(36 100% 55% / .4);
  border-radius: 10px;
  padding: .55rem .7rem;
}

.pay-status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  margin: 1rem 0 .2rem;
  font-size: 0.946rem;
  font-weight: 600;
  color: hsl(var(--l-mid));
}
.pay-status.detected { color: hsl(var(--l-hot)); }
.pay-spinner {
  width: 15px; height: 15px;
  border-radius: 50%;
  border: 2px solid hsl(var(--l-mid) / .35);
  border-top-color: hsl(var(--l-mid));
  animation: pay-spin .8s linear infinite;
}
.pay-status.detected .pay-spinner { display: none; }
@keyframes pay-spin { to { transform: rotate(360deg); } }

.donor-detected {
  margin: 0 0 1rem;
  font-size: 0.924rem;
  color: hsl(var(--l-hot));
  background: hsl(42 100% 60% / .12);
  border-radius: 10px;
  padding: .5rem .7rem;
}

@media (prefers-reduced-motion: reduce) {
  .pay-spinner { animation: none; }
}

.pay-wait-sub {
  margin: .15rem 0 0;
  font-size: 0.858rem;
  line-height: 1.45;
  color: hsl(var(--muted-foreground));
  text-align: center;
}

/* fundraising total (live pot balance), shown as an exact VND figure */
.raised {
  margin: 0.55rem 0 0;
  font-size: 1.1rem;
  font-weight: 800;
  letter-spacing: 0.01em;
  color: hsl(var(--l-hot));
  text-shadow: 0 1px 12px hsl(260 30% 4% / .9);
}

/* explicit success state in the donor modal */
#donor-success { text-align: center; padding: 1rem 0 .4rem; }
.donor-success-emoji { font-size: 3.3rem; line-height: 1; margin-bottom: .4rem; animation: caller-bob 3s ease-in-out infinite; }
#donor-success h2 { margin: 0 0 .5rem; font-size: 1.485rem; font-weight: 800; }
.donor-success-sub { margin: 0; font-size: 0.99rem; color: hsl(var(--muted-foreground)); }
/* financial transparency (shown on every verified transfer's end) */
.donor-transp {
  margin: 1.1rem 0 0;
  padding: .85rem .95rem;
  text-align: left;
  font-size: 0.946rem;
  line-height: 1.55;
  border: 1px solid hsl(var(--border, 230 30% 40%) / .55);
  border-radius: 12px;
  background: hsl(230 40% 14% / .35);
}
.donor-transp p { margin: 0; }
.donor-transp-note { margin-bottom: .55rem !important; }
.donor-transp-row + .donor-transp-row { margin-top: .35rem !important; }
.donor-transp a {
  color: hsl(var(--l-hot, 38 100% 68%));
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;            /* the long link wraps on a phone, never overflows */
}
.donor-transp-pw {
  font: 600 .92rem/1 ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: .04em;
  padding: .15rem .4rem;
  border-radius: 6px;
  background: hsl(230 40% 8% / .5);
  user-select: all;                   /* one tap selects the whole password */
}
.donor-transp-copy {
  margin-left: .35rem;
  min-height: 32px;
  padding: .2rem .7rem;
  font: 600 0.858rem/1 var(--font);
  color: inherit;
  border: 1px solid hsl(var(--border, 230 30% 40%) / .7);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  vertical-align: middle;
}
.donor-transp-copy.is-done { color: hsl(140 55% 70%); border-color: hsl(140 45% 50% / .6); }

/* ---------- "Xem thêm" on a long wish (inside the lantern detail) ---------- */
.reader-more {
  display: inline-block;
  margin: .35rem 0 0;
  padding: .34rem .9rem;
  font: 700 0.902rem/1 var(--font);
  color: hsl(20 72% 24%);
  background: hsl(24 82% 52% / .16);
  border: 1px solid hsl(22 70% 38% / .4);
  border-radius: 999px;
  cursor: pointer;
  transition: background .25s ease, transform .15s ease;
}
.reader-more::after { content: " →"; }
.reader-more:hover { background: hsl(24 82% 52% / .28); transform: translateY(-1px); }
.reader-more:focus-visible { outline: 2px solid hsl(22 80% 40%); outline-offset: 2px; }

/* ---------- full-screen parchment: reading a long wish ---------- */
.paper {
  position: fixed;
  inset: 0;
  z-index: 22;                 /* above the lantern detail (20) */
  display: grid;
  place-items: center;
  padding: clamp(.8rem, 3vw, 1.6rem);
  background: hsl(260 30% 2% / .86);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.paper-stage {
  position: relative;
  width: 100%;
  max-width: 40rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  outline: none;
  animation: paper-in 380ms cubic-bezier(.2, .9, .3, 1) both;
}
@keyframes paper-in {
  from { opacity: 0; transform: scale(.94) translateY(10px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .paper-stage { animation: none; } }

.paper-sheet {
  position: relative;
  width: 100%;
  max-height: min(84vh, 780px);
  overflow: hidden;            /* the inner .paper-scroll scrolls */
  color: hsl(28 48% 24%);
  border-radius: 16px 22px 14px 20px;
  /* warm aged parchment */
  background:
    radial-gradient(140% 60% at 50% -10%, hsl(43 62% 92% / .9), transparent 60%),
    radial-gradient(60% 40% at 100% 100%, hsl(30 45% 66% / .5), transparent 70%),
    radial-gradient(50% 40% at 0% 90%, hsl(32 45% 68% / .45), transparent 70%),
    linear-gradient(160deg, hsl(41 58% 88%), hsl(37 46% 80%) 55%, hsl(33 40% 73%));
  box-shadow:
    0 0 70px hsl(38 92% 56% / .3),
    inset 0 0 70px hsl(30 48% 42% / .28),
    inset 0 0 0 1px hsl(33 40% 60% / .5),
    0 26px 64px hsl(20 60% 5% / .6);
}
/* faint fiber/speckle texture */
.paper-sheet::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: .5;
  background-image:
    radial-gradient(1px 1px at 20% 30%, hsl(30 40% 40% / .18), transparent 60%),
    radial-gradient(1px 1px at 70% 20%, hsl(30 40% 40% / .14), transparent 60%),
    radial-gradient(1px 1px at 40% 70%, hsl(30 40% 40% / .16), transparent 60%),
    radial-gradient(1px 1px at 85% 65%, hsl(30 40% 40% / .12), transparent 60%),
    radial-gradient(1px 1px at 55% 45%, hsl(30 40% 40% / .12), transparent 60%);
  background-size: 220px 220px, 260px 260px, 180px 180px, 240px 240px, 200px 200px;
}
/* soft darker deckled frame just inside the edge */
.paper-sheet::after {
  content: "";
  position: absolute;
  inset: 6px;
  border-radius: inherit;
  border: 1px dashed hsl(24 40% 44% / .3);
  pointer-events: none;
}

.paper-compass {
  position: absolute;
  top: 14px; right: 16px;
  width: clamp(64px, 12vw, 96px);
  height: auto;
  color: hsl(20 55% 34%);
  opacity: .16;
  pointer-events: none;
}
.paper-corner {
  position: absolute;
  width: 26px; height: 26px;
  border-color: hsl(22 55% 36% / .5);
  border-style: solid;
  border-width: 0;
  pointer-events: none;
}
.paper-corner.tl { top: 12px; left: 12px; border-top-width: 2px; border-left-width: 2px; border-top-left-radius: 6px; }
.paper-corner.tr { top: 12px; right: 12px; border-top-width: 2px; border-right-width: 2px; border-top-right-radius: 6px; }
.paper-corner.bl { bottom: 12px; left: 12px; border-bottom-width: 2px; border-left-width: 2px; border-bottom-left-radius: 6px; }
.paper-corner.br { bottom: 12px; right: 12px; border-bottom-width: 2px; border-right-width: 2px; border-bottom-right-radius: 6px; }

.paper-scroll {
  position: relative;
  max-height: min(84vh, 780px);
  overflow-y: auto;
  padding: clamp(1.8rem, 5vw, 2.8rem) clamp(1.4rem, 5vw, 2.6rem);
  scrollbar-width: none;                 /* Firefox: hide scrollbar, keep scroll */
}
.paper-scroll::-webkit-scrollbar { width: 0; height: 0; }

.paper-label {
  margin: 0 0 .5rem;
  font-size: 0.814rem;
  font-weight: 700;
  letter-spacing: .28em;
  text-transform: uppercase;
  text-align: center;
  color: hsl(20 60% 34% / .85);
}
.paper-from {
  margin: 0 0 1.1rem;
  font-size: 1.265rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-align: center;
  color: hsl(18 72% 30%);
}
.paper-from::before, .paper-from::after {
  content: "·"; margin: 0 .6rem; color: hsl(22 55% 40% / .6);
}
.paper-msg {
  margin: 0;
  font-size: clamp(1.1rem, 1.4vw, 1.188rem);
  line-height: 1.8;
  color: hsl(24 50% 22%);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  text-align: left;
}

.paper-close {
  padding: .55rem 1.7rem;
  font: 700 0.935rem/1 var(--font);
  color: hsl(40 60% 92%);
  background: hsl(24 45% 18% / .8);
  border: 1px solid hsl(36 100% 62% / .3);
  border-radius: 999px;
  cursor: pointer;
  transition: var(--transition-smooth);
}
.paper-close:hover { border-color: hsl(var(--l-mid) / .7); box-shadow: 0 0 22px hsl(var(--l-mid) / .3); }
.paper-close:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }

@media (max-width: 640px) {
  .paper-compass { opacity: .12; width: 58px; }
  .paper-from::before, .paper-from::after { display: none; }
}

/* donor modal must sit ABOVE the lantern detail (Flow 2 opens it over the reader) */
#donor { z-index: 21; }

/* field hint under an input (e.g. the Drive-link explanation) */
.field-hint {
  display: block;
  margin-top: .35rem;
  font-size: 0.814rem;
  line-height: 1.4;
  color: hsl(var(--muted-foreground) / .8);
}

/* QR success: optional "add gift details" / "skip" actions */
.pay-actions {
  display: flex;
  gap: .6rem;
  justify-content: center;
  flex-wrap: wrap;
  margin: .2rem 0 .1rem;
}
.pay-fill {
  padding: .6rem 1.2rem;
  font: 700 0.935rem/1 var(--font);
  color: hsl(24 60% 12%);
  background: var(--gradient-primary);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 0 22px hsl(var(--l-mid) / .3);
}
.pay-skip {
  padding: .6rem 1.2rem;
  font: 600 0.935rem/1 var(--font);
  color: hsl(var(--muted-foreground));
  background: transparent;
  border: 1px solid hsl(var(--border));
  border-radius: 999px;
  cursor: pointer;
}
.pay-fill:hover { transform: translateY(-1px); }
.pay-skip:hover { color: hsl(var(--foreground)); border-color: hsl(var(--l-mid) / .6); }

/* "add gift details" CTA inside the lantern detail (after reveal) */
.reader-donate {
  margin: .2rem 0 0;
  padding: .55rem 1.4rem;
  font: 700 0.902rem/1 var(--font);
  color: hsl(24 60% 12%);
  background: var(--gradient-primary);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 0 22px hsl(var(--l-mid) / .3);
}
.reader-donate:hover { transform: translateY(-1px); }
.reader-donate:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }

/* "Claim this wish" CTA reuses the donate pill styling with a softer
   secondary gradient so it doesn't compete with the primary gift-info CTA. */
.reader-claim {
  margin: .2rem 0 0;
  padding: .55rem 1.4rem;
  font: 700 0.902rem/1 var(--font);
  color: hsl(36 100% 92%);
  background: hsl(28 32% 18% / .85);
  border: 1px solid hsl(36 100% 62% / .4);
  border-radius: 999px;
  cursor: pointer;
}
.reader-claim:hover { transform: translateY(-1px); border-color: hsl(36 100% 62% / .8); }
.reader-claim:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }
.reader-claim-status {
  margin: .35rem 0 0;
  font: 600 0.858rem/1.3 var(--font);
  color: hsl(var(--muted-foreground));
  text-align: center;
}

/* ============================================================
   MOBILE TAP-TO-OPEN PERFORMANCE
   The tap-to-open latency on modals (Wish List, Wish Detail,
   Reveal, Claim, Guide, etc.) was caused by two things:

     1) `backdrop-filter: blur(...)` on the modal overlay,
        composited over an ACTIVELY animating scene (25+ sky
        lanterns, waves, boat heave/roll, beam, moon). On mobile
        GPUs this serialises paint on the main thread and the
        first frame after tap can stall for many seconds.
     2) Two live layout probes — the beam rAF loop and the crew
        setInterval — kept reading getBoundingClientRect each
        tick even while the scene was hidden under a modal.

   Fix (no visual removal, no logic change):
     - JS toggles `body.modal-open` when any modal is visible
       (see the MutationObserver in app.js).
     - CSS below then PAUSES the expensive background animations
       and — on mobile only — drops backdrop-filter from the
       modal overlays in favour of a slightly darker solid
       background. Desktop keeps the pretty blur.
   ============================================================ */
body.modal-open .lantern,
body.modal-open .boat-heave,
body.modal-open .boat-roll,
body.modal-open .flag,
body.modal-open .wake,
body.modal-open .wake::before,
body.modal-open .ripple,
body.modal-open .paddle-ripple,
body.modal-open .bow-wave,
body.modal-open .lc-flame,
body.modal-open .isle-flame,
body.modal-open .moon-body,
body.modal-open .moon-haze,
body.modal-open .lamp-halo,
body.modal-open .lh-beam-rot,
body.modal-open .lh-cone,
body.modal-open .lh-flare,
body.modal-open .arm,
body.modal-open .wish-light,
body.modal-open .wish-halo,
body.modal-open .bday-title,
body.modal-open .bday-title::before,
body.modal-open .bday-title::after {
  animation-play-state: paused !important;
}
/* The beam is written via JS transform; the trackBeam rAF loop already
   skips its layout read while modal-open (see app.js), so nothing extra
   is needed here for the beam element. */

@media (max-width: 640px) {
  /* On phones, drop backdrop-filter from EVERY modal overlay. The blur
     was the single largest tap-to-open cost. The solid dark background
     preserves the "night" feel and is indistinguishable at a glance. */
  body.modal-open .logbook,
  body.modal-open .reader,
  body.modal-open .reveal,
  body.modal-open .paper,
  body.modal-open .guide,
  body.modal-open .donor,
  body.modal-open .pay {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    background: hsl(260 30% 2% / .96) !important;
  }
  /* Modal CARDS keep their own subtle glass look — no change there. */
}

/* ============================================================
   MOBILE SKY LANTERN TIERS
   Only the newest SKY_GLOW_COUNT lanterns keep their soft glow;
   older ones get `.lantern-lite`. This drops the two large
   box-shadows (22px + 54px blur radii) which are the single
   biggest per-lantern compositor cost on phones. Colour is
   preserved by the radial gradient in .lantern-body; the
   silhouette still reads as a warm lantern.
   Desktop is untouched.
   ============================================================ */
@media (max-width: 640px) {
  .lantern.lantern-lite .lantern-body {
    box-shadow: 0 0 8px hsl(var(--l-mid) / .45);   /* one small halo instead of two big ones */
  }
  .lantern.lantern-lite .lantern-body::after {
    filter: none;                                   /* skip the sub-pixel blur on lite lanterns */
  }
  .lantern.lantern-lite .lantern-name {
    text-shadow: 0 1px 3px hsl(260 30% 4% / .85);  /* lighter text-shadow */
  }
}


/* ============================================================
   MOBILE COMPOSITOR RELIEF (v2)
   The remaining idle cost on phones — after the earlier
   modal-open pauses and lantern-lite tiers — was the AMBIENT
   scene: 4 wave layers animating background-position (a CPU
   repaint of a large area every frame on iOS Safari), the
   lighthouse beam's rAF-driven mix-blend cone, and the boat's
   three concurrent animations plus wake/ripple/bow-wave. This
   block freezes those on mobile only. Silhouettes and colours
   are unchanged; the scene still looks like the same night,
   it just doesn't sample the compositor 60x/s.
   Desktop is untouched.
   ============================================================ */
@media (max-width: 640px) {
  /* 1) waves keep drifting on phones (paused only under an open modal) */
  /* 2) lighthouse beam stays on phones: it is a single CSS transform
        animation (compositor-only, no JS), see .lh-beam. */
  /* 3) boat: keep ONE motion (heave) so it still feels alive;
        drop roll, flag pace, wake pump, ripple rings and the
        bow-wave scale animation. */
  .boat-roll,
  .flag,
  .arm,
  .wake,
  .wake::before,
  .ripple,
  .bow-wave {
    animation: none !important;
  }
  /* 4) misc ambient loops that add up */
  .moon-haze,
  .lamp-halo,
  .lc-flame,
  .isle-flame,
  .wish-light,
  .wish-halo,
  .caller {
    animation: none !important;
  }
}

/* ============================================================
   MODAL-OPEN SCENE CULL (mobile)
   With the modal opaque on phones (the earlier override made
   overlays solid dark), the scene behind it is fully occluded
   yet the compositor still keeps its layers live. Setting
   visibility:hidden on the four big scene groups drops them
   from the composite tree entirely, so the FIRST paint of the
   modal is trivial — no wave/lantern/boat/beam layers to fold
   in. Reversed the moment the modal closes.
   Desktop still shows the glass blur so it must NOT hide the
   scene there.
   ============================================================ */
@media (max-width: 640px) {
  body.modal-open .sky,
  body.modal-open .sea,
  body.modal-open .headland,
  body.modal-open .boat-lane,
  body.modal-open .moon {
    visibility: hidden;
  }
}

/* Desktop lantern tier: beyond the newest DESKTOP_GLOW_COUNT, lanterns drop
   the two large halos and the blurred inner glow (mobile has its own tier). */
@media (min-width: 641px) {
  .lantern.lantern-lite .lantern-body { box-shadow: 0 0 12px hsl(var(--l-mid) / .5); }
  .lantern.lantern-lite .lantern-body::after { filter: none; }
}

/* Mobile: while a modal covers the screen, freeze every remaining scene
   animation (the scene is already visibility:hidden, but hidden animations
   still tick the style engine). The blurred water-lantern shimmer is also
   frozen at idle — its drift keeps the sea alive without animating a blur. */
@media (max-width: 640px) {
  body.modal-open .scene *,
  body.modal-open .lantern-sway,
  body.modal-open .wish-caller,
  body.modal-open .wish-bubble {
    animation-play-state: paused !important;
  }
  .wl-reflect { animation: none !important; }
}

/* ============================================================
   MILESTONE ISLAND DETAIL
   Islands + the lighthouse are buttons (see app.js). The panel is
   a bottom sheet on phones and a centred floating card on desktop.
   It deliberately does NOT set body.modal-open: the ocean stays
   visible (and alive) behind a light veil, and the tapped island
   keeps its own animation — it only gains a soft extra glow.
   ============================================================ */
.island { pointer-events: auto; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.headland .island-label,
.headland-art .tower,
.headland-art .cliff,
.headland-art .lamp-room,
.headland-art .gallery,
.headland-art .stair { pointer-events: auto; cursor: pointer; }
.headland { -webkit-tap-highlight-color: transparent; }
.island:focus-visible, .headland:focus-visible { outline: none; }
.island:focus-visible .island-label,
.headland:focus-visible .island-label { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }

/* tapped island: a soft golden bloom behind it + a brighter label. Uses a
   pseudo-element / label only, so the island's own transform + filter
   (arrival, lit, approaching) are never touched or restarted. */
.island::before {
  content: "";
  position: absolute;
  left: -30%; right: -30%; top: -40%; bottom: -25%;
  background: radial-gradient(closest-side, hsl(42 100% 62% / .38), hsl(36 100% 55% / .12) 60%, transparent);
  opacity: 0;
  transition: opacity .3s ease;
  pointer-events: none;
}
.island.inspecting::before { opacity: 1; }
.island.inspecting .island-label,
.headland.inspecting .island-label {
  opacity: 1;
  color: hsl(var(--l-hot));
  border-color: hsl(42 100% 64% / .75);
  box-shadow: 0 0 22px hsl(38 100% 58% / .5);
}
.headland.inspecting .lamp-halo { opacity: 1; }
@media (hover: hover) {
  .island:hover .island-label,
  .headland .island-label:hover { border-color: hsl(42 100% 64% / .55); }
}

/* ---- the sheet ---- */
.isle-sheet[hidden] { display: none; }
.isle-sheet {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  /* light veil: the night ocean should still read behind the card */
  background: hsl(250 40% 3% / .5);
  animation: isle-veil-in .28s ease both;
}
.isle-sheet.closing { animation: isle-veil-out .22s ease both; pointer-events: none; }
@keyframes isle-veil-in { from { opacity: 0; } }
@keyframes isle-veil-out { to { opacity: 0; } }

.isle-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 30rem;
  max-height: min(86dvh, 44rem);
  overflow: hidden;
  outline: none;
  color: hsl(var(--foreground));
  background:
    radial-gradient(120% 60% at 50% 0%, hsl(40 100% 70% / .13), transparent 70%),
    linear-gradient(180deg, hsl(30 38% 13% / .97), hsl(262 30% 9% / .97));
  border: 1px solid var(--glass-border);
  border-bottom: 0;
  border-radius: 1.4rem 1.4rem 0 0;
  box-shadow: 0 -18px 50px -14px hsl(22 92% 30% / .5), 0 0 80px hsl(var(--l-deep) / .18);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  animation: isle-card-in .34s cubic-bezier(.2, .8, .2, 1) backwards;
  transition: transform .22s ease;
}
.isle-sheet.closing .isle-card { animation: isle-card-out .22s ease both; }
@keyframes isle-card-in { from { transform: translateY(40px); opacity: 0; } }
@keyframes isle-card-out { to { transform: translateY(40px); opacity: 0; } }

@media (min-width: 641px) {
  .isle-sheet {
    align-items: center;
    padding: 1.5rem;
    background: hsl(250 40% 3% / .42);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
  }
  .isle-card {
    border-bottom: 1px solid var(--glass-border);
    border-radius: 1.4rem;
    box-shadow: var(--shadow-card), 0 0 90px hsl(var(--l-deep) / .22);
  }
  @keyframes isle-card-in { from { transform: translateY(14px) scale(.97); opacity: 0; } }
  @keyframes isle-card-out { to { transform: translateY(10px) scale(.98); opacity: 0; } }
}

/* header: grip, close, hero, status + progress — always visible */
.isle-grab {
  flex: 0 0 auto;
  position: relative;
  padding: .55rem 1.25rem .9rem;
  text-align: center;
  touch-action: none;           /* the swipe-to-close gesture lives here */
  border-bottom: 1px solid hsl(36 100% 62% / .12);
}
.isle-grip {
  display: block;
  width: 42px; height: 5px;
  margin: 0 auto .35rem;
  border-radius: 999px;
  background: hsl(36 40% 80% / .35);
}
@media (min-width: 641px) { .isle-grip { display: none; } .isle-grab { padding-top: 1.1rem; } }
.isle-x {
  position: absolute;
  top: .55rem; right: .6rem;
  display: grid; place-items: center;
  width: 44px; height: 44px;
  padding: 0;
  color: hsl(var(--foreground) / .9);
  background: hsl(28 40% 10% / .55);
  border: 1px solid var(--glass-border);
  border-radius: 50%;
  cursor: pointer;
}
.isle-x svg { width: 20px; height: 20px; }
.isle-x:hover { border-color: hsl(var(--l-mid) / .6); }
.isle-x:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 2px; }

.isle-emblem {
  width: 64px; height: 48px;
  margin: .2rem auto .35rem;
  filter: drop-shadow(0 0 10px hsl(42 100% 60% / .35));
}
.isle-emblem svg { display: none; width: 100%; height: 100%; overflow: visible; }
.isle-card[data-kind="island"] .isle-emblem-island,
.isle-card[data-kind="lighthouse"] .isle-emblem-lighthouse { display: block; }
.ie-water { fill: none; stroke: hsl(205 60% 72% / .55); stroke-width: 1.6; stroke-linecap: round; }
.ie-land, .ie-tower { fill: hsl(34 40% 26%); stroke: hsl(40 90% 72% / .7); stroke-width: 1.2; stroke-linejoin: round; }
.ie-palm { fill: none; stroke: hsl(40 70% 70% / .8); stroke-width: 1.6; stroke-linecap: round; }
.ie-leaf { fill: hsl(40 80% 70% / .75); }
.ie-rays { fill: none; stroke: hsl(46 100% 75% / .8); stroke-width: 1.6; stroke-linecap: round; }
.ie-glow { fill: hsl(46 100% 78%); }
.isle-card[data-state="locked"] .isle-emblem { filter: grayscale(.6) brightness(.75); }
.isle-card[data-state="locked"] .ie-rays { opacity: 0; }
.isle-card[data-state="done"] .isle-emblem { filter: drop-shadow(0 0 16px hsl(42 100% 62% / .7)); }

.isle-kicker {
  margin: 0;
  font-size: 0.748rem;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: hsl(var(--l-hot) / .85);
}
.isle-title {
  margin: .15rem 2.6rem 0;
  font-size: clamp(1.43rem, 5.6vw, 1.76rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -.01em;
  overflow-wrap: anywhere;
}
.isle-target {
  margin: .25rem 0 0;
  font-size: 1.155rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: hsl(var(--foreground));
}
.isle-tagline {
  margin: .15rem 0 0;
  font-size: 0.946rem;
  font-style: italic;
  color: hsl(var(--muted-foreground));
}

.isle-prog { margin: .75rem auto 0; max-width: 22rem; text-align: left; }
.isle-prog-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.isle-status {
  display: inline-flex; align-items: center;
  padding: .22rem .6rem;
  font-size: 0.814rem; font-weight: 700;
  border-radius: 999px;
  color: hsl(var(--muted-foreground));
  background: hsl(220 22% 30% / .35);
  border: 1px solid hsl(220 20% 60% / .22);
}
.isle-card[data-state="active"] .isle-status { color: hsl(205 80% 86%); background: hsl(205 60% 40% / .28); border-color: hsl(205 70% 70% / .4); }
.isle-card[data-state="done"] .isle-status { color: hsl(var(--l-hot)); background: hsl(36 90% 45% / .22); border-color: hsl(42 100% 64% / .55); box-shadow: 0 0 14px hsl(38 100% 55% / .3); }
.isle-pct { font-size: 0.99rem; font-weight: 800; font-variant-numeric: tabular-nums; color: hsl(var(--l-hot)); }
.isp-bar {
  height: 7px;
  margin-top: .4rem;
  border-radius: 999px;
  background: hsl(220 22% 30% / .5);
  overflow: hidden;
}
.isp-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, hsl(42 100% 66%), hsl(28 100% 56%));
  box-shadow: 0 0 10px hsl(38 100% 58% / .6);
  transition: width .6s cubic-bezier(.4, 0, .2, 1);
}
.isle-funded {
  margin: .3rem 0 0;
  font-size: 0.792rem;
  font-variant-numeric: tabular-nums;
  color: hsl(var(--muted-foreground) / .9);
}

/* body: description + items, scrolls on its own */
.isle-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: .9rem 1.1rem 1.2rem;
}
.isle-desc {
  margin: 0;
  font-size: 0.99rem;
  line-height: 1.55;
  color: hsl(var(--foreground) / .88);
  overflow-wrap: anywhere;
}
.isle-cum { margin: .45rem 0 0; font-size: 0.792rem; color: hsl(var(--muted-foreground) / .8); }
.isle-items-title {
  margin: 1.05rem 0 .6rem;
  font-size: 0.902rem;
  font-weight: 700;
  letter-spacing: .02em;
  color: hsl(var(--l-hot));
}
.isle-items { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }

.isle-item {
  display: flex;
  gap: .7rem;
  align-items: flex-start;
  padding: .7rem .8rem;
  border-radius: .9rem;
  /* warm parchment tint, kept light */
  background: linear-gradient(180deg, hsl(38 45% 88% / .07), hsl(38 45% 88% / .03));
  border: 1px solid hsl(38 60% 75% / .14);
  animation: isle-item-in .3s cubic-bezier(.2, .8, .2, 1) backwards;
  animation-delay: calc(90ms + var(--n, 0) * 45ms);
}
@keyframes isle-item-in { from { opacity: 0; transform: translateY(8px); } }
.isle-item-ico {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 2.2rem; height: 2.2rem;
  font-size: 1.1rem;
  border-radius: 50%;
  background: hsl(36 60% 40% / .22);
  border: 1px solid hsl(40 80% 70% / .2);
}
.isle-item-body { min-width: 0; flex: 1 1 auto; }
.isle-item-name { margin: 0; font-size: 1.012rem; font-weight: 700; line-height: 1.3; overflow-wrap: anywhere; }
.isle-item-desc { margin: .2rem 0 0; font-size: 0.88rem; line-height: 1.45; color: hsl(var(--muted-foreground)); overflow-wrap: anywhere; }
.isle-item-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .3rem .6rem; margin: .45rem 0 0; }
.isle-item-amt { font-size: 0.836rem; font-weight: 600; font-variant-numeric: tabular-nums; color: hsl(var(--foreground) / .75); }
.isle-chip {
  padding: .14rem .5rem;
  font-size: 0.748rem; font-weight: 700;
  border-radius: 999px;
  white-space: nowrap;
  color: hsl(var(--muted-foreground));
  background: hsl(220 22% 30% / .35);
}
.isle-item.is-locked { opacity: .62; }
.isle-item.is-locked .isle-item-ico { filter: grayscale(.8); }
.isle-item.is-active { border-color: hsl(205 70% 70% / .35); }
.isle-item.is-active .isle-chip { color: hsl(205 80% 86%); background: hsl(205 60% 40% / .3); }
.isle-item.is-done {
  border-color: hsl(42 100% 64% / .4);
  background: linear-gradient(180deg, hsl(40 90% 60% / .13), hsl(40 90% 60% / .05));
  box-shadow: 0 0 18px -6px hsl(38 100% 55% / .45);
}
.isle-item.is-done .isle-item-ico { background: hsl(40 90% 50% / .3); border-color: hsl(42 100% 66% / .5); }
.isle-item.is-done .isle-chip { color: hsl(30 60% 14%); background: hsl(42 100% 70%); }

@media (prefers-reduced-motion: reduce) {
  .isle-sheet, .isle-card, .isle-item, .isle-sheet.closing, .isle-sheet.closing .isle-card { animation: none !important; }
  .island::before, .isp-fill { transition: none; }
}

/* ============================================================
   LIGHTHOUSE SEARCHLIGHT — one rotating mechanism, two opposed beams
   .lh-beam        pivot, placed exactly on the lamp (same point as
                   .beacon, inside the headland, so it follows the
                   lighthouse at every viewport)
   .lh-beam-plane  squashes the rotation into a flat ellipse — the
                   2D picture of a horizontal sweep seen from the
                   front: right -> down toward the viewer (over the
                   sea) -> left -> up behind the tower -> right
   .lh-beam-rot    THE single source of rotation (rotationAngle)
     .lh-arm-a     beam A = rotationAngle
     .lh-arm-b     beam B = rotationAngle + 180deg (static offset)
   A uniform squash keeps opposite directions opposite, so the two
   beams are ALWAYS exactly 180deg apart on screen. Pure 2D: the beams
   never turn edge-on or vanish. One CSS animation; no JS, no rAF.
   z-index -1 inside the headland's stacking context = painted
   behind the tower/cliff SVG, in front of the moon and sky.
   ============================================================ */
.headland { --beam-t: 13s; }         /* one full turn (rotation, fade, flare share it) */
.lh-beam {
  --beam-l: 78vmax;                  /* reach */
  --beam-h: 24vmax;                  /* spread at the far end (before the squash) */
  position: absolute;
  left: 59.5%;
  top: 26.2%;
  width: 0;
  height: 0;
  z-index: -1;
  pointer-events: none;
}
.lh-beam-plane,
.lh-beam-rot,
.lh-arm {
  position: absolute;
  left: 0; top: 0;
  width: 0; height: 0;
}
.lh-beam-plane { transform: scaleY(.45); }
.lh-beam-rot { animation: lh-sweep var(--beam-t) linear infinite; }
/* rotationAngle: 0deg = A right, 90deg = A down/front,
   180deg = A left, 270deg = A up/back (behind the tower) */
@keyframes lh-sweep {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
.lh-arm-a { transform: rotate(0deg); }
.lh-arm-b { transform: rotate(180deg); }    /* fixed opposite of A */

.lh-cone {
  position: absolute;
  left: 0;
  top: calc(var(--beam-h) / -2);
  width: var(--beam-l);
  height: var(--beam-h);
  transform-origin: 0 50%;
  /* narrow at the lamp, widest far out */
  clip-path: polygon(0 45%, 100% 4%, 100% 96%, 0 55%);
  background: linear-gradient(90deg,
    hsl(48 100% 94% / 1) 0%,
    hsl(46 100% 84% / .78) 10%,
    hsl(43 100% 76% / .52) 32%,
    hsl(40 100% 70% / .26) 60%,
    hsl(38 100% 66% / .08) 84%,
    transparent 100%);
  filter: blur(9px);
  /* brighter while facing us, faint while behind the tower. Same timeline
     as the rotation; beam B's is offset by exactly half a turn. */
  animation: lh-cone-fade var(--beam-t) linear infinite;
}
.lh-arm-b .lh-cone { animation-delay: calc(var(--beam-t) / -2); }
@keyframes lh-cone-fade {
  0%   { opacity: .75; }   /* sideways */
  25%  { opacity: 1; }     /* toward the viewer */
  50%  { opacity: .75; }   /* sideways */
  66%  { opacity: .3; }
  75%  { opacity: .18; }   /* behind the tower */
  84%  { opacity: .3; }
  100% { opacity: .75; }
}
/* a soft glare on the lamp whenever either beam faces the viewer
   (A at 25%, B at 75% of the turn) */
.lh-flare {
  position: absolute;
  left: 59.5%;
  top: 26.2%;
  width: 34%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, hsl(48 100% 90% / .75), hsl(44 100% 72% / .28) 38%, transparent 72%);
  pointer-events: none;
  opacity: 0;
  animation: lh-flare var(--beam-t) linear infinite;
}
@keyframes lh-flare {
  0%, 15%, 35%, 65%, 85%, 100% { opacity: 0; }
  25%, 75% { opacity: .8; }
}
@media (max-width: 640px) {
  .lh-beam { --beam-l: 92vmax; --beam-h: 28vmax; }
  .lh-cone { filter: blur(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .lh-beam-rot { animation: none; transform: rotate(160deg); }
  .lh-cone { animation: none; opacity: .6; }
  .lh-flare { animation: none; opacity: 0; }
}

/* =====================================================================
   THE WORLD IS THE UI
   Journey state is read from the scene itself: completed destinations
   glow warm, the current one is the only marker that shows progress,
   future ones recede into the haze, and a dotted sea route links them.
   Everything below is class-driven (paintProgress / JOURNEY.paintScene)
   and composited — no new loops, no backdrop-filter over the sea.
   ===================================================================== */

/* ---- island markers: a light map pin, not a dashboard box ---- */
.island-label {
  min-width: 0;
  padding: .1rem .25rem;
  gap: 4px;
  align-items: center;
  font: 600 0.792rem/1.15 var(--font);
  letter-spacing: .02em;
  color: hsl(36 30% 86% / .8);
  background: none;
  border: 0;
  border-radius: 8px;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  text-shadow: 0 1px 2px hsl(250 45% 6% / .95), 0 0 10px hsl(250 45% 8% / .85);
}
/* hairline pin dropping from the marker to its island */
.island .island-label::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  width: 1px;
  height: 7px;
  background: linear-gradient(currentColor, transparent);
  opacity: .5;
}
.isle-dot { width: 6px; height: 6px; background: hsl(225 20% 66% / .6); box-shadow: none; }
/* only the current destination carries numbers */
.isle-progress, .isle-bar { display: none; }

/* completed: warm and lit */
.island.done .island-label, .headland.done .island-label,
.island.lit .island-label, .headland.lit .island-label { color: hsl(var(--l-hot)); }
.island.done .isle-dot, .headland.done .isle-dot {
  background: hsl(46 100% 72%);
  box-shadow: 0 0 8px hsl(42 100% 62%);
}

/* current destination */
.island.active .island-label, .headland.active .island-label {
  opacity: 1;
  color: hsl(var(--foreground));
  box-shadow: none;
}
.island.active .isle-dot, .headland.active .isle-dot {
  background: hsl(44 100% 72%);
  box-shadow: 0 0 0 3px hsl(44 100% 70% / .2), 0 0 10px hsl(42 100% 62% / .85);
}
.island.active .isle-progress, .headland.active .isle-progress {
  display: block;
  font-size: 0.682rem;
  color: hsl(var(--l-hot) / .95);
  font-variant-numeric: tabular-nums;
}
.island.active .isle-bar, .headland.active .isle-bar {
  display: block;
  width: 64px;
  height: 2px;
  background: hsl(220 30% 84% / .2);
}

/* future: quiet */
.island.locked .island-label, .headland.locked .island-label { opacity: .55; }

/* tapped / keyboard-focused marker */
.island.inspecting .island-label,
.headland.inspecting .island-label { color: hsl(var(--l-hot)); box-shadow: none; opacity: 1; }
@media (hover: hover) {
  .island:hover .island-label,
  .headland .island-label:hover { opacity: 1; color: hsl(var(--l-hot)); }
}

/* ---- future destinations sit farther off, in the night haze ---- */
.island.locked:not(.next):not(.focused):not(.lit) .island-art {
  filter: blur(calc((1 - var(--iscale)) * 1.7px + .35px)) saturate(.5) brightness(.76);
}

/* ---- completed destinations: a few warm motes drift up off the island.
   One pseudo-element per lit island (max 3), transform + opacity only. ---- */
.island::after {
  content: "";
  position: absolute;
  left: 18%; right: 18%;
  bottom: 22%;
  height: 78%;
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(circle at 18% 82%, hsl(48 100% 86%) 0 1.3px, transparent 2px),
    radial-gradient(circle at 46% 62%, hsl(44 100% 80%) 0 1px, transparent 1.6px),
    radial-gradient(circle at 74% 86%, hsl(48 100% 88%) 0 1.4px, transparent 2px),
    radial-gradient(circle at 62% 38%, hsl(42 100% 78%) 0 .9px, transparent 1.4px),
    radial-gradient(circle at 30% 30%, hsl(46 100% 84%) 0 1px, transparent 1.5px);
}
.island.lit::after { animation: isle-motes 6.5s ease-in-out infinite; }
@keyframes isle-motes {
  0%   { opacity: 0;  transform: translateY(14%); }
  35%  { opacity: .9; }
  100% { opacity: 0;  transform: translateY(-24%); }
}

/* ---- the sea route ---- */
.sea-route { position: absolute; inset: 0; z-index: 2; pointer-events: none; }   /* on the sea (1), under the islands */
.sea-route i {
  position: absolute;
  width: 5px;
  height: 4px;
  margin-left: -2.5px;
  border-radius: 50%;
  background: hsl(210 40% 90% / .34);
  transform: scale(var(--s, 1));
}
.sea-route i.is-done {
  background: hsl(46 100% 80%);
  box-shadow: 0 0 7px 1px hsl(40 100% 62% / .8);
}
.sea-route i.is-active {
  background: hsl(44 100% 80%);
  box-shadow: 0 0 5px hsl(40 100% 62% / .6);
  opacity: .25;
  animation: route-flow 2.6s ease-in-out infinite;
  animation-delay: calc(var(--j) * .3s);
}
@keyframes route-flow { 0%, 100% { opacity: .22; } 40% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .island.lit::after { animation: none; opacity: .6; }
  .sea-route i.is-active { animation: none; opacity: .7; }
}
@media (max-width: 640px) {
  .island-label { font-size: 0.726rem; }
  .island.active .isle-bar, .headland.active .isle-bar { width: 52px; }
}

/* ---- tapping a destination: a ring spreads on the water at its foot ---- */
.isle-ripple {
  position: absolute;
  left: 50%;
  bottom: 6%;
  width: 120%;
  aspect-ratio: 4 / 1;
  border-radius: 50%;
  border: 1.5px solid hsl(44 100% 80% / .8);
  box-shadow: 0 0 12px hsl(40 100% 62% / .45), inset 0 0 10px hsl(40 100% 62% / .3);
  transform: translate(-50%, 50%) scale(.35);
  opacity: 0;
  pointer-events: none;
}
/* the lighthouse's ring sits at the waterline in front of the cliff */
.headland > .isle-ripple { left: 42%; bottom: 2%; width: 60%; }
/* the panel follows the camera's first move instead of racing it */
.isle-card { animation-duration: .3s; animation-delay: .08s; }

/* ---- the boat invites you aboard: a quiet tag that rides with it ---- */
.boat-tag {
  position: absolute;
  left: 50%;
  /* just above the rider's head, never over it: the art scales from its
     bottom edge, and the swell lifts and tilts the head up to ~10% of the boat's height
     above the art's box (.boat-art paints past its viewBox) */
  bottom: calc(100% * var(--boat-scale) * 1.1 + 8px);
  transform: translateX(calc(-50% + var(--tag-dx, 0px)));   /* (--tag-dx: app.js › keepTagOnScreen) */
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .28rem .65rem;
  font: 600 0.814rem/1 var(--font);
  letter-spacing: .02em;
  white-space: nowrap;
  color: hsl(var(--l-hot));
  background: hsl(28 40% 9% / .62);
  border: 1px solid hsl(36 100% 62% / .3);
  border-radius: 999px;
  box-shadow: 0 0 14px hsl(36 100% 60% / .2);
  pointer-events: none;
  transition: opacity .6s ease, transform .3s ease;
}
.boat-tag-ico { font-size: .8rem; line-height: 1; opacity: .9; }
.boat:hover .boat-tag, .boat:focus-visible .boat-tag { transform: translateX(calc(-50% + var(--tag-dx, 0px))) translateY(-2px); }
/* once the Wish List has been opened the tag steps back */
.boat-lane.explored .boat-tag { opacity: .55; }
@media (max-width: 640px) { .boat-tag { font-size: .7rem; padding: .26rem .58rem; } }

/* ---- a tapped sky lantern lifts and brightens before its wish opens ---- */
.lantern-body { transition: transform .22s cubic-bezier(.2, .8, .2, 1), box-shadow .22s ease; }
.lantern.tapped .lantern-body {
  transform: translateY(-8px) scale(1.06);
  box-shadow: 0 0 34px hsl(var(--l-hot) / .9), 0 0 90px hsl(var(--l-mid) / .5), inset 0 -6px 12px hsl(var(--l-dark) / .45);
}
@media (prefers-reduced-motion: reduce) { .lantern.tapped .lantern-body { transform: none; } }

/* ---- phones: no backdrop blur over the moving sky ----
   Sky lanterns drift behind the topbar pills, the countdown and the wish
   bubble, so each backdrop-filter was re-blurred on every frame they moved.
   A slightly denser fill gives the same glassy read for free. */
@media (max-width: 640px) {
  .site-link, .guide-toggle, .lang-toggle, .cd-row, .wish-bubble {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .site-link, .guide-toggle, .lang-toggle { background: hsl(250 34% 12% / .62); }
  .cd-row { background: hsl(250 34% 12% / .55); }
  .wish-bubble { background: hsl(28 40% 10% / .8); }
}

/* =====================================================================
   STORY TYPOGRAPHY
   Cinematic title cards at meaningful moments (see STORY in app.js).
   Grammar borrowed from lyric-motion typography: a tiny index line, two
   hairline rules that sweep open, a light wide-tracked title whose letters
   rise out of a mask while drawing together (a letter-spacing settle done
   with transform, not layout), and a serif line beneath. Exit: the letters
   exhale outward and fade. Transform + opacity only; ≤ ~20 spans per card.
   ===================================================================== */
.story {
  position: fixed;
  left: 0;
  right: 0;
  top: 40%;
  /* above the scene and what stands in it — the title block (6), the Wish
     Guide on its island (7), the camera vignette (3) — so nothing passing
     under it while the camera moves can cover the words; below the controls
     (15) and every panel (20+) */
  z-index: 9;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .45rem;
  padding: 0 1rem;
  text-align: center;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  --st-rise: .55em;          /* how far each letter rises out of its mask */
  --st-spread: .22em;        /* letter-spacing "settle" distance, per letter from centre */
  --st-stagger: 55ms;
  --st-dur: 900ms;
}
.story.is-in { opacity: 1; visibility: visible; }
/* a soft pool of night behind the words so they read over lanterns */
.story::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: -1;
  width: min(92vw, 34rem);
  height: 180%;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side, hsl(240 40% 10% / .42), transparent);
  opacity: 0;
  transition: opacity 1.2s ease;
}
.story.is-in::before { opacity: 1; }

.story-idx {
  font: 500 .66rem/1 ui-monospace, "SF Mono", Menlo, monospace;
  letter-spacing: .32em;
  text-transform: uppercase;
  color: hsl(var(--l-hot) / .75);
  opacity: 0;
}
.story.is-in .story-idx { animation: st-fade .9s ease .15s forwards; }

.story-rule {
  display: block;
  width: min(62vw, 20rem);
  height: 1px;
  background: linear-gradient(90deg, transparent, hsl(var(--l-hot) / .75), transparent);
  transform: scaleX(0);
}
.story.is-in .story-rule { animation: st-rule 1.1s cubic-bezier(.6, 0, .2, 1) .1s forwards; }
.story.is-in .story-rule-b { animation-delay: .35s; }

.story-title {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  font: 300 clamp(1.87rem, 4.2vw, 2.86rem)/1.12 var(--font);
  letter-spacing: .26em;
  margin-right: -.26em;      /* balance the trailing tracking */
  color: hsl(var(--l-core));
  text-shadow: 0 0 22px hsl(var(--l-mid) / .45), 0 1px 3px hsl(250 45% 6% / .7);
}
/* outer: the mask (room above for Vietnamese diacritics) + the tracking settle */
.story-title .ch {
  display: inline-block;
  padding-top: .3em;
  margin-top: -.3em;
  clip-path: inset(-.4em -1em 0 -1em);
  transform: translateX(calc(var(--d) * var(--st-spread)));
}
.story-title .ch > span {
  display: inline-block;
  transform: translateY(var(--st-rise));
  opacity: 0;
}
.story.is-in .story-title .ch {
  animation: st-settle calc(var(--st-dur) + 500ms) cubic-bezier(.2, .7, .2, 1) calc(var(--i) * var(--st-stagger)) forwards;
}
.story.is-in .story-title .ch > span {
  animation: st-rise var(--st-dur) cubic-bezier(.2, .7, .2, 1) calc(var(--i) * var(--st-stagger)) forwards;
}

.story-line {
  max-width: 26rem;
  font: italic 400 clamp(0.99rem, 2.2vw, 1.188rem)/1.45 var(--font);
  letter-spacing: .02em;
  color: hsl(36 60% 90% / .92);
  text-shadow: 0 1px 10px hsl(250 45% 6% / .85);
  opacity: 0;
  transform: translateY(.45em);
}
.story.is-in .story-line {
  animation: st-line 1s cubic-bezier(.2, .7, .2, 1) calc(var(--n, 0) * var(--st-stagger) + 450ms) forwards;
}
/* a whisper is just the line, a little larger, and it arrives on its own */
.story.is-whisper .story-line { font-size: clamp(0.968rem, 2vw, 1.155rem); }
.story.is-in.is-whisper .story-line { animation-delay: .1s; animation-duration: 1.3s; }

/* exit: the letters exhale outward, everything fades up and away */
.story.is-out { transition: opacity .8s ease, visibility 0s linear .8s; opacity: 0; }
.story.is-out .story-title .ch { animation: st-exhale .8s cubic-bezier(.4, 0, .6, 1) forwards; }
.story.is-out .story-line { animation: st-away .8s ease forwards; }
.story.is-out .story-rule { animation: st-rule-out .7s cubic-bezier(.6, 0, .4, 1) forwards; }

@keyframes st-fade { to { opacity: 1; } }
@keyframes st-rule { to { transform: scaleX(1); } }
@keyframes st-rule-out { from { transform: scaleX(1); } to { transform: scaleX(0); opacity: 0; } }
@keyframes st-settle { to { transform: translateX(0); } }
@keyframes st-rise { to { transform: translateY(0); opacity: 1; } }
@keyframes st-line { to { transform: translateY(0); opacity: 1; } }
@keyframes st-exhale {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(var(--d) * var(--st-spread) * .6)) translateY(-.15em); }
}
@keyframes st-away {
  from { transform: translateY(0); opacity: 1; }
  to   { transform: translateY(-.35em); opacity: 0; }
}

/* phones: smaller type, shorter distances, quicker beats */
@media (max-width: 640px) {
  .story { --st-rise: .45em; --st-spread: .14em; --st-stagger: 40ms; --st-dur: 750ms; gap: .45rem; }
  .story-title { font-size: clamp(1.595rem, 7.4vw, 2.2rem); letter-spacing: .2em; margin-right: -.2em; }
  .story-idx { font-size: 0.66rem; letter-spacing: .26em; }
}

/* reduced motion: the card simply fades in and out, nothing travels */
@media (prefers-reduced-motion: reduce) {
  .story-title .ch, .story-title .ch > span, .story-line, .story-rule, .story-idx {
    transform: none !important;
    animation: none !important;
    opacity: 1 !important;
  }
  .story.is-in { transition: opacity .5s ease; }
  .story.is-out { transition: opacity .4s ease, visibility 0s linear .4s; }
}

/* never over a modal */
body.modal-open .story { opacity: 0 !important; }

/* =====================================================================
   CINEMATIC OPENING
   One veil over the whole world (a single backdrop-filter layer: soften,
   desaturate, dim, lower contrast + a vignette) so the moon and the big
   silhouettes still breathe through; the title is the only bright thing.
   Timeline (from .is-title): the wordmark rises line by line and its light
   blooms (transform and opacity only) ~0–1.5s · warm light crosses the
   letters ~1.5–2.6s · subtitle opens ~1.3–2.4s · hold · dissolve.
   ===================================================================== */
.opening[hidden] { display: none; }
.opening {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: 0 1.25rem;
  cursor: default;
  -webkit-tap-highlight-color: transparent;
}
.opening-veil {
  position: absolute;
  inset: 0;
  opacity: 0;
  -webkit-backdrop-filter: blur(6px) saturate(.6) brightness(.5) contrast(.86);
  backdrop-filter: blur(6px) saturate(.6) brightness(.5) contrast(.86);
  background:
    radial-gradient(ellipse 70% 55% at 50% 42%, hsl(250 40% 8% / .05), hsl(250 45% 5% / .45) 72%, hsl(250 50% 3% / .78));
  transition: opacity 1.25s cubic-bezier(.4, 0, .2, 1);   /* the world coming into focus */
}
.opening.is-dim .opening-veil { opacity: 1; transition-duration: .9s; }

.opening-type {
  --ot-y: -5vh;                         /* the open sky, above the horizon */
  position: relative;
  text-align: center;
  transform: translateY(var(--ot-y));
}

/* ---- title: the wordmark ----
   The user's art, one image per line, so it can sit on two lines or one.
   --u is one pixel of the source art on screen; every offset below is in
   those pixels. Each image has 44px of empty margin, where the glow spreads.
   The glow is a blurred copy baked into its own image: only its opacity and
   scale animate, so it costs a phone nothing while the page loads. */
.opening-title {
  position: relative;
  display: inline-block;
  margin: 0;
  --u: calc(min(86vw, 620px) / 589);
  --w: 589; --h: 219;                   /* two lines, as drawn */
  --x2: 75; --y2: 106;
}
@media (min-width: 761px) and (min-aspect-ratio: 4/3) {
  .opening-title {
    --u: calc(min(88vw, 1000px) / 1053);
    --w: 1053; --h: 119;                /* one line: baselines level */
    --x2: 539; --y2: 6;
  }
}
.ot-word {
  position: relative;
  display: block;
  width: calc(var(--u) * var(--w));
  height: calc(var(--u) * var(--h));
}
.ot-line {
  position: absolute;
  left: calc(var(--u) * -44);
  top: calc(var(--u) * -44);
  width: calc(var(--u) * 603);          /* 515 + the margins */
  height: calc(var(--u) * 207);         /* 119 + the margins */
  opacity: 0;
  transform: translateY(.9rem) scale(.975);
}
.ot-l2 {
  left: calc(var(--u) * (var(--x2) - 44));
  top: calc(var(--u) * (var(--y2) - 44));
  width: calc(var(--u) * 602);
  height: calc(var(--u) * 201);
}
.ot-line img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.ot-glow { opacity: 0; transform: scale(.98); }
.opening.is-title .ot-line { animation: ot-word 1.2s cubic-bezier(.16, 1, .3, 1) forwards; }
.opening.is-title .ot-l2 { animation-delay: .22s; }
.opening.is-title .ot-glow { animation: ot-bloom 3.4s ease-in-out .35s forwards; }
.opening.is-title .ot-l2 .ot-glow { animation-delay: .57s; }

/* one soft light crossing the letters: a gradient moved behind the art's own shape */
.ot-sweep {
  position: absolute;
  inset: 0;
  overflow: hidden;
  -webkit-mask: url("/art/ntdn-line1.png?v=1") center / 100% 100% no-repeat;
  mask: url("/art/ntdn-line1.png?v=1") center / 100% 100% no-repeat;
}
.ot-l2 .ot-sweep {
  -webkit-mask-image: url("/art/ntdn-line2.png?v=1");
  mask-image: url("/art/ntdn-line2.png?v=1");
}
.ot-sweep::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(112deg, transparent 22%, hsl(40 100% 78% / .7) 42%, hsl(0 0% 100%) 50%, hsl(40 100% 78% / .7) 58%, transparent 78%);
  transform: translateX(-100%);
}
.opening.is-title .ot-sweep::before { animation: ot-leak 2.1s cubic-bezier(.45, 0, .3, 1) 1.2s forwards; }
.opening.is-title .ot-l2 .ot-sweep::before { animation-delay: 1.6s; }
/* …and the wider, unclipped glow of that light crossing the frame */
.opening-title::after {
  content: "";
  position: absolute;
  top: -40%;
  bottom: -40%;
  left: 0;
  width: 42%;
  background: radial-gradient(closest-side, hsl(46 100% 72% / .3), hsl(38 100% 60% / .1) 55%, transparent);
  mix-blend-mode: screen;
  opacity: 0;
  transform: translateX(-70%);
  pointer-events: none;
}
.opening.is-title .opening-title::after { animation: ot-leak-glow 3s cubic-bezier(.45, 0, .3, 1) .9s forwards; }

/* a quiet halo of starlight around the words: two layers of a few points,
   twinkling out of phase (opacity + a tiny drift) — only while it plays */
.opening-type::before, .opening-type::after {
  content: "";
  position: absolute;
  inset: -55% -14%;
  pointer-events: none;
  opacity: 0;
}
.opening-type::before {
  background:
    radial-gradient(circle at 8% 30%, hsl(46 100% 92%) 0 1.2px, transparent 2px),
    radial-gradient(circle at 22% 84%, hsl(0 0% 100%) 0 1px, transparent 1.6px),
    radial-gradient(circle at 70% 12%, hsl(46 100% 90%) 0 1.4px, transparent 2.2px),
    radial-gradient(circle at 93% 64%, hsl(0 0% 100%) 0 1.1px, transparent 1.8px);
}
.opening-type::after {
  background:
    radial-gradient(circle at 16% 58%, hsl(0 0% 100%) 0 1px, transparent 1.6px),
    radial-gradient(circle at 48% 6%, hsl(46 100% 92%) 0 1.2px, transparent 2px),
    radial-gradient(circle at 84% 22%, hsl(0 0% 100%) 0 .9px, transparent 1.5px),
    radial-gradient(circle at 62% 92%, hsl(46 100% 90%) 0 1.2px, transparent 2px);
}
.opening.is-dim .opening-type::before { animation: ot-twinkle 2.4s ease-in-out .3s infinite; }
.opening.is-dim .opening-type::after  { animation: ot-twinkle 3.1s ease-in-out 1s infinite; }

/* ---- subtitle: gentler — opacity, a small rise, the spacing settles ---- */
.opening-sub {
  margin: .95em 0 0;
  font: italic 700 clamp(1.21rem, .9vw + 0.935rem, 1.76rem)/1.35 var(--font);
  letter-spacing: .16em;
  color: hsl(52 100% 88%);
  /* the same light as the wordmark, a dark edge so it holds over the bright moon */
  text-shadow: 0 0 12px hsl(46 100% 66% / .6), 0 0 30px hsl(42 100% 60% / .35), 0 1px 8px hsl(250 50% 4% / .55);
  opacity: 0;
  transform: translateY(10px);
}
.opening.is-title .opening-sub { animation: ot-sub .9s cubic-bezier(.2, .7, .2, 1) .65s forwards; }

/* ---- exit: soft-blur — the title drifts out of focus as the world sharpens ---- */
.opening.is-out .opening-type { animation: ot-unfocus .7s cubic-bezier(.4, 0, .2, 1) forwards; }
.opening.is-out .opening-type::before,
.opening.is-out .opening-type::after { animation: none; opacity: 0; transition: opacity .4s; }

@keyframes ot-word { to { opacity: 1; transform: none; } }
/* the light swells out of the letters, then rests a little lower */
@keyframes ot-bloom {
  0%   { opacity: 0;   transform: scale(.98); }
  40%  { opacity: 1;   transform: scale(1.02); }
  100% { opacity: .72; transform: scale(1); }
}
@keyframes ot-leak { to { transform: translateX(100%); } }
@keyframes ot-leak-glow {
  0%   { opacity: 0;  transform: translateX(-70%); }
  25%  { opacity: 1; }
  75%  { opacity: .8; }
  100% { opacity: 0;  transform: translateX(310%); }
}
@keyframes ot-twinkle {
  0%, 100% { opacity: .15; transform: translateY(0); }
  50%      { opacity: .85; transform: translateY(-3px); }
}
@keyframes ot-sub {
  from { opacity: 0; transform: translateY(10px); letter-spacing: .16em; }
  to   { opacity: 1; transform: translateY(0);    letter-spacing: .04em; }
}
@keyframes ot-sub-phone {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes ot-unfocus {
  from { opacity: 1; transform: translateY(var(--ot-y)) scale(1);     filter: blur(0); }
  to   { opacity: 0; transform: translateY(var(--ot-y)) scale(1.045); filter: blur(7px); }
}

/* the boat waits hidden under the veil until the route's first solve has
   placed it (app.js › firstLayout, run once the opening hands the world back) */
.boat-lane.is-solving .boat { opacity: 0; }

/* ---- nothing else competes while it plays ---- */
html.is-opening .shell,
html.is-opening .wish-caller,
html.is-opening .controls,
html.is-opening .island-label,
html.is-opening .boat-tag,
html.is-opening .story {
  opacity: 0 !important;
  pointer-events: none !important;
  transition: opacity .5s ease;
}
/* …and it drifts back in, top to bottom, as the world clears */
html.opening-return .shell { animation: ui-return 1.1s ease both; }
html.opening-return .island-label,
html.opening-return .boat-tag { animation: ui-return 1.1s ease .25s both; }
html.opening-return .wish-caller,
html.opening-return .controls { animation: ui-return 1.1s ease .45s both; }
@keyframes ui-return { from { opacity: 0; } }

/* PHONES: a lighter opening, built for what a phone can draw while the page
   is still loading (tested on an iPhone: the full version stalled there).
   - the veil is a plain dark wash, no live blur: the world keeps drifting
     underneath (lanterns, stars, moon) at no extra cost — only the WebGL sea
     holds still under it (app.js › OCEAN), it would compete with the load
   - the wordmark and its baked glow only move and fade; the light across
     the letters (a mask) is left out, the wider glow crossing the frame stays
   Same art and colours as on a larger screen. */
@media (max-width: 640px) {
  .opening-veil {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background:
      radial-gradient(ellipse 80% 60% at 50% 40%, hsl(250 40% 7% / .55), hsl(250 45% 5% / .72) 70%, hsl(250 50% 3% / .88));
  }
  .opening-type { --ot-y: 0px; }          /* dead centre on a phone */
  .ot-sweep { display: none; }
  .opening.is-title .ot-line { animation-duration: 1s; }
  .opening.is-title .ot-l2 { animation-delay: .15s; }
  .opening.is-title .ot-glow { animation-duration: 2.4s; animation-delay: .2s; }
  .opening.is-title .ot-l2 .ot-glow { animation-delay: .35s; }
  .opening.is-title .opening-title::after { animation: ot-leak-glow 2.4s cubic-bezier(.45, 0, .3, 1) .5s forwards; }
  /* the subtitle only rises and fades in (its wider starting spacing wrapped
     it onto two lines for a moment) */
  .opening .opening-sub { letter-spacing: .04em; }
  .opening.is-title .opening-sub { animation: ot-sub-phone .8s cubic-bezier(.2, .7, .2, 1) .5s forwards; }
  /* the exit: fade and a slight scale — no animated blur */
  .opening.is-out .opening-type { animation: ot-fade-out .6s cubic-bezier(.4, 0, .2, 1) forwards; }
}
@keyframes ot-fade-out {
  from { opacity: 1; transform: translateY(var(--ot-y)) scale(1); }
  to   { opacity: 0; transform: translateY(var(--ot-y)) scale(1.03); }
}

/* reduced motion: no per-letter movement — the title simply fades and settles */
@media (prefers-reduced-motion: reduce) {
  .ot-line { opacity: 1; transform: none; }
  .opening.is-title .ot-line { animation: none; }
  .opening.is-title .ot-glow { animation: st-fade .8s ease forwards; }
  .opening-title { opacity: 0; transition: opacity .8s ease; }
  .opening.is-title .opening-title { opacity: 1; }
  .opening.is-out .opening-title { opacity: 0; transition-duration: .4s; }
  .ot-sweep, .opening-title::after,
  .opening-type::before, .opening-type::after { display: none; }
  .opening.is-out .opening-type { animation: none; }
  .opening-sub { clip-path: none; transform: none; }
  .opening.is-title .opening-sub { animation: st-fade .6s ease .3s forwards; }
  .opening.is-out .opening-sub { animation: none; opacity: 0; transition: opacity .4s; }
}

/* =====================================================================
   WORLD-FIRST LAYOUT
   No header, no hero, no dashboard. The scene fills the screen; the only
   fixed UI is two quiet anchors — the date (top-left, opens the countdown)
   and the menu (bottom-right, by the thumb). Everything else is the world:
   the character (wish), the boat (gallery), lanterns (read), islands
   (milestones). None of these layers uses backdrop-filter at rest.
   ===================================================================== */
.shell {
  position: fixed;
  inset: 0;
  z-index: 6;
  min-height: 0;
  display: block;
  pointer-events: none;
}

/* ---- anchor 1: the date ---- */
.hud-date {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  left: calc(env(safe-area-inset-left, 0px) + 8px);
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  min-height: 44px;
  padding: 0 .75rem;
  font: 600 1.1rem/1 var(--font);
  letter-spacing: .14em;
  color: hsl(40 70% 90% / .9);
  background: none;
  border: 0;
  cursor: pointer;
  text-shadow: 0 0 14px hsl(36 100% 60% / .45), 0 1px 3px hsl(250 45% 6% / .8);
  -webkit-tap-highlight-color: transparent;
}
.hud-date-star {
  font-size: 0.792rem;
  color: hsl(var(--l-hot));
  animation: hud-twinkle 4.5s ease-in-out infinite;
}
@keyframes hud-twinkle { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
.hud-date:hover, .hud-date:focus-visible { color: hsl(var(--l-core)); }
.hud-date:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 2px; border-radius: 10px; }

/* the sky's one line of status — only when there is something to say */
.hint {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 64px);
  left: 1.5rem;
  right: 1.5rem;
  margin: 0;
  text-align: center;
  font: italic 400 0.968rem/1.45 var(--font);
  color: hsl(38 40% 88% / .78);
  text-shadow: 0 1px 10px hsl(250 45% 6% / .9);
}
.hint:empty { display: none; }
.dock {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 96px);
  left: 1rem;
  right: 1rem;
  margin: 0;
}

/* ---- anchor 2: the menu, where the thumb is ---- */
.menu-btn {
  position: fixed;
  right: calc(env(safe-area-inset-right, 0px) + 14px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 14px);
  z-index: 15;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid hsl(36 100% 70% / .22);
  background: hsl(250 34% 10% / .55);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .2s ease, background-color .2s ease;
}
.menu-btn svg { width: 20px; height: 20px; fill: none; stroke: hsl(var(--l-hot)); stroke-width: 1.8; stroke-linecap: round; }
.menu-btn:hover, .menu-btn[aria-expanded="true"] { border-color: hsl(36 100% 70% / .5); background: hsl(250 34% 12% / .8); }
.menu-btn:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }

.menu[hidden] { display: none; }
.menu {
  position: fixed;
  inset: 0;
  z-index: 25;
  background: hsl(250 40% 3% / .32);
  animation: menu-veil .2s ease both;
}
.menu-card {
  position: absolute;
  right: calc(env(safe-area-inset-right, 0px) + 12px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 72px);
  width: min(20.5rem, calc(100vw - 24px));
  max-height: calc(100dvh - 96px);
  overflow-y: auto;
  padding: .45rem;
  outline: none;
  color: hsl(var(--foreground));
  background:
    radial-gradient(120% 50% at 80% 100%, hsl(38 100% 60% / .1), transparent 70%),
    linear-gradient(180deg, hsl(250 30% 12% / .97), hsl(262 30% 8% / .98));
  border: 1px solid hsl(36 100% 62% / .18);
  border-radius: 20px;
  box-shadow: 0 18px 50px -16px hsl(250 60% 2% / .9);
  transform-origin: 100% 100%;
  animation: menu-in .24s cubic-bezier(.2, .8, .2, 1) both;
}
.menu.closing { animation: menu-veil-out .2s ease both; }
.menu.closing .menu-card { animation: menu-out .2s ease both; }
@keyframes menu-veil { from { opacity: 0; } }
@keyframes menu-veil-out { to { opacity: 0; } }
@keyframes menu-in { from { opacity: 0; transform: translateY(10px) scale(.96); } }
@keyframes menu-out { to { opacity: 0; transform: translateY(8px) scale(.97); } }

/* the journey, told in one card: leg, raised, lanterns */
.menu-journey {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .18rem;
  width: 100%;
  padding: .85rem 2.2rem .85rem .95rem;
  text-align: left;
  font: inherit;
  color: inherit;
  background: linear-gradient(135deg, hsl(36 80% 40% / .18), hsl(262 40% 20% / .1));
  border: 1px solid hsl(36 100% 62% / .16);
  border-radius: 14px;
  cursor: pointer;
}
.menu-journey:hover, .menu-journey:focus-visible { border-color: hsl(36 100% 62% / .45); outline: none; }
.mj-kicker { font-size: 0.682rem; font-weight: 700; letter-spacing: .22em; text-transform: uppercase; color: hsl(var(--l-hot) / .8); }
.mj-stage { font: 600 1.122rem/1.3 var(--font); color: hsl(var(--l-core)); }
.menu-journey .raised { margin: .1rem 0 0; font-size: 0.935rem; font-weight: 700; text-shadow: none; }
.menu-journey .count { margin: 0; min-height: 0; font-size: 0.836rem; font-weight: 500; letter-spacing: .02em; color: hsl(var(--muted-foreground)); }
.menu-journey .count:empty { display: none; }
.mj-go { position: absolute; right: .9rem; top: 50%; transform: translateY(-50%); font-size: 1.54rem; color: hsl(var(--l-hot) / .7); }

.menu-list { display: flex; flex-direction: column; margin-top: .3rem; }
.menu .menu-item {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: .8rem;
  width: 100%;
  height: auto;
  min-height: 48px;
  padding: 0 .95rem;
  font: 500 1.045rem/1.2 var(--font);
  color: hsl(var(--foreground) / .92);
  text-align: left;
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: 12px;
  box-shadow: none;
  opacity: 1;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  cursor: pointer;
}
.menu .menu-item:hover, .menu .menu-item:focus-visible { background: hsl(36 100% 60% / .08); outline: none; color: hsl(var(--l-core)); }
.mi-ico { display: grid; place-items: center; width: 22px; font-size: 1rem; color: hsl(var(--l-hot)); }
.mi-ico svg { width: 18px; height: 18px; fill: none; stroke: hsl(var(--l-hot)); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.mi-val { margin-left: auto; font-size: 0.858rem; font-weight: 700; letter-spacing: .1em; color: hsl(var(--l-hot)); }
.menu #music-toggle.playing { opacity: 1; }
.menu #music-toggle.playing::after,
.menu #sound-toggle:not(.muted)::after {
  content: "";
  margin-left: auto;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: hsl(46 100% 70%);
  box-shadow: 0 0 8px hsl(42 100% 62%);
}

/* ---- focused countdown: the world dims, only time remains ---- */
.cdv[hidden] { display: none; }
.cdv {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: 1.5rem;
}
.cdv-veil {
  position: absolute;
  inset: 0;
  opacity: 0;
  -webkit-backdrop-filter: blur(6px) saturate(.6) brightness(.45) contrast(.86);
  backdrop-filter: blur(6px) saturate(.6) brightness(.45) contrast(.86);
  background: radial-gradient(ellipse 70% 55% at 50% 45%, hsl(250 40% 8% / .1), hsl(250 45% 4% / .6) 72%, hsl(250 50% 3% / .82));
  transition: opacity .7s cubic-bezier(.4, 0, .2, 1);
}
.cdv.is-on .cdv-veil { opacity: 1; }
.cdv-body {
  position: relative;
  text-align: center;
  outline: none;
  opacity: 0;
  transform: translateY(12px) scale(.98);
  transition: opacity .6s ease .12s, transform .8s cubic-bezier(.16, 1, .3, 1) .12s;
}
.cdv.is-on .cdv-body { opacity: 1; transform: none; }
.cdv.closing .cdv-body { transition-delay: 0s; transition-duration: .35s; }
.cdv-kicker { margin: 0; font-size: 0.726rem; font-weight: 700; letter-spacing: .3em; text-transform: uppercase; color: hsl(var(--l-hot) / .8); }
.cdv-date {
  margin: .45rem 0 1.6rem;
  font: 600 clamp(1.87rem, 3vw + 1.1rem, 3.3rem)/1 var(--font);
  letter-spacing: .12em;
  margin-right: -.12em;
  color: hsl(40 78% 93%);
  text-shadow: 0 0 34px hsl(36 100% 62% / .35);
}
.cdv .countdown { margin: 0; }
.cdv .cd-row {
  display: flex;
  justify-content: center;
  gap: clamp(1.4rem, 5vw, 3rem);
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.cdv .cd-cell { gap: .45rem; min-width: 3ch; }
.cdv .cd-num {
  font: 600 clamp(2.64rem, 5vw + 1.1rem, 4.84rem)/1 var(--font);
  font-variant-numeric: lining-nums tabular-nums;
  color: hsl(46 90% 92%);
  text-shadow: 0 0 22px hsl(36 100% 60% / .4);
}
.cdv .cd-lbl { font-size: 0.66rem; letter-spacing: .24em; color: hsl(var(--muted-foreground) / .85); }
.cdv-note {
  max-width: 24rem;
  margin: 1.6rem auto 0;
  font: italic 400 1.078rem/1.5 var(--font);
  color: hsl(38 45% 86% / .8);
}
.cdv-close {
  margin-top: 1.4rem;
  min-height: 44px;
  padding: 0 1.3rem;
  font: 600 0.935rem/1 var(--font);
  color: hsl(var(--muted-foreground));
  background: none;
  border: 1px solid hsl(36 100% 62% / .25);
  border-radius: 999px;
  cursor: pointer;
}
.cdv-close:hover, .cdv-close:focus-visible { color: hsl(var(--l-core)); border-color: hsl(36 100% 62% / .6); outline: none; }
@media (max-width: 640px) {
  .cdv-veil {
    -webkit-backdrop-filter: blur(3px) saturate(.6) brightness(.45) contrast(.86);
    backdrop-filter: blur(3px) saturate(.6) brightness(.45) contrast(.86);
  }
}

/* ---- islands speak through light, not labels ----
   The name appears only when pointed at or focused; the numbers live in
   the island panel and the menu's journey card. */
.islands .island .island-label,
.scene .headland .island-label {
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s ease;
}
.islands .island:hover .island-label,
.islands .island:focus-visible .island-label,
.scene .headland:focus-visible .island-label { opacity: .95; }
.isle-progress, .isle-bar { display: none !important; }

/* ---- the character speaks now and then, never constantly ---- */
.wish-caller .wish-bubble {
  opacity: 0;
  transform: translateY(5px) scale(.94);
  transform-origin: 50% 100%;
  box-shadow: 0 0 16px hsl(36 100% 60% / .25);
  animation: caller-say 24s ease-in-out 7s infinite;
}
@keyframes caller-say {
  0%       { opacity: 0; transform: translateY(5px) scale(.94); }
  3%, 25%  { opacity: 1; transform: none; }
  29%, 100% { opacity: 0; transform: translateY(5px) scale(.94); }
}
.wish-caller:hover .wish-bubble,
.wish-caller:focus-visible .wish-bubble { animation: none; opacity: 1; transform: none; }

/* ---- the boat: a first-visit hint, and an answer to the tap ---- */
.boat-tag { opacity: 0; transform: translateX(calc(-50% + var(--tag-dx, 0px))) translateY(4px); }
.boat-lane.hinting .boat-tag { opacity: 1; transform: translateX(calc(-50% + var(--tag-dx, 0px))); }
.boat-lane.explored .boat-tag { opacity: 0; }
.boat .boat-art { transition: filter .25s ease; }
.boat-lane.hinting .boat-art,
.boat.answer .boat-art {
  filter: drop-shadow(0 0 8px hsl(var(--l-hot) / .75)) drop-shadow(0 3px 6px hsl(222 60% 8% / .35));
}

/* ---- phones: the gallery is a place, not a boxed card ---- */
@media (max-width: 640px) {
  .logbook { padding: 0; background: hsl(250 40% 4%); }
  .logbook-card {
    max-width: none;
    height: 100dvh;
    padding: calc(env(safe-area-inset-top, 0px) + 1.1rem) 1rem calc(env(safe-area-inset-bottom, 0px) + .7rem);
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background:
      radial-gradient(1px 1px at 12% 8%, hsl(45 100% 90% / .6), transparent 60%),
      radial-gradient(1px 1px at 84% 14%, hsl(45 100% 90% / .45), transparent 60%),
      radial-gradient(1.4px 1.4px at 62% 4%, hsl(45 100% 90% / .55), transparent 60%),
      radial-gradient(120% 40% at 50% 0%, hsl(34 70% 30% / .35), transparent 70%),
      linear-gradient(180deg, hsl(248 36% 11%), hsl(262 34% 6%));
  }
}

/* ---- the opening hides the anchors too, then lets them drift back ---- */
html.is-opening .menu-btn { opacity: 0 !important; pointer-events: none !important; transition: opacity .5s ease; }
html.opening-return .menu-btn { animation: ui-return 1.1s ease .45s both; }
/* with the hero gone, the sea can claim more of the phone screen: the horizon
   rises from ~78% to ~70% so the world, not the sky, carries the composition */
@media (max-width: 640px) {
  /* 33vh (was 29vh): the smallest sea in which the boat has real water
     beside every island AND a passage between them on a 375×812 phone —
     measured by the journey's water graph, not guessed */
  :root { --sea-h: clamp(150px, 33vh, 290px); }
}

/* =====================================================================
   REFINEMENT — title in the sky, time in the corner, sound by the thumb
   Hierarchy: 1 birthday title · 2 the world · 3 countdown + audio ·
   4 menu (secondary only). Still no backdrop-filter on any of these.
   ===================================================================== */

/* ---- 1. the birthday title, centred in the open sky ---- */
.bday {
  position: fixed;
  left: 0;
  right: 0;
  top: 24vh;
  padding: 0 1.25rem;
  text-align: center;
  pointer-events: none;
}
/* the night gathers a little around the words: a soft local shade (no
   backdrop blur) that deepens as the title arrives, then rests faintly */
.bday::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: -1;
  width: min(94vw, 40rem);
  height: 190%;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side, hsl(245 42% 9% / .5), hsl(245 42% 9% / .18) 60%, transparent);
  opacity: 0;
  pointer-events: none;
}
html.world-ready .bday::before { animation: bday-shade 1.4s ease forwards; }

/* ---- CHÚC MỪNG SINH NHẬT: first, gently ---- */
.bday-eyebrow {
  --eb-ls: .16em;
  margin: 0 0 var(--hero-gap, .25rem);
  font-size: 1.155rem;
  font-weight: 700;
  letter-spacing: var(--eb-ls);
  margin-right: calc(var(--eb-ls) * -1);
  text-transform: uppercase;
  color: hsl(var(--l-hot) / .88);
  text-shadow: 0 1px 10px hsl(250 45% 6% / .8);
  opacity: 0;
  transform: translateY(8px);
}
html.world-ready .bday-eyebrow { animation: bday-eyebrow .9s cubic-bezier(.16, 1, .3, 1) .3s forwards; }

/* ---- UPRIZE PN: the main moment — revealed from the centre outward ----
   one mask (clip-path) opening both ways, a slight scale 0.96 → 1, the
   tracking settling in, opacity rising. The word moves as one piece. */
/* The word is the user's font, Wano Quin with its swash R (R.ss01), thickened
   for bold and saved as public/art/uprize-pn.png, used
   as a mask: the fill and its light live on ::after, so the glow on the title
   itself still surrounds the letters. The real text stays for screen readers. */
.bday-title {
  position: relative;
  display: block;                       /* no line box around it: the gaps above and below are the margins */
  margin: 0 auto;
  --t-h: min(clamp(3.2rem, 6.4vw + 1.3rem, 5.6rem), calc(86vw / 4.6903));
  width: calc(var(--t-h) * 4.6903);
  height: var(--t-h);
  font-size: 0;
  background: none;
  opacity: 0;
  transform: scale(.96);
  clip-path: inset(-120% 50% -120% 50%);
  /* two glows so the breathing can interpolate (same filter list always) */
  filter: drop-shadow(0 0 7px hsl(42 100% 70% / .16)) drop-shadow(0 0 30px hsl(32 100% 50% / .38));
}
/* After the reveal (+ the one strong leak below) the title never rests:
   the band crosses the letters left → right every 6.5s, and the glow
   breathes on its own 4.6s beat. Only the light moves — never the word. */
html.world-ready .bday-title {
  animation:
    bday-open    .75s cubic-bezier(.33, 0, .15, 1) .7s forwards,
    bday-track   1.1s cubic-bezier(.16, 1, .3, 1) .7s both,
    bday-glow    4.6s ease-in-out 2.2s infinite;
}

/* the letters' fill. Two layers:
     1) the shimmer: a narrow warm-cream band, transparent elsewhere, 3x the
        word's width so it starts fully off the left edge and ends off the right;
     2) the base title gradient, deepened so the band reads as light. */
.bday-title::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(105deg, transparent 42%, hsl(42 100% 78% / .55) 46%, hsl(48 100% 96%) 50%,
                    hsl(42 100% 78% / .55) 54%, transparent 58%),
    linear-gradient(135deg, hsl(43 100% 82%), hsl(34 100% 64%) 55%, hsl(22 92% 52%));
  background-size: 300% 100%, 100% 100%;
  background-repeat: no-repeat;
  background-position: 88% 0, 0 0;
  -webkit-mask: url("/art/uprize-pn.png?v=2") center / 100% 100% no-repeat;
  mask: url("/art/uprize-pn.png?v=2") center / 100% 100% no-repeat;
  pointer-events: none;
}
html.world-ready .bday-title::after { animation: bday-shimmer 6.5s linear 1.5s infinite; }

/* a few distant stars catch that light — sparse, once, then gone */
.bday-title::before {
  content: "";
  position: absolute;
  inset: -55% -16%;
  background:
    radial-gradient(circle at 6% 30%,  hsl(44 100% 88%) 0 1.3px, transparent 2.2px),
    radial-gradient(circle at 30% 2%,  hsl(0 0% 100%)   0 1px,   transparent 1.8px),
    radial-gradient(circle at 71% 96%, hsl(44 100% 86%) 0 1.2px, transparent 2px),
    radial-gradient(circle at 95% 22%, hsl(0 0% 100%)   0 1.4px, transparent 2.3px),
    radial-gradient(circle at 88% 80%, hsl(40 100% 80%) 0 .9px,  transparent 1.6px);
  opacity: 0;
  pointer-events: none;
}
/* …and afterwards, very occasionally, ONE distant star catches the light
   somewhere on the word — three fixed spots, one at a time, long gaps */
html.world-ready .bday-title::before {
  animation:
    bday-spark   .8s ease-out 1.65s forwards,
    bday-twinkle 23s linear 7s infinite;
}

@keyframes bday-shade { to { opacity: 1; } }
@keyframes bday-eyebrow {
  from { opacity: 0; transform: translateY(8px); letter-spacing: calc(var(--eb-ls) + .16em); }
  to   { opacity: 1; transform: none;            letter-spacing: var(--eb-ls); }
}
@keyframes bday-open {
  0%   { opacity: 0; clip-path: inset(-120% 50% -120% 50%); }
  35%  { opacity: 1; }
  100% { opacity: 1; clip-path: inset(-120% -30% -120% -30%); }
}
@keyframes bday-track {
  from { transform: scale(.96); }
  to   { transform: scale(1); }
}
@keyframes bday-spark {
  0%   { opacity: 0;  transform: scale(.94); }
  45%  { opacity: 1; }
  100% { opacity: 0;  transform: scale(1); }
}

/* ---- the ongoing life of the title (after the intro) ----
   shimmer: a steady left → right glide for ~5.1s (0 → 78%), then ~1.4s
   fully off the right edge before it re-enters from the left.
   88% → 12% on a 300%-wide layer = band centre from −0.26 to 1.26 text
   widths — just outside at both ends (the band's half-width is 0.24),
   so the restart is never seen; ~3.4s of each pass is on the letters. */
@keyframes bday-shimmer {
  0%   { background-position: 88% 0, 0 0; }
  78%  { background-position: 12% 0, 0 0; }
  100% { background-position: 12% 0, 0 0; }
}
/* breathing glow: soft → a little brighter → soft */
@keyframes bday-glow {
  0%, 100% { filter: drop-shadow(0 0 7px hsl(42 100% 70% / .16)) drop-shadow(0 0 30px hsl(32 100% 50% / .38)); }
  50%      { filter: drop-shadow(0 0 10px hsl(42 100% 72% / .42)) drop-shadow(0 0 38px hsl(34 100% 55% / .62)); }
}
/* one star at a time: position only changes while it is invisible */
@keyframes bday-twinkle {
  0%, 100% {
    background-image: radial-gradient(circle, hsl(46 100% 94%) 0 1.1px, hsl(42 100% 76% / .45) 1.9px, hsl(40 100% 70% / .12) 3.6px, transparent 6px);
    background-size: 14px 14px;
    background-repeat: no-repeat;
  }
  0%     { opacity: 0;   background-position: 26% 30%; }
  2.5%   { opacity: .85; background-position: 26% 30%; }
  6%     { opacity: 0;   background-position: 26% 30%; }
  6.1%   { opacity: 0;   background-position: 79% 70%; }
  40%    { opacity: 0;   background-position: 79% 70%; }
  42.5%  { opacity: .7;  background-position: 79% 70%; }
  46%    { opacity: 0;   background-position: 79% 70%; }
  46.1%  { opacity: 0;   background-position: 57% 27%; }
  74%    { opacity: 0;   background-position: 57% 27%; }
  76.5%  { opacity: .8;  background-position: 57% 27%; }
  80%    { opacity: 0;   background-position: 57% 27%; }
  100%   { opacity: 0;   background-position: 57% 27%; }
}

.bday .hint {
  position: static;
  max-width: 30rem;
  margin: .9rem auto 0;
}
.dock { top: auto; bottom: calc(env(safe-area-inset-bottom, 0px) + 78px); }

/* ---- 2. the countdown, always running, top-right ---- */
.hud-date {
  left: auto;
  right: calc(env(safe-area-inset-right, 0px) + 8px);
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: .3rem;
  padding: .45rem .7rem;
}
.hud-date-top { display: inline-flex; align-items: center; gap: .4rem; }
.hud-cd {
  font: 600 0.836rem/1 var(--font);
  letter-spacing: .05em;
  font-variant-numeric: tabular-nums;
  color: hsl(38 45% 90% / .85);
  text-shadow: 0 1px 8px hsl(250 45% 6% / .9);
}
.hud-cd:empty { display: none; }

/* ---- 3. sea sound + music, beside the menu, one tap each ---- */
.controls {
  right: calc(env(safe-area-inset-right, 0px) + 72px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
  gap: .5rem;
}
.controls .ctrl {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  opacity: 1;
  background: hsl(250 34% 10% / .5);
  border: 1px solid hsl(36 100% 70% / .18);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.controls .ctrl svg { width: 17px; height: 17px; }
.controls .ctrl:hover, .controls .ctrl:focus-visible { background: hsl(250 34% 13% / .8); border-color: hsl(36 100% 70% / .45); }
/* state reads at a glance: muted sea is dim, playing music glows */
.controls #sound-toggle.muted { opacity: .62; }
.controls #music-toggle.playing {
  opacity: 1;
  border-color: hsl(42 100% 66% / .65);
  box-shadow: 0 0 14px hsl(38 100% 58% / .35);
}

/* ---- 4. the menu: secondary only, and lighter ---- */
.menu-card { width: min(16.5rem, calc(100vw - 24px)); padding: .35rem; border-radius: 18px; }
.menu-journey { padding: .65rem 2rem .65rem .8rem; border-radius: 12px; }
.mj-stage { font-size: 1.012rem; }
.menu .menu-item { min-height: 44px; padding: 0 .8rem; font-size: 0.99rem; }

/* ---- the lighthouse speaks like the islands: same label, on hover ---- */
@media (hover: hover) {
  .scene .headland:has(.tower:hover, .cliff:hover, .lamp-room:hover, .gallery:hover, .stair:hover, .dome:hover) .island-label {
    opacity: .95;
  }
}

/* the opening hides these too, then lets them drift back */
html.is-opening .bday { opacity: 0; }

@media (max-width: 640px) {
  .bday { top: 21vh; }
  .bday-eyebrow { --eb-ls: .14em; font-size: 1.045rem; }
}

@media (prefers-reduced-motion: reduce) {
  .bday-eyebrow, .bday-title { transform: none; clip-path: none; }
  html.world-ready .bday-eyebrow, html.world-ready .bday-title { animation: bday-fade .8s ease forwards; }
  .bday-title::before { display: none; }
  html.world-ready .bday-title::after { animation: none; }
}
@keyframes bday-fade { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
}

/* =====================================================================
   THE ARRIVAL CARD — an island introduces itself while the boat is moored
   ===================================================================== */
.isle-arrival {
  --tail: 50%;
  position: fixed;
  z-index: 40;
  width: min(16rem, calc(100vw - 24px));
  padding: .7rem .9rem .65rem;
  border-radius: 16px;
  color: hsl(38 45% 92%);
  background:
    radial-gradient(120% 90% at 50% 0%, hsl(40 100% 62% / .14), transparent 60%),
    hsl(250 36% 11% / .82);
  border: 1px solid hsl(40 100% 72% / .28);
  box-shadow: 0 14px 38px hsl(250 60% 4% / .55), 0 0 26px hsl(38 100% 58% / .16);
  text-align: left;
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px) scale(.97);
  transform-origin: var(--tail) 100%;
  transition: opacity .45s ease, transform .55s cubic-bezier(.16, 1, .3, 1);
  pointer-events: none;
}
.isle-arrival.is-in { opacity: 1; transform: none; pointer-events: auto; }
body.modal-open .isle-arrival { opacity: 0; pointer-events: none; }
/* a small tail toward the island */
.isle-arrival::after {
  content: "";
  position: absolute;
  width: 12px; height: 12px;
  background: hsl(250 36% 11% / .82);
  border: 1px solid hsl(40 100% 72% / .28);
  border-width: 0 1px 1px 0;
  left: calc(var(--tail) - 6px);
  bottom: -7px;
  transform: rotate(45deg);
}
.isle-arrival[data-side="left"]::after  { left: auto; right: -7px; bottom: auto; top: 40%; transform: rotate(-45deg); }
.isle-arrival[data-side="right"]::after { left: -7px; bottom: auto; top: 40%; transform: rotate(135deg); }
.isle-arrival[data-side="below"]::after { bottom: auto; top: -7px; transform: rotate(-135deg); }
.isle-arrival[data-side="below"] { transform-origin: var(--tail) 0; }
.isle-arrival[data-side="left"]  { transform-origin: 100% 40%; }
.isle-arrival[data-side="right"] { transform-origin: 0 40%; }

.ia-kicker {
  margin: 0;
  font-size: 0.704rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: hsl(var(--l-hot) / .9);
}
.ia-name {
  margin: .1rem 0 .4rem;
  font-size: 1.122rem;
  font-weight: 700;   
  line-height: 1.2;
  color: hsl(44 90% 90%);
}
.ia-nums { display: flex; gap: .9rem; margin: 0; }
.ia-nums div { min-width: 0; }
.ia-nums dt { font-size: 0.704rem; letter-spacing: .06em; color: hsl(38 30% 78% / .75); }
.ia-nums dd { margin: .05rem 0 0; font-size: 0.946rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ia-prog { display: flex; align-items: center; gap: .5rem; margin: .5rem 0 0; }
.ia-bar {
  flex: 1;
  height: 5px;
  border-radius: 99px;
  background: hsl(250 30% 26% / .8);
  overflow: hidden;
}
.ia-bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, hsl(34 100% 58%), hsl(46 100% 76%));
  box-shadow: 0 0 8px hsl(40 100% 62% / .7);
  transition: width 1.1s cubic-bezier(.16, 1, .3, 1) .25s;
}
.isle-arrival:not(.is-in) .ia-bar i { width: 0 !important; }
.ia-pct { font-size: 0.792rem; font-weight: 700; color: hsl(var(--l-hot) / .95); font-variant-numeric: tabular-nums; }
.ia-for {
  margin: .45rem 0 0;
  font-size: 0.814rem;
  line-height: 1.35;
  color: hsl(38 40% 88% / .9);
}
.ia-for-l { display: block; color: hsl(38 30% 78% / .7); }
.ia-items { margin: .1rem 0 0; padding: 0; list-style: none; }
.isle-arrival.is-compact .ia-for { display: none; }   /* tight spot: the panel (tap) has the items */
@media (max-width: 480px) {
  .isle-arrival { padding: .55rem .75rem .5rem; }
  .ia-name { margin-bottom: .3rem; font-size: 1.045rem; }
  .ia-prog { margin-top: .4rem; }
  .ia-for { margin-top: .35rem; }
}
.ia-items li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (hover: hover) {
  .isle-arrival.is-in:hover { border-color: hsl(40 100% 72% / .5); }
}
@media (prefers-reduced-motion: reduce) {
  .isle-arrival { transition: opacity .3s ease; transform: none; }
}

/* ============================================================
   BOAT POV — "ride the boat" (POV in app.js)
   The same scene, seen from the boat: app.js projects the islands,
   lighthouse, moon and lotus lanterns each frame (transform / opacity
   only). These rules just switch the scene's furniture for the ride.
   ============================================================ */
.pov-btn[hidden] { display: none !important; }
.pov-btn {
  position: fixed;
  /* top-left, beside the journey map button: the two ways of seeing the
     voyage, kept out of the sea (the boat moors along its lower edge) */
  left: calc(env(safe-area-inset-left, 0px) + 68px);
  top: calc(env(safe-area-inset-top, 0px) + 16px);
  z-index: 15;
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  min-height: 44px;
  /* a fixed width: the journey keeps its moorings clear of this button,
     measured before its label is written */
  width: 8.6rem;
  justify-content: center;
  white-space: nowrap;
  overflow: hidden;
  padding: 0 .8rem;
  border-radius: 999px;
  font: 600 0.88rem/1 var(--font);
  letter-spacing: .02em;
  color: hsl(40 70% 88%);
  background: hsl(250 34% 10% / .55);
  border: 1px solid hsl(36 100% 70% / .22);
  box-shadow: 0 6px 22px -10px hsl(24 80% 30% / .8);
  cursor: pointer;
  transition: border-color .3s ease, background .3s ease, opacity .5s ease;
}
.pov-btn:hover, .pov-btn:focus-visible { border-color: hsl(var(--l-mid) / .6); background: hsl(250 34% 12% / .75); }
.pov-btn:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }
.pov-ico { font-size: 1.05rem; line-height: 1; }
html.is-opening .pov-btn { opacity: 0 !important; pointer-events: none !important; }
body.modal-open .pov-btn { opacity: 0; pointer-events: none; }

/* the ride: the sea reaches up to a horizon at 44% of the screen (and a
   little past the bottom, so a look downward never shows its edge) */
/* (reaching well below the screen: looking down over the stern never shows its edge) */
.scene.pov .sea { bottom: -34vh; height: calc(var(--pov-sea, 75vh) + 34vh); }
.scene.pov .boat-lane,
.scene.pov .sea-route { opacity: 0; visibility: hidden; }
.scene.pov .island { transform-origin: 50% 100%; will-change: transform, opacity; }
.scene.pov .headland { transition: filter 1.6s ease; will-change: transform, opacity; }
.scene.pov .island-label { opacity: 0 !important; }
.scene.pov .water-lanterns { overflow: visible; z-index: auto; }
.scene.pov .water-lantern { will-change: translate, scale, opacity; }
html.pov-mode .cam-vignette { opacity: 0 !important; }
/* the island Wish Guide keeps its own size inside the projected island */
.scene.pov .wish-guide-isle { transform: translateX(-50%) scale(calc(var(--gs-pov, 1) * var(--pov-g, 1) / var(--pov-s, 1))); }
/* riding: the title steps back so the sea and the islands have the sky */
.bday { transition: opacity .8s ease; }
html.pov-mode .bday { opacity: 0; }

/* the boat, from just behind and above. It is NOT pinned to the screen:
   app.js projects its hull from its world position and heading through the
   camera and maps this 300 × 380 drawing onto it (a matrix, every frame) —
   it lies on the water where it really is. Its rock is visual only. */
.pov-boat {
  position: fixed;
  left: 0;
  top: 0;
  width: 300px;
  height: 380px;
  z-index: 4;                       /* over the world, under the title + UI */
  transform-origin: 0 0;
  opacity: 0;
  transition: opacity 1.2s ease;
  pointer-events: none;
}
.pov-boat.is-on { opacity: 1; }
.pov-boat svg { display: block; width: 300px; height: 380px; overflow: visible; }
.pv-rock { transform-origin: 50% 70%; animation: pv-rock 6.4s ease-in-out infinite alternate; }
@keyframes pv-rock {
  0%   { transform: rotate(-1.1deg) translateY(0); }
  50%  { transform: rotate(.4deg) translateY(-3px); }
  100% { transform: rotate(1.1deg) translateY(1px); }
}
.pv-hull   { fill: url(#pvHullG); }          /* lighter at the bow, darker toward the stern */
.pv-deck   { fill: url(#pvDeckG); }          /* the lamp-and-wish light pooling inside the boat */
.pv-plank  { stroke: #6e3a17; stroke-opacity: .5; stroke-width: 1.4; }
.pv-rib    { stroke: #5c3214; stroke-opacity: .35; stroke-width: 3; stroke-linecap: round; }
.pv-thwart { fill: #dc9d57; }
.pv-thwart .pv-thwart-edge { fill: #9a5a26; }
.pv-rim    { stroke: url(#pvRimG); stroke-width: 6; stroke-linejoin: round; }
.pv-rim-under { stroke: #6f3b18; stroke-width: 10; stroke-linejoin: round; }
.pv-rim-hi { stroke: #fff0c8; stroke-opacity: .75; stroke-width: 1.4; stroke-linejoin: round; }   /* the gunwale catching the light */
/* depth: the hull's side toward the camera, its waterline, the warm light
   inside, its shadow on the water and the lamp's reflection (app.js moves
   the side, shadow and reflection with the camera's view of the boat) */
.pv-topside   { fill: #4a250d; }
.pv-waterline { stroke: hsl(200 70% 86% / .22); stroke-width: 2.5; }
.pv-inglow    { mix-blend-mode: screen; }
.pv-shadow, .pv-reflect { pointer-events: none; }
.pv-reflect   { animation: lamp-flicker 3s ease-in-out infinite alternate; }
/* going astern: the wake fades back and the oars pull the other way */
.pov-boat.is-reversing .pv-wake { opacity: .3; }
.pov-boat.is-reversing .pv-oar-l, .pov-boat.is-reversing .pv-oar-r { animation-direction: reverse; }
.pv-pole   { stroke: hsl(28 40% 26%); stroke-width: 3; stroke-linecap: round; }
.pv-lamp   {
  fill: hsl(46 100% 90%);
  filter: drop-shadow(0 0 8px hsl(42 100% 74%)) drop-shadow(0 0 22px hsl(34 100% 60%));
  animation: lamp-flicker 3s ease-in-out infinite alternate;
}
/* Phúc Nguyên from behind — the design's colours: black hair, pink ears and
   hands, the blue shirt; the lantern he holds lights his hands and the boards */
.pv-coat { fill: url(#pvShirtG); }
.pv-sleeve { stroke: #4a6ed3; stroke-width: 11; stroke-linecap: round; fill: none; }
.pv-hand, .pv-ear { fill: #ffe0d6; }
.pv-ear { fill: #f6c3b6; }
.pv-hair { fill: #262626; }
.pv-hair-hi { stroke: #5a5a5a; stroke-opacity: .6; stroke-width: 2.4; stroke-linecap: round; fill: none; }
.pv-lantern { filter: drop-shadow(0 0 6px hsl(38 100% 62%)) drop-shadow(0 0 16px hsl(30 100% 55% / .7)); animation: lamp-flicker 3s ease-in-out infinite alternate; }
.pv-lantern-rib { stroke: #c4520a; stroke-opacity: .45; stroke-width: 1; fill: none; }
.pv-lantern-glow { mix-blend-mode: screen; }
.pv-staff { stroke: #c27a2c; stroke-width: 2.6; stroke-linecap: round; }
.pv-pn { font: 800 10px/1 var(--font); fill: #2b6cb0; }
.pv-oar-shaft { stroke: hsl(38 46% 58%); stroke-width: 4; stroke-linecap: round; }
.pv-oar-blade { fill: hsl(40 52% 56%); }
.pv-oar { transform-box: view-box; }
.pv-oar-l { transform-origin: 112px 236px; }
.pv-oar-r { transform-origin: 188px 236px; }
.pov-boat.is-under-way .pv-oar-l { animation: pv-row-l 3.2s ease-in-out infinite; }
.pov-boat.is-under-way .pv-oar-r { animation: pv-row-r 3.2s ease-in-out infinite; }
@keyframes pv-row-l { 0%, 100% { transform: rotate(-9deg); } 50% { transform: rotate(10deg); } }
@keyframes pv-row-r { 0%, 100% { transform: rotate(9deg); } 50% { transform: rotate(-10deg); } }
.pv-wake path { stroke: hsl(200 70% 88% / .35); stroke-width: 3; stroke-linecap: round; }
.pv-wake .pv-foam { stroke: hsl(200 80% 92% / .4); stroke-width: 5; }
.pv-wake { opacity: 0; transition: opacity 1.4s ease; }
.pov-boat.is-under-way .pv-wake { opacity: 1; animation: pv-wake 2.2s ease-in-out infinite alternate; }
@keyframes pv-wake { from { opacity: .55; } to { opacity: 1; } }
.pv-wishes { pointer-events: auto; cursor: pointer; }

/* glints on the water, placed from app.js (world-anchored, recycled) */
.pov-glints { display: none; position: absolute; left: 0; right: 0; bottom: 0; height: 0; pointer-events: none; }
.scene.pov .pov-glints { display: block; }
.pov-glints i {
  position: absolute;
  left: 0; top: 0;
  width: 24px; height: 5px;
  margin-left: -12px;
  border-radius: 50%;
  opacity: 0;
  transform-origin: 50% 50%;
  background: radial-gradient(closest-side, hsl(48 100% 94%), hsl(205 90% 82% / .45) 55%, transparent);
  will-change: translate, scale, opacity;
}

/* switching views: the world dims for a breath — the sky and UI stay */
.scene { transition: opacity .38s ease; }
.scene.pov-dip { opacity: .12; }

/* riding: a swipe looks around (the page itself never scrolls) */
html.pov-mode body { touch-action: none; overscroll-behavior: none; }

@media (max-width: 480px) {
  .pov-btn { font-size: 0.814rem; width: 8rem; }
}
@media (prefers-reduced-motion: reduce) {
  .scene { transition: none; }
  .pv-rock, .pv-lamp, .pv-lantern, .pov-boat.is-under-way .pv-oar-l, .pov-boat.is-under-way .pv-oar-r, .pov-boat.is-under-way .pv-wake { animation: none; }
}

/* the oars pushing water: a ring spreading from each blade as it strokes */
.pv-splash { fill: none; stroke: hsl(199 70% 90% / .5); stroke-width: 2; opacity: 0; transform-box: fill-box; transform-origin: 50% 50%; }
.pov-boat.is-under-way .pv-splash { animation: pv-splash 3.2s ease-out infinite; }
.pov-boat.is-under-way .pv-splash-r { animation-delay: .05s; }
@keyframes pv-splash {
  0%, 38% { opacity: 0; transform: translateY(0) scale(.3); }
  46%     { opacity: .8; }
  100%    { opacity: 0; transform: translateY(22px) scale(1.8, 1.4); }
}

/* ---- the voyage, in a few quiet words (VOYAGE_STATUS in app.js) ---- */
.voyage-status {
  position: fixed;
  left: 50%;
  /* high in the sky, under the top buttons: nothing in the world sails there */
  top: calc(env(safe-area-inset-top, 0px) + 22px);
  z-index: 14;
  max-width: min(62vw, 30rem);
  margin: 0;
  transform: translate(-50%, -4px);
  font: italic 400 0.88rem/1.35 var(--font);
  letter-spacing: .02em;
  text-align: center;
  color: hsl(40 60% 88% / .82);
  text-shadow: 0 1px 10px hsl(240 60% 4% / .9), 0 0 18px hsl(38 100% 55% / .18);
  opacity: 0;
  pointer-events: none;
  transition: opacity .5s ease, transform .6s ease;
}
.voyage-status.is-in { opacity: 1; transform: translate(-50%, 0); }
html.is-opening .voyage-status, body.modal-open .voyage-status { opacity: 0 !important; }

/* ---- the journey map (JOURNEY_MAP in app.js) ---- */
.map-btn {
  position: fixed;
  left: calc(env(safe-area-inset-left, 0px) + 16px);
  top: calc(env(safe-area-inset-top, 0px) + 16px);
  z-index: 15;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  color: hsl(40 70% 86%);
  background: hsl(250 34% 10% / .5);
  border: 1px solid hsl(36 100% 70% / .2);
  cursor: pointer;
  transition: border-color .3s ease, background .3s ease, opacity .5s ease;
}
.map-btn svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }
.map-btn:hover, .map-btn:focus-visible, .map-btn.is-open { border-color: hsl(var(--l-mid) / .6); background: hsl(250 34% 13% / .78); }
.map-btn:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }
html.is-opening .map-btn { opacity: 0 !important; pointer-events: none !important; }
body.modal-open .map-btn { opacity: 0; pointer-events: none; }

.voyage-map {
  position: fixed;
  left: calc(env(safe-area-inset-left, 0px) + 16px);
  top: calc(env(safe-area-inset-top, 0px) + 68px);
  z-index: 15;
  width: min(15rem, calc(100vw - 32px));
  padding: .6rem .7rem .55rem;
  border-radius: 16px;
  background: hsl(250 36% 9% / .86);
  border: 1px solid hsl(40 100% 72% / .22);
  box-shadow: 0 14px 38px hsl(250 60% 4% / .55);
}
.voyage-map[hidden] { display: none; }
.vm-title { margin: 0 0 .35rem; font: 700 0.704rem/1 var(--font); letter-spacing: .16em; text-transform: uppercase; color: hsl(var(--l-hot) / .9); }
.vm-sea { display: block; width: 100%; height: auto; max-height: 46vh; border-radius: 10px; }
.vm-water { fill: hsl(222 42% 20%); }
.vm-route { fill: none; stroke: hsl(40 90% 72% / .45); stroke-width: 3; stroke-dasharray: 2 9; stroke-linecap: round; }
.vm-isle { fill: hsl(250 28% 12%); stroke: hsl(38 70% 64% / .35); stroke-width: 2; }
.vm-stop.is-lit .vm-isle { fill: hsl(34 60% 26%); stroke: hsl(44 100% 76% / .8); }
.vm-stop.is-visited .vm-label { fill: hsl(44 100% 80%); }
.vm-stop.is-dest .vm-isle { stroke: hsl(46 100% 82%); stroke-width: 4; filter: drop-shadow(0 0 6px hsl(42 100% 66%)); }
.vm-label { font-family: var(--font); font-weight: 700; fill: hsl(40 30% 76% / .75); }
.vm-boat-halo { fill: hsl(42 100% 66% / .18); }
.vm-boat-mark { fill: hsl(44 100% 84%); stroke: hsl(28 60% 30%); stroke-width: 2; stroke-linejoin: round; }
.vm-now { margin: .4rem 0 0; min-height: 1.1em; font: italic 400 0.792rem/1.3 var(--font); color: hsl(40 50% 86% / .85); }
@media (max-width: 640px) {
  .voyage-status { top: calc(env(safe-area-inset-top, 0px) + 70px); max-width: 86vw; }
  html.map-open .voyage-status { opacity: 0 !important; }   /* (the map card says it too) */
}
@media (prefers-reduced-motion: reduce) {
  .voyage-status { transition: none; }
  .pov-boat.is-under-way .pv-splash { animation: none; }
}

/* ============================================================
   EXPLORE — the explorer's switch (EXPLORE_BTN / JOURNEY.explore in app.js)
   The voyage starts paused: the boat floats still until "Bắt đầu khám phá".
   Off the ride the button sits in the open sky just under the title (its
   top is measured: --explore-top); riding, it tucks into the top row.
   ============================================================ */
.explore-btn {
  position: fixed;
  left: 50%;
  top: var(--explore-top, 38vh);
  transform: translateX(-50%);
  z-index: 15;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  min-height: 44px;
  padding: 0 1.25rem;
  border-radius: 999px;
  white-space: nowrap;
  font: 600 0.968rem/1 var(--font);
  letter-spacing: .02em;
  color: hsl(30 60% 12%);
  background: linear-gradient(180deg, hsl(40 100% 74%), hsl(32 95% 60%));
  border: 1px solid hsl(40 100% 82% / .7);
  box-shadow: 0 8px 26px -10px hsl(30 95% 55% / .75), 0 0 0 4px hsl(36 100% 62% / .10);
  cursor: pointer;
  transition: opacity .5s ease, background .3s ease, color .3s ease, box-shadow .3s ease;
}
.explore-btn:hover { box-shadow: 0 10px 30px -10px hsl(30 95% 55% / .9), 0 0 0 5px hsl(36 100% 62% / .16); }
.explore-btn:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }
.explore-ico { display: inline-grid; place-items: center; width: .8rem; height: .8rem; line-height: 1; }
.explore-ico svg, .gd-btn svg { display: block; width: .8rem; height: .8rem; }
/* under way: the switch steps back to a quiet glass pill (it is now "pause") */
.explore-btn[aria-pressed="true"] {
  color: hsl(40 70% 88%);
  background: hsl(250 34% 10% / .55);
  border-color: hsl(36 100% 70% / .22);
  box-shadow: 0 6px 22px -10px hsl(24 80% 30% / .8);
}
html.is-opening .explore-btn { opacity: 0 !important; pointer-events: none !important; }
body.modal-open .explore-btn { opacity: 0; pointer-events: none; }
/* under way (and riding): a small round icon-only switch in the top row,
   right beside the journey map; "Lên thuyền" steps along to make room
   (the label stays for assistive tech and the tooltip) */
.explore-btn[aria-pressed="true"],
html.pov-mode .explore-btn {
  left: calc(env(safe-area-inset-left, 0px) + 68px);
  top: calc(env(safe-area-inset-top, 0px) + 16px);
  transform: none;
  width: 44px;
  padding: 0;
}
.explore-btn[aria-pressed="true"] .explore-label,
html.pov-mode .explore-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
html:not(.explore-paused) .pov-btn,
html.pov-mode .pov-btn { left: calc(env(safe-area-inset-left, 0px) + 120px); }
@media (max-width: 480px) {
  .explore-btn { font-size: 0.902rem; padding: 0 1.05rem; }
  /* phones: moved along, "Lên thuyền" keeps just its 🛶 (aria-label stays)
     so the top row never reaches the countdown in the corner */
  html:not(.explore-paused) .pov-btn,
  html.pov-mode .pov-btn { width: 44px; padding: 0; }
  html:not(.explore-paused) .pov-btn > span:not(.pov-ico),
  html.pov-mode .pov-btn > span:not(.pov-ico) { display: none; }
}

/* paused (and before the first start): the boat's own water holds still —
   a real pause, nothing left animating for it (the sea, lanterns and sky
   are not the voyage, and carry on) */
html.explore-paused .boat,
html.explore-paused .boat *,
html.explore-paused .boat *::before,
html.explore-paused .boat *::after,
html.explore-paused .pov-boat,
html.explore-paused .pov-boat * {
  animation-play-state: paused !important;
}

/* =====================================================================
   BẦU TRỜI ĐIỀU ƯỚC — the Wish Sky (the boat's Wish List)
   A magical layer above the ocean: the sea dims away below, and every
   wish hangs in the night as a lantern on a slowly turning ring. app.js
   places the dozen recycled lanterns (transform / opacity / a stepped
   blur — never layout); everything else here is static paint or a few
   compositor-only CSS animations.
   ===================================================================== */
.logbook.ws {
  padding: 0;
  display: block;
  background:
    radial-gradient(70% 38% at 50% 40%, hsl(34 80% 40% / .16), transparent 70%),
    radial-gradient(120% 60% at 50% 110%, hsl(214 60% 22% / .5), transparent 60%),
    linear-gradient(180deg, hsl(250 42% 6% / .985) 0%, hsl(256 38% 8% / .97) 62%, hsl(232 44% 10% / .9) 100%);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
body.modal-open .logbook.ws {
  background:
    radial-gradient(70% 38% at 50% 40%, hsl(34 80% 40% / .16), transparent 70%),
    radial-gradient(120% 60% at 50% 110%, hsl(214 60% 22% / .5), transparent 60%),
    linear-gradient(180deg, hsl(250 42% 6% / .985) 0%, hsl(256 38% 8% / .97) 62%, hsl(232 44% 10% / .9) 100%) !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
/* (the sea's own title would ghost through the sky) */
html:has(#logbook.ws:not([hidden])) .bday { visibility: hidden; }
.ws-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: none;
  height: 100dvh;
  padding: 0;
  overflow: hidden;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  outline: none;
}

/* the sky itself: static stars, one slow twinkle, a warm haze where the
   lanterns gather, and a few motes of light drifting up */
.ws-sky { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.ws-stars {
  position: absolute; inset: 0;
  background-image:
    radial-gradient(1px 1px at 12% 18%, hsl(45 100% 92% / .7), transparent 60%),
    radial-gradient(1px 1px at 78% 12%, hsl(45 100% 92% / .55), transparent 60%),
    radial-gradient(1.4px 1.4px at 33% 6%, hsl(45 100% 92% / .6), transparent 60%),
    radial-gradient(1px 1px at 58% 27%, hsl(220 100% 92% / .5), transparent 60%),
    radial-gradient(1px 1px at 91% 38%, hsl(45 100% 92% / .45), transparent 60%),
    radial-gradient(1px 1px at 6% 46%, hsl(220 100% 92% / .4), transparent 60%);
  background-size: 260px 260px;
}
.ws-stars-b {
  background-size: 410px 410px;
  background-position: 120px 60px;
  animation: ws-twinkle 5.5s ease-in-out infinite alternate;
}
@keyframes ws-twinkle { from { opacity: .25; } to { opacity: .85; } }
.ws-haze {
  position: absolute;
  left: 50%; top: 40%;
  width: min(120vw, 900px); height: min(70vh, 560px);
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side, hsl(36 100% 60% / .12), hsl(28 90% 50% / .05) 55%, transparent);
}
.ws-mote {
  position: absolute;
  left: var(--mx);
  bottom: -12px;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: hsl(var(--l-hot));
  box-shadow: 0 0 8px 2px hsl(var(--l-mid) / .55);
  opacity: 0;
  animation: ws-mote var(--mt) linear var(--md) infinite;
}
@keyframes ws-mote {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  12%  { opacity: .75; }
  80%  { opacity: .5; }
  100% { transform: translate3d(14px, -104vh, 0); opacity: 0; }
}

/* header: compact, the lanterns stay the subject */
.ws-head {
  position: relative;
  z-index: 3;
  flex: 0 0 auto;
  padding: calc(env(safe-area-inset-top, 0px) + 0.9rem) 4rem 0;
  text-align: center;
}
.ws-title {
  margin: 0;
  font-size: clamp(1.078rem, 4.2vw, 1.43rem);
  font-weight: 800;
  letter-spacing: 0.16em;
  line-height: 1.25;
  color: hsl(var(--l-core));
  text-shadow: 0 0 18px hsl(var(--l-mid) / .45);
}
.ws-sub {
  margin: 0.3rem 0 0;
  font-size: 0.88rem;
  color: hsl(var(--foreground) / .66);
}
.ws-tabs { margin: 0.65rem auto 0; }
.ws-tabs .logbook-tab { min-height: 36px; }

/* small round actions: search (left), close (right), the ring's arrows */
.ws-icon {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  padding: 0;
  color: hsl(var(--l-hot) / .9);
  background: hsl(250 34% 10% / .55);
  border: 1px solid hsl(var(--l-hot) / .22);
  border-radius: 50%;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color .2s ease, border-color .2s ease, opacity .2s ease;
}
.ws-icon svg { width: 20px; height: 20px; }
.ws-icon:hover, .ws-icon.is-on { background: hsl(34 60% 20% / .6); border-color: hsl(var(--l-hot) / .5); }
.ws-icon:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 2px; }
.ws-icon:disabled { opacity: .35; cursor: default; }
.ws-search-btn, .ws-close {
  position: absolute;
  z-index: 6;
  top: calc(env(safe-area-inset-top, 0px) + 0.75rem);
}
.ws-search-btn { left: 0.85rem; }
.ws-close { right: 0.85rem; }

/* the stage: the ring, the current wish's words, the arrows */
.ws-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  margin: 0;
  display: block;
  touch-action: none;           /* horizontal drags turn the sky; the page never scrolls */
  user-select: none;
  -webkit-user-select: none;
}
.ws-ring { position: absolute; inset: 0; }

.ws-lantern {
  position: absolute;
  left: 0; top: 0;
  width: var(--ws-size);
  padding: 0;
  color: inherit;
  font: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  transform-origin: 50% 33%;
  will-change: transform, opacity;
  -webkit-tap-highlight-color: transparent;
}
.ws-lantern[hidden] { display: none; }
.ws-lantern:focus-visible { outline: none; }
.ws-lantern:focus-visible .ws-body { outline: 2px solid hsl(var(--l-hot)); outline-offset: 6px; }
.ws-rise { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
.ws-float {
  position: relative;
  display: block;
  width: 100%;
  animation: ws-float var(--fl-t, 5s) ease-in-out var(--fl-d, 0s) infinite alternate;
}
@keyframes ws-float { from { transform: translateY(-5px) rotate(-1.2deg); } to { transform: translateY(5px) rotate(1.2deg); } }
.ws-lantern .ws-body {
  display: block;
  width: 100%;
  height: calc(var(--ws-size) * 1.32);
  box-shadow:
    0 0 calc(var(--ws-size) * .32) hsl(var(--l-mid) / .5),
    inset 0 -8px 16px hsl(var(--l-dark) / .45);
  transition: box-shadow .45s ease;
}
.ws-halo {
  position: absolute;
  left: 50%; top: 52%;
  width: 280%; height: 230%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, hsl(40 100% 70% / .34), hsl(30 100% 55% / .1) 55%, transparent);
  opacity: .5;
  transition: opacity .5s ease;
  pointer-events: none;
}
/* the wish in front: closer, brighter, embers rising off it */
.ws-lantern.is-focus .ws-body {
  box-shadow:
    0 0 calc(var(--ws-size) * .45) hsl(var(--l-hot) / .75),
    0 0 calc(var(--ws-size) * 1.1) hsl(var(--l-mid) / .4),
    inset 0 -8px 16px hsl(var(--l-dark) / .4);
}
.ws-lantern.is-focus .ws-halo { opacity: 1; }
.ws-sparks { position: absolute; inset: 0; pointer-events: none; }
.ws-sparks i {
  position: absolute;
  bottom: 30%;
  width: 3px; height: 3px;
  border-radius: 50%;
  background: hsl(var(--l-core));
  box-shadow: 0 0 6px hsl(var(--l-hot));
  opacity: 0;
}
.ws-sparks i:nth-child(1) { left: 18%; } .ws-sparks i:nth-child(2) { left: 46%; }
.ws-sparks i:nth-child(3) { left: 70%; } .ws-sparks i:nth-child(4) { left: 86%; }
.ws-lantern.is-focus .ws-sparks i { animation: ws-spark 3.2s ease-out infinite; }
.ws-lantern.is-focus .ws-sparks i:nth-child(2) { animation-delay: .8s; }
.ws-lantern.is-focus .ws-sparks i:nth-child(3) { animation-delay: 1.6s; }
.ws-lantern.is-focus .ws-sparks i:nth-child(4) { animation-delay: 2.4s; }
@keyframes ws-spark {
  0%   { transform: translate(0, 0) scale(1); opacity: 0; }
  15%  { opacity: 1; }
  100% { transform: translate(6px, calc(var(--ws-size) * -1.4)) scale(.4); opacity: 0; }
}
.ws-label {
  max-width: 12ch;
  font-size: 0.814rem;
  font-weight: 600;
  color: hsl(var(--l-hot) / .9);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-shadow: 0 1px 8px hsl(260 40% 4% / .95);
  opacity: clamp(0, calc(var(--near, 1) * 1.8 - 0.75), 1);
}
.ws-lantern.is-focus .ws-label { opacity: 0; }   /* its words show below, larger */
.ws-lantern.is-unnamed .ws-label { font-style: italic; color: hsl(var(--foreground) / .7); }

/* the current wish: its sender and a short preview, under its lantern */
.ws-focus {
  position: absolute;
  z-index: 120;
  /* centred on the ring's axis (the stage's centre) by its box, not by a
     transform — the entrance animation and the turning state animate
     `transform`, and must never be able to knock it off-centre */
  left: 0;
  right: 0;
  margin: 0 auto;
  top: var(--ws-focus-top, 60%);
  width: min(88vw, 28rem);
  text-align: center;
  pointer-events: none;
  transition: opacity .35s ease, transform .35s ease;
}
.ws-focus[hidden] { display: none; }
.ws-focus.is-turning { opacity: .18; transform: translateY(6px); }
.ws-name {
  margin: 0;
  font-size: 1.122rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: hsl(var(--l-core));
  text-shadow: 0 0 16px hsl(var(--l-mid) / .55);
  overflow-wrap: anywhere;
}
.ws-name.is-unnamed { font-style: italic; letter-spacing: 0.08em; text-transform: none; color: hsl(var(--foreground) / .82); }
.ws-preview {
  margin: 0.45rem 0 0;
  font-size: 1.045rem;
  font-style: italic;
  line-height: 1.55;
  color: hsl(var(--foreground) / .86);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  overflow-wrap: anywhere;
  text-shadow: 0 1px 10px hsl(260 40% 4% / .9);
}
.ws-preview::before { content: "“"; }
.ws-preview::after { content: "”"; }
.ws-preview:empty::before, .ws-preview:empty::after { content: none; }
.ws-read {
  margin-top: 0.8rem;
  min-height: 44px;
  padding: 0 1.3rem;
  font: 700 0.968rem/1 var(--font);
  color: hsl(24 60% 12%);
  background: var(--gradient-primary);
  border: 0;
  border-radius: 999px;
  box-shadow: 0 0 22px hsl(var(--l-mid) / .35);
  cursor: pointer;
  pointer-events: auto;
}
.ws-read:focus-visible { outline: 2px solid hsl(var(--l-core)); outline-offset: 3px; }

.ws-nav {
  position: absolute;
  z-index: 130;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.9rem;
}
.ws-count {
  min-width: 7.5rem;
  margin: 0;
  text-align: center;
  font: 600 0.88rem/1.35 var(--font);
  letter-spacing: 0.04em;
  color: hsl(var(--l-hot) / .85);
}
.ws-count .wl-partial { font-size: 0.77rem; }

.ws-stage .wl-empty {
  position: absolute;
  left: 1.5rem; right: 1.5rem; top: 38%;
  margin: 0;
  text-align: center;
  font: 500 1.045rem/1.5 var(--font);
  color: hsl(var(--foreground) / .75);
}
.ws-stage .wl-empty[hidden] { display: none; }

/* search: a small panel dropping from the search action */
.ws-search {
  position: absolute;
  z-index: 140;
  top: 0.25rem;
  left: 0.85rem;
  right: 0.85rem;
  max-width: 30rem;
  margin: 0 auto;
  max-height: min(64%, 30rem);
  display: flex;
  flex-direction: column;
  padding: 0.75rem;
  background: hsl(250 36% 9% / .95);
  border: 1px solid hsl(var(--l-hot) / .22);
  border-radius: 20px;
  box-shadow: 0 18px 50px hsl(260 40% 2% / .6), 0 0 40px hsl(var(--l-deep) / .14);
  touch-action: pan-y;
}
.ws-search[hidden] { display: none; }
.ws-results {
  flex: 1 1 auto;
  min-height: 0;
  margin: 0.55rem 0 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ws-results:empty { display: none; }
.ws-hit {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 0.65rem;
  align-items: center;
  width: 100%;
  min-height: 52px;
  padding: 0.45rem 0.55rem;
  text-align: left;
  color: inherit;
  font: inherit;
  background: none;
  border: 0;
  border-radius: 12px;
  cursor: pointer;
}
.ws-hit:hover, .ws-hit:focus-visible { background: hsl(34 60% 30% / .22); outline: none; }
.ws-hit-lamp {
  grid-row: 1 / span 2;
  width: 14px; height: 18px;
  border-radius: 46% 46% 40% 40% / 24% 24% 50% 50%;
  background: radial-gradient(56% 40% at 50% 84%, hsl(var(--l-core)), hsl(var(--l-mid)) 50%, hsl(var(--l-dark)));
  box-shadow: 0 0 10px hsl(var(--l-mid) / .6);
}
.ws-hit-name { font-weight: 700; font-size: 0.99rem; color: hsl(var(--l-hot)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-hit-msg { font-size: 0.88rem; color: hsl(var(--foreground) / .7); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-hit-none { padding: 0.6rem; font-size: 0.946rem; color: hsl(var(--foreground) / .7); text-align: center; }

/* Top Donors keeps its own panel, in the same sky */
.ws-card .logbook-sub { position: relative; z-index: 2; margin: 0.9rem 1rem 0.4rem; }
.ws-card .top-donors {
  position: relative;
  z-index: 2;
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  max-width: 30rem;
  margin: 0 auto;
  padding: 0 1rem calc(env(safe-area-inset-bottom, 0px) + 1rem);
  overflow-y: auto;
}

/* entering: the sea dims away, the lanterns rise into their ring (the far
   ones first), the title last. Leaving: the sky dissolves, the sea returns */
.logbook.ws.is-entering { animation: ws-in .7s ease both; }
.logbook.ws.is-entering .ws-rise { animation: ws-rise 1.4s cubic-bezier(.2, .8, .2, 1) both; animation-delay: calc(var(--near, 1) * 520ms + 120ms); }
.logbook.ws.is-entering .ws-head { animation: ws-title 1s ease .75s both; }
.logbook.ws.is-entering .ws-focus:not(.is-turning) { animation: ws-title .9s ease 1.25s both; }
@keyframes ws-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ws-rise { from { transform: translateY(42vh) scale(.6); opacity: 0; } 60% { opacity: 1; } to { transform: none; opacity: 1; } }
@keyframes ws-title { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.logbook.ws.is-leaving { animation: ws-out .5s ease both; pointer-events: none; }
.logbook.ws.is-leaving .ws-rise { animation: ws-up .5s ease-in both; }
@keyframes ws-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes ws-up { to { transform: translateY(-28px) scale(.92); opacity: 0; } }

@media (max-width: 440px) {
  .ws-title { letter-spacing: 0.07em; font-size: 1.078rem; }
  .ws-title > [aria-hidden] { display: none; }   /* (the subtitle carries the sparkle) */
  .ws-head { padding-left: 3.6rem; padding-right: 3.6rem; }
}
@media (min-width: 900px) {
  .ws-head { padding-top: 1.4rem; }
  .ws-title { font-size: 1.54rem; }
}
@media (prefers-reduced-motion: reduce) {
  .ws-mote, .ws-stars-b, .ws-float, .ws-lantern.is-focus .ws-sparks i { animation: none !important; }
  .ws-mote { display: none; }
  .logbook.ws.is-entering .ws-rise, .logbook.ws.is-leaving .ws-rise { animation: none !important; }
  .logbook.ws.is-entering, .logbook.ws.is-leaving { animation-duration: .01s !important; }
  .ws-focus, .ws-halo, .ws-lantern .ws-body { transition: none; }
}

/* =====================================================================
   THE PROJECT'S MEANING — “Nguyện Thành Đăng Nguyên”
   A quiet entry in the open sky just under the hero (on the same measured
   line as the start button, or in its place once the voyage is under way),
   and a reading panel that dims the world rather than hiding it. Fades
   only; no blur (the phone performance rule), nothing animated while
   reading. MEANING in app.js.
   ===================================================================== */
.meaning-btn {
  position: fixed;
  left: 50%;
  top: calc(var(--hero-bottom, 38vh) + 56px);
  transform: translateX(-50%);
  z-index: 15;
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  min-height: 40px;
  padding: 0 1rem;
  font: 500 0.88rem/1 var(--font);
  letter-spacing: .04em;
  white-space: nowrap;
  color: hsl(40 70% 90% / .9);
  background: hsl(250 34% 10% / .38);
  border: 1px solid hsl(36 100% 70% / .22);
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity .5s ease, background-color .25s ease, border-color .25s ease, top .5s ease;
}
.meaning-btn-ico { font-size: .72rem; color: hsl(var(--l-hot)); }
.meaning-btn:hover { background: hsl(250 34% 12% / .62); border-color: hsl(36 100% 70% / .5); }
.meaning-btn:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 3px; }
/* under way, the start button has moved to the top row: take its place */
html:not(.explore-paused) .meaning-btn { top: var(--hero-bottom, 38vh); }
html.is-opening .meaning-btn,
html.pov-mode .meaning-btn,
body.modal-open .meaning-btn { opacity: 0; pointer-events: none; }

.meaning[hidden] { display: none; }
.meaning {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: calc(env(safe-area-inset-top, 0px) + 1rem) 1rem calc(env(safe-area-inset-bottom, 0px) + 1rem);
  background:
    radial-gradient(70% 60% at 50% 30%, hsl(240 40% 14% / .5), transparent 70%),
    hsl(245 42% 6% / .7);
  animation: meaning-in .35s ease both;
}
.meaning-card {
  position: relative;
  /* the whole artwork in view: as tall as the screen allows, 3:4 like the file */
  width: min(100%, calc((100dvh - 2rem - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)) * .75));
  overflow: hidden;
  background: hsl(226 70% 36%);
  border-radius: 18px;
  box-shadow: 0 30px 80px -24px hsl(250 60% 2% / .9);
  outline: none;
  animation: meaning-rise .5s cubic-bezier(.2, .8, .2, 1) both;
}
.meaning-zoom {
  display: block;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  cursor: zoom-in;
}
.meaning-zoom:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: -3px; }
.meaning-art { display: block; width: 100%; height: auto; }
.meaning-hint {
  position: absolute;
  left: 50%;
  bottom: 2.6rem;
  transform: translateX(-50%);
  padding: .4rem .8rem;
  font-size: 0.88rem;
  white-space: nowrap;
  color: hsl(40 90% 94%);
  background: hsl(226 60% 16% / .72);
  border-radius: 999px;
  pointer-events: none;
}
/* zoomed: the page takes the whole screen and the artwork scrolls inside it;
   122%: the text column (78% of the art, plus its margins) just fits a phone */
.meaning.is-zoomed { padding: 0; }
.meaning.is-zoomed .meaning-card { width: 100%; height: 100dvh; border-radius: 0; }
.meaning.is-zoomed .meaning-scroll { display: flex; height: 100%; overflow: auto; overscroll-behavior: contain; }
.meaning.is-zoomed .meaning-zoom {
  flex: none;
  width: min(1080px, 122%);
  margin: auto;
  cursor: zoom-out;
}
.meaning.is-zoomed .meaning-hint { display: none; }
.meaning.is-zoomed .meaning-close { top: calc(env(safe-area-inset-top, 0px) + .6rem); right: .6rem; z-index: 1; }
.meaning-close {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + .6rem);
  right: .6rem;
  z-index: 1;
}
.meaning-close {
  position: absolute;
  top: .7rem;
  right: .7rem;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: hsl(40 90% 94%);
  /* a dark disc: the artwork's corner is bright */
  background: hsl(226 60% 16% / .72);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}
.meaning-close svg { width: 20px; height: 20px; }
.meaning-close:hover { background: hsl(226 60% 12% / .9); color: hsl(var(--l-core)); }
.meaning-close:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 2px; }
.meaning.is-leaving { animation: meaning-out .26s ease both; }
.meaning.is-leaving .meaning-card { animation: meaning-sink .26s ease both; }
@keyframes meaning-in { from { opacity: 0; } }
@keyframes meaning-out { to { opacity: 0; } }
@keyframes meaning-rise { from { opacity: 0; transform: translateY(14px); } }
@keyframes meaning-sink { to { opacity: 0; transform: translateY(8px); } }
/* phones: edge to edge, the close button clear of the notch */
@media (max-width: 560px) {
  .meaning { padding-left: .5rem; padding-right: .5rem; }
  .meaning-close { top: .5rem; right: .5rem; }
}
@media (prefers-reduced-motion: reduce) {
  .meaning, .meaning-card, .meaning.is-leaving, .meaning.is-leaving .meaning-card { animation: none; }
  .meaning-btn { transition: none; }
}

/* =====================================================================
   WISH SKY · VIEW SWITCH + LIST VIEW
   The lantern ring is the default; the list (the original Wish List) is
   the second presentation of the same wishes (app.js: LIST VIEW). Same palette and type as the
   sky; the list scrolls inside itself (the sea behind never moves).
   ===================================================================== */
.ws-view {
  display: inline-flex;
  gap: 2px;
  margin: 0.55rem auto 0;
  padding: 3px;
  background: hsl(250 34% 10% / .5);
  border: 1px solid hsl(36 100% 70% / .16);
  border-radius: 999px;
}
.ws-view-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: 32px;
  padding: 0 0.8rem;
  font: 600 0.858rem/1 var(--font);
  color: hsl(40 50% 88% / .72);
  background: none;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color .2s ease, color .2s ease;
}
.ws-view-btn svg { width: 15px; height: 15px; flex: none; }
.ws-view-btn:hover { color: hsl(var(--l-core)); }
.ws-view-btn.is-on { color: hsl(30 60% 14%); background: linear-gradient(180deg, hsl(40 100% 76%), hsl(34 95% 62%)); }
.ws-view-btn:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 2px; }
/* (the search action belongs to the ring) */
.logbook.is-list .ws-search-btn { display: none; }

/* LIST VIEW: the original Wish List (search, filters, virtual tile grid),
   centred in the sky; only the tile grid scrolls (the sea never moves) */
.ws-listview[hidden] { display: none; }
.ws-listview {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 44rem;
  margin: 0.65rem auto 0;
  padding: 0 1rem calc(env(safe-area-inset-bottom, 0px) + 0.6rem);
  touch-action: pan-y;
}
.ws-listview .wl-list { -webkit-overflow-scrolling: touch; }

.wl-list {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  contain: strict;
  outline: none;
  /* faint starfield so the list still sits in the night sky (static) */
  background:
    radial-gradient(1px 1px at 12% 18%, hsl(45 100% 90% / .35), transparent 60%),
    radial-gradient(1px 1px at 78% 42%, hsl(45 100% 90% / .25), transparent 60%),
    radial-gradient(1px 1px at 38% 76%, hsl(45 100% 90% / .3), transparent 60%);
  background-attachment: local;
}
.wl-spacer { position: relative; width: 100%; margin: 4px 0 14px; }

/* ---- Lantern Gallery tile: a little paper lantern-card lit from its lamp.
   No per-tile shadow or filter at rest — the glow is a background gradient,
   so a screenful of recycled tiles costs almost nothing to paint. ---- */
.wl-item {
  position: absolute;
  top: 0;
  left: 0;
  width: calc((100% - (var(--cols, 2) - 1) * 10px) / var(--cols, 2));
  height: 124px;
  display: flex;
  flex-direction: column;
  gap: .45rem;
  padding: .8rem .8rem .7rem;
  overflow: hidden;
  text-align: left;
  color: inherit;
  font: inherit;
  background:
    radial-gradient(90% 70% at 14% 0%, hsl(38 100% 62% / .2), transparent 70%),
    linear-gradient(180deg, hsl(34 40% 16% / .72), hsl(262 28% 10% / .72));
  border: 1px solid hsl(var(--l-hot) / .14);
  border-radius: 16px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .2s ease, background-color .2s ease;
}
.wl-item[hidden] { display: none; }
.wl-item:hover { border-color: hsl(var(--l-hot) / .38); }
.wl-item:focus-visible { outline: 2px solid hsl(var(--l-hot) / .8); outline-offset: 1px; }
/* press: the lamp flares, the card lifts a touch */
.wl-item:active { border-color: hsl(var(--l-hot) / .5); }
.wl-item:active .wl-lamp { box-shadow: 0 0 18px hsl(var(--l-hot) / .9); transform: translateY(-2px); }

.wl-head {
  display: flex;
  align-items: center;
  gap: .55rem;
  min-width: 0;
}
/* room for the masked-amount badge, only when it is there */
.wl-item:has(.wl-meta.is-masked) .wl-head { padding-right: 2.6rem; }

/* static lantern icon — one element + two pseudo-elements, no animation */
.wl-lamp {
  position: relative;
  flex: 0 0 auto;
  width: 16px;
  height: 21px;
  border-radius: 46% 46% 40% 40% / 26% 26% 50% 50%;
  background: radial-gradient(60% 45% at 50% 70%, hsl(var(--l-core)) 0%, hsl(var(--l-hot)) 30%,
                              hsl(var(--l-mid)) 60%, hsl(var(--l-deep)) 100%);
  box-shadow: 0 0 10px hsl(var(--l-mid) / .5);
  transition: transform .2s ease, box-shadow .2s ease;
}
.wl-lamp::before, .wl-lamp::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 9px;
  height: 2px;
  border-radius: 2px;
  background: hsl(20 60% 22%);
  transform: translateX(-50%);
}
.wl-lamp::before { top: -1px; }
.wl-lamp::after { bottom: -1px; width: 10px; }

.wl-name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font: 700 1.012rem/1.2 var(--font);
  letter-spacing: .01em;
  color: hsl(var(--l-core));
}
.wl-msg {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  font: 400 0.88rem/1.4 var(--font);
  color: hsl(var(--foreground) / .72);
  overflow-wrap: anywhere;
}

/* unnamed Timo lantern: paler, dashed — waiting for its owner */
.wl-item.is-unnamed {
  background:
    radial-gradient(90% 70% at 14% 0%, hsl(40 40% 70% / .1), transparent 70%),
    linear-gradient(180deg, hsl(250 20% 15% / .7), hsl(262 28% 10% / .7));
  border-style: dashed;
  border-color: hsl(var(--l-hot) / .26);
}
.wl-item.is-unnamed .wl-lamp {
  background: radial-gradient(60% 45% at 50% 70%, hsl(40 40% 80%) 0%, hsl(32 30% 52%) 55%, hsl(26 26% 30%) 100%);
  box-shadow: none;
}
.wl-item.is-unnamed .wl-name { font-weight: 600; font-style: italic; color: hsl(var(--muted-foreground)); }

.wl-meta {
  position: absolute;
  top: .7rem;
  right: .6rem;
  max-width: 45%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font: 600 0.704rem/1 var(--font);
  color: hsl(var(--muted-foreground));
}
.wl-meta:empty { display: none; }
.wl-meta.is-masked {
  padding: .26rem .42rem;
  letter-spacing: .14em;
  color: hsl(42 100% 76%);
  background: hsl(38 100% 50% / .12);
  border: 1px solid hsl(38 100% 55% / .25);
  border-radius: 999px;
}
/* claim / pending sit at the foot of the card so the name keeps its line */
.wl-meta.is-claim, .wl-meta.is-pending {
  top: auto;
  bottom: .6rem;
  left: .8rem;
  right: auto;
  max-width: calc(100% - 1.6rem);
  padding: .26rem .5rem;
  border: 1px dashed hsl(var(--l-hot) / .4);
  border-radius: 999px;
  color: hsl(var(--l-hot) / .9);
}
.wl-meta.is-pending { border-style: solid; color: hsl(var(--muted-foreground)); border-color: hsl(var(--border)); }
.wl-item:has(.wl-meta.is-claim) .wl-msg,
.wl-item:has(.wl-meta.is-pending) .wl-msg { -webkit-line-clamp: 2; line-clamp: 2; }

.wl-list .wl-empty {
  position: absolute;
  inset: 2.5rem 1rem auto;
  margin: 0;
  text-align: center;
  font: 500 1.012rem/1.5 var(--font);
  color: hsl(var(--muted-foreground));
}
.wl-list .wl-empty[hidden] { display: none; }

@media (max-width: 440px) {
  .ws-view-btn { padding: 0 0.7rem; }
  .ws-listview { padding-left: 0.75rem; padding-right: 0.75rem; }
}
@media (max-width: 360px) {
  .wl-chip { font-size: 0.858rem; }
}

/* =====================================================================
   COPY REFINEMENT · small text additions
   ===================================================================== */
/* the hero's supporting line, under UPRIZE PN: quiet, short measure */
.bday-lede {
  max-width: 27rem;
  margin: .8rem auto 0;
  font-size: 0.924rem;
  line-height: 1.6;
  color: hsl(40 45% 90% / .78);
  text-shadow: 0 1px 10px hsl(250 45% 6% / .85);
  text-wrap: pretty;
  opacity: 0;
}
html.world-ready .bday-lede { animation: bday-fade 1s ease 1.1s forwards; }
@media (max-width: 480px) {
  .bday-lede { max-width: 21rem; font-size: 0.858rem; line-height: 1.55; }
}

/* the wish form's invitation, under its title */
.sheet-card .sheet-sub {
  margin: -.6rem 0 1.1rem;
  font-size: 0.968rem;
  line-height: 1.55;
  text-align: center;
  color: hsl(var(--muted-foreground));
}

/* the opening's title is a single unbroken line: never wider than the screen */
.opening-title { font-size: min(clamp(2.585rem, 4.2vw + 1.1rem, 6.16rem), 5.4vw); }
.opening-sub { letter-spacing: .1em; }
/* the longer filter names wrap inside their chips on phones (three equal
   columns that never push past the screen) */
.wl-chip { min-width: 0; }
@media (max-width: 480px) {
  .wl-filters { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .wl-chip {
    min-height: 44px;
    padding: .3rem .45rem;
    font-size: 0.858rem;
    line-height: 1.2;
    white-space: normal;
    text-wrap: balance;
  }
}
/* the Wish List header: only the title row keeps clear of the round search /
   close actions in the corners; the tabs and the view switch below use the
   full width, each label on one line */
.ws-tabs .logbook-tab, .ws-view-btn { white-space: nowrap; }
@media (max-width: 480px) {
  .ws-head { padding-left: .75rem; padding-right: .75rem; }
  .ws-title, .ws-sub { padding: 0 2.9rem; }
  .ws-tabs .logbook-tab { padding: .4rem .75rem; font-size: 0.88rem; }
}

/* =====================================================================
   DESIGN ART (public/art/, from the designer's “web ntdn_*” SVGs)
   The design's sky, sea, waves, light particles, moon, islands, lighthouse,
   boat, Wish Guide and sky lantern replace the hand-drawn placeholders.
   Positions, depth, the journey and every interaction are unchanged; only
   the drawings are new. The background and the particles are ONE 3016×1472
   frame whose horizon (the top of the design's sea layer, y 861 = 58.5%)
   is laid on the scene's own horizon: the sky never stretches, it covers
   the screen at its own proportions and is cropped at the top / sides.
   ===================================================================== */
:root {
  /* the design frame's width on screen: wide enough to fill the screen and
     tall enough that the part above its horizon covers the sky */
  --sky-w: max(100vw, calc((100dvh - var(--sea-h)) * 3.5026));
}
#sky,
.sky-particles {
  background-image: url("/art/sky-night.svg");          /* the design's night sky (same frame and horizon as before) */
  background-repeat: no-repeat;
  background-size: var(--sky-w) auto;
  /* the frame's horizon (0.585 of its height = 0.2855 of its width) on the sea's top edge */
  background-position: 50% calc(100dvh - var(--sea-h) - var(--sky-w) * 0.2855);
}
#sky { background-color: #162c7e; }
.sky-particles {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image: url("/art/light-particles.svg");
  animation: sky-particles 7s ease-in-out infinite alternate;
}
@keyframes sky-particles { from { opacity: .65; } to { opacity: 1; } }

/* sea: the design's base water (#586593 → #1e3659) — what shows if WebGL is
   unavailable — under the ocean canvas (index.html › .ocean-gl, app.js ›
   OCEAN), which fades in once its first frame is drawn. */
.sea { background: linear-gradient(0deg, #213eb6 0%, #213eb6 37%, #5ca9e3 100%); }   /* the design's sea (#5ca9e3 far → #213eb6 near) */
/* the design's points of light on the water, screen-blended over the waves
   (static: the waves move under them) */
.sea-spots {
  position: absolute;
  inset: 0;
  background: url("/art/sea-spots.svg?v=2") center / cover no-repeat;   /* its own proportions, cropped (never stretched into streaks) */
  mix-blend-mode: screen;
  pointer-events: none;
}

/* extra points of light, placed by hand over the sea (index.html ›
   .sea-spots i): each is the design's OWN spot, cut from its image
   (public/art/sea-spot-sprites.png: the same glints the design draws, black
   around them, so the screen blend leaves only the light), at the design's
   scale. --x / --y: where, as a share of the sea (from its left / from the
   horizon); --n / --o: the sprite's size and its place in the sheet (px of
   the source). They sit IN the sea layer: islands, headland and boat stay in
   front of them. */
.sea-spots i {
  --u: calc(var(--sea-h) * 0.004785);              /* one source px, at the design's scale (2.16 × its 915/906 over 455.91) */
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  width: calc(var(--u) * var(--n));
  height: calc(var(--u) * var(--n));
  transform: translate(-50%, -50%);
  background: url("/art/sea-spot-sprites.png?v=1") calc(var(--u) * var(--o) * -1) 0 / calc(var(--u) * 444) calc(var(--u) * 88) no-repeat;
  -webkit-mask-image: radial-gradient(circle, #000 52%, transparent 70%);
  mask-image: radial-gradient(circle, #000 52%, transparent 70%);
  mix-blend-mode: screen;
  pointer-events: none;
}

.sea::before { display: none; }
.sea::after { content: none; }
.ocean-gl {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  opacity: 0;
  transition: opacity .8s ease;
}
.ocean-gl.is-on { opacity: 1; }

/* moon */
.moon-body {
  background: url("/art/moon.svg") center / contain no-repeat;
  opacity: 1;
  filter: none;
}

/* sky lanterns (and the Wish List's ring): the design's đèn trời, with a
   soft warm glow behind instead of the old drawn body */
.lantern-body {
  border-radius: 0;
  background: url("/art/lantern.svg") center / contain no-repeat;
  box-shadow: none !important;
}
.lantern-body::before {
  left: -45%; right: -45%;
  top: -25%; bottom: -35%;
  height: auto;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(closest-side, hsl(36 100% 62% / .5), hsl(28 100% 50% / .16) 55%, transparent);
  opacity: .75;
  transition: opacity .25s ease;
}
.lantern-body::after { display: none; }
.lantern:hover .lantern-body::before,
.lantern:focus-visible .lantern-body::before,
.lantern.tapped .lantern-body::before,
.ws-lantern.is-focus .ws-body::before { opacity: 1; }
.lantern.lantern-lite .lantern-body::before { opacity: .5; }

/* every UI lantern icon is the design's đèn trời too (one asset, sized per
   place; the boxes keep its 640 × 822 proportions) — the Top Donors podium
   and its loading lamp, the List View cards, the search results, the menu,
   the view switch and the donor thank-you. Glow follows the drawing
   (drop-shadow), never the box. */
.td-lamp, .td-icon, .top-donors-empty.is-loading::before, .wl-lamp, .ws-hit-lamp,
.mi-lantern::before, .ws-view-lamp, .donor-success-sub::after {
  border-radius: 0;
  background: url("/art/lantern.svg") center / contain no-repeat;
  box-shadow: none;
}
.td-lamp { filter: drop-shadow(0 0 10px hsl(var(--l-mid) / .6)); }
.top-donor-rank-1 .td-lamp { box-shadow: none; filter: drop-shadow(0 0 14px hsl(var(--l-hot) / .75)); }
.wl-lamp { filter: drop-shadow(0 0 5px hsl(var(--l-mid) / .5)); }
.wl-lamp::before, .wl-lamp::after { display: none; }
.wl-item:active .wl-lamp { box-shadow: none; filter: drop-shadow(0 0 8px hsl(var(--l-hot) / .9)); }
.wl-item.is-unnamed .wl-lamp { background: url("/art/lantern.svg") center / contain no-repeat; filter: saturate(.2) brightness(.9); opacity: .8; }
.ws-hit-lamp { filter: drop-shadow(0 0 5px hsl(var(--l-mid) / .6)); }
/* the Top Donors list rows (List View): a small lantern before each rank */
.td-icon { flex: none; width: 18px; height: 23px; filter: drop-shadow(0 0 5px hsl(var(--l-mid) / .5)); }
.mi-lantern::before { content: ""; width: 14px; height: 18px; }
.ws-view-lamp { flex: none; width: 12px; height: 15.4px; }
.donor-success-sub::after {
  content: "";
  display: inline-block;
  width: .85em; height: 1.09em;
  margin-left: .3em;
  vertical-align: -.2em;
}

/* the lantern a wish rises in (lit by the Wish Guide, app.js › releaseFromBoat) */
.sky-launch {
  border-radius: 0;
  background: url("/art/lantern.svg") center / contain no-repeat;
  box-shadow: none;
  filter: drop-shadow(0 0 14px hsl(36 100% 60% / .75));
}

/* islands: each has its own drawing; its waterline is the box's bottom edge */
.island[data-art="1"] { --iar: .2905; }
.island[data-art="2"] { --iar: .329; }
.island[data-art="3"] { --iar: .324; }
.island[data-art="4"] { --iar: .3715; }
.island { max-width: calc((var(--sea-h) * (1 - var(--iy))) / var(--iar, .375)); }

/* the lighthouse: the design's drawing is not mirrored; its lamp sits at
   62.8% across / 20.5% down, the tower's foot at 62.6% across */
.headland-art { transform: none; }
.headland, .headland > .isle-focus { transform-origin: 62.6% 100%; }
.beacon, .lh-beam, .lh-flare { left: 62.8%; top: 20.5%; }
.reflect-lh { left: 62.6%; }
.lamp-halo { fill: hsl(46 100% 70% / .2); filter: blur(30px); }

/* the Wish Guide: Phúc Nguyên holding a lantern */
.wish-person { width: clamp(52px, 6.2vw, 76px); }
.wish-person-glow { filter: blur(60px); }

/* ---- the design's composition (reference screen): the lighthouse a
   little smaller at the right edge, its rocks set down into the sea; the
   moon large in the upper right, just left of the tower; the Wish Guide
   (the otter) sitting on the rocks left of the tower ---- */
:root {
  /* the lighthouse is about half the screen's height in the design (rocks
     to lamp), and at most 30% of the width, so its rocks leave the boat the
     water it needs round Nguyên Hội (checked at 375–1440px) */
  --lh-w: clamp(220px, min(45vh, 30vw), 440px);
  --guide-w: calc(var(--lh-w) * .125);  /* the otter: small on its rock shelf, never dominating the tower */
}
/* (its rocks reach into the sea, but leave the boat a channel below them
   to come round from Nguyên Hội: the journey checks this at every size) */
.headland { bottom: calc(var(--sea-h) * .78); }
.moon {
  right: calc(var(--lh-w) * .52);
  /* high in the sky, but never closer than ~9% to the top of the screen */
  bottom: calc(var(--sea-h) + min(29vh, 91vh - var(--sea-h) - clamp(104px, 11.5vw, 200px)));
  width: clamp(104px, 11.5vw, 200px);
  height: clamp(104px, 11.5vw, 200px);
}
.moon-reflect { right: calc(var(--lh-w) * .52 + clamp(104px, 11.5vw, 200px) / 2); }
@media (max-width: 720px) {

}
/* the otter sits on the flat rock shelf just left of the tower (x 380–470 of
   the drawing, its top at y ≈ 720), sized to that shelf, so it is always
   seated, never floating. Placed through the headland's own pose: bottom
   78% of the sea, then translate (2vw, +3% of the sea) scale .92 about the
   tower's foot (62.6% across) — and scale 1, +5% once the lighthouse is lit.
   The shelf's centre (x 425 → 43.5% across) lies (0.374 + 0.191·s)·w from
   the headland's right edge; its top 0.2825·s·w above its foot; the otter
   settles 2% of the lighthouse's width into the rock. */
.wish-caller {
  /* (the button is as wide as its speech bubble: placed by its centre) */
  right: calc(var(--lh-right) - 2vw + var(--lh-w) * .566);
  transform: translateX(50%);
  bottom: calc(var(--sea-h) * .75 + var(--lh-w) * .2499);
  animation: none;                 /* (seated: no bob) */
  transition: right 2.2s cubic-bezier(.33, 0, .2, 1), bottom 2.2s cubic-bezier(.33, 0, .2, 1), opacity .7s ease, visibility 0s;
}
html:has(.headland.lit) .wish-caller {
  right: calc(var(--lh-right) - 2vw + var(--lh-w) * .581);
  bottom: calc(var(--sea-h) * .73 + var(--lh-w) * .2725);
}
.wish-person { width: var(--guide-w); }

/* =====================================================================
   DESIGN THEME for every panel, form and pop-up — the scene's own palette
   (public/art/background.svg, sea-base.svg, island-N.svg):
     sky navy  #21325e   deep sea  #1e3659   island ink #05224d
     periwinkle #5e6491  sea mid   #586593   cloud      #c2cfff / #f7faff
     horizon peach #e0ac9d            lantern  #fdc306 → #fe9a02 → #ff6a00
   Surfaces are the night sky (navy → deep sea) with a breath of the peach
   horizon at their top; edges are cloud-lavender; backdrops are the sea's
   ink. The lantern orange stays the one warm accent (buttons, highlights),
   as the lanterns are the warm light in the design.
   ===================================================================== */
:root {
  --bg: 223 48% 25%;
  --foreground: 228 60% 97%;
  --muted-foreground: 228 30% 80%;
  --border: 228 35% 45%;
  --input: 218 52% 15%;
  --card: 223 45% 24%;
  --glass-bg: hsl(223 48% 22% / .74);
  --glass-border: hsl(227 100% 88% / .22);
  --shadow-card: 0 24px 60px -18px hsl(215 88% 8% / .8);
  --gradient-primary: linear-gradient(135deg, #fde7dd, #e0ac9d 42%, #fe9a02);
  --ds-surface:
    radial-gradient(120% 55% at 50% 0%, hsl(13 55% 75% / .14), transparent 70%),
    linear-gradient(180deg, hsl(223 48% 27% / .97), hsl(215 60% 17% / .98));
  --ds-backdrop: hsl(215 80% 7% / .7);
  --ds-chip: hsl(223 48% 20% / .62);
  --ds-chip-hover: hsl(225 40% 30% / .78);
  --ds-edge: hsl(227 100% 88% / .24);
  --ds-edge-strong: hsl(227 100% 90% / .5);
}

/* the panels themselves */
.sheet-card, .chooser-card, .pay-card, .logbook-card, .reveal-card,
.isle-card, .menu-card, .guide-card {
  background: var(--ds-surface);
  border-color: var(--ds-edge);
  box-shadow: var(--shadow-card);
}
.isle-card { box-shadow: 0 -18px 50px -14px hsl(215 88% 8% / .7); }
.isle-arrival, .voyage-map {
  background:
    radial-gradient(120% 90% at 50% 0%, hsl(13 55% 75% / .14), transparent 60%),
    hsl(220 50% 18% / .88);
  border-color: var(--ds-edge);
  color: hsl(var(--foreground));
}
.isle-arrival::after { background: hsl(220 50% 18% / .88); }

/* what dims the world behind a panel: the sea's ink, not black */
.reader, .sheet, .chooser, .pay, .reveal, .guide, .logbook, .paper { background: var(--ds-backdrop); }
.isle-sheet, .menu, .meaning { background: hsl(215 80% 7% / .45); }
.cdv-veil { background: radial-gradient(ellipse 70% 55% at 50% 45%, hsl(215 70% 12% / .1), hsl(215 75% 8% / .6) 72%, hsl(215 80% 6% / .85)); }

/* the Wish List: the night sky of the scene */
body.modal-open .logbook.ws, .logbook.ws {
  background:
    radial-gradient(70% 38% at 50% 40%, hsl(13 55% 75% / .12), transparent 70%),
    linear-gradient(180deg, #21325e 0%, #2b3a6c 50%, #1e3659 100%) !important;
}

/* small round controls and pills on the scene */
.ctrl, .controls .ctrl, .menu-btn, .pov-btn, .map-btn, .ws-icon, .meaning-btn,
.gd-btn, .ws-view, .logbook-tabs, .cd-row, .site-link, .guide-toggle, .lang-toggle {
  background: var(--ds-chip);
  border-color: var(--ds-edge);
}
.ctrl:hover, .ctrl:focus-visible, .controls .ctrl:hover, .controls .ctrl:focus-visible,
.menu-btn:hover, .menu-btn[aria-expanded="true"], .pov-btn:hover, .pov-btn:focus-visible,
.map-btn:hover, .map-btn:focus-visible, .map-btn.is-open, .ws-icon:hover, .ws-icon.is-on,
.meaning-btn:hover {
  background: var(--ds-chip-hover);
  border-color: var(--ds-edge-strong);
}
.wish-bubble, .wish-bubble::after, .boat-tag, .boat-hint, .boat-hint::after, .island-label {
  background: hsl(220 52% 16% / .74);
  border-color: var(--ds-edge);
}

/* fields */
.wl-search input, .ws-search { background: hsl(218 52% 14% / .72); border-color: var(--ds-edge); }
.wl-chip { background: hsl(223 48% 22% / .45); border-color: var(--ds-edge); }
.sheet-card input, .sheet-card textarea, .pay-card input, .pay-card textarea,
.reveal-card input, .claim-card input, .claim-card textarea {
  background: hsl(218 52% 13% / .8);
  border-color: hsl(227 100% 88% / .2);
}
.sheet-card input:focus, .sheet-card textarea:focus, .pay-card input:focus, .pay-card textarea:focus,
.reveal-card input:focus {
  border-color: hsl(13 55% 75% / .7);
  box-shadow: 0 0 0 3px hsl(13 55% 75% / .18);
}
/* the boat flag's PN lettering turns about its own centre (app.js › pnCss) */
.boat .pn-text { transform-box: fill-box; transform-origin: 50% 50%; }

/* ---- the Wish Guide's label, “Ước một điều ✨”: small and quiet, the same
   on-screen size wherever the otter is — on the shore rocks, on an island,
   at the lighthouse. (The island / lighthouse otter is scaled as a whole by
   --gs; its label is divided by --guide-k, the otter's on-screen share of the
   shore otter, so the words never grow with it.) ---- */
.wish-caller, .wish-guide-isle { gap: .22rem; }
.wish-caller .wish-bubble,
.wish-guide-isle .wish-bubble {
  padding: .22rem .5rem;
  font-size: calc(0.715rem / var(--guide-k, 1));
  border-radius: 999px;
}
.wish-guide-isle .wish-bubble { padding: calc(.22rem / var(--guide-k, 1)) calc(.5rem / var(--guide-k, 1)); }
.wish-caller .wish-bubble::after,
.wish-guide-isle .wish-bubble::after { width: 5px; height: 5px; }

/* =====================================================================
   THE BOAT ON THE WATER — a small wooden boat riding a real swell
   Three independent layers, each a transform only (compositor, no paint):
     .boat-lean   pitch from the boat's own acceleration + the settle when
                  it moors (Web Animations, app.js › JOURNEY › lean)
     .boat-heave  the swell lifting and dropping it
     .boat-roll   the swell rocking it
   The swell and the roll run on unrelated periods (7.3 s / 5.7 s) with
   uneven keyframes, so the motion never visibly repeats; amplitudes stay
   small (a few px, under 2°). Phúc Nguyên rides inside it (.boat-rider):
   anchored to the hull, he only keeps his balance a little against the roll.
   ===================================================================== */
.boat-lean { transform-origin: 50% 90%; }
.boat-heave { animation: boat-swell 7.3s ease-in-out infinite; }
.boat-roll { transform-origin: 50% 90%; animation: boat-rock 5.7s ease-in-out infinite; }
@keyframes boat-swell {
  0%   { transform: translateY(0); }
  17%  { transform: translateY(-3.5px); }
  34%  { transform: translateY(-6px); }
  49%  { transform: translateY(-2.5px); }
  66%  { transform: translateY(-5px); }
  83%  { transform: translateY(-1px); }
  100% { transform: translateY(0); }
}
@keyframes boat-rock {
  0%   { transform: rotate(-1.1deg); }
  21%  { transform: rotate(1.5deg); }
  38%  { transform: rotate(.3deg); }
  57%  { transform: rotate(1.8deg); }
  78%  { transform: rotate(-1.5deg); }
  100% { transform: rotate(-1.1deg); }
}
/* the passenger keeps his balance: a slight counter-sway and a breath of
   lift, lagging the roll, pivoting where he sits in the hull */
.boat .boat-rider {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: rider-balance 5.7s ease-in-out -1.3s infinite;
}
@keyframes rider-balance {
  0%   { transform: rotate(.7deg) translateY(0); }
  24%  { transform: rotate(-1deg) translateY(-1.2%); }
  41%  { transform: rotate(-.2deg) translateY(-.4%); }
  60%  { transform: rotate(-1.2deg) translateY(-1%); }
  80%  { transform: rotate(.9deg) translateY(0); }
  100% { transform: rotate(.7deg) translateY(0); }
}
/* where the hull meets the sea: a soft contact shadow and the warm light of
   the lantern Phúc Nguyên holds, lying on the water under the boat */
.boat-shadow, .boat-glow {
  position: absolute;
  left: 50%;
  top: 85%;
  pointer-events: none;
}
.boat-shadow {
  width: calc(var(--bw) * .92);
  height: 10px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, hsl(216 70% 10% / .5), transparent);
  filter: blur(2px);
}
.boat-glow {
  width: calc(var(--bw) * 1.15);
  height: 22px;
  transform: translate(-50%, -30%);
  border-radius: 50%;
  background: radial-gradient(closest-side, hsl(40 100% 66% / .34), hsl(30 100% 56% / .1) 60%, transparent);
  mix-blend-mode: screen;
  animation: boat-glow 4.3s ease-in-out infinite alternate;
}
@keyframes boat-glow {
  from { opacity: .62; transform: translate(-50%, -30%) scaleX(.92); }
  to   { opacity: 1;   transform: translate(-50%, -30%) scaleX(1.06); }
}
/* phones: the rock is a compositor-only transform, cheap enough to keep */
@media (max-width: 640px) {
  .boat-roll { animation: boat-rock 5.7s ease-in-out infinite !important; }
}
body.modal-open .boat-lean, body.modal-open .boat-rider, body.modal-open .boat-glow { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .boat-heave, .boat-roll, .boat .boat-rider, .boat-glow { animation: none !important; }
}

/* ---- ♐ Sagittarius in the upper sky (index.html .sky-sagittarius) ----
   Fixed with the sky (the camera never moves it), centred above the title
   block, clear of the moon (upper right) and the lighthouse. Ivory stars,
   champagne lines a hair wide, a soft golden halo on the brightest; the
   lines draw in once, a few stars breathe slowly. */
.sky-sagittarius {
  /* the clear sky above the design's clouds: their tops lie 236 units above
     its horizon (624 vs 861 of the 3016×1472 frame) = 0.0782 of the frame's
     on-screen width (--sky-w) above the sea's top edge */
  --cloud-top: calc(100dvh - var(--sea-h) - var(--sky-w) * .0782);
  --sg-h: calc(var(--cloud-top) * .66);          /* about two thirds of that sky */
  position: fixed;
  z-index: 1;
  left: 50%;
  top: calc(var(--cloud-top) - 2.5vh - var(--sg-h));   /* its foot just above the clouds */
  height: var(--sg-h);
  width: auto;
  aspect-ratio: 205 / 228;
  transform: translateX(-50%);
  overflow: visible;
  pointer-events: none;
  opacity: .9;
}
.sg-lines line {
  stroke: #f3dcae;
  stroke-opacity: .32;
  stroke-width: .7px;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
}
.sg-star { fill: #fff4dc; }
.sg-halo { fill: url(#sg-glow); }
.sg-lines { opacity: 0; }
html.world-ready .sg-lines { animation: sg-draw 2.6s ease-out .6s forwards; }
@keyframes sg-draw { from { opacity: 0; } to { opacity: 1; } }
.sg-stars .t1, .sg-stars .t2, .sg-stars .t2b, .sg-stars .t3 { animation: sg-twinkle 7s ease-in-out infinite; }
.sg-stars .t2  { animation-duration: 9s;  animation-delay: -3s; }
.sg-stars .t2b { animation-duration: 8s;  animation-delay: -5.5s; }
.sg-stars .t3  { animation-duration: 10s; animation-delay: -1.5s; }
@keyframes sg-twinkle { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

@media (prefers-reduced-motion: reduce) {
  .sg-lines, .sg-stars g { animation: none !important; }
  .sg-lines { opacity: 1; }
}

/* the sea's dotted route trail (app.js › drawRoute) is off: the design's sea
   is clear. The route itself still plans and sails, and the voyage map
   (the map button) still shows it. Remove this rule to bring the dots back. */
.sea-route { display: none; }

/* ---- phones: the lighthouse must leave Nguyên Hội its water ----
   Root cause of the overlap: on a phone 30vw is far below the 220px floor,
   so the lighthouse sat at 220px (≈59% of a 375px screen) and its rocks —
   set into the sea at 78% of its height, the very row Nguyên Hội lies on
   (--iy .78, 60% across) — reached over that island. Here the lighthouse
   follows the screen's width instead: its rocks' left foot stays right of
   the island (rocks ≈ (W + 35) − 0.95·w; island right edge ≈ 0.6·W + 50
   → w ≤ 0.42·W − 16). No island moves. */
@media (max-width: 640px) {
  :root {
    --lh-w: calc(41vw - 16px);
    --guide-w: max(24px, calc(var(--lh-w) * .125));   /* (the otter stays legible) */
  }
}

/* ---- the moon belongs to the lighthouse (the reference screen): just left
   of the tower's top, its centre level with the dome, about 45% of the
   lighthouse's width across — on every screen, so wherever the lighthouse is
   sized (phones included) the moon keeps that place beside it.
   Measured through the headland's planted pose (foot at 75% of the sea after
   its +3% shift, translate 2vw, scale .92 about the tower's foot): the
   tower's centre lies (0.374·w − 2vw + --lh-right) from the right edge, its
   dome top 0.938·w above the foot; the moon's centre sits 0.41·w left of the
   tower and level with the dome. ---- */
.moon {
  --moon-d: calc(var(--lh-w) * .45);
  width: var(--moon-d);
  height: var(--moon-d);
  right: calc(var(--lh-right) - 2vw + var(--lh-w) * .784 - var(--moon-d) / 2);
  bottom: calc(var(--sea-h) * .75 + var(--lh-w) * .938 - var(--moon-d) / 2);
}
.moon-reflect { right: calc(var(--lh-right) - 2vw + var(--lh-w) * .784); }

/* ---- the donation-flow overlays (wish form, donor form, chooser, bank
   transfer) scroll when their card is taller than the screen ----
   Root cause of "the donor form can't reach its bottom on phones": these
   overlays are position:fixed; inset:0 with overflow left visible and the
   card centred by grid place-items — a card taller than the screen (the
   donor form is ~840px) simply ran off the bottom edge, and nothing could
   scroll (the overlay is fixed, the page behind it doesn't move). The
   overlay itself is now the ONE scroll container, and the card is centred
   with auto margins instead: centred when it fits, scrolled from its own
   top when it doesn't (place-items:center would push an over-tall card's
   top out of reach). Safe-area padding keeps the last button clear of the
   home indicator. Desktop, where the cards fit, looks exactly as before. */
.sheet, .chooser, .pay {
  place-items: initial;
  overflow-y: auto;
  overscroll-behavior: contain;          /* (the scene behind never scrolls along) */
  -webkit-overflow-scrolling: touch;
  padding-top: calc(1.25rem + env(safe-area-inset-top, 0px));
  padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
}
.sheet > .sheet-card, .chooser > .chooser-card, .pay > .pay-card { margin: auto; }

/* the boat and Phúc Nguyên in the water below them (index.html ›
   .boat-reflect, the design's reflection): seen through the surface — softly,
   fading with depth (its mask), and wavering a little as the water moves,
   never a solid dark copy. It stays level on the water (outside the swell). */
.boat-reflect {
  opacity: .62;
  filter: blur(.7px);
  transform-box: view-box;
  transform-origin: 492px 452px;
  animation: boat-reflect 4.6s ease-in-out infinite alternate;
}
@keyframes boat-reflect {
  from { transform: skewX(-1.6deg) scaleY(.97); }
  to   { transform: skewX(1.6deg) scaleY(1.03); }
}
body.modal-open .boat-reflect { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .boat-reflect { animation: none; } }

/* the swell, INSIDE the drawing (index.html › .boat-heave-g / .boat-roll-g).
   It used to run on the HTML wrappers .boat-heave / .boat-roll — and Chrome
   keeps a running transform animation's layer as first drawn: started far out
   at sea with the camera wide, the boat was stretched ~2× into blur at every
   island (the camera's 1.42× push-in × its near-water scale). On SVG groups
   the swell is painted with the boat, at its real on-screen size, always
   sharp. Same rhythm and rock; offsets in the art's own units (≈5.5 per css
   px of a full-size boat), the rock about the same point (50% / 90%). */
.boat .boat-heave, .boat .boat-roll { animation: none !important; }
.boat-settle-g { transform-box: view-box; transform-origin: 492px 422px; }   /* (app.js › settle) */
/* riding the swell (inside the boat's drawing, never its route position).
   The water at the hull (index.html › .boat-sea) and its contact shadow stay
   level OUTSIDE these groups, so the hull rises a little and settles a little
   deeper IN the water — never lifting clear of it (a 28-unit lift used to
   carry the shadow and the water up with it: the boat hovered). The pitch runs
   a quarter-beat behind the rise: bow up as the swell lifts, down past it. */
.boat-heave-g { animation: boat-ride 8.3s ease-in-out -2s infinite; }
.boat-roll-g {
  transform-box: view-box;
  transform-origin: 492px 452px;
  animation: boat-pitch 9.7s ease-in-out -3.9s infinite;
}
/* …and a very slight roll, side to side about the keel (on its own, shorter
   period): bob, pitch and roll never line up, so the float never repeats */
.boat-sway-g {
  transform-box: view-box;
  transform-origin: 492px 452px;
  animation: boat-sway 5.9s ease-in-out -1.1s infinite;
}
/* (uneven steps, same small ranges as before: a living swell, not a metronome) */
@keyframes boat-ride {
  0%   { transform: translateY(-6px); }
  23%  { transform: translateY(-1.5px); }
  41%  { transform: translateY(2px); }
  60%  { transform: translateY(-3.5px); }
  78%  { transform: translateY(1px); }
  100% { transform: translateY(-6px); }
}
@keyframes boat-pitch {
  0%   { transform: rotate(-.7deg); }
  26%  { transform: rotate(.45deg); }
  44%  { transform: rotate(-.2deg); }
  63%  { transform: rotate(.8deg); }
  82%  { transform: rotate(-.35deg); }
  100% { transform: rotate(-.7deg); }
}
@keyframes boat-sway {
  0%   { transform: skewY(-.5deg) translateX(-.6px); }
  31%  { transform: skewY(.35deg) translateX(.4px); }
  57%  { transform: skewY(-.2deg) translateX(-.2px); }
  79%  { transform: skewY(.55deg) translateX(.6px); }
  100% { transform: skewY(-.5deg) translateX(-.6px); }
}
body.modal-open .boat-heave-g, body.modal-open .boat-roll-g, body.modal-open .boat-sway-g { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .boat-heave-g, .boat-roll-g, .boat-sway-g { animation: none !important; } }

/* ---- the wish, read off THE design's lantern (art/lantern.svg) ----
   The reader drew its own CSS lantern whose height followed the text (21rem
   up to 74vh): a long wish stretched it into a tall capsule, a short one
   left it squat. Now it is the approved đèn trời at its own fixed
   proportions (640 × 822), sized by the screen alone; the words sit inside
   its body — 15–85% across, 17–74% down, above the flame — and never size
   it (app.js › fitReaderMsg keeps a longer, older wish inside, with "Xem
   thêm" for the whole letter). Type scales with the lantern (cqw). */
.lantern-dialog { width: auto; max-width: none; }
.lantern-shape {
  width: min(86vw, calc(min(66vh, 560px) * .7786));
  aspect-ratio: 640 / 822;
  height: auto;
  min-height: 0;
  max-height: none;
  padding: 0;
  display: block;
  border-radius: 0;
  background: url("/art/lantern.svg") center / contain no-repeat;
  box-shadow: none;
  filter: drop-shadow(0 0 34px hsl(var(--l-mid) / .42)) drop-shadow(0 0 90px hsl(var(--l-deep) / .22));
  animation: none;
  container-type: inline-size;
}
.lantern-shape::before, .lc-cord, .lc-cap, .lc-base, .lc-flame { display: none; }
.lantern-shape .lc-paper {
  position: absolute;
  left: 15%; right: 15%;
  top: 17%; bottom: 26%;
  width: auto;
  max-height: none;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.lantern-shape .reader-from { margin: 0 0 .15em; font-size: clamp(0.99rem, 5cqw, 1.265rem); color: hsl(15 85% 16%); }
.lantern-shape .reader-label { margin: .2em 0 .35em; font-size: clamp(0.638rem, 2.8cqw, 0.77rem); color: hsl(18 70% 20% / .78); }
.lantern-shape .reader-msg {
  font-size: clamp(1.045rem, 4.9cqw, 1.32rem);
  line-height: 1.42;
  color: hsl(15 90% 12%);
  text-shadow: 0 0 8px hsl(42 100% 72% / .55);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: normal;
}
.lantern-shape .reader-more { margin-top: .5em; font-size: clamp(0.77rem, 3.3cqw, 0.902rem); color: hsl(15 85% 16%); background: hsl(40 100% 85% / .35); border-color: hsl(18 70% 22% / .45); }
.lantern-shape .reader-time { margin-top: .45em; font-size: clamp(0.66rem, 2.9cqw, 0.792rem); color: hsl(18 70% 18% / .75); }
.lantern-shape .reader-amount-val { color: hsl(15 85% 18%); background: hsl(42 100% 85% / .4); border-color: hsl(18 70% 25% / .35); }

/* ---- reading the wish on the lantern: the words first, the glow second ----
   The wish sat straight on the brightest part of the đèn trời — its orange
   rising to yellow, with the otter emblem's pale lines crossing the words —
   and carried its own pale-gold glow (text-shadow 8px), which haloed every
   letter into the light. Now: a calm reading zone in the lantern's OWN
   orange, soft-edged and oval like the lantern (no box), which quiets the
   emblem lines and the bright patches only behind the words; the text a
   deep warm brown, a touch heavier, with no glow — just a hairline of light
   beneath for crispness. The lantern, its colours and its outer glow are as
   designed. */
.lantern-shape .lc-paper { z-index: 1; }
.lantern-shape::after {        /* (on the lantern, not the clipped text box: its edge stays soft) */
  content: "";
  position: absolute;
  left: 6%; right: 6%;
  top: 14%; bottom: 21%;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side, hsl(26 100% 49% / .62) 0%, hsl(26 100% 49% / .5) 52%, hsl(26 100% 49% / .22) 78%, transparent 100%);
  pointer-events: none;
}
.lantern-shape .reader-msg {
  font-weight: 500;
  letter-spacing: .005em;
  line-height: 1.45;
  color: hsl(14 85% 9%);
  text-shadow: 0 1px 0 hsl(36 100% 74% / .3);
}
.lantern-shape .reader-from { color: hsl(14 85% 10%); text-shadow: none; }
.lantern-shape .reader-label { color: hsl(14 70% 13% / .82); font-weight: 700; }
.lantern-shape .reader-time { color: hsl(14 70% 12% / .8); font-weight: 600; }

/* Cormorant's default figures are old-style (they dip and rise): money and dates
   read better level. !important because every `font:` shorthand resets this. */
body * { font-variant-numeric: lining-nums !important; }

/* soft grain over the world, like the posters: one small tile, never moving, never
   catching a tap. It sits right above .scene (z 2, and this comes later in the page)
   and under all the UI (z 3+): the art is grainy, titles, buttons and dialogs stay
   clean. A phone skips the blend (it would redraw the whole screen with every frame
   of the sea) and shows the same tile a little fainter. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 2;
  background: url("/art/grain.png?v=1") 0 0 / 256px 256px;
  mix-blend-mode: soft-light;
  opacity: .13;
  pointer-events: none;
}
@media (max-width: 640px) {
  body::after { mix-blend-mode: normal; opacity: .04; }
}

/* island names: a step bigger again than the rest of the small text. The face has
   no weight above 700, so a hairline stroke in the text's own colour thickens it. */
.island-label .isle-text {
  font-size: 1.1em;
  -webkit-text-stroke: .35px currentColor;
}
.ia-name {
  font-size: 1.234rem;
  -webkit-text-stroke: .35px currentColor;
}
@media (max-width: 640px) {
  .ia-name { font-size: 1.15rem; }
  /* the lighthouse stands at the screen's edge: its label (no overall goal on a
     phone, see milestoneLabel) moves in so it stays on screen */
  .lh-label { transform: translateX(calc(-50% - 1.6rem)); }
}

/* the same grain on the big panels: a faint tile as their top background layer,
   so the text above it stays clean (no blend: they sit over a moving world) */
.isle-card, .guide-card, .sheet-card, .chooser-card, .pay-card, .reveal-card, .logbook-card {
  background: url("/art/grain-panel.png?v=4") 0 0 / 256px 256px, var(--ds-surface);
}
/* the wish as it is read: the paper itself, and the dimmed world around the lantern */
.reader, .paper { background: url("/art/grain-panel.png?v=4") 0 0 / 256px 256px, var(--ds-backdrop); }
.paper-sheet {
  background:
    url("/art/grain-panel.png?v=4") 0 0 / 256px 256px,
    radial-gradient(140% 60% at 50% -10%, hsl(43 62% 92% / .9), transparent 60%),
    radial-gradient(60% 40% at 100% 100%, hsl(30 45% 66% / .5), transparent 70%),
    radial-gradient(50% 40% at 0% 90%, hsl(32 45% 68% / .45), transparent 70%),
    linear-gradient(160deg, hsl(41 58% 88%), hsl(37 46% 80%) 55%, hsl(33 40% 73%));
}

/* a browser gives form controls its own font unless told otherwise */
:where(button, input, select, textarea) { font-family: var(--font); }

/* the "i" beside a label: a small ring that shows a short note on tap */
.label-row { display: flex; align-items: center; gap: .45rem; }
.label-row .label { margin: 0; }
.info-btn {
  display: inline-grid;
  place-items: center;
  width: 1.15rem;
  height: 1.15rem;
  padding: 0;
  font: italic 700 .8rem/1 var(--font);
  color: hsl(var(--l-hot));
  background: none;
  border: 1px solid hsl(var(--l-hot) / .6);
  border-radius: 50%;
  cursor: pointer;
}
.info-btn:hover, .info-btn[aria-expanded="true"] { background: hsl(var(--l-hot) / .16); }
.info-btn:focus-visible { outline: 2px solid hsl(var(--l-hot)); outline-offset: 2px; }
.info-note[hidden] { display: none; }
.info-note { margin: .35rem 0 .15rem; }
