/* maunoh — globe homepage.
   One fixed background (the globe) that never scrolls away; scrolling
   only re-aims its camera and fades the layers stacked above it. */

:root {
  --bg: #faf8f5;
  --ink: rgba(30, 29, 28, 0.94);
  --ink-dim: rgba(58, 56, 54, 0.62);
  --ink-faint: rgba(58, 56, 54, 0.42);
  --line: rgba(30, 29, 28, 0.12);
  --bubble-fill: rgba(255, 255, 255, 0.6);
  /* the app's glass pane is translucent frosted glass, not flat opaque
     white — it's backed by backdrop-filter blur, and a faint tint of
     whatever's behind (map, glow) shows through the face itself */
  --blip-fill: rgba(255, 255, 255, 0.64);
  --blue: #a1d8dc;
  --purple: #726eb3;
  --orange: #c67458;
  --font: 'Courier Prime', 'Courier New', Courier, monospace;
}
/* dark palette: applied automatically when the system is in dark mode,
   unless the little toggle in the corner has set an explicit choice
   (data-theme on <html>), in which case that choice always wins */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #141414;
    --ink: rgba(255, 255, 255, 0.94);
    --ink-dim: rgba(255, 255, 255, 0.6);
    --ink-faint: rgba(255, 255, 255, 0.4);
    --line: rgba(255, 255, 255, 0.14);
    --bubble-fill: rgba(22, 22, 22, 0.56);
    --blip-fill: rgba(27, 26, 25, 0.58);
  }
}
:root[data-theme="dark"] {
  --bg: #141414;
  --ink: rgba(255, 255, 255, 0.94);
  --ink-dim: rgba(255, 255, 255, 0.6);
  --ink-faint: rgba(255, 255, 255, 0.4);
  --line: rgba(255, 255, 255, 0.14);
  --bubble-fill: rgba(22, 22, 22, 0.56);
  --blip-fill: rgba(27, 26, 25, 0.58);
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  letter-spacing: -0.2px;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}
a { color: inherit; }
button { font-family: var(--font); }
::selection { background: var(--purple); color: #fff; }

/* ───────── custom cursor: a small dot that swells into a soft ring
   over anything clickable, instead of the system's hand icon ───────── */
@media (hover: hover) and (pointer: fine) {
  /* !important because several specific elements (buttons, blips, the
     back-to-top link...) set their own `cursor: pointer`, which beats a
     plain `body` rule -- that's what kept leaving the native hand
     showing almost everywhere instead of the custom dot */
  body, body * { cursor: none !important; }
  /* the globe keeps its native grab/grabbing hand -- a clearer drag
     cue than a dot -- the custom cursor steps aside for it in JS too */
  #globe-map, #globe-map * { cursor: grab !important; }
  #globe-map:active, #globe-map:active * { cursor: grabbing !important; }
}
#cursor-dot {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  pointer-events: none;
  z-index: 95;
  opacity: 0;
  transition: opacity 0.2s ease;
}
#cursor-dot.show { opacity: 1; }
#cursor-dot i {
  position: absolute;
  top: -5px;
  left: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--ink);
  display: block;
  transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.18s ease;
}
#cursor-dot.hover i { transform: scale(2.6); opacity: 0.16; }

/* ───────── loading screen ───────── */
#loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  transition: opacity 0.6s ease;
}
#loader.hide { opacity: 0; pointer-events: none; }

/* ───────── dark mode toggle ─────────
   a small round button, fixed in the corner, on top of everything.
   the sun/moon glyph is pure CSS: a disc with a second, offset disc
   cut out of it via box-shadow, which becomes a crescent (moon) or
   a plain dot (sun) just by moving the cutout. */
#theme-toggle {
  position: fixed;
  top: max(20px, env(safe-area-inset-top));
  right: max(20px, env(safe-area-inset-right));
  z-index: 90;
  width: 38px;
  height: 38px;
  padding: 0;
  border-radius: 50%;
  background: var(--bubble-fill);
  border: 1px solid var(--line);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  cursor: pointer;
  transition: transform 0.2s ease;
}
#theme-toggle:hover { transform: scale(1.07); }
#theme-toggle::before {
  content: '';
  display: block;
  width: 13px;
  height: 13px;
  margin: 12px auto 0;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 5px -5px 0 0 var(--bubble-fill);
  transition: box-shadow 0.25s ease, transform 0.25s ease;
  transform: scale(0.78);
}
/* dark mode (or the toggle set to dark): the cutout moves off-disc,
   so the full circle reads as a plain sun instead of a crescent */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) #theme-toggle::before { box-shadow: 14px -14px 0 0 var(--bubble-fill); transform: scale(1); }
}
:root[data-theme="dark"] #theme-toggle::before { box-shadow: 14px -14px 0 0 var(--bubble-fill); transform: scale(1); }
.loader-mark {
  width: 56px;
  height: auto;
  opacity: 0;
  transform: scale(0.82);
  animation: mark-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) 0.15s forwards,
             mark-pulse 1.6s ease-in-out 1.3s infinite;
}
/* two marks, one shown per colour scheme. Falls back to prefers-color-
   scheme alone for the very first paint, before site.js (loaded at the
   end of body) has had a chance to run and read/apply the saved
   data-theme override -- so there's still a correct guess from the
   instant the page paints, with nothing to wait on since both marks are
   inlined as data URIs (see index.html). But once data-theme IS set
   (a few ms later, or instantly on repeat visits where it was already
   applied before this stylesheet is even parsed), that explicit choice
   wins over the system setting -- same :root[data-theme] pattern as the
   theme-toggle icon above. Without this half, someone who manually
   switched the site to dark while their OS is still in light mode (or
   vice versa) would get the mark picked for the OS, on a background
   coloured for the site's real theme -- e.g. the dark mark on a dark
   background, nearly invisible. */
.loader-mark-light { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .loader-mark-dark { display: none; }
  :root:not([data-theme="light"]) .loader-mark-light { display: block; }
}
:root[data-theme="dark"] .loader-mark-dark { display: none; }
:root[data-theme="dark"] .loader-mark-light { display: block; }
:root[data-theme="light"] .loader-mark-light { display: none; }
:root[data-theme="light"] .loader-mark-dark { display: block; }
@keyframes mark-in { to { opacity: 1; transform: scale(1); } }
@keyframes mark-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }

/* three drifting dots under the mark, same colours/sizes/motion as the
   app's cold-start loader. each dot's own cycle there is delay(i*300ms)
   + out(900ms) + back(900ms), looped as one unit -- so the stagger isn't
   a fixed phase offset, each dot genuinely has a different-length loop
   (1800/2100/2400ms). reproduced here as one animation per dot, sized to
   its own full loop length, with the delay baked in as a held 0% segment
   rather than via animation-delay, so the three stay in that same
   slowly-drifting relationship on repeat instead of simply offset. */
.loader-dots { display: flex; align-items: center; justify-content: center; margin-top: 6px; opacity: 0; animation: dots-in 0.6s ease 0.6s forwards; }
.loader-dot { display: inline-block; border-radius: 50%; margin: 0 3px; }
.loader-dot-1 { width: 16px; height: 16px; background: #a1d8dc; animation: drift-1 1.8s ease-in-out infinite; }
.loader-dot-2 { width: 12px; height: 12px; background: #726eb3; animation: drift-2 2.1s ease-in-out infinite; }
.loader-dot-3 { width: 14px; height: 14px; background: #c67458; animation: drift-3 2.4s ease-in-out infinite; }
@media (prefers-color-scheme: dark) {
  .loader-dot-1 { background: #87b2b4; }
  .loader-dot-2 { background: #625f94; }
  .loader-dot-3 { background: #a4644d; }
}
@keyframes dots-in { to { opacity: 1; } }
@keyframes drift-1 { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-10px); } }
/* dot 2's own spread is 0 in the app (it pulses in place, no horizontal
   travel) -- kept as a real keyframe rather than dropped, so its timing
   stays part of the same staggered-length family as the other two. */
@keyframes drift-2 { 0%, 100% { transform: translateX(0); } 57.143% { transform: translateX(0); } }
@keyframes drift-3 { 0%, 25% { transform: translateX(0); } 62.5% { transform: translateX(10px); } 100% { transform: translateX(0); } }

/* ───────── the fixed globe ───────── */
#globe-stage {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--bg);
  transition: opacity 0.3s ease;
}
/* while the dark-mode toggle swaps Mapbox's style, the map has to
   reload every tile from scratch and flashes its own blank/black
   canvas in the meantime -- fading the whole stage out (down to the
   page's own --bg colour, set above) hides that flash instead of
   exposing it, then it fades back in once the new style is ready */
#globe-stage.style-switching { opacity: 0; }
#globe-map {
  position: absolute;
  inset: 0;
  cursor: grab;
  /* without this, click-dragging the globe fires the browser's native
     text-selection gesture instead of panning the map */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
#globe-map:active { cursor: grabbing; }
#globe-map .mapboxgl-ctrl-logo,
#globe-map .mapboxgl-ctrl-attrib { opacity: 0.35; }

/* the blur/reveal layer: blurs the whole globe except a soft circle that
   follows the pointer (or a finger drag), where the sharp map shows
   through. Driven entirely by --mx/--my/--r, set in site.js. */
#reveal-veil {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--bg);
  -webkit-backdrop-filter: blur(22px) saturate(1.05);
  backdrop-filter: blur(22px) saturate(1.05);
  opacity: 0.94;
  /* a wider, multi-stop feather instead of one straight-line ramp —
     the single "transparent → black over 70px" stop read as a fairly
     hard edge; easing it through a few intermediate alpha steps over a
     longer distance gives a softer, more gradual falloff, closer to
     the Framer dot references */
  /* --fw is the width of the fade from clear to fully opaque. It's wide at
     rest (a soft, gradual hover edge) and narrowed right down during the
     burst, where a wide fade read as a big half-transparent band that then
     snapped to opaque. */
  -webkit-mask-image: radial-gradient(circle at var(--mx, 50%) var(--my, 32%),
      transparent 0, transparent var(--r, 0px),
      rgba(0, 0, 0, 0.18) calc(var(--r, 0px) + var(--fw, 140px) * 0.28),
      rgba(0, 0, 0, 0.45) calc(var(--r, 0px) + var(--fw, 140px) * 0.54),
      rgba(0, 0, 0, 0.78) calc(var(--r, 0px) + var(--fw, 140px) * 0.78),
      black calc(var(--r, 0px) + var(--fw, 140px)), black 100%);
  mask-image: radial-gradient(circle at var(--mx, 50%) var(--my, 32%),
      transparent 0, transparent var(--r, 0px),
      rgba(0, 0, 0, 0.18) calc(var(--r, 0px) + var(--fw, 140px) * 0.28),
      rgba(0, 0, 0, 0.45) calc(var(--r, 0px) + var(--fw, 140px) * 0.54),
      rgba(0, 0, 0, 0.78) calc(var(--r, 0px) + var(--fw, 140px) * 0.78),
      black calc(var(--r, 0px) + var(--fw, 140px)), black 100%);
  transition: opacity 0.5s ease;
  pointer-events: none;
}
/* browsers without backdrop-filter: fall back to a plain translucent
   scrim rather than a broken, unblurred reveal hole */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #reveal-veil { background: var(--bg); opacity: 0.55; -webkit-mask-image: none; mask-image: none; }
}

/* ───────── hero ───────── */
#hero-ui {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
}
#hero-ui a, #hero-ui button { pointer-events: auto; }

.hero-mid {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  width: min(90vw, 420px);
}
.hero-logo {
  width: clamp(130px, 28vw, 230px);
  height: auto;
  opacity: 0;
  animation: rise-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) 0.1s forwards;
  filter: drop-shadow(0 2px 18px rgba(0, 0, 0, 0.12));
}
.hero-tagline {
  margin: 14px 0 0;
  font-size: clamp(11.5px, 1.7vw, 13.5px);
  color: var(--ink-dim);
  opacity: 0;
  animation: rise-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) 0.32s forwards;
}
.hero-sub {
  margin: 6px 0 0;
  font-size: clamp(11px, 1.6vw, 12.5px);
  color: var(--ink-faint);
  opacity: 0;
  animation: rise-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) 0.46s forwards;
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes rise-in-center {
  from { opacity: 0; transform: translate(-50%, 10px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

.corner-btn {
  position: absolute;
  bottom: max(26px, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 18px;
  border-radius: 16px;
  background: var(--bubble-fill);
  border: 1px solid var(--line);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  font-size: 12px;
  letter-spacing: -0.2px;
  text-decoration: none;
  color: var(--ink);
  opacity: 0;
  animation: rise-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) 0.6s forwards;
  transition: transform 0.2s ease;
}
.corner-btn:hover { transform: translateY(-2px); }
.corner-label {
  font-size: 9px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.corner-btn { width: 210px; align-items: center; text-align: center; }   /* both the same size */
.corner-left { right: calc(50% + 10px); }      /* both buttons sit near the centre */
.corner-right { left: calc(50% + 10px); }

.scroll-hint {
  position: absolute;
  bottom: 110px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  font-size: 10px;
  letter-spacing: 0.5px;
  color: var(--ink-faint);
  opacity: 0;
  /* own keyframes (not the shared rise-in) because an animated
     `transform` replaces the base transform wholesale -- rise-in's
     translateY alone would wipe out the translateX(-50%) centering
     once the animation finished, leaving the hint off-centre */
  animation: rise-in-center 1.1s ease 0.9s forwards;
}
.scroll-hint i {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--purple);
  animation: bob 2.2s ease-in-out infinite;
}
@keyframes bob {
  0%, 100% { transform: translateY(0); opacity: 0.35; }
  50% { transform: translateY(7px); opacity: 1; }
}

@media (max-width: 560px) {
  /* "get it on the app store" wraps to two lines at this width while
     "join the beta" stays on one, so without a shared height the two
     buttons came out visibly different sizes. min-height plus centering
     the content inside it makes both the same size regardless of which
     one wraps. */
  .corner-btn { padding: 10px 14px; font-size: 10.5px; border-radius: 14px; width: calc(50vw - 26px); max-width: 170px; min-height: 58px; justify-content: center; }
  .corner-left { bottom: max(22px, env(safe-area-inset-bottom)); }
  .corner-right { bottom: max(22px, env(safe-area-inset-bottom)); }
}

/* ───────── city overlay ───────── */
#city-ui {
  position: fixed;
  inset: 0;
  z-index: 2;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
.city-label {
  position: absolute;
  top: max(26px, env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px;
  border-radius: 999px;
  background: var(--bubble-fill);
  border: 1px solid var(--line);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  font-size: 11px;
  letter-spacing: -0.1px;
  color: var(--ink-dim);
  white-space: nowrap;
}
.city-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--orange);
  box-shadow: 0 0 8px 2px rgba(198, 116, 88, 0.6);
}

/* ───────── map markers (both the hero's scattered dots and the city's
   real-looking blip cards use these) ───────── */
.mk-dot {
  width: var(--s, 14px);
  height: var(--s, 14px);
  border-radius: 50%;
  background: var(--c, var(--blue));
  box-shadow: 0 0 calc(var(--s, 14px) * 0.9) calc(var(--s, 14px) * 0.12) var(--cg, rgba(161,216,220,.5));
  opacity: 0.85;
  transition: opacity 0.3s ease, transform 0.3s ease;
  cursor: pointer;
}
.mk-dot.near { transform: scale(1.25); opacity: 1; }

/* Matches the real app's components/Bubble.js structure: the glass
   pane itself is a nearly-opaque frosted fill with NO colour painted
   inside it. All the colour lives in a separate shape sitting directly
   BEHIND the glass (GlowOrb in the app) that's only a few px bigger,
   filled with one diagonal 3-stop gradient and blurred — so it only
   ever shows as a thin soft-edged colour halo bleeding around the
   glass's outside silhouette, never as patches inside the bubble.

   Important: the glow is a real SIBLING element painted before the
   glass, not a ::before on the glass itself. A box's own background is
   always the floor of its own paint order — nothing inside it, not even
   z-index:-1, can paint beneath that background, so a ::before here
   would (and did) just paint ON TOP of the glass's fill instead of
   behind it, flooding the whole face with colour instead of staying a
   thin edge ring. Only two actually-separate elements, stacked via
   normal DOM order, get this right. */
/* this wrapper sizes itself to match the (often-hidden) bubble inside
   it — up to 160px wide even while the bubble sits at opacity:0 — so
   without pointer-events:none here, every one of the 30 hero markers
   left an invisible ~160x40px hotspot that swallowed the mousedown
   before it ever reached the map canvas underneath, which is why
   click-drag stopped working almost everywhere on the globe once this
   wrapper was introduced. The bubble/glow inside were already
   pointer-events:none, but that only ever covered themselves, not
   this box. */
.mk-bubble-stack { position: relative; pointer-events: none; }
.mk-bubble-glow {
  position: absolute;
  inset: -2px;
  border-radius: 17px;
  background: linear-gradient(to bottom left, var(--c1, var(--blue)), var(--c2, var(--purple)), var(--c3, var(--orange)));
  filter: blur(3px);
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
.mk-bubble-glow.show { opacity: 0.8; }
.mk-bubble {
  position: relative;
  max-width: 160px;
  padding: 9px 13px;
  border-radius: 16px;
  background: var(--blip-fill);
  box-shadow: 0 8px 16px -6px rgba(60, 55, 50, 0.16);
  -webkit-backdrop-filter: blur(9px);
  backdrop-filter: blur(9px);
  font-size: 10.5px;
  line-height: 1.4;
  text-align: center;
  color: var(--ink);
  opacity: 0;
  transform: translateY(6px) scale(0.92);
  transition: opacity 0.3s ease, transform 0.3s ease;
  pointer-events: none;
  overflow: hidden;
}
/* glass sheen: a soft white highlight in the top-left corner, like light
   catching the edge of curved glass — gives the flat fill a 3D feel */
.mk-bubble::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  /* a tight glint tucked into each corner, not a wash across the glass --
     small radial highlights, brightest right at the corner, gone within
     a short radius */
  background: radial-gradient(circle at 6% 4%, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0) 38%),
               radial-gradient(circle at 94% 96%, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0) 32%);
  pointer-events: none;
}
/* the glint above is absolutely positioned, which (per normal CSS stacking
   rules) paints it ON TOP of the bubble's plain, non-positioned text --
   not behind it as the visual "sheen over glass" was meant to look. lifting
   the text into its own stacking context (z-index above the glint's 0)
   fixes that without changing the glint itself. */
.mk-bubble > span, .mk-bubble .meta {
  position: relative;
  z-index: 1;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .mk-bubble::before {
    background: radial-gradient(circle at 6% 4%, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0) 38%),
                 radial-gradient(circle at 94% 96%, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0) 32%);
  }
}
:root[data-theme="dark"] .mk-bubble::before {
  background: radial-gradient(circle at 6% 4%, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0) 38%),
               radial-gradient(circle at 94% 96%, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0) 32%);
}
.mk-bubble.show { opacity: 1; transform: translateY(0) scale(1); }
.mk-bubble .meta {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-top: 5px;
  font-size: 8.5px;
  letter-spacing: -0.1px;
  color: var(--ink-faint);
}
.mk-bubble .meta .t { color: var(--orange); }

.mk-wrap { display: flex; flex-direction: column; align-items: center; gap: 8px; -webkit-user-select: none; user-select: none; }

/* ───────── spacer that drives the scroll-scrubbed camera move ─────────
   invisible and never meant to catch clicks — it used to sit (via its
   z-index) directly on top of the globe for its full 340vh and block
   every mouse event aimed at the map, which is why dragging did nothing. */
/* the leftover globe sits BELOW the veil so it keeps the same frosted look it
   has at rest; the particles sit ABOVE it so they stay sharp and coloured */
#globe-canvas { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
#burst-canvas { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 3; pointer-events: none; }
#scene-spacer { height: 320vh; position: relative; z-index: 1; pointer-events: none; }

/* ───────── footer: normal flow, scrolls up over the fixed globe ───────── */
#site-footer {
  position: relative;
  z-index: 3;
  background: color-mix(in srgb, var(--bg) 62%, transparent);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  padding: 40px max(28px, 4vw) calc(36px + env(safe-area-inset-bottom));
  display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap;
}
.footer-inner { display: flex; align-items: center; gap: 40px; flex-wrap: wrap; }
.footer-logo { width: 110px; height: auto; display: block; }
.footer-brand p {
  margin: 14px 0 0;
  font-size: 12px;
  color: var(--ink-faint);
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 22px;
  font-size: 12px;
  color: var(--ink-dim);
}
.footer-links a {
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: 3px;
}
#back-to-top {
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 10px 18px;
  font-size: 11px;
  color: var(--ink-dim);
  cursor: pointer;
  letter-spacing: -0.1px;
}
#back-to-top:hover { color: var(--ink); border-color: var(--ink-faint); }

/* ───────── reduced motion ───────── */
@media (prefers-reduced-motion: reduce) {
  .loader-mark { animation: none; opacity: 1; transform: none; }
  .loader-dots { animation: none; opacity: 1; }
  .loader-dot { animation: none; }
  .hero-logo, .hero-tagline, .hero-sub, .corner-btn, .scroll-hint { animation: none; opacity: 1; transform: none; }
  .scroll-hint i { animation: none; }
  #reveal-veil { display: none; }
}

/* ───────── info, over the city map ───────── */
#info {
  position: relative; z-index: 3;
  max-width: 760px; margin: 0 auto; padding: 30vh 24px 90px;
  color: var(--ink);
}
#info::before {           /* frosted panel so the map stays faintly visible behind */
  content: ''; position: absolute; inset: 0 calc(50% - 50vw);
  background: radial-gradient(ellipse 60% 70% at 50% 50%, color-mix(in srgb, var(--bg) 55%, transparent), transparent 75%);
  z-index: -1;
}
#info .pains { text-align: center; margin-bottom: 120px; }
#info .pains .lead { font-size: clamp(30px, 5.4vw, 52px); color: var(--ink); letter-spacing: -1px; margin-bottom: 40px; }
#info p, #info h2 { text-shadow: 0 0 22px color-mix(in srgb, var(--bg) 90%, transparent); }   /* lifts the text off the map without a panel */
#info .pains p { font-size: clamp(17px, 2.6vw, 23px); margin: 0 0 12px; color: var(--ink-dim); letter-spacing: -0.3px; }
#info .pains .turn { color: var(--ink); margin-top: 34px; font-size: clamp(19px, 3vw, 27px); }
#info .meet { text-align: center; margin-bottom: 64px; }
#info .meet h2 { font-size: clamp(24px, 4.2vw, 38px); letter-spacing: -0.6px; line-height: 1.25; }
#info .uses { text-align: center; font-size: 13px; line-height: 2; color: var(--ink-faint); margin: -40px 0 110px; }
#info .nos { text-align: center; margin-bottom: 110px; }
#info .nos p { font-size: clamp(20px, 3.4vw, 30px); margin: 0 0 10px; color: var(--ink-dim); letter-spacing: -0.4px; }
#info .nos .yes { color: var(--ink); margin-top: 26px; }
#info .points { display: grid; gap: 38px; margin-bottom: 110px; }
#info .point { display: flex; gap: 18px; align-items: flex-start; }
#info .pdot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 50%; margin-top: 7px;
  background: var(--c); box-shadow: 0 0 12px 2px color-mix(in srgb, var(--c) 55%, transparent); }
#info h2 { font-size: 19px; margin: 0 0 6px; font-weight: 700; letter-spacing: -0.3px; }
#info .point p { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink-dim); }
#info .close-line { text-align: center; font-size: clamp(22px, 3.8vw, 34px); letter-spacing: -0.5px; margin: 0 0 110px; }
#info .get { text-align: center; }
#info .get h2 { font-size: 26px; }
#info .get-sub { color: var(--ink-faint); font-size: 13px; margin: 0 0 22px; }
#info .cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
#info .btn { padding: 12px 20px; border-radius: 999px; font-size: 13px; text-decoration: none; border: 1px solid var(--line); }
#info .btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
#info .btn.ghost { background: var(--bubble-fill); color: var(--ink); }
#info .fine { margin-top: 16px; font-size: 11px; color: var(--ink-faint); }

/* copied-message toast for the instagram DM buttons */
#dm-toast { position: fixed; left: 50%; bottom: 90px; transform: translate(-50%, 10px); z-index: 50;
  background: var(--ink); color: var(--bg); font-size: 12px; padding: 10px 16px; border-radius: 999px;
  opacity: 0; transition: opacity .25s ease, transform .25s ease; pointer-events: none; }
#dm-toast.show { opacity: 1; transform: translate(-50%, 0); }

/* logo in place of the "drop a blip" headline */
#info .get-logo { width: clamp(150px, 22vw, 230px); height: auto; display: block; margin: 0 auto 12px; }

/* horizontal drift band, moved by scroll */
#info .drift { margin: 0 calc(50% - 50vw) 110px; overflow: hidden; white-space: nowrap; }
#info .drift-track { display: inline-block; font-size: clamp(28px, 5vw, 56px); letter-spacing: -1px;
  color: var(--ink-faint); will-change: transform; padding: 6px 0; }
#info .drift-track.rev { display: block; font-size: clamp(16px, 2.4vw, 24px); color: var(--ink-dim); }

/* scroll reveals: each word blurs up into place */
.rv .w { display: inline-block; opacity: 0; filter: blur(8px); transform: translateY(0.45em);
  transition: opacity .7s ease, filter .7s ease, transform .7s cubic-bezier(.2,.7,.2,1);
  transition-delay: calc(var(--i) * 45ms); }
.rv.in .w { opacity: 1; filter: blur(0); transform: none; }
.rv-dot { transform: scale(0); transition: transform .6s cubic-bezier(.3,1.6,.5,1) .1s; }
.rv-dot.in { transform: scale(1); }
.rv-fade { opacity: 0; transform: translateY(16px); transition: opacity .8s ease, transform .8s ease; }
.rv-fade.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .rv .w, .rv-dot, .rv-fade { opacity: 1 !important; filter: none !important; transform: none !important; transition: none !important; }
}

/* ───────── the pinned story stage ───────── */
/* margin-top pulls the story's sticky pin up so it starts overlapping the
   tail of the scene-spacer's scroll -- the more negative, the earlier
   "just say the thing." appears over the still-unfolding globe/city
   animation rather than waiting for it to fully settle first */
#story { position: relative; z-index: 3; height: 640vh; margin-top: -170vh; pointer-events: none; }
#story .stage { position: sticky; top: 0; height: 100vh; overflow: hidden; }
#story .stage::before { content: ''; position: absolute; inset: 0;
  background: radial-gradient(ellipse 55% 55% at 50% 50%, color-mix(in srgb, var(--bg) 60%, transparent), transparent 75%);
  opacity: var(--wash, 0); }
#story .sx, #story .card { text-shadow: 0 0 22px color-mix(in srgb, var(--bg) 90%, transparent); }
#story .lead { position: absolute; left: 0; right: 0; top: 50%; margin: 0; text-align: center;
  font-size: clamp(30px, 5.4vw, 54px); letter-spacing: -1px; font-weight: 400; color: var(--ink);
  transform: translateY(calc(-50% - clamp(22px, 3vw, 34px) + var(--ly, 0px))); }   /* sits a touch above centre so it and the line below are centred as a pair */
#story .beat, #story .group, #story .drifts { position: absolute; left: 0; right: 0; text-align: center; margin: 0; }
#story .beat { top: calc(50% + clamp(4px, 1vw, 12px)); font-size: clamp(17px, 2.6vw, 24px); color: var(--ink-dim);
  transform: translateY(var(--ly, 0px)); }   /* the last beat lifts away on its own once it's done holding -- see site.js */
/* retired: this used to fade the same two lines back in for a second look
   right after the beats had already shown them, which read as the text
   disappearing and reappearing. the last beat now carries straight through
   into the lift instead (site.js), so this recap is no longer used. */
#story .group { display: none; }
#story .skip { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(calc(-50% - clamp(22px, 3vw, 34px))); margin: 0; text-align: center; font-size: clamp(20px, 3.2vw, 32px); letter-spacing: -0.5px; color: var(--ink); }   /* same size as the cards' heading, resting where the lead rests */
#story .group p { margin: 0 0 10px; font-size: clamp(17px, 2.6vw, 24px); color: var(--ink-dim); }
#story .drifts { position: static; font-size: clamp(20px, 3.2vw, 32px); letter-spacing: -0.5px; color: var(--ink); padding: 0 24px; margin: 0 0 34px; }
#story .w { display: inline-block; will-change: opacity, filter, transform; }

#story .carousel { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); opacity: 0; padding: 0 24px; }
/* all three cards sit in view together, side by side -- scroll only
   dims the two that aren't "it" and lifts the current one, nothing slides */
#story .track { display: flex; gap: 20px; justify-content: center; max-width: 980px; margin: 0 auto; }
#story .card { flex: 1 1 0; min-width: 0; max-width: 300px; padding: 28px 24px; border-radius: 22px;
  background: color-mix(in srgb, var(--bg) 70%, transparent); border: 1px solid var(--line);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  transition: opacity .5s ease, transform .5s ease, border-color .5s ease; opacity: .4; transform: scale(.96); }
#story .card.on { opacity: 1; transform: scale(1.03); border-color: color-mix(in srgb, var(--c) 45%, var(--line)); }
#story .card .pdot { display: block; width: 12px; height: 12px; border-radius: 50%; margin-bottom: 18px;
  background: var(--c); box-shadow: 0 0 12px 2px color-mix(in srgb, var(--c) 55%, transparent); }
#story .card h3 { margin: 0 0 8px; font-size: 19px; letter-spacing: -0.3px; }
#story .card p { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--ink-dim); }
@media (max-width: 760px) {
  #story .track { flex-direction: column; align-items: center; max-width: 340px; gap: 14px; }
  #story .card { max-width: 100%; width: 100%; padding: 22px 22px; }
  /* on phone the cards no longer wait for a long pinned scroll to cycle
     through one at a time -- site.js reveals all three together instead,
     each with its own short transition-delay for a quick cascade, rising
     up slightly from underneath "drop a blip..." as it lifts out of the
     way, rather than a hold-and-highlight you have to keep scrolling
     through. fully hidden (not just dimmed) beforehand, so nothing peeks
     in before drifts has had the stage to itself. */
  #story .card {
    opacity: 0; transform: translateY(18px) scale(.97);
    transition: opacity .5s ease, transform .5s ease, border-color .5s ease, background .5s ease;
  }
  #story .card:nth-child(1) { transition-delay: 0s; }
  #story .card:nth-child(2) { transition-delay: .08s; }
  #story .card:nth-child(3) { transition-delay: .16s; }
  /* more see-through outline and fill than the desktop "on" state --
     three of these stacked solid felt heavier than the rest of the glassy
     site -- but the text and the coloured dot stay fully opaque; it's
     only the card itself (background + border) that's more transparent,
     not what's written on it */
  #story .card.on {
    opacity: 1; transform: translateY(0) scale(1.02);
    background: color-mix(in srgb, var(--bg) 55%, transparent);
    border-color: color-mix(in srgb, var(--c) 35%, transparent);
  }

  /* "drop a blip and see what drifts back." gets the stage to itself
     first (site.js), so it needs the same lift-away transform the lead
     and beats already use */
  #story .drifts { transform: translateY(var(--ly, 0px)); }

  /* "the best conversations..." -- at the fixed 22px floor this clamp()
     was landing on for narrow phones, the first line alone was wider than
     the screen and wrapped a second time, turning the intended two lines
     into three. a lower floor here keeps each half on its own line. */
  #info { padding-top: 0; }

  /* a real pinned stage, same pattern as #story above: .close-wrap
     reserves its own tall scroll distance and .close-stage sticks in
     place for the length of it, so the line can come in, hold fully
     stationary/centred for a proper beat, then visibly lift and fade on
     continued scroll -- site.js turns .close-wrap's own scroll progress
     into that cascade/hold/lift, rather than reacting to some other
     element merely arriving on screen. */
  .close-wrap { position: relative; height: 220vh; }
  .close-stage {
    position: sticky; top: 0; height: 100vh;
    display: flex; align-items: center; justify-content: center;
    padding: 0 16px; overflow: hidden;
  }
  /* the first half ("the best conversations happen") is the longer of
     the two lines the <br> splits this into -- sized + padded so that
     half reliably stays on one line of its own, keeping the whole thing
     to two lines rather than wrapping a second time into three */
  #info .close-line {
    font-size: clamp(13px, 4.2vw, 16px);
    margin: 0;
    transform: translateY(var(--ly, 0px));
  }

  /* footer: logo, links and the back-to-top button were left-aligning
     once they wrapped onto their own stacked rows -- centre the whole
     thing instead */
  #site-footer { flex-direction: column; text-align: center; }
  .footer-inner { flex-direction: column; text-align: center; }
  .footer-links { justify-content: center; }

  /* the footer comes up on its own, once the closing line has had its
     moment and lifted away, rather than just sitting there fully visible
     the whole time you scroll toward it */
  #site-footer.rv-fade {
    opacity: 0; transform: translateY(16px);
    transition: opacity .7s ease, transform .7s ease;
  }
  #site-footer.rv-fade.in { opacity: 1; transform: none; }

  /* the maunoh mark, tagline and buttons reveal the same scroll-tied
     word-cascade way the rest of the section does (site.js) instead of
     the plain slide-and-fade -- neutralise that generic .rv-fade here so
     the per-word/per-element opacity set directly by site.js is what
     shows */
  .get.rv-fade { opacity: 1; transform: none; transition: none; }
  .get-logo { opacity: 0; will-change: opacity, filter, transform; }
  .get .w { display: inline-block; will-change: opacity, filter, transform; }
}

/* tap-to-burst: element vanishes while its fragments fly, then fades back */
.burst-gone { visibility: hidden !important; }
.burst-back { animation: burstBack 0.7s ease-out; }
@keyframes burstBack { from { filter: blur(10px); } to { filter: blur(0); } }
