/* Anno XL — descent.css
   The sky: altitude loader, the roamable aerial, survey pins, the ledger
   strip, the index sheet and the sealed-pin toast.                       */

/* ---- loader ----------------------------------------------------------- */

/* This panel has to OBSCURE the photograph behind it whatever the viewport
   resolves to, and Safari lays out fixed layers it isn't otherwise touching
   against STALE viewport metrics: with the same var(--screen-h) as #sky the
   loader was measured ~100px shorter than #sky on a real iPhone, and a
   bottom-offset skirt went stale against a transient early-load viewport the
   same way (b9). Two defences now: main.js pins this height in real px from
   a freshly-created 100lvh probe on every visualViewport change (a fresh
   element always resolves current units — the CSS height below is only the
   first-frame fallback), and the ::after skirt hangs more ink off the box's
   actual bottom edge, wherever that ends up. */
#loader {
  position: fixed; top: 0; left: 0; right: 0; z-index: 90;
  height: var(--screen-h);
  display: grid; place-items: center;
  background: var(--ink);
  transition: opacity .6s var(--ease-soft), visibility .6s;
}
#loader::after {
  content: '';
  position: absolute; top: 100%; left: 0; right: 0; height: 600px;
  background: var(--ink);
}
#loader.gone { opacity: 0; visibility: hidden; }
#loader.gone .drop { transform: scale(1.05); }

/* The altitude readout stays hidden until the descent actually starts.
   It used to paint immediately, so a guest saw "2 400 m" sitting frozen behind
   the roll and then again behind the onboarding screens, and only counted down
   afterwards. The ink backdrop still covers the page from the first frame; it
   is only the readout that waits for its cue. */
.drop {
  text-align: center;
  opacity: 0;
  transition: opacity .4s var(--ease-soft), transform .6s var(--ease-soft);
}
#loader.counting .drop { opacity: 1; }
.drop .chev {
  width: 108px; margin: 0 auto 26px;
  stroke: var(--sand); fill: none;
  stroke-width: 6.5; stroke-linecap: round; stroke-linejoin: round;
}
/* Gated too, or the chevrons draw themselves while invisible and are already
   finished by the time anyone sees them. */
.drop .chev path { stroke-dasharray: 1; stroke-dashoffset: 1; }
#loader.counting .drop .chev path { animation: draw 1s var(--ease-soft) forwards; }
#loader.counting .drop .chev path:nth-child(2) { animation-delay: .25s; }
#loader.counting .drop .chev path:nth-child(3) { animation-delay: .5s; }
@keyframes draw { to { stroke-dashoffset: 0; } }

#alt {
  font: italic 700 clamp(2.7rem, 9vw, 4.2rem)/1 var(--serif);
  color: var(--bone);
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
}
.drop-label {
  margin-top: 14px;
  font: 600 11px/1.6 var(--sans);
  letter-spacing: .22em; text-transform: uppercase;
  color: rgba(231, 217, 189, .62);
}
.drop-hello {
  margin-top: 30px;
  font: 400 1.25rem var(--thai);
  color: var(--gold);
  opacity: 0;
  animation: hello .8s .9s var(--ease-soft) forwards;
}
@keyframes hello { to { opacity: 1; } }

/* ---- top bar ---------------------------------------------------------- */

#topbar {
  position: fixed; z-index: 60;
  top: 0; left: 0; right: 0;
  display: flex; align-items: center; justify-content: space-between;
  padding: calc(14px + env(safe-area-inset-top)) 18px 0;
  pointer-events: none;
  transition: opacity .45s var(--ease-soft), transform .45s var(--ease-soft);
}
/* A wash under the top chrome. Pins are anchored to the map, so on a device
   where the aerial's top edge sits near the screen's, a high pin's label can
   end up behind the logo or the clock. This makes that read as chrome layered
   over the map rather than two things colliding — and it keeps the white logo
   legible over the bright villa roofs. */
#topbar::before {
  content: '';
  position: absolute; left: 0; right: 0; top: 0;
  height: calc(100% + 26px);
  background: linear-gradient(180deg, rgba(6, 20, 24, .68), rgba(6, 20, 24, 0));
  pointer-events: none;
  z-index: -1;
}
#topbar img { height: 31px; width: auto; position: relative; }

/* TEMPORARY build marker — bumped every deploy so a phone can be checked
   against what was actually shipped. Delete before cutover. */
#build {
  position: relative;
  margin-left: 10px; margin-right: auto;   /* hug the logo, not the centre */
  font: 700 9px/1 var(--sans);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--chili);
  border: 1px solid rgba(212, 85, 58, .5);
  padding: 4px 6px;
  align-self: center;
}
/* Right-hand chrome, grouped so it holds its place on production — where the
   build marker (and the margin-right:auto that was pushing this over) is
   stripped out of the HTML entirely. */
.top-right { display: flex; align-items: center; gap: 12px; position: relative; }

/* ---- the bell ------------------------------------------------------------
   #topbar is pointer-events:none so the map can be dragged under it, so the
   one control living up here has to opt back in. */
#bell {
  position: relative;
  pointer-events: auto;
  background: none; border: 0; padding: 4px;
  color: rgba(247, 240, 224, .85);
  cursor: pointer;
  display: grid; place-items: center;
  /* 44px of reachable target around a 20px glyph. */
  width: 44px; height: 44px; margin: -4px -10px -4px 0;
  transition: color .2s;
}
#bell:hover { color: var(--sand); }
#bell svg { width: 20px; height: 20px; }
/* A dot until there is a number to put in it. Both kinds of unread — an
   announcement nobody has read, and replies on a thread that is about you —
   are counted into one badge, because a guest does not care which sort it is
   until they have tapped it. */
#bell-dot {
  position: absolute; top: 3px; right: 4px;
  min-width: 8px; height: 8px; border-radius: 9px;
  background: var(--gold);
  box-shadow: 0 0 0 2px rgba(6, 20, 24, .85);
}
#bell-dot.many {
  min-width: 16px; height: 16px; padding: 0 4px;
  display: grid; place-items: center;
  font: 700 9.5px/1 var(--sans); letter-spacing: .02em;
  color: #0c2126;
  font-variant-numeric: tabular-nums;
}

/* ---- the announcements panel --------------------------------------------- */
#notices {
  position: fixed; z-index: 90;
  top: 0; right: 0;
  width: min(420px, 100%);
  height: 100vh; height: 100dvh;
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--ink, #0c2126);
  border-left: 1px solid rgba(231, 217, 189, .12);
  padding: calc(18px + env(safe-area-inset-top)) 18px
           calc(24px + env(safe-area-inset-bottom));
  transform: translateX(100%);
  transition: transform .32s var(--ease-soft);
}
#notices.in { transform: translateX(0); }
#notices[hidden] { display: none; }

.nt-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.nt-eyebrow {
  font: 600 10px/1 var(--sans); letter-spacing: .18em; text-transform: uppercase;
  color: var(--gold); margin: 0;
}
#notices-close {
  background: none; border: 0; cursor: pointer;
  color: rgba(231, 217, 189, .55); font-size: 15px;
  width: 44px; height: 44px; margin: -12px -12px -12px 0;
}
#notices-close:hover { color: var(--sand); }

#notices-list { list-style: none; margin: 0; padding: 0; }
.nt-item {
  padding: 13px 0;
  border-top: 1px solid rgba(231, 217, 189, .1);
}
.nt-item:first-child { border-top: 0; }
.nt-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.nt-title { font: 400 17px/1.3 var(--serif); color: var(--sand); }
.nt-badge {
  font: 700 8.5px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase;
  color: #0c2126; background: var(--gold); padding: 3px 5px;
}
/* Replies and reactions on a thread that is ABOUT you, since you last looked.
   Its own colour because it is a different fact from "you have not read this
   announcement" — an announcement you read last week can still have three
   answers on it you have not. */
.nt-badge-talk {
  background: var(--lagoon-2, #7fccdb);
  min-width: 15px; text-align: center; letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
}
/* A hairline down the side, so the thread is findable by scrolling rather
   than by reading every badge. */
.nt-item.nt-talk {
  margin-left: -10px; padding-left: 8px;
  border-left: 2px solid rgba(127, 204, 219, .55);
}
.nt-when {
  margin-left: auto;
  font: 600 10px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase;
  color: rgba(231, 217, 189, .4); white-space: nowrap;
}
/* The whole message, wrapped — this panel exists precisely because the lock
   screen would not show it all. */
.nt-body {
  margin: 7px 0 0;
  font: 400 14.5px/1.55 var(--sans);
  color: rgba(231, 217, 189, .82);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.nt-item.nt-new .nt-body { color: var(--sand); }
.nt-go {
  margin-top: 9px; padding: 7px 12px;
  background: none; border: 1px solid rgba(212, 169, 74, .5); color: var(--gold);
  font: 600 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase;
  cursor: pointer;
}
.nt-go:hover { background: rgba(212, 169, 74, .1); }
.nt-empty {
  color: rgba(231, 217, 189, .45); font-style: italic;
  font-size: 14px; padding: 12px 0;
}

/* ---- talking back to an announcement -------------------------------------
   The panel has only ever been a transcript of the site talking at people.
   Two ways to answer, weighted differently on purpose: a reaction is one tap
   and costs nothing, a reply is writing and is asked for explicitly.

   The gesture is iMessage's, because everyone already has it: the strip
   shows what has been said, a tap on a pill toggles yours, and a HOLD opens
   the picker together with the register of who reacted. The ＋ is the same
   door for anyone on a mouse, a keyboard or a screen reader, for whom a
   long-press does not exist.

   ⚠ EMOJI ARE TALLER THAN THEIR LINE BOX ON iOS. The salute clipped at the
   top of every button until the glyph was given room of its own — hence the
   explicit line-height and the min-heights below rather than padding alone.
   Test a new one on a real iPhone; it is always the recent additions. */
.nt-rx-wrap { margin-top: 10px; }
.nt-rx {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  /* A control, not prose: a held finger must not start selecting text or
     raise the iOS callout instead of opening the picker. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.nt-rx-b, .nt-rx-add {
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  min-height: 32px; padding: 3px 9px;
  background: none; border: 1px solid rgba(231, 217, 189, .16);
  clip-path: var(--cut-s); cursor: pointer;
  color: rgba(231, 217, 189, .8);
}
/* The glyph gets its own line box, tall enough for the ones that overshoot.
   System stack: asking for the serif renders emoji as boxes on some Android
   builds. */
.nt-rx-b .g, .nt-pick-b, .nt-who .e {
  font: 400 16px/1.5 system-ui, "Apple Color Emoji", "Segoe UI Emoji",
        "Noto Color Emoji", sans-serif;
}
.nt-rx-b .n {
  font: 700 10.5px/1 var(--sans); letter-spacing: .06em;
  color: rgba(231, 217, 189, .6); font-variant-numeric: tabular-nums;
}
/* Yours. The only lit state on the strip. */
.nt-rx-b.on { border-color: rgba(212, 169, 74, .65); background: rgba(212, 169, 74, .1); }
.nt-rx-b.on .n { color: var(--gold); }
.nt-rx-b[disabled] { cursor: default; opacity: .5; }

.nt-rx-add { padding: 3px 8px; color: rgba(231, 217, 189, .45); }
.nt-rx-add svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.nt-rx-add i {
  font: 600 9.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase;
  font-style: normal;
}
.nt-rx-add:hover { color: var(--gold); border-color: rgba(212, 169, 74, .4); }
/* Nothing has been said yet, so it is the only thing on the row and may as
   well say what it is. */
.nt-rx-add.bare { color: rgba(231, 217, 189, .38); }

/* ---- the picker, and who ----------------------------------------------- */
.nt-sheet {
  margin-top: 7px; padding: 9px 10px;
  background: rgba(4, 16, 19, .72);
  border: 1px solid rgba(231, 217, 189, .18);
  clip-path: var(--cut-s);
  animation: nt-rise .16s var(--ease-soft, ease-out);
}
@keyframes nt-rise {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .nt-sheet { animation: none; } }

.nt-pick { display: flex; flex-wrap: wrap; gap: 3px; }
.nt-pick-b {
  flex: 1 1 auto;
  min-width: 40px; min-height: 40px;      /* a thumb target, and room for the glyph */
  display: grid; place-items: center;
  background: none; border: 1px solid transparent; border-radius: 50%;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: transform .12s ease, background .12s ease;
}
.nt-pick-b:hover { background: rgba(231, 217, 189, .08); transform: scale(1.12); }
.nt-pick-b.on { background: rgba(212, 169, 74, .18); border-color: rgba(212, 169, 74, .6); }

.nt-who {
  list-style: none; margin: 8px 0 0; padding: 8px 0 0;
  border-top: 1px solid rgba(231, 217, 189, .12);
  display: grid; gap: 4px;
}
.nt-who li { display: flex; align-items: baseline; gap: 8px; }
.nt-who .e { flex: none; }
.nt-who .nm {
  font: 400 12.5px/1.45 var(--sans);
  color: rgba(231, 217, 189, .7);
  overflow-wrap: anywhere;
}
.nt-who-none {
  font: italic 400 13px var(--serif); color: rgba(231, 217, 189, .4);
}

.nt-rp { margin-top: 9px; }
.nt-rp-row {
  display: flex; align-items: baseline; gap: 6px;
  padding: 4px 0;
  font: 400 13.5px/1.5 var(--sans);
  color: rgba(231, 217, 189, .8);
  overflow-wrap: anywhere;
}
.nt-rp-row b { font: 700 12px/1.5 var(--sans); color: var(--gold); white-space: nowrap; }
.nt-rp-x {
  margin-left: auto; flex: none;
  background: none; border: 0; cursor: pointer;
  color: rgba(231, 217, 189, .3); font-size: 15px; line-height: 1;
  width: 26px; height: 26px;
}
.nt-rp-x:hover { color: var(--chili); }

.nt-rp-form { display: flex; gap: 6px; margin-top: 7px; }
.nt-rp-in {
  flex: 1 1 auto; min-width: 0;
  padding: 8px 10px;
  background: rgba(4, 16, 19, .5);
  border: 1px solid rgba(231, 217, 189, .18);
  color: var(--sand);
  /* 16px or iOS Safari zooms the whole panel on focus and never zooms back. */
  font: 400 16px/1.3 var(--sans);
}
.nt-rp-in:focus { outline: none; border-color: rgba(212, 169, 74, .55); }
.nt-rp-go {
  flex: none; padding: 8px 12px;
  background: none; border: 1px solid rgba(212, 169, 74, .5); color: var(--gold);
  font: 600 10px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase;
  cursor: pointer;
}
.nt-rp-go:hover { background: rgba(212, 169, 74, .1); }
.nt-rp-go[disabled] { opacity: .45; cursor: default; }
.nt-rp-err {
  margin: 6px 0 0;
  font: 400 12.5px/1.45 var(--sans); color: var(--chili);
}
.nt-rp-err[hidden] { display: none; }

#clock {
  font: 600 10.5px/1 var(--sans);
  letter-spacing: .16em; text-transform: uppercase;
  color: rgba(247, 240, 224, .85);
  border: 1px solid rgba(231, 217, 189, .28);
  clip-path: var(--cut-s);
  padding: 9px 13px;
  background: rgba(10, 28, 33, .45);
  backdrop-filter: blur(6px);
}
#clock i { font-style: normal; color: rgba(231, 217, 189, .55); }

body[data-state='loading'] #topbar,
body[data-state='diving'] #topbar,
body[data-state='room'] #topbar,
body[data-state='surfacing'] #topbar { opacity: 0; transform: translateY(-8px); pointer-events: none; }

/* ---- the sky (map) ---------------------------------------------------- */

/* top/right/left + an explicit --screen-h, NOT inset:0. See base.css: in a
   standalone PWA iOS reports a viewport shorter than the screen, and inset:0
   dutifully fills only that, leaving the canvas showing as a band. This layer
   is the photograph, so it extends under the collapsible bar area instead.
   mapstage fit() reads sky.clientHeight, so this sizes the whole map. */
#sky {
  position: fixed; top: 0; left: 0; right: 0;
  height: var(--screen-h);
  overflow: hidden;
  background: var(--ink);
  touch-action: none;
  user-select: none;
}
/* Safari 26's Liquid Glass tints its floating toolbar by sampling the
   background-color of fixed elements near the screen edges — it cannot see
   the photo or the video, only this colour. It's invisible behind the
   cover-fit picture, so it tracks the picture's bottom-edge tone per scene
   (ffmpeg average of each still's bottom 2%; same values as the chrome band
   in main.js) and the glass reads as part of the image instead of a grey
   slab. */
body[data-scene="day"] { --edge-tone: #5b683d; }
body[data-scene="night"] { --edge-tone: #243035; }
/* Map state only: while the loader/gate/onboarding ink covers the page, every
   colour Safari could sample near the bottom edge must be ink too — the tone
   arrives with the map, in step with the root colour from paintChromeBand. */
body[data-state="map"][data-scene] #sky { background: var(--edge-tone, var(--ink)); }

/* NO bottom fade on the photo. b10 blended the photo's last 36px into
   --edge-tone to soften the chrome hand-off, and the user read it as the
   background no longer filling the screen — the photograph now bleeds naked
   to its edge, and whatever hairline Safari draws where its chrome begins is
   accepted (user's call, 2026-08-10). */
#stage {
  position: absolute; left: 0; top: 0;
  width: 1920px; height: 1080px;
  transform-origin: 0 0;
  will-change: transform;
}
#stage img { width: 100%; height: 100%; -webkit-user-drag: none; user-select: none; }

/* The looping aerial, laid over the still in the same coordinate space so the
   pins keep their alignment. Starts invisible and is faded in by main.js only
   once it can actually play — a half-buffered video popping in mid-descent
   would be worse than no video at all. */
#stage video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.4s var(--ease-soft);
}
#stage video.on { opacity: 1; }
/* ?still and prefers-reduced-motion keep the photograph. */
html.still #stage video { display: none; }
@media (prefers-reduced-motion: reduce) { #stage video { display: none; } }
body[data-state='diving'] #stage,
body[data-state='room'] #stage {
  filter: blur(5px) saturate(1.12) brightness(1.1);
}
#stage { transition: filter .9s var(--ease-dive); }

#haze {
  position: fixed; top: 0; left: 0; right: 0; z-index: 45;
  bottom: calc(-1 * var(--skirt));   /* overlay — skirted, see base.css */
  pointer-events: none;
  background: radial-gradient(circle at 50% 46%,
    rgba(247, 240, 224, .97),
    rgba(240, 226, 197, .8) 44%,
    rgba(196, 178, 142, .5) 100%);
  opacity: 0;
  transition: opacity .5s var(--ease-soft);
}
body[data-state='diving'] #haze { opacity: 1; transition: opacity .62s var(--ease-dive) .3s; }
body[data-state='surfacing'] #haze { opacity: 1; transition: opacity .25s; }

/* ---- fireworks -------------------------------------------------------- */

/* A child of #stage, so the map transform carries the bursts with the
   landscape (user: static on the background, independent of the scroll).
   Drawn at the stage's own 1920x1080; the transform scales it with the
   photo. Sits after the video in the DOM, under the pins layer. Explicit
   width/height because canvas is a replaced element and won't stretch from
   offsets alone. */
#pyro {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
  pointer-events: none;
}

/* ---- pins ------------------------------------------------------------- */

/* Must match #sky exactly. Pins are projected from the map's own vw/vh, so if
   this layer and the photo disagree on height every pin drifts vertically. */
#pins {
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  height: var(--screen-h);
  pointer-events: none;
  transition: opacity .25s;
}
body[data-state='diving'] #pins,
body[data-state='room'] #pins,
body[data-state='surfacing'] #pins,
body[data-state='loading'] #pins { opacity: 0; pointer-events: none; }

.pin {
  position: absolute; left: 0; top: 0;
  width: 52px; height: 52px;
  margin: -26px 0 0 -26px;
  pointer-events: auto;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
/* Distances from the pin's CENTRE to the near edge of its plate: --plate-gap
   for a plate beside the pin, --stack-gap for one above or below. 28px was
   44px until the user called the gap roughly twice as wide as it should be. */
.pin { --plate-gap: 28px; --stack-gap: 22px; }
/* Leader geometry for the one label that has one (TV Room, dy 24):
   hypotenuse √(28² + 24²) = 36.9, less the 15px disc radius ≈ 22px, at
   atan(24/28) ≈ 41°. Recompute both if that dy or --plate-gap changes. */
.pin.has-leader { --leader-len: 22px; --leader-rot: -41deg; }
.pin.offstage { visibility: hidden; }
.pin:focus-visible { outline: none; }
.pin:focus-visible .pin-mark { outline: 2px solid var(--gold); outline-offset: 5px; border-radius: 50%; }

.pin-b {
  position: absolute; inset: 0;
  animation: bob 4.2s var(--bob, 0s) ease-in-out infinite alternate;
}
@keyframes bob { from { transform: translateY(0); } to { transform: translateY(-4px); } }
.pin-b.shake { animation: shake .4s var(--ease-soft); }
@keyframes shake {
  20% { transform: translateX(-4px); }
  45% { transform: translateX(4px); }
  70% { transform: translateX(-2px); }
  100% { transform: translateX(0); }
}

/* Sonar. The outer reach of this is what sets --pin-reach below: at 24px base
   × 2.5 the ring tops out at 60px across, i.e. 30px from centre. The plate has
   to start beyond that or the pulse slides under the rectangle — which is
   exactly what it did at the old 30px offset. */
.pin-halo { position: absolute; left: 50%; top: 50%; }
.pin-halo::before, .pin-halo::after {
  content: '';
  position: absolute;
  width: 24px; height: 24px;
  margin: -12px 0 0 -12px;
  border: 1px solid rgba(230, 163, 62, .75);
  border-radius: 50%;
  animation: sonar 3s ease-out infinite;
}
.pin-halo::after { animation-delay: 1.5s; }
/* 2.3 not 2.5: the ring must be fully faded by the time it reaches the plate,
   which now starts 28px from centre (24px base × 2.3 ÷ 2 = 27.6px). */
@keyframes sonar {
  from { transform: scale(1); opacity: .9; }
  to { transform: scale(2.3); opacity: 0; }
}
.pin.is-sealed .pin-halo::before,
.pin.is-sealed .pin-halo::after {
  border-color: rgba(212, 85, 58, .5);
  animation-duration: 6s;
}

/* Open pins were 15px, which read as an afterthought next to the 30px sealed
   numeral discs. 24px puts the two families on the same footing. */
.pin-mark {
  position: absolute; left: 50%; top: 50%;
  width: 24px; height: 24px;
  margin: -12px 0 0 -12px;
  border: 1.5px solid var(--bone);
  border-radius: 50%;
  background: rgba(12, 33, 38, .35);
  transition: transform .25s var(--ease-soft);
}
.pin-mark::after {
  content: '';
  position: absolute; left: 50%; top: 50%;
  width: 7px; height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: var(--gold);
}
.pin:hover .pin-mark, .pin:focus-visible .pin-mark { transform: scale(1.35); }

/* ---- sealed places ----------------------------------------------------
   A greyed numeral disc with its name drained beside it. The numeral says
   a place exists and which one it is in the order; the grey says the light
   hasn't reached it yet. Open pins keep their gold dot and sonar, so live
   and inert read differently at a glance. */

.pin.is-sealed .pin-halo { display: none; }
.pin.is-sealed .pin-mark {
  width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border: 1px solid rgba(150, 176, 186, .5);
  border-style: solid;
  background: rgba(9, 26, 31, .62);
  backdrop-filter: blur(3px);
}
.pin.is-sealed .pin-mark::after { display: none; }
.pin.is-sealed .pin-mark i { display: none; }   /* the old red cross */

/* The map itself darkens under a sealed place. */
.pin.is-sealed .pin-b::before {
  content: '';
  position: absolute; left: 50%; top: 50%;
  width: 104px; height: 104px; margin: -52px 0 0 -52px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(4, 14, 17, .6), rgba(4, 14, 17, 0) 70%);
  pointer-events: none;
}

.pin-num {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  display: none;
  font: 700 11px/1 var(--serif);
  letter-spacing: .04em;
  color: rgba(198, 214, 220, .78);
  white-space: nowrap;
}
.pin.is-sealed .pin-num { display: block; }
.pin.is-sealed:hover .pin-mark { border-color: rgba(198, 214, 220, .9); }
.pin.is-sealed:hover .pin-num { color: var(--bone); }

/* Drained, not coloured — a sealed name should never compete with an open one. */
.pin.is-sealed .pin-plate {
  background: rgba(9, 26, 31, .58);
  border-color: rgba(231, 217, 189, .1);
}
.pin.is-sealed .pin-plate b { color: rgba(198, 214, 220, .52); }
.pin.is-sealed .pin-plate small,
.pin.is-sealed .pin-plate small .th { color: rgba(198, 214, 220, .34); }
.pin.is-sealed .pin-plate small .pl-rel { color: rgba(198, 214, 220, .5); }

/* Plate placement is FIXED per place — the `label` key in places.js picks
   right (the default), above or below, plus px nudges. There is no solver:
   a plate that re-solves as you pan visibly jumps, which is worse than one
   that occasionally runs past the viewport edge.

   --plate-gap is measured from the pin's CENTRE, hence calc(50% + gap): CSS
   `left` measures from the box edge, and the box is 52px wide with a -26px
   margin. A bare `left: 28px` would put the plate only 2px from centre. */
.pin-plate {
  position: absolute; left: calc(50% + var(--plate-gap)); top: 50%;
  transform: translate(var(--lab-dx, 0px), calc(-50% + var(--lab-dy, 0px)));
  white-space: nowrap;
  text-align: left;
  background: rgba(9, 26, 31, .86);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(231, 217, 189, .22);
  clip-path: var(--cut-s);
  padding: 7px 11px 8px;
  transition: border-color .25s, background-color .25s, opacity .25s;
}

/* Above / below: centred on the pin instead of beside it. --stack-gap is the
   distance from the pin's centre to the plate's near edge. */
.pin.lab-above .pin-plate,
.pin.lab-below .pin-plate {
  left: 50%; right: auto; text-align: center;
  transform: translate(calc(-50% + var(--lab-dx, 0px)), var(--lab-dy, 0px));
}
.pin.lab-above .pin-plate { top: auto; bottom: calc(50% + var(--stack-gap)); }
.pin.lab-below .pin-plate { top: calc(50% + var(--stack-gap)); }

/* A hairline from the disc to the middle of the plate's left border, for a
   label sitting far enough out that it reads as unattached. It pivots on the
   plate end, so a label nudged vertically gets a sloped line that still
   points at its disc. --leader-len/rot are the hypotenuse and angle for the
   current --plate-gap and the label's dy; they're set per breakpoint because
   the gap changes. Right-side placement only. */
.pin.has-leader .pin-plate::before {
  content: '';
  position: absolute; right: 100%; top: 50%;
  width: var(--leader-len, 12px);
  height: 1px;
  /* .45 disappeared against the bright villa roof; .62 still reads as a
     hairline but survives a busy background. */
  background: rgba(198, 214, 220, .62);
  transform-origin: 100% 50%;
  transform: rotate(var(--leader-rot, 0deg));
}
.pin.lab-above.has-leader .pin-plate::before,
.pin.lab-below.has-leader .pin-plate::before { display: none; }

/* A sealed place says nothing but its numeral. Hover/focus still reveals the
   name on a desktop; on touch, tapping raises the toast instead. */
.pin.no-label .pin-plate { opacity: 0; }
.pin.no-label:hover .pin-plate,
.pin.no-label:focus-visible .pin-plate { opacity: 1; z-index: 2; }
.pin:hover .pin-plate { border-color: rgba(230, 163, 62, .55); background: rgba(9, 26, 31, .88); }
.pin-plate b {
  display: block;
  font: 700 10.5px/1.35 var(--sans);
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--bone);
}
.pin-plate small {
  display: block;
  font-size: 10.5px; line-height: 1.4;
  color: rgba(231, 217, 189, .68);
}
/* The chilli-coloured sealed-plate rules that used to live here were dead
   weight overriding the greyed treatment 60 lines above — same specificity,
   later in the file, so they won. Sealed plate colours are set once, up
   there, under "sealed places". Don't add a second set. */

/* ---- roam hint / ledger ----------------------------------------------- */

#hint {
  position: fixed; z-index: 40;
  left: 0; right: 0;
  bottom: calc(72px + env(safe-area-inset-bottom));
  text-align: center;
  font: 600 10.5px/1.7 var(--sans);
  letter-spacing: .2em; text-transform: uppercase;
  color: rgba(231, 217, 189, .7);
  pointer-events: none;
  transition: opacity .6s;
  animation: hint-out .5s 9s forwards;
}
#hint .th { display: block; letter-spacing: .06em; text-transform: none; color: rgba(231, 217, 189, .45); }

/* Plain text over sunlit grass was too faint to read. Same trick as the
   countdown: an elliptical wash behind it rather than a visible panel. */
#hint::before {
  content: '';
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(340px, 78vw); height: 210%;
  background: radial-gradient(ellipse at center,
    rgba(5, 17, 21, .72) 0%, rgba(5, 17, 21, .48) 45%, rgba(5, 17, 21, 0) 75%);
  pointer-events: none;
  z-index: -1;
}

/* On a phone the aerial is cover-fit, so it only has room to pan sideways —
   a horizontal cue is the honest one. Drifts rather than pulses. */
.hint-swipe { display: block; margin-top: 7px; color: rgba(230, 163, 62, .8); }
.hint-swipe svg {
  width: 44px; height: 12px; display: inline-block;
  animation: swipe 2.6s ease-in-out infinite;
}
@keyframes swipe {
  0%, 100% { transform: translateX(-4px); opacity: .4; }
  50%      { transform: translateX(4px);  opacity: 1; }
}

/* Gone the moment you've actually roamed. mapstage.js has always set .roamed
   on the first drag; nothing was listening to it, so the hint sat there for
   its full nine seconds even after you'd used the map. */
body.roamed #hint { opacity: 0; }
body.roamed #hint, body[data-state='diving'] #hint, body[data-state='room'] #hint { opacity: 0; }
body[data-state='loading'] #hint { opacity: 0; }
/* Paused while Gary has an unread message — his bouncing arrow leads instead. */
body.gary-news #hint { opacity: 0; }
@keyframes hint-out { to { opacity: 0; } }

#ledger {
  position: fixed; z-index: 40;
  left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 26px 18px calc(12px + env(safe-area-inset-bottom));
  transition: opacity .4s, transform .4s;
  /* THE BAR IS FULL-WIDTH AND HAS NO BACKGROUND, so nearly all of it is
     empty space over the photo - and at z 40 it sits above Gary (z 38).
     It was swallowing the taps meant for him: 6 of 9 points across his
     hit box in portrait, and ALL NINE in landscape once he was sized down
     to fit a short screen (Justin, 10 Sep: "I can't tap Gary at all").
     Only the MENU button is a control; the count is a label, and it is
     meant to READ over his lower body, not to intercept it. So the bar
     takes no taps and its button takes its own. */
  pointer-events: none;
}
#ledger-index { pointer-events: auto; }
/* NO full-width wash here, and don't reinstate one.
   It used to carry `linear-gradient(180deg, transparent, rgba(6,19,23,.85) 55%)`,
   which ended dead at the viewport bottom. Measured on an iPhone: a hard step
   of ~50 brightness levels, 25x sharper than its surroundings — the dark wash
   meeting Safari's pale toolbar. It reads as a shadowed box across the photo.
   Extending the gradient downward does NOT fix it: Safari's chrome is painted
   over that area, so the continuation is invisible.
   Each item carries its own local wash instead, so nothing spans the width and
   the picture simply bleeds away under the URL bar. */
body[data-state='loading'] #ledger,
body[data-state='diving'] #ledger,
body[data-state='room'] #ledger,
body[data-state='surfacing'] #ledger,
body[data-state='loading'] #announce,
body[data-state='diving'] #announce,
body[data-state='room'] #announce,
body[data-state='surfacing'] #announce { opacity: 0; transform: translateY(10px); pointer-events: none; }

/* ---- the announcement plate ------------------------------------------- */
/* A child of #stack: always above the countdown, never over it. */
#announce {
  display: flex; align-items: stretch;
  pointer-events: auto;
  background: rgba(10, 28, 33, .82);
  border: 1px solid rgba(230, 163, 62, .5);
  clip-path: var(--cut-s);
  backdrop-filter: blur(6px);
  transition: opacity .3s, transform .3s;
}
#announce[hidden] { display: none; }
.an-go {
  display: block; text-align: left;
  padding: 11px 16px 12px;
}
.an-eyebrow {
  display: block;
  font: 700 9px/1 var(--sans);
  letter-spacing: .3em; text-transform: uppercase;
  color: var(--gold);
}
.an-name {
  display: block; margin-top: 5px;
  font: 400 17px/1.15 var(--serif);
  color: var(--bone);
}
.an-name .th { font-size: .82em; color: rgba(231, 217, 189, .55); }
.an-go:hover .an-name { color: var(--gold); }
.an-x {
  padding: 0 14px;
  border-left: 1px solid rgba(231, 217, 189, .14);
  font: 400 13px/1 var(--sans);
  color: rgba(231, 217, 189, .5);
  transition: color .2s;
}
.an-x:hover { color: var(--sand); }

/* Its own wash, since the ledger no longer has a full-width one. Local, so
   nothing draws a line across the photograph. */
#ledger-count {
  position: relative;
  text-shadow: 0 1px 10px rgba(4, 14, 17, .95), 0 0 4px rgba(4, 14, 17, .8);
  font: 600 10.5px/1.5 var(--sans);
  letter-spacing: .16em; text-transform: uppercase;
  color: rgba(231, 217, 189, .92);
}
#ledger-count b { color: var(--gold); font-weight: 800; }
#ledger-count span.more { color: rgba(231, 217, 189, .45); }
@media (max-width: 620px) { #ledger-count span.more { display: none; } }

#ledger-index {
  display: inline-flex; align-items: center; gap: 8px;
  font: 700 10.5px/1 var(--sans);
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--gold);
  border: 1px solid rgba(230, 163, 62, .55);
  clip-path: var(--cut-s);
  padding: 12px 16px;
  background: rgba(10, 28, 33, .5);
  transition: background-color .25s, color .25s;
}
#ledger-index:hover { background: var(--gold); color: #142a30; }
/* The guest's own toon face where the hamburger used to be — a little "you"
   that rides along on the map. A muted disc until their avatar loads (or if
   they haven't said who they are yet). */
.ix-face {
  flex: none;
  width: 31px; height: 31px;
  border-radius: 50%;
  background: #0c2126 center/cover no-repeat;
  border: 1px solid rgba(230, 163, 62, .5);
}
.ix-face:not(.has-face) { opacity: .55; }
#ledger-index:hover .ix-face { border-color: #142a30; }

/* ---- Gary, the corner octopus ----------------------------------------- */
/* He leans in from the very corner, overhanging the bottom-left edge. The
   motion is baked into the animated WebP (idle wave loop; crawl-out/in on
   tap), so there's no CSS animation on the sprite itself. */
#gary {
  /* Below the ledger (z 40) so the "places open" line reads OVER his lower
     body, as intended; still above the map and pins. */
  position: fixed; z-index: 38;
  /* Push into the safe-area so he sits flush on the physical bottom edge on
     mobile (a plain bottom:0 floats him above the home indicator). */
  left: 0; bottom: calc(-1 * env(safe-area-inset-bottom, 0px));
  width: clamp(280px, 98vw, 620px);
  container-type: inline-size;   /* so the sign text can scale with Gary */
  padding: 0; border: 0; background: none; cursor: pointer;
  opacity: 0; pointer-events: none;
  transform: translateY(6%);     /* bleed a little further off the bottom (Safari toolbar) */
  transition: opacity .45s, transform .8s ease;
}
/* ⚠ LANDSCAPE IS THE SHORT EDGE, AND HE IS SIZED OFF THE LONG ONE.
   `width: clamp(280px, 98vw, 620px)` is a portrait measurement: in landscape
   98vw is the LONG edge, so every phone hits the 620px cap and the sprite
   (560x388, so 0.693 tall per wide) comes out 430px tall. Measured on the
   sizes phones actually give:
       390x844 portrait     sprite 382x265   31% of the screen height
       844x390 landscape    sprite 620x430  110%  ← taller than the screen
       740x360 landscape    sprite 620x430  119%
   and .gary-hit, which is 42% of his width by 82% of his height, went from
   11% of the screen to 28-34% of it. Guests meet this every time they leave
   Sawarin, which is played sideways (Justin, 10 Sep).
   So in landscape he is sized off the SHORT edge instead: 58vh of width is
   40vh of sprite, near the share of the screen he takes in portrait.
   max-height 700px is the divider because no phone's short edge reaches it
   and no tablet's falls below it — a tablet in landscape has the room and
   keeps the original rule. */
@media (orientation: landscape) and (max-height: 700px) {
  #gary { width: clamp(200px, 58vh, 400px); }
}

/* Only out on the map — gone while diving, in a room or the arcade. */
/* The BUTTON takes no taps; .gary-hit does. Gary's sprite occupies only the
   left 39.6% of this box (measured off the poster frame, from 20.9% down), and
   the empty remainder was swallowing taps meant for the Beach House pin behind
   it. Keyboard focus is unaffected: pointer-events does not touch it. */
body[data-state='map'] #gary { opacity: 1; pointer-events: none; }
body[data-state='map'] #gary .gary-hit { pointer-events: auto; }
/* Drift left as he presents so the sign sits more centred on screen. */
#gary.presenting { transform: translate(-9%, 6%); }
/* Gary's own pixels, padded a little for fingers: bottom-left corner only.
   Measured off the poster frame — he occupies the left 39.6%, from 20.9% down.
   The rest of the button is empty and belongs to the map behind it. */
.gary-hit {
  position: absolute; left: 0; right: 58%; top: 18%; bottom: 0;
  pointer-events: none;   /* switched on only on the map, above */
}
/* WHILE THE SIGN IS UP, take the whole box back. The sign is a separate
   overlay spanning 34%-88% of the button, well outside Gary himself, and a tap
   on it means "I have read this" — if it fell through to the map it would
   dismiss the sign AND open whatever pin was behind it, from one tap.

   Keyed on the sign's own .in class, which is what main.js actually toggles.
   NOT on #gary.presenting: that class is only ever set in gary-mock.html, so a
   rule hung off it here would look right and never once fire. */
#gary-sign.in ~ .gary-hit { right: 0; top: 0; }

.gary-sprite {
  display: block; width: 100%; height: auto;
  /* The crop already hangs him off the bottom-left corner; no nudge needed. */
  transform-origin: 0 100%;
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, .55));
}
/* Hand-lettered sign face — Caveat, subset + weight-700, sign only. */
@font-face {
  font-family: 'CaveatSign';
  src: url('/fonts/caveat-bold-v1.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
/* The announcement, hand-written onto Gary's white sign while he presents it.
   The box tracks the clear upper zone of the card (measured from the clip),
   starting right of the left tentacle tip and above the bottom-right one. */
.gary-sign {
  position: absolute;
  left: 34%; top: 6%; width: 54%; height: 42%;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity .28s;
  pointer-events: none;
}
.gary-sign .gs-text {
  width: 100%; text-align: center;
  color: #26362e;                        /* dark pine ink — not pure black, reads real */
  font: 700 5.5cqw/1.16 'CaveatSign', var(--serif);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .5);   /* faint emboss into the card */
}
.gary-sign .gw { white-space: nowrap; }    /* a word never splits across lines */
.gary-sign .gc { display: inline-block; }  /* per-letter, so the hand-jitter transforms apply */
.gary-sign.in { opacity: 1; }

/* The gripping tentacles, re-laid OVER the words so the card reads as truly
   held — the text passes behind Gary's suckers. Static (the grip is rock-steady
   in the present loop, measured), shown only while the sign is up. */
.gary-fg {
  position: absolute; top: 0; left: 0; width: 100%; height: auto;
  opacity: 0; transition: opacity .28s; pointer-events: none;
}
.gary-sign.in ~ .gary-fg { opacity: 1; }

/* Unseen-message cue: a gold "look here" arrow that bounces above Gary while
   there's news, and clears the moment he's tapped. */
.gary-alert {
  position: absolute; left: 14%; top: 26%;
  width: 12cqw; height: 12cqw;
  background: center/contain no-repeat
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23e6a33e' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v14'/%3E%3Cpath d='M6 11l6 6 6-6'/%3E%3C/svg%3E");
  filter: drop-shadow(0 2px 5px rgba(0, 0, 0, .55));
  opacity: 0; pointer-events: none;
  transition: opacity .3s;
}
#gary.has-news .gary-alert {
  opacity: 1;
  animation: gary-bounce 1.05s ease-in-out infinite;
}
#gary.presenting .gary-alert { opacity: 0; animation: none; }
@keyframes gary-bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-16%); }
}
@media (prefers-reduced-motion: reduce) {
  #gary.has-news .gary-alert { opacity: 1; animation: none; }
}
.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;
}
/* The one-off hint. Sits just above his head, fades itself out. */
.gary-tip {
  position: absolute; left: 30%; bottom: 58%;
  white-space: nowrap;
  padding: 7px 11px;
  background: rgba(10, 28, 33, .95);
  border: 1px solid rgba(230, 163, 62, .5);
  clip-path: var(--cut-s);
  font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase;
  color: var(--gold);
  opacity: 0; transform: translateY(4px); pointer-events: none;
  transition: opacity .3s, transform .3s;
}
.gary-tip.show { opacity: 1; transform: none; }

/* ---- index sheet ------------------------------------------------------ */

#sheet-back {
  position: fixed; top: 0; left: 0; right: 0; z-index: 65;
  bottom: calc(-1 * var(--skirt));   /* overlay — skirted, see base.css */
  opacity: 0; transition: opacity .3s;
}
/* Scrim colour and blur live on a pseudo-element, NOT on the fixed element:
   Liquid Glass reads background-color and backdrop-filter off fixed elements
   near the screen edges — even at opacity 0 — and tints the toolbar with
   them. Pseudo-elements are never sampled. */
#sheet-back::after {
  content: '';
  position: absolute; inset: 0;
  background: rgba(5, 16, 19, .55);
  backdrop-filter: blur(3px);
}
#sheet-back.in { opacity: 1; }

#index-sheet {
  position: fixed; z-index: 70;
  top: 0; right: 0;
  height: var(--screen-h);
  width: min(392px, 100vw);
  display: flex; flex-direction: column;
  background: rgba(13, 38, 45, .96);
  border-left: 1px solid rgba(231, 217, 189, .14);
  transform: translateX(103%);
  transition: transform .38s var(--ease-soft);
}
#index-sheet.in { transform: none; }
/* The display:flex above is an ID rule and OVERRIDES the UA's [hidden] rule,
   so `sheet.hidden = true` never actually removed the closed sheet from the
   display tree — it idled off-screen at full opacity with a near-opaque slate
   background, and Safari 26's Liquid Glass samples idle fixed elements for
   its toolbar tint (measured: closing the index left the toolbar flat slate
   instead of the blurred aerial). [hidden] must genuinely hide it. */
#index-sheet[hidden] { display: none; }
#index-sheet header {
  display: flex; align-items: center; justify-content: space-between;
  padding: calc(16px + env(safe-area-inset-top)) 20px 14px;
  border-bottom: 1px solid rgba(231, 217, 189, .12);
}
.ix-title { font: italic 700 1.4rem var(--serif); color: var(--bone); }
.ix-title .th { font-size: 1.05rem; color: var(--gold); }
.ix-close {
  width: 42px; height: 42px;
  border: 1px solid rgba(231, 217, 189, .25);
  clip-path: var(--cut-s);
  color: var(--sand); font-size: 15px;
  transition: border-color .2s, background-color .2s;
}
.ix-close:hover { background: rgba(231, 217, 189, .1); }
#index-sheet ul { flex: 1; overflow-y: auto; }
.ix-row {
  display: flex; align-items: baseline; gap: 13px;
  width: 100%; text-align: left;
  padding: 16px 20px;
  border-bottom: 1px solid rgba(231, 217, 189, .08);
  transition: background-color .2s;
}
.ix-row:hover { background: rgba(231, 217, 189, .07); }
.ix-n { font: 700 1rem var(--serif); color: var(--gold); min-width: 2.4ch; }
.ix-name { font: 500 1.05rem var(--serif); color: var(--bone); }
.ix-name .th { font-size: .85rem; color: rgba(231, 217, 189, .55); }
.ix-state {
  margin-left: auto;
  font: 700 10px/1.4 var(--sans);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--gold);
  white-space: nowrap;
}
.ix-sealed { opacity: .8; }
.ix-sealed .ix-n { color: rgba(231, 217, 189, .4); }
.ix-sealed .ix-state { color: rgba(212, 85, 58, .95); }
#index-sheet footer {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 22px 22px calc(26px + env(safe-area-inset-bottom));
  border-top: 1px solid rgba(231, 217, 189, .12);
  font-size: .86rem; line-height: 1.6;
  color: rgba(231, 217, 189, .6);
}

@media (max-width: 640px) {
  #index-sheet {
    /* Bottom sheet on a phone. bottom/height are restated because the base
       rule sets height:var(--screen-h) for the desktop side panel, and this
       one must hug the viewport bottom instead. */
    top: auto; left: 0; bottom: 0;
    width: 100%;
    height: auto;
    max-height: 78dvh;
    border-left: 0;
    border-top: 1px solid rgba(231, 217, 189, .18);
    transform: translateY(103%);
  }
}

/* ---- toast ------------------------------------------------------------ */

#toast {
  position: fixed; z-index: 55;
  left: 50%; bottom: calc(76px + env(safe-area-inset-bottom));
  transform: translate(-50%, 14px);
  width: min(408px, calc(100vw - 28px));
  background: rgba(15, 44, 52, .96);
  border: 1px solid rgba(231, 217, 189, .22);
  clip-path: var(--cut-m);
  padding: 15px 18px 16px;
  opacity: 0;
  transition: opacity .26s var(--ease-soft), transform .26s var(--ease-soft);
  cursor: pointer;
}
#toast.up { opacity: 1; transform: translate(-50%, 0); }
#toast b {
  display: block;
  font: 700 10.5px/1.4 var(--sans);
  letter-spacing: .15em; text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 6px;
}
#toast b .th { font-size: 11px; letter-spacing: .02em; color: rgba(230, 163, 62, .8); }
#toast p { font: 400 .97rem/1.55 var(--serif); color: var(--sand); }
#toast p em { color: var(--bone); }

/* A sealed room's plate is drained rather than coloured (.pin.is-sealed above),
   and its toast follows the same rule: tapping a locked pin should READ locked,
   not like an open room's announcement. Same blue-grey, same opacities. The
   closing <em> stays the brightest thing in the panel — it carries the
   instruction, and it is the line guests actually act on. */
#toast.sealed { background: rgba(12, 34, 41, .96); }
#toast.sealed b { color: rgba(198, 214, 220, .62); }
#toast.sealed b .th { color: rgba(198, 214, 220, .42); }
#toast.sealed p { color: rgba(198, 214, 220, .58); }
#toast.sealed p em { color: rgba(198, 214, 220, .84); }

/* ======================================================================
   THE ARCADE
   Forty Gates is a self-contained 1.4MB artifact in an iframe. It owns the
   whole viewport while it's up — the game needs every pixel on a phone, and
   its own audio controls sit top-right, so the close button goes top-LEFT.
   ====================================================================== */

/* 80, not 70 — #index-sheet already owns 70 and the game must cover it. */
#arcade {
  position: fixed; top: 0; left: 0; right: 0; z-index: 80;
  height: var(--screen-h);
  background: #04101300;
  opacity: 0;
  transition: opacity .3s var(--ease-soft);
}
#arcade[hidden] { display: none; }
#arcade.lit { opacity: 1; background: #041013; }

#game { width: 100%; height: 100%; border: 0; display: block; }

#arcade-close {
  position: absolute; z-index: 71;
  top: calc(env(safe-area-inset-top) + 10px);
  left: calc(env(safe-area-inset-left) + 10px);
  width: 40px; height: 40px;
  display: grid; place-items: center;
  background: rgba(7, 24, 28, .6);
  border: 1px solid rgba(231, 217, 189, .3);
  color: var(--sand);
  font-size: 15px; line-height: 1;
  backdrop-filter: blur(5px);
  clip-path: var(--cut-s);
}
#arcade-close:hover { background: rgba(7, 24, 28, .85); color: var(--bone); }
/* `display: grid` above BEATS the UA's [hidden]{display:none} — without this
   rule, hiding the button from JS silently does nothing. Sawarin hides it
   (the game draws its own ✕ beside its LIFE bar). */
#arcade-close[hidden] { display: none; }

/* The map keeps running behind the overlay otherwise — pointless work, and
   on a phone it costs the game frames. */
body[data-arcade='on'] #sky,
body[data-arcade='on'] #pins,
body[data-arcade='on'] #topbar,
body[data-arcade='on'] #ledger,
body[data-arcade='on'] #announce,
body[data-arcade='on'] #hint { visibility: hidden; }

.ix-arrived-wrap { display: flex; align-items: center; gap: 12px; min-width: 0; }
.ix-arrived {
  font: 400 italic 15px/1.5 var(--serif);
  color: rgba(231, 217, 189, .68);
}
.ix-arrived b { color: var(--sand); font-weight: 700; font-style: normal; }
/* The guest's face again, larger, beside "arrived as". */
.ix-facebig {
  flex: none;
  width: 52px; height: 52px;
  border-radius: 50%;
  background: #0c2126 center/cover no-repeat;
  border: 1px solid rgba(230, 163, 62, .5);
}
.ix-facebig:not(.has-face) { opacity: .5; }
/* An unmistakable button, same idiom as the menu chip. */
.ix-rename {
  flex: none;
  font: 700 10.5px/1 var(--sans);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--gold);
  border: 1px solid rgba(230, 163, 62, .55);
  clip-path: var(--cut-s);
  padding: 11px 16px;
  background: rgba(10, 28, 33, .5);
  transition: background-color .25s, color .25s;
}
.ix-rename:hover { background: var(--gold); color: #142a30; }

/* Narrow phones: a full-width plate beside a pin on the right half of a 390px
   screen barely fits. Tightening the type and the gap buys back ~45px per
   plate, and with sealed places now label-less there are only six to fit. */
@media (max-width: 560px) {
  .pin { --plate-gap: 24px; --stack-gap: 20px; }
  .pin-plate { padding: 6px 9px 7px; }
  .pin-plate b { font-size: 9.5px; letter-spacing: .1em; }
  .pin-plate small { font-size: 9.5px; }
  /* gap 24, same dy 24: √(24² + 24²) = 33.9, less the disc radius ≈ 19px, at 45°. */
  .pin.has-leader { --leader-len: 19px; --leader-rot: -45deg; }

  /* The notch is what put the Living Room's plate against the logo: the topbar
     is offset by env(safe-area-inset-top) (~59px on an iPhone), which walks the
     logo down onto a plate anchored to the map. Shrinking the bar on phones
     lifts the logo ~11px clear without touching the label geometry. */
  #topbar { padding-top: calc(8px + env(safe-area-inset-top)); }
  #topbar img { height: 25px; }
  #clock { padding: 7px 10px; font-size: 9.5px; }
}

/* ======================================================================
   THE VIGIL — countdown over the aerial
   Prominent by request: the premiere clock has to be visible from the map
   without diving into the Game Room. Sits above the ledger strip, on its own
   wash so it survives both jungle and white roofs. pointer-events:none —
   it must never eat a drag. Hidden unless LAUNCH_ISO is set and we're on
   the map (main.js paintVigilHud / states below).
   ====================================================================== */

/* The stack owns the bottom-centre geometry; announce and vigil are just its
   children, so they can never overlap — the plate stacks above the clock. */
#stack {
  position: fixed; z-index: 42;
  left: 0; right: 0;
  bottom: calc(84px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; align-items: center;
  gap: 18px;
  pointer-events: none;
}
#vigil {
  width: 100%;
  text-align: center;
  pointer-events: none;
  transition: opacity .5s var(--ease-soft);
}
#vigil[hidden] { display: none; }

.vh-room {
  /* Matches the air between the headline and the face above, so the clock
     sits centred in its plate rather than crowding its own caption. A real
     button — the one interactive thing in the vigil — so pointer-events
     comes back on for it alone. */
  margin-top: 16px;
  padding: 6px 10px;
  background: none; border: 0;
  pointer-events: auto;
  font: 400 italic 13px/1 var(--serif);
  color: rgba(231, 217, 189, .72);
  text-shadow: 0 1px 8px rgba(4, 14, 17, .95);
  transition: color .2s;
}
.vh-room:hover { color: var(--sand); }
.vh-room b { color: var(--gold); font-weight: 700; font-style: normal; }
.vh-room .th { font-size: .85em; color: rgba(231, 217, 189, .45); }
.vh-next {
  font: 700 9.5px/1 var(--sans);
  letter-spacing: .22em; text-transform: uppercase;
  font-style: normal;
  color: var(--gold);
  margin-right: 3px;
}

/* Text shadows alone weren't enough over sunlit lawn. An elliptical wash sits
   behind the whole block so the numerals read on jungle, grass and roofs
   alike, without drawing a visible panel. */
#vigil::before {
  content: '';
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(520px, 96vw); height: 190%;
  background: radial-gradient(ellipse at center,
    rgba(5, 17, 21, .82) 0%, rgba(5, 17, 21, .62) 42%, rgba(5, 17, 21, 0) 74%);
  pointer-events: none;
  z-index: -1;
}

.vh-label {
  font: 600 9.5px/1 var(--sans);
  letter-spacing: .38em; text-indent: .38em;
  text-transform: uppercase;
  color: var(--bone);
  text-shadow: 0 1px 10px rgba(4, 14, 17, .9);
}
/* The final minute: everything falls away but the seconds, blown up to fill
   the screen — 59, 58, 57… counting down huge and centred, so the last minute
   reads across the whole map. The vigil lifts out of its bottom-centre stack
   to a full-screen overlay for this one stretch. (User's call, launch-eve.) */
/* :not([hidden]) is load-bearing: without it this rule (equal specificity to
   #vigil[hidden], but later in the sheet) keeps winning after paintVigilHud
   sets hidden at zero, and the giant "00" never leaves the screen. */
#vigil.final-minute:not([hidden]) {
  position: fixed; inset: 0;
  width: auto;
  display: grid; place-items: center;
  z-index: 44;
}
#vigil.final-minute .vh-label,
#vigil.final-minute .vh-room,
#vigil.final-minute .vh-sep,
#vigil.final-minute .vh-cell { display: none; }
#vigil.final-minute .vh-face { margin: 0; }
#vigil.final-minute .vh-cell:last-child { display: flex; }
#vigil.final-minute .vh-cell:last-child i { display: none; }   /* drop "Secvndae" */
#vigil.final-minute .vh-cell:last-child b {
  font-size: 72vmin;
  line-height: .8;
  letter-spacing: -.03em;
  text-shadow: 0 2px 6px rgba(4, 14, 17, .9), 0 8px 60px rgba(4, 14, 17, .85);
}
/* A round vignette sized to the number, not the old narrow ellipse. */
#vigil.final-minute::before {
  width: min(96vw, 96vh); height: min(96vw, 96vh);
  background: radial-gradient(circle at center,
    rgba(5, 17, 21, .8) 0%, rgba(5, 17, 21, .5) 46%, rgba(5, 17, 21, 0) 72%);
}

.vh-face {
  margin-top: 10px;
  display: flex; align-items: baseline; justify-content: center;
  gap: clamp(5px, 1.8vw, 14px);
}
.vh-cell { display: flex; flex-direction: column; align-items: center; }
.vh-cell b {
  font: 700 clamp(1.9rem, 9.5vw, 3.1rem)/1 var(--serif);
  color: var(--bone);
  font-variant-numeric: tabular-nums;
  /* Two shadows: a tight one for edge definition on pale roofs, a wide one
     to lift the numerals off busy jungle. */
  text-shadow: 0 1px 2px rgba(4, 14, 17, .85), 0 4px 26px rgba(4, 14, 17, .8);
}
.vh-cell i {
  margin-top: 7px;
  font: 400 7.5px/1 var(--sans);
  font-style: normal;
  letter-spacing: .2em; text-indent: .2em;
  text-transform: uppercase;
  color: rgba(231, 217, 189, .82);
  text-shadow: 0 1px 8px rgba(4, 14, 17, .95);
}
.vh-sep {
  align-self: center;
  margin-bottom: 1.1rem;
  font: 400 1.5rem var(--serif);
  color: var(--gold);
  text-shadow: 0 1px 10px rgba(4, 14, 17, .9);
  animation: vhPulse 2s ease-in-out infinite;
}
.vh-sep:last-of-type { animation-delay: 1s; }
@keyframes vhPulse { 0%, 100% { opacity: 1; } 50% { opacity: .22; } }

/* Only on the map. Diving, in a room, or in the arcade, it goes. */
body[data-state='loading'] #vigil,
body[data-state='diving'] #vigil,
body[data-state='room'] #vigil,
body[data-state='surfacing'] #vigil,
body[data-arcade='on'] #vigil { opacity: 0; }

/* The roam hint shares this space. It used to be hidden outright when the
   vigil appeared, which — once LAUNCH_ISO was set — meant permanently. It
   moves above the countdown instead — and further up again when the
   announcement plate is stacked on top of the clock. */
body.has-vigil #hint { bottom: calc(200px + env(safe-area-inset-bottom)); }
body.has-vigil.has-announce #hint { bottom: calc(282px + env(safe-area-inset-bottom)); }
body.has-announce:not(.has-vigil) #hint { bottom: calc(160px + env(safe-area-inset-bottom)); }
@media (max-height: 680px) {
  body.has-vigil #hint { bottom: calc(180px + env(safe-area-inset-bottom)); }
  body.has-vigil.has-announce #hint { bottom: calc(258px + env(safe-area-inset-bottom)); }
}

@media (max-height: 680px) {
  #stack { bottom: calc(74px + env(safe-area-inset-bottom)); gap: 12px; }
  .vh-cell b { font-size: clamp(1.7rem, 8.5vw, 2.4rem); }
}

/* The index button and the count each carry their own backing now that the
   ledger has no full-width wash. Keeps them legible over sunlit grass without
   anything spanning the screen. */
#ledger-index { backdrop-filter: blur(6px); }
#ledger-count::before {
  content: '';
  position: absolute; left: -14px; right: -14px; top: 50%;
  height: 300%;
  transform: translateY(-50%);
  background: radial-gradient(ellipse at center,
    rgba(5, 17, 21, .78) 0%, rgba(5, 17, 21, .5) 45%, rgba(5, 17, 21, 0) 76%);
  pointer-events: none;
  z-index: -1;
}
