/* 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;
}
#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); }

/* clock + ambience toggle travel together on the right of the bar */
#tb-right { display: inline-flex; align-items: center; gap: 8px; }
#amb {
  display: inline-grid; place-items: center;
  width: 34px; height: 34px;
  color: rgba(231, 217, 189, .85);
  background: rgba(10, 28, 33, .45);
  backdrop-filter: blur(6px);
  clip-path: var(--cut-s);
  transition: color .2s;
}
#amb svg { width: 16px; height: 16px; }
#amb:hover { color: var(--bone); }
/* The two states must read at arm's length on a phone, not on inspection:
   ON — sand speaker, gold waves. OFF — dimmed speaker, chilli slash, and
   the chip itself goes dark and bordered. */
#amb .amb-waves { stroke: var(--gold); }
#amb .amb-slash { display: none; stroke: var(--chili); stroke-width: 2.4; }
#amb.is-off .amb-waves { display: none; }
#amb.is-off .amb-slash { display: block; }
#amb.is-off {
  color: rgba(231, 217, 189, .38);
  background: rgba(7, 18, 21, .7);
  box-shadow: inset 0 0 0 1px rgba(212, 85, 58, .5);
}
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; }
@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;
}
/* 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; }
.ix-burger {
  width: 14px; height: auto; flex: none;
  fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round;
}

/* ---- 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); }

/* ======================================================================
   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); }

/* 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 {
  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; }
/* 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);
}

.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;
}
