*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* Strip along the bottom owned by the two fixed circular buttons:
     45px button + its bottom offset + a 12px breathing gap. The scrolling
     panel stops here, so body copy can never slide underneath them. */
  --btn-zone: calc(max(20px, env(safe-area-inset-bottom)) + 57px);
}

html, body {
  height: 100%;
  overscroll-behavior: none;
}

body {
  font-family: Helvetica, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* Ink, not #fff. iOS Safari tints its status bar and toolbar from the page's
     own background at the top/bottom edges and does NOT simply obey
     theme-color, so a white body painted white bars around a near-black video.
     TRUE BLACK (owner 2026-08-03 "black out the background"): OLED pixels
     switch off, the portrait margins/letterbox vanish into the panel, and
     iOS paints black system bars — the record floats in nothing. */
  background: #000;
  overflow: hidden;
}

main {
  position: relative;
  height: 100svh;
  width: 100vw;
  overflow: hidden;
  /* Stacking context so #bg-video (z-index:0) sits under overlay/nav/panels
     without fighting the document root or fixed glass buttons. */
  isolation: isolate;
  z-index: 1;
}

/* ---- background video ----
   Fixed full-viewport plate under all UI. object-fit:cover crops, never letter-
   boxes. -webkit-transform promotes a compositor layer on iOS Safari so the
   video doesn't stutter under backdrop-filter glass buttons. pointer-events:none
   so taps fall through to the menu/sound controls (the video is decoration). */
#bg-video {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: 100vw;
  max-height: 100svh;
  object-fit: cover;
  object-position: center center;
  background: #000;
  z-index: 0;
  top: 0;
  left: 0;
  pointer-events: none;
  /* Hide native controls chrome if a browser ignores our attributes. */
  -webkit-appearance: none;
  appearance: none;
  /* Compositor layer — critical on Chrome Android + iOS Safari for smooth
     cover scaling while glass buttons apply backdrop-filter above. */
  -webkit-transform: translateZ(0);
  transform: translateZ(0);
}

/* When poster is showing (before first frame / autoplay blocked), keep the
   plate full-bleed so the UI never flashes white around the JPEG. */
#bg-video[poster] {
  background: #000 center / cover no-repeat;
}

/* ---- blur/wash overlay ----
   NO backdrop-filter in this base rule ON PURPOSE. It lives ONLY inside the
   @supports block below, together with the opacity:1 pin — because pairing a
   backdrop-filter with an opacity transition on the same element is the exact
   iOS trap this file was bitten by (WebKit drops the blur the moment opacity<1).
   Keeping the two apart means the plain-opacity path here is filter-free and
   safe, the filtered path never animates opacity, and `css-guards.mjs` can tell
   the difference. This rule is now the DEGRADE path: wash only, no blur. */
#overlay {
  position: fixed;
  inset: 0;
  /* Measured, not guessed: at 0.5 the composited plate behind the text sat at
     ~#818181, which put the 12px secondary greys at 2.0:1 and capped even the
     near-black active nav at 4.6:1. Darkening the type alone could not reach
     4.5:1 from there — the plate had to move. 0.66 lifts it to ~#a9a9a9, the
     most it can take before the concert frame stops reading as video; the
     type ramp was then compressed into the band that clears 4.5:1 on it. */
  background: rgba(255, 255, 255, 0.66);
  opacity: 0;
  pointer-events: none;
  z-index: 10;
}

body.menu-open #overlay,
body.panel-open #overlay {
  opacity: 1;
}

/* The opacity fade belongs ONLY to the no-filter path. Declaring it on the base
   rule made the element "filtered somewhere + fades on opacity" — the exact
   iOS-trap signature, and css-guards.mjs flagged it (correctly: the next person
   to touch this file would not know the @supports block neutralises it). Here
   there is no backdrop-filter at all, so opacity is safe and is the only tool. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #overlay { transition: opacity 300ms ease; }
}

/* ===== iOS: THE GLASS MUST NOT LEAVE BEFORE THE LINKS =====================
   Owner 2026-08-05, on a real iPhone (twice): "the glass still disappears
   before the links do."  Reproduced NOWHERE in emulation — and now we know
   why: headless WebKit does not render backdrop-filter AT ALL. Measured on a
   striped test page, edge energy with the glass fully opaque: Chromium 0.16
   (stripes genuinely blurred), WebKit/Firefox 14.43 — which is exactly a 66%
   white wash over sharp stripes (42.29 x 0.34), i.e. ZERO blur. Every glass
   check on headless WebKit was measuring a plain white rectangle, so the whole
   matrix was structurally blind to this. (Same shape as the mediump lesson:
   the test environment could not host the bug.)

   MECHANISM: on WebKit, an element carrying backdrop-filter loses the filter
   as soon as its own opacity drops below 1 — the backdrop sampling is
   abandoned when the element is promoted for the opacity animation. So frame 1
   of the close snaps the BLUR off while the 66% tint is still ~fully painted.
   The blur IS the glass; losing it reads as "the glass vanished", while the
   links carry on their 300ms fade. Chromium keeps sampling and stays in sync,
   which is why Android and desktop measured perfectly synced.

   FIX: never animate OPACITY on the element that carries the filter. Hold
   opacity at 1 and animate the two things that actually constitute the glass —
   the blur radius and the tint alpha — so there is no sub-1 opacity to trigger
   the drop. Visually equivalent on every engine for a full-bleed wash (a
   uniform layer fading out vs its own tint+blur ramping to nothing), and it
   removes the trigger rather than compensating for it.
   Scoped to engines that actually do backdrop-filter, so anything relying on
   the plain-opacity path (or the @supports degrade further down) is untouched. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #overlay {
    opacity: 1;                       /* never sub-1 while filtered */
    background: rgba(255, 255, 255, 0);
    backdrop-filter: blur(0px);
    -webkit-backdrop-filter: blur(0px);
    visibility: hidden;
    transition:
      background-color 300ms ease,
      backdrop-filter 300ms ease,
      -webkit-backdrop-filter 300ms ease,
      visibility 0s linear 300ms;     /* stays paintable for the whole fade-out */
  }
  body.menu-open #overlay,
  body.panel-open #overlay {
    opacity: 1;
    background: rgba(255, 255, 255, 0.66);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    visibility: visible;
    transition:
      background-color 300ms ease,
      backdrop-filter 300ms ease,
      -webkit-backdrop-filter 300ms ease,
      visibility 0s linear 0s;
  }
}

/* ===== THE MENU'S OWN GLASS =================================================
   Owner 2026-08-06: "click crescent to open menu, click link and that goes to its own
   page with no links ... the next crescent click will pull out a NEW glass with the menu
   so everything else is faded behind. keep this consistent everywhere."

   So the menu is not the page's glass with links switched on — it is a SECOND pane that
   rises over whatever you were reading. Stack:
       #overlay      z-10   the PAGE's reading wash (unchanged, still carries 4.5:1)
       .panel        z-20   your content
       .panel        z-28   ...but 28 once the map is the backdrop (see below)
       #menu-glass   z-29   THIS - the menu's pane; the page recedes behind it
       #nav          z-30   the links, crisp on top
   Because it sits ABOVE the panel, an open article visibly falls back when the menu
   arrives, and the two washes compound so the menu reads as the thicker, nearer
   material — which is exactly how Apple differentiates a menu from the surface it was
   summoned from (LIQUID_GLASS_GAP.md 1.5, "larger sizes ... simulate a thicker, more
   substantial material").

   ITS Z-INDEX NEVER CHANGES. Lifting the shared #overlay on .menu-open was tried and
   reverted: z flips instantly on a class change, so the glass teleported behind the
   world on the first frame of the close instead of fading, while the links carried on
   their 300ms — the owner's "the glass disappears before the links ... clunky and not
   smooth" (see the note at body.map-open #overlay). A separate element with a constant
   z has nothing to teleport.

   OPACITY IS PINNED AT 1 and the tint+blur carry the transition — WebKit drops
   backdrop-filter the instant an element's own opacity goes below 1, which is the
   documented iOS "glass vanished" trap. Same treatment as #overlay. */
#menu-glass {
  position: fixed;
  inset: 0;
  /* 29, NOT 25. A page is normally z-20, but `body.map-backdrop .panel:not(#panel-map)`
     lifts it to 28 so an article can sit over the map-as-scenery. At 25 the menu pane
     rose BEHIND any page reached once the map had been visited — owner 2026-08-06:
     "it worked until i got to stickers and then it broke ... the glass was behind
     everything else and not in front with the menu."
     The pane must clear the HIGHEST page elevation, not the default one. 29 sits above
     both 20 and 28 and still below the nav at 30, so the links stay crisp on top.
     Guard: scripts/menu_glass_stack_check.mjs walks every ordered PAIR of links and taps
     the crescent after each hop — the first check only tested a FRESH open of #4nbt and
     was structurally blind to this (12 of its 24 paths were failing). */
  z-index: 29;
  pointer-events: none;
  opacity: 1;                          /* never sub-1 while filtered */
  background: rgba(255, 255, 255, 0);
  visibility: hidden;
  transition:
    background-color 300ms ease,
    visibility 0s linear 300ms;        /* stays paintable for the whole fade-out */
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  #menu-glass {
    backdrop-filter: blur(0px);
    -webkit-backdrop-filter: blur(0px);
    transition:
      background-color 300ms ease,
      backdrop-filter 300ms ease,
      -webkit-backdrop-filter 300ms ease,
      visibility 0s linear 300ms;
  }
  body.menu-open #menu-glass {
    /* AN OPEN MENU SWALLOWS TAPS (owner 2026-08-06: "when links tab is clicked, and then
       we hit the crescent, the links behind are active. i try to click sticker and it
       redirects me to twitter"). The pane was pointer-events:none, so a tap that missed a
       ~19px-tall nav link fell straight through to the PAGE's own hyperlinks — measured
       97 reachable points, including a tap 14px below WORLD MAP landing on
       t.me/YEEZYCOINLANDING. Capturing here makes the page inert scenery for exactly as
       long as the menu is up; #nav (z-30) and the controls (z-40) sit ABOVE this pane and
       stay live, so nothing the user is aiming at is blocked. */
    pointer-events: auto;
    /* Deliberately LIGHTER than #overlay's 0.66: it compounds ON TOP of the page wash,
       so the two together must not bury the film. Over the bare film (no page open) it
       is the only wash, and 0.34 + 14px still clears the type ramp the nav uses. */
    background: rgba(255, 255, 255, 0.34);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    visibility: visible;
    transition:
      background-color 300ms ease,
      backdrop-filter 300ms ease,
      -webkit-backdrop-filter 300ms ease,
      visibility 0s linear 0s;
  }
}

/* No-filter engines: the wash alone has to do the whole job, so it carries more. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.menu-open #menu-glass {
    background: rgba(255, 255, 255, 0.5);
    visibility: visible;
    transition: background-color 300ms ease, visibility 0s linear 0s;
  }
}

/* ---- left-hand nav ---- */
#nav {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 20px;
  padding: 20px;
  padding-top: max(20px, env(safe-area-inset-top));
  padding-left: max(20px, env(safe-area-inset-left));
  opacity: 0;
  pointer-events: none;
  /* opacity:0 alone left the clock and both links in the tab order while they
     were invisible AND inside aria-hidden="true" — three dead tab stops before
     the first real control. visibility:hidden removes them; the 0s-delayed
     visibility transition holds them visible for the length of the fade-out so
     the animation still reads. */
  visibility: hidden;
  transition: opacity 300ms ease, visibility 0s linear 300ms;
}

/* MEASURED, NOT YET SHIPPED — why the links can never move as smoothly as the glass:
   #nav is `inset: 0`, i.e. FULL-VIEWPORT, so fading its opacity recomposites the whole
   screen every frame over a playing video. At 390px / 4x CPU throttle, effective
   opacity (container x link):
       container-fade (here)          ->  6 steps, worst jump 0.320
       per-child fade (the page path) -> 17 steps, worst jump 0.125
   Same duration, same easing; only the composited AREA differs. Moving the fade onto
   the children was tried and REVERTED: it broke the home close (links read 0 from the
   first frame instead of fading), and a half-verified fix to the site's most-used
   transition is worse than a known-stepped one. The finding stands; the fix needs its
   own pass. */

body.menu-open #nav {
  opacity: 1;
  visibility: visible;
  transition: opacity 300ms ease, visibility 0s linear 0s;
}

/* Container never captures clicks (panel scrolls beneath); its controls do.
   This MUST cover <button> as well as <a>: the clock is a <button> and an
   `#nav a`-only rule left it pointer-events:none, i.e. the "back to home"
   control was unclickable for a real user while still responding to a
   scripted .click(). */
body.menu-open #nav a,
body.menu-open #nav button {
  pointer-events: auto;
}

/* A <button> so it's keyboard- and screen-reader-reachable, stripped back to
   look exactly like the plain text it replaced. */
#timestamp {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  text-align: left;
  cursor: pointer;
  color: #3d3d3d;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  transition: color 300ms ease;
}

#timestamp:hover,
#timestamp:focus-visible {
  color: #171717;
}

#nav a {
  /* was #525252 — 2.0:1 over the wash at 12px. #3d3d3d keeps the two-tier
     "inactive / active" read against #171717 while clearing 4.5:1. */
  color: #3d3d3d;
  font-size: 12px;
  text-decoration: none;
  letter-spacing: 0.02em;
  transition: color 300ms ease;
  padding: 2px 0;
}

#nav a:hover { color: #262626; }
#nav a.active { color: #171717; }

/* ---- content panels ---- */
.panel {
  position: fixed;
  /* Bottom inset, not 0: #menu-btn (bottom centre) and #sound-btn (bottom
     right) are fixed and used to sit on top of the text column — mid-scroll
     they covered whole words of body copy on mobile and the ATH paragraph on
     desktop. Ending the scrollport above them means text scrolls out of view
     instead of under a button. */
  inset: 0 0 var(--btn-zone) 0;
  z-index: 20;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  opacity: 0;
  transition: opacity 300ms ease;
}

body.panel-open .panel:not([hidden]) {
  opacity: 1;
}

/* ---- WORLD MAP panel ----
   Full-bleed black frame — the map is its own world; the white wash and the
   ink-on-white nav palette would fight it, so map-open flips both. */
#panel-map {
  inset: 0;
  background: #000;
}
#map-frame {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  background: #000;
}
/* THE MENU ALWAYS GETS ITS GLASS (owner 2026-08-04: "on the crescent press, don't forget
   the glass background like 4NBT/YzY and Links has. that should populate every time you
   press that crescent"). Same material, same blur, same 300ms — SURFACE-AWARE INK:
     over the film  -> the light wash above (the owner's reference; untouched)
     over the map   -> near-black glass, because the map is a #000 world and a white wash
                       there is exactly the "grey top" he objected to. body.map-open already
                       flips the nav ink light, so this completes a decision the design had
                       half-made. Measured composited top rows: rgb(0,0,0) — no grey lift.
   It rides body.menu-open ONLY: press the crescent -> glass + nav in, press again -> out. */
/* Bare map, menu closed = NO GLASS. This must beat body.panel-open (the map IS
   a panel), hence !important. It hides via tint+blur+visibility, NOT opacity:
   under the filtered path an opacity below 1 re-arms the WebKit filter-drop
   that the iOS block above exists to remove. The plain-opacity form is kept
   only for engines without backdrop-filter, where there is no filter to drop. */
body.map-open:not(.menu-open) #overlay {
  background: rgba(255, 255, 255, 0) !important;
  backdrop-filter: blur(0px) !important;
  -webkit-backdrop-filter: blur(0px) !important;
  visibility: hidden !important;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.map-open:not(.menu-open) #overlay { opacity: 0 !important; }
}
body.map-open #overlay {
  /* THE SAME GLASS AS EVERY OTHER PAGE (owner 2026-08-04: "by glass I mean the same
     glass background clicking 4NBT/YzY gives you. every page needs this type of
     feature"). Identical material to the panels above — light frosted wash, same
     blur, same 300ms. z-25 lifts it between the map panel (20) and the nav (30),
     because #overlay's own z-10 rendered it BEHIND the world and it never showed.
     UNCONDITIONAL ON THE MAP, not gated on .menu-open (owner 2026-08-05: "the glass
     disappears before the links ... clunky and not smooth"): z-index flips INSTANTLY
     on a class change, so tying the lift to the menu state dropped the glass BEHIND
     the world on the first frame of closing — it never faded, it teleported, while
     the links (z-30) finished their 300ms over a suddenly-sharp map. With the lift
     unconditional the glass keeps its 300ms opacity fade in view, on the same clock
     as the links. Closed it is opacity:0 + pointer-events:none, so the elevation is
     inert; the map-backdrop rule already runs it at 25 anyway. */
  z-index: 25;
}

/* Over the light wash the nav must read DARK, exactly as it does on the film —
   body.map-open's light ink is for the bare map, not for the menu state. */
body.map-open.menu-open #nav a { color: #3d3d3d; }
body.map-open.menu-open #nav a:hover { color: #262626; }
body.map-open.menu-open #nav a.active { color: #171717; }
body.map-open.menu-open #timestamp { color: #3d3d3d; }
body.map-open.menu-open #timestamp:hover,
body.map-open.menu-open #timestamp:focus-visible { color: #171717; }

/* THE FILM TOOLS ARE NOT PART OF THE PRODUCT (owner 2026-08-04: "the sliders will
   not be there ... and the timer on the top right. that was solely for video
   editing purposes"). Both are already FX-gated off 4nbt.media, but they render on
   STAGING — which is the surface the owner reviews, so inside the map they must be
   gone everywhere. It also means staging's map is pixel-identical to prod's, and it
   frees the lower-left slot for the camera. */
/* (the old `body.map-open #fx-btn/#fx-panel/#vtime {display:none}` rule is gone —
   owner 2026-08-04 took the sliders and the timer off EVERY page, so app.js now
   REMOVES those elements from the DOM unless ?fx=1. Nothing left to hide.) */

/* The map's own surfaces own the screen while they are up: the running clock used to print
   through a sticker card, and the fixed glass row (z-40) painted the crescent ON TOP of the
   enlarged photo. #panel-map is a z-index-20 stacking context, so the map cannot out-index
   them from inside — they step back from here instead. */
/* INVISIBLE MUST ALSO MEAN INERT (audit H3/H4/M3, 2026-08-04). opacity:0 alone leaves a
   control clickable, focusable and in the accessibility tree — the documented dead-tab-stop
   bug, reintroduced on the map: the hidden clock still exited home on a tap through an open
   card, the hidden nav links were Enter-activatable, and the glass buttons under the photo
   viewer opened the menu BEHIND it. visibility:hidden removes them from hit-testing and the
   tab order; the 0s-delayed transition keeps the fade visible on the way out. */
body.map-open.stk-lightbox-open #nav,
body.map-open.stk-lightbox-open .glass-btn {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: opacity 300ms ease, visibility 0s linear 300ms;
}
body.map-open.stk-card-open #timestamp,
body.map-open.stk-lightbox-open #timestamp {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}
body.map-open #nav a { color: #9a9aa4; }
body.map-open #nav a:hover { color: #e6e6ea; }
body.map-open #nav a.active { color: #e6e6ea; }
body.map-open #timestamp { color: #9a9aa4; }

/* A PAGE SHOWS THE GLASS AND THE CLOCK — NEVER THE LINKS. EVERY WIDTH.

   Owner 2026-08-06: "the crescent needs to only be the menu open and menu closed" —
   "when i press the crescent, the entire frosted glass should come up with links;
   pressing it off brings me back to the existing page ... only when you press the
   crescent they appear."

   The links are THE MENU, not page furniture. That is the whole idea: one sentence
   describes the crescent on every screen, and a page is only ever your content.
   An earlier cut scoped this to phones and kept the links on desktop; it was replaced
   BY REQUEST once the unified model was chosen — consistency of the crescent's contract
   beat filling the desktop gutter. If the desktop gutter reads empty, the fix is the
   page composition (column width, or a thinner glass letting the film through), NOT
   putting navigation back onto the page.

   The three measured defects this rule removed, all from links living on a page:
     - #stickers  links painted at opacity 0.15 for the full 300ms after the tap
     - #4nbt      links sat on the body text at opacity 1 until you scrolled
     - #map       links snapped to opacity 1 in 17ms while the glass took ~1000ms

   The RUNNING CLOCK stays on every surface — it is the way home. */
body.panel-open:not(.menu-open) #nav {
  opacity: 1;
  visibility: visible;
  transition: opacity 300ms ease, visibility 0s linear 0s;
}
body.panel-open:not(.menu-open) #nav a {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;                       /* not just transparent — see the note above */
  /* DISPLAY:NONE, DELAYED — the Android ghost-tile fix (owner 2026-08-05: after
     minimize, "4nbt/yzy is not clickable but shown"). visibility:hidden removes
     hit-testing but leaves the element IN the layer tree, and Android Chrome's
     compositor can keep a stale raster tile of it on screen indefinitely — a
     ghost that every DOM probe calls hidden, because it IS hidden; only the
     pixels are stale. display:none removes it from the layer tree entirely, so
     there is no tile to go stale. `allow-discrete` delays the flip to the end
     of the 300ms fade; engines without support just skip the fade-out (links
     pop instead of fading — correct, merely less pretty). Fade-IN is untouched:
     opening the menu unmatches this rule (display restores instantly) and the
     links ride the #nav container's own 300ms fade. */
  display: none;
  /* NO FADE-OUT WHEN A PAGE OPENS (owner 2026-08-06: "If were loading a 4nbt/yzy page,
     why would we have the links still show and then disappear?"). Exactly right — a
     page never had links, so there is nothing to animate away. The 300ms fade here was
     the last remnant of treating a page like a menu that happens to be closing: it
     painted all four links over your content for a third of a second on every single
     navigation. Instant.
     The MENU's own open/close fade is untouched — that transition lives on `#nav` and
     is the smooth one worth keeping. */
  transition: none;
}

/* THE CLOCK IS CLICKABLE WHEREVER IT IS PAINTED (owner 2026-08-06: "make sure the clock
   runs on all pages and it routes to home when clicked").
   #nav is pointer-events:none and only `body.menu-open #nav button` re-enabled it, with a
   map-only exception below. Once a PAGE stopped setting menu-open, the clock was painted,
   ticking, and DEAD — elementFromPoint at its centre returned the panel underneath on
   #4nbt, #links and #stickers, and a real tap did nothing. Exactly the same shape as the
   frozen-clock bug an hour earlier: a rule that enumerated states instead of stating the
   condition. It is the only way home from a page, so this is not cosmetic.
   Guard: scripts/clock_check.mjs taps it by COORDINATES on every surface — a scripted
   .click() resolves the element directly and cannot see the panel stealing the tap. */
body.panel-open #timestamp {
  pointer-events: auto;
}

body.map-open:not(.menu-open) #timestamp {
  pointer-events: auto;
  color: #6b6b74;
}
body.map-open #timestamp:hover,
body.map-open #timestamp:focus-visible { color: #e6e6ea; }

/* nav stays visible with a panel open: content clears it below on mobile,
   to the right on wider screens */
.panel-inner {
  max-width: 560px;
  /* 140px of tail padding existed to keep the last line clear of the buttons;
     the panel's --btn-zone inset now does that, so this is just end-of-text
     breathing room. */
  /* REFORMATTED FOR A PAGE WITH NO LINKS (owner 2026-08-06: "this needs to be
     reformatted like there is no links"). The old 240px existed for one reason: to push
     the first line clear of the four FIXED nav links in the top-left. A page no longer
     shows links, so that space became a hole the article floated below - the "unaligned
     and looks weird" report. 112px now clears only what is actually still up there: the
     running clock. */
  padding: 64px 24px 64px;
  /* MEASURED AGAINST THE ONLY THING STILL UP THERE. The clock's bottom edge sits at
     y=36, so 112px left a 76px void above the first line — owner 2026-08-06: "there is
     still a good gap in the 4nbt page at the top, the whole article should be higher."
     64px clears the clock by 28px, which is the site's own 20-24px rhythm.
     The safe-area arm tracks the clock rather than a fixed number: #nav's own top padding
     is max(20px, env(safe-area-inset-top)) and the clock is ~16px tall, so
     safe-area + 44px reproduces the same 28px gap on a notched phone. */
  padding-top: max(64px, calc(env(safe-area-inset-top) + 44px));
}

@media (min-width: 1000px) {
  .panel-inner {
    margin: 0 auto;
    /* Was 120px — the desktop twin of the 240px hole. Both existed to clear the four
       FIXED nav links; a page has none, so the only thing above the text is the clock
       (bottom edge y=36). 72px gives desktop a slightly airier 36px gap than mobile's
       28px, which suits the wider measure without reopening the void. */
    padding-top: max(72px, calc(env(safe-area-inset-top) + 52px));
  }
}

.panel p {
  color: #2e2e2e;
  font-size: 14px;
  line-height: 1.7;
  margin-bottom: 18px;
}

/* tight multi-line block, matches <br>-stacked line spacing */
.stanza {
  margin-bottom: 26px;
}

.stanza p,
.panel .stanza p.em {
  margin: 0;
}

.panel p.em {
  color: #171717;
  letter-spacing: 0.04em;
  margin: 26px 0;
}

.tweet {
  display: block;
  border: 1px solid rgba(23, 23, 23, 0.25);
  padding: 18px 20px;
  margin: 26px 0;
  text-decoration: none;
  transition: border-color 300ms ease, background-color 300ms ease;
}

.tweet:hover {
  border-color: rgba(23, 23, 23, 0.6);
  background: rgba(255, 255, 255, 0.35);
}

.tweet-quote {
  display: block;
  color: #171717;
  font-size: 16px;
  line-height: 1.5;
  margin-bottom: 10px;
}

.tweet-meta {
  display: block;
  color: #3d3d3d;
  font-size: 12px;
}

/* ===== (REMOVED) SCROLL-AWAY NAV =========================================
   Links used to fade out as you scrolled a reading panel, because they were
   painted over the body text. Pages no longer show links at all, so there is
   nothing to get out of the way. The feature, its directional hysteresis, its
   :focus-visible guard and the `nav-scrolled` state are all gone — the state that
   leaked out of its panel and left the menu dead cannot recur. */


}

/* (The old STICKERS-only roll-up lived here. It is now covered by the single
   `body.panel-open:not(.menu-open)` rule above — stickers was simply the first
   page to get the behaviour every page now has.) */


body.stickers-open:not(.menu-open) #timestamp {
  pointer-events: auto;
}

/* STICKERS SITS APART (owner 2026-08-05: "a few spaces below links"). The other
   three entries are PLACES YOU GO; this one ASKS SOMETHING OF YOU — take a sheet,
   put it on a wall. The gap is the whole distinction, so it is deliberately wider
   than the nav's own 20px rhythm rather than a divider rule: this menu has no
   lines in it and shouldn't gain one for a single item. */
#nav a[href="#stickers"] {
  margin-top: 26px;
}

/* ---- STICKERS panel (owner 2026-08-05) ----------------------------------
   Panels here are ink-on-glass, so this borrows the panel's existing type
   ramp rather than inventing a second one. The two sheets are shown at the
   polarity they print at — the white-on-black plate keeps its own black
   ground, which is why the figure carries the colour and not the panel. */
.stk-dl-lead {
  font-size: 15px;
  letter-spacing: 0.06em;
  color: #171717;
  margin-bottom: 18px;
}

.stk-dl-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 22px;
  margin: 30px 0 26px;
}

.stk-dl {
  margin: 0;
  border: 1px solid rgba(23, 23, 23, 0.25);
}

.stk-dl img {
  display: block;
  width: 100%;
  height: auto;
}

.stk-dl figcaption {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border-top: 1px solid rgba(23, 23, 23, 0.25);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: #2e2e2e;
}

/* the only "button" in a panel — kept a rule, not a pill, so it reads as
   part of the same monochrome system as the links list */
.stk-dl-btn {
  color: #171717;
  text-decoration: none;
  border-bottom: 1px solid #171717;
  padding-bottom: 2px;
  transition: opacity 300ms ease;
}

.stk-dl-btn:hover { opacity: 0.55; }

.stk-dl-foot {
  font-size: 11px;
  letter-spacing: 0.14em;
  color: #4a4a4a;
  margin-bottom: 8px;
}

.stk-dl-foot a { color: #171717; text-decoration: none; border-bottom: 1px solid #171717; }

.links-list {
  list-style: none;
}

.links-list li {
  margin-bottom: 4px;
}

.links-list a {
  display: inline-block;
  color: #2e2e2e;
  font-size: 14px;
  text-decoration: none;
  letter-spacing: 0.02em;
  padding: 10px 0;
  transition: color 300ms ease;
}

.links-list a:hover { color: #171717; }

.links-list a::after {
  content: ' \2197';
  font-size: 12px;
  color: #3d3d3d;
}

/* ticker stays on-site — no outbound arrow */
.links-list a.internal::after {
  content: '';
}

/* ---- ticker chart ---- */
.panel-inner-wide {
  max-width: 780px;
}

#ticker-stats {
  display: flex;
  justify-content: space-between;
  /* Without these the two spans collide on narrow screens and render as
     "$9.9M MCAPLAUNCH → NOW · LOG". */
  flex-wrap: wrap;
  gap: 4px 14px;
  color: #3d3d3d;
  font-size: 12px;
  letter-spacing: 0.04em;
  margin-bottom: 14px;
  font-variant-numeric: tabular-nums;
}

#ticker-chart {
  position: relative;
  width: 100%;
}

/* Was a style="" attribute on the injected <svg>; moved here so the CSP can
   forbid inline styles entirely. pan-y keeps vertical scrolling working over
   the chart on touch devices. */
#ticker-chart svg {
  display: block;
  touch-action: pan-y;
}

#ticker-tip {
  position: absolute;
  z-index: 5;
  max-width: 230px;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid rgba(23, 23, 23, 0.25);
  padding: 8px 10px;
  font-size: 12px;
  line-height: 1.5;
  color: #3d3d3d;
  pointer-events: none;
}

#ticker-tip strong {
  color: #171717;
  font-weight: 400;
}

/* focus lands here programmatically after a keyboard pin activation; the box
   appearing is the indicator, so no extra ring */
#ticker-detail:focus { outline: none; }

#ticker-detail {
  position: relative;
  border: 1px solid rgba(23, 23, 23, 0.25);
  padding: 16px 18px;
  margin-top: 18px;
  font-variant-numeric: tabular-nums;
}

#ticker-detail .td-meta {
  display: block;
  color: #3d3d3d;
  font-size: 12px;
  letter-spacing: 0.04em;
  margin-bottom: 8px;
  /* Reserve the close button's column. At 390px the meta ran to x=321 while
     the close button began at x=313, rendering as "…$325K VOL×". */
  padding-right: 34px;
}

#ticker-detail .td-title {
  display: block;
  color: #171717;
  font-size: 14px;
  letter-spacing: 0.04em;
  margin-bottom: 10px;
}

#ticker-detail p {
  margin-bottom: 10px;
}

#ticker-detail .td-src {
  color: #3d3d3d;
  font-size: 12px;
  text-decoration: none;
}

#ticker-detail .td-src:hover {
  color: #171717;
}

#ticker-detail-close {
  position: absolute;
  top: 6px;
  right: 10px;
  border: none;
  background: none;
  color: #3d3d3d;
  font-size: 18px;
  cursor: pointer;
  padding: 4px 8px;
}

#ticker-chart .band .band-span {
  opacity: 0;
  transition: opacity 200ms ease;
}

#ticker-chart .band:hover .band-span,
#ticker-chart .band.selected .band-span {
  opacity: 1;
}

/* ---- pin keyboard focus ----
   The 47 pins are the whole point of this page and were mouse-only: no
   tabindex, no role, no name. chart.js now emits each <g class="pin"> as a
   focusable role="button". A CSS `outline` on an SVG child is inconsistently
   painted across engines, so each pin also ships a pre-drawn `.pin-focus`
   ring that this rule reveals — deterministic, and near-black so it reads
   against the light wash. */
#ticker-chart .pin {
  outline: none;
}

#ticker-chart .pin .pin-focus {
  opacity: 0;
}

#ticker-chart .pin:focus-visible .pin-focus {
  opacity: 1;
}

/* Fallback for engines without :focus-visible on SVG elements. */
#ticker-chart .pin:focus .pin-focus {
  opacity: 1;
}

.td-eli5 {
  border-left: 2px solid rgba(23, 23, 23, 0.2);
  padding-left: 12px;
  margin: 12px 0;
}

.td-eli5-label {
  display: block;
  color: #3d3d3d;
  font-size: 11px;
  letter-spacing: 0.06em;
  margin-bottom: 6px;
}

.td-imgs {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: 12px 0;
}

.td-imgs img {
  height: 130px;
  width: auto;
  flex: 0 0 auto;
  border: 1px solid rgba(23, 23, 23, 0.15);
}

/* `.panel p` outranks a bare `.ticker-note`, so this needs the extra
   specificity to land at all. */
.panel p.ticker-note {
  margin-top: 18px;
  font-size: 12px;
  color: #3d3d3d;
}

/* ---- menu toggle button ---- */
/* ---- glass controls (crescent + sound) ----
   "Glass" here is EDGE + BLUR, not a see-through fill. These sit over a live
   video whose luminance swings frame to frame, so a translucent white fill
   (the obvious reading of the look) disappears entirely on a bright frame.
   The fill therefore stays a near-opaque luminance anchor and the glass reads
   through the backdrop blur, the hairline stroke and the inset top highlight.
   Both buttons share the recipe so they look like one system. */
.glass-btn {
  position: fixed;
  bottom: max(20px, env(safe-area-inset-bottom));
  z-index: 40;
  width: 45px;
  height: 45px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The map's drop circle is the SAME MATERIAL, not a lookalike. It used to be a
   simplified copy in map.css that was missing the Fresnel rim, the 300ms
   transitions and the reduced-transparency opt-out, so the map's circle and the
   site's circles were visibly two different objects sitting in one row. One
   recipe, both surfaces; .stk-glass keeps only its geometry in map.css. */
.glass-btn, .stk-glass {
  cursor: pointer;
  isolation: isolate;
  overflow: hidden;
  border: 0;

  /* Adaptive tint — a directional fill rather than one flat alpha, so the pane
     reads as lit from the top-left instead of as a uniform wash. */
  background: linear-gradient(
    155deg,
    rgba(255, 255, 255, 0.72) 0%,
    rgba(236, 236, 236, 0.58) 38%,
    rgba(190, 190, 190, 0.52) 100%
  );
  /* Scatter. Only ONE of Apple's four passes is reachable in CSS — true IOR
     refraction needs an SVG feDisplacementMap graph, which is GPU-hungry and
     Safari-fragile, so it is deliberately not used on a always-on control. */
  /* blur only. saturate() was in here to counter blur desaturation, but the
     footage behind these controls is black-and-white — saturating monochrome
     is a mathematical no-op, and it was costing a real filter pass per
     composited frame on two always-on elements sitting over playing video. */
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  /* Layered depth: top specular stroke, inner hairline, bottom Fresnel fade,
     contact shadow, ambient, float. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    inset 0 0 0 1px rgba(255, 255, 255, 0.10),
    inset 0 -1px 0 rgba(0, 0, 0, 0.12),
    0 1px 2px rgba(0, 0, 0, 0.30),
    0 6px 18px rgba(0, 0, 0, 0.26);
  transition: background 300ms ease, box-shadow 300ms ease;
}

/* Fresnel rim. Grazing-angle reflectance brightens the PERIMETER — this is the
   pass that separates Liquid Glass from ordinary frosted blur. Faked with a
   1px border-gradient punched out by mask-composite so only the ring paints.
   Honest limitation: real Fresnel is view-angle dependent; this only ever
   lights the top-left. */
.glass-btn::before, .stk-glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(135deg,
    rgba(255, 255, 255, 0.95) 0%,
    rgba(255, 255, 255, 0.55) 22%,
    rgba(255, 255, 255, 0.15) 48%,
    rgba(255, 255, 255, 0.06) 70%,
    rgba(255, 255, 255, 0.70) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}

/* No ambient specular sweep. There was a 9s looping highlight here; Apple's
   specular is coupled to INPUT (scroll, tilt, drag) — it is a response to the
   user, not a timer. An uncoupled shimmer on an idle control is motion nobody
   asked for and reads as a cheap web effect rather than a material. The static
   inset highlight in the box-shadow above carries the "lit from above" read. */

/* Degrade paths, shipped day one. Without backdrop-filter the fill is the ONLY
   thing separating the glyph from a moving video, so it goes near-opaque and
   the fake-optics layers are dropped rather than left floating. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass-btn, .stk-glass { background: rgba(236, 236, 236, 0.96); }
  .glass-btn::before, .stk-glass::before,
  .glass-btn::after { display: none; }
}

/* Apple pulled back its own beta glass opacity after contrast complaints —
   translucency is exactly what drops text below 4.5:1. Honour the OS opt-out. */
@media (prefers-reduced-transparency: reduce) {
  .glass-btn, .stk-glass {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(236, 236, 236, 0.97);
  }
  .glass-btn::after { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .glass-btn::after { animation: none; }
}

.glass-btn:hover, .stk-glass:hover {
  background: linear-gradient(155deg,
    rgba(255, 255, 255, 0.82) 0%,
    rgba(226, 226, 226, 0.68) 38%,
    rgba(180, 180, 180, 0.60) 100%);
}

.glass-btn:focus-visible, .stk-glass:focus-visible {
  outline: 2px solid #171717;
  outline-offset: 3px;
}

#menu-btn {
  left: 50%;
  transform: translateX(-50%);
  padding: 6px;
}

#menu-btn img {
  width: 100%;
  height: 100%;
  display: block;
  transition: filter 300ms ease;
}

/* One honest boolean: pale = nothing is open, dark = something is open and
   this control closes it. Previously only panel-open inverted, so the
   menu-open state was pixel-identical to home while the button's behaviour had
   already flipped — a mode error with no signal. A third mid-grey was rejected:
   three greys are not reliably distinguishable at 45px over moving video. */
body.menu-open #menu-btn,
body.panel-open #menu-btn,
body.map-open .stk-drop {
  background: linear-gradient(155deg,
    rgba(64, 64, 64, 0.78) 0%,
    rgba(23, 23, 23, 0.74) 40%,
    rgba(8, 8, 8, 0.80) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.28),
    inset 0 0 0 1px rgba(255, 255, 255, 0.06),
    inset 0 -1px 0 rgba(0, 0, 0, 0.5),
    0 1px 2px rgba(0, 0, 0, 0.5),
    0 6px 20px rgba(0, 0, 0, 0.45);
}

/* Dark glass needs its own rim ramp — the pale one's near-white stops would
   blow out against an ink fill and lose the Fresnel read entirely. */
body.menu-open #menu-btn::before,
body.panel-open #menu-btn::before,
body.map-open .stk-drop::before {
  background: linear-gradient(135deg,
    rgba(255, 255, 255, 0.60) 0%,
    rgba(255, 255, 255, 0.28) 22%,
    rgba(255, 255, 255, 0.08) 48%,
    rgba(255, 255, 255, 0.03) 70%,
    rgba(255, 255, 255, 0.40) 100%);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.menu-open #menu-btn,
  body.panel-open #menu-btn,
  body.map-open .stk-drop { background: rgba(23, 23, 23, 0.95); }
}

@media (prefers-reduced-transparency: reduce) {
  body.menu-open #menu-btn,
  body.panel-open #menu-btn,
  body.map-open .stk-drop { background: rgba(23, 23, 23, 0.97); }
}

body.menu-open #menu-btn:hover,
body.panel-open #menu-btn:hover,
body.map-open .stk-drop:hover {
  background: linear-gradient(155deg,
    rgba(84, 84, 84, 0.82) 0%,
    rgba(46, 46, 46, 0.78) 40%,
    rgba(20, 20, 20, 0.84) 100%);
}

body.menu-open #menu-btn img,
body.panel-open #menu-btn img {
  filter: invert(1);
}

/* the camera flips with them: #ededed is the exact ink the speaker wears on dark glass */
body.map-open .stk-drop { color: #ededed; }

/* ---- sound toggle ---- */
/* Layout + glyph colour only; the glass recipe comes from .glass-btn. */
#sound-btn {
  right: max(20px, env(safe-area-inset-right));
  padding: 12px;
  color: #303030;
}

/* Both controls darken together when anything opens. NOT so the mute button
   can signal state — its state is the icon — but because opening the menu
   raises a white frosted overlay, so the backdrop behind BOTH buttons flips
   from near-black video to pale frost, and a pale disc on a pale field loses
   its edge. The material adapts to what is behind it. Keeping one pale while
   the other inverted also read as a bug, being a visually matched pair. */
body.menu-open #sound-btn,
body.panel-open #sound-btn {
  background: linear-gradient(155deg,
    rgba(64, 64, 64, 0.78) 0%,
    rgba(23, 23, 23, 0.74) 40%,
    rgba(8, 8, 8, 0.80) 100%);
  /* The glyph is currentColor — it MUST flip or it disappears into the fill. */
  color: #ededed;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.28),
    inset 0 0 0 1px rgba(255, 255, 255, 0.06),
    inset 0 -1px 0 rgba(0, 0, 0, 0.5),
    0 1px 2px rgba(0, 0, 0, 0.5),
    0 6px 20px rgba(0, 0, 0, 0.45);
}

body.menu-open #sound-btn::before,
body.panel-open #sound-btn::before {
  background: linear-gradient(135deg,
    rgba(255, 255, 255, 0.60) 0%,
    rgba(255, 255, 255, 0.28) 22%,
    rgba(255, 255, 255, 0.08) 48%,
    rgba(255, 255, 255, 0.03) 70%,
    rgba(255, 255, 255, 0.40) 100%);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.menu-open #sound-btn,
  body.panel-open #sound-btn { background: rgba(23, 23, 23, 0.95); }
}

@media (prefers-reduced-transparency: reduce) {
  body.menu-open #sound-btn,
  body.panel-open #sound-btn { background: rgba(23, 23, 23, 0.97); }
}

body.menu-open #sound-btn:hover,
body.panel-open #sound-btn:hover {
  background: linear-gradient(155deg,
    rgba(84, 84, 84, 0.82) 0%,
    rgba(46, 46, 46, 0.78) 40%,
    rgba(20, 20, 20, 0.84) 100%);
}

#sound-btn svg {
  width: 100%;
  height: 100%;
  display: block;
}

#sound-btn svg[hidden] {
  display: none;
}

/* ---- FX mixer ---- */
/* Canvas mirrors the #bg-video plate exactly; it only exists while a look is
   active, so the base experience pays zero GPU. */
#fx-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  pointer-events: none;
  background: #000;
}

#fx-btn {
  left: max(20px, env(safe-area-inset-left));
  padding: 12px;
  color: #303030;
}

#fx-panel {
  position: fixed;
  left: max(20px, env(safe-area-inset-left));
  bottom: calc(max(20px, env(safe-area-inset-bottom)) + 58px);
  z-index: 41;
  width: min(260px, calc(100vw - 40px));
  padding: 14px 14px 12px;
  border-radius: 14px;
  background: linear-gradient(155deg,
    rgba(20, 20, 22, 0.82) 0%,
    rgba(20, 20, 22, 0.90) 100%);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.12),
    0 6px 18px rgba(0, 0, 0, 0.4);
  font: 600 10px/1 ui-monospace, Consolas, monospace;
  letter-spacing: 0.14em;
  color: #cfcfd8;
}

.fx-label {
  margin: 10px 0 6px;
  color: #6b6b74;
  font-size: 9px;
  letter-spacing: 0.22em;
}
.fx-label:first-child { margin-top: 0; }

.fx-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.fx-presets button {
  font: inherit;
  color: #cfcfd8;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  padding: 6px 9px;
  cursor: pointer;
}
.fx-presets button.active,
.fx-presets button[aria-pressed="true"] {
  background: #e6e6ea;
  color: #131316;
  border-color: #e6e6ea;
}
.fx-presets button:focus-visible {
  outline: 2px solid #e6e6ea;
  outline-offset: 1px;
}

.fx-row {
  display: grid;
  grid-template-columns: 52px 1fr;
  align-items: center;
  gap: 8px;
  margin: 7px 0;
}

.fx-row input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.22);
  cursor: pointer;
}
.fx-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #e6e6ea;
  border: 0;
}
.fx-row input[type="range"]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #e6e6ea;
  border: 0;
}

/* FX panel grew sliders; keep it scrollable on short viewports. */
#fx-panel {
  max-height: calc(100svh - 120px);
  overflow-y: auto;
}
.fx-row { grid-template-columns: 56px 1fr; }

/* The hidden attribute's UA display:none loses to .glass-btn's display:flex —
   make it win, or prod shows a dead FX button (caught in the 8/2 audit). */
.glass-btn[hidden] { display: none; }

/* Icon truth follows the button state — immune to the SVG hidden-property
   trap that kept the speaker icon frozen (2026-08-03). */
#sound-btn[aria-pressed="true"] #icon-muted { display: none; }
#sound-btn[aria-pressed="true"] #icon-sound { display: inline; }
#sound-btn[aria-pressed="false"] #icon-sound { display: none; }
#sound-btn[aria-pressed="false"] #icon-muted { display: inline; }

/* ---- entry door ----
   Full-bleed black door over everything. The tap that opens it is the
   autoplay gesture, so the room always opens WITH sound. */
#enter-gate {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  transition: opacity 900ms cubic-bezier(0.65, 0, 0.35, 1);
}
#enter-gate.leaving { opacity: 0; pointer-events: none; }
/* The door's content steps out first (during the crescent pulse), so the
   backdrop reads as a lifting veil, not a vanishing UI. */
#enter-gate.tapped #enter-btn {
  opacity: 0;
  transition: opacity 500ms ease 250ms;
}
#enter-gate[hidden] { display: none; }
#enter-btn {
  background: none;
  border: 0;
  cursor: pointer;
  color: #e6e6ea;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 40px;
}
.enter-crescent { display: block; }
.enter-title {
  font: 700 22px/1 ui-monospace, Consolas, monospace;
  letter-spacing: 0.5em;
  margin-left: 0.5em; /* optically recenters the tracked-out text */
}
.enter-hint {
  font: 600 10px/1 ui-monospace, Consolas, monospace;
  letter-spacing: 0.22em;
  color: #6b6b74;
}
/* Device-true verb: TAP on touch, CLICK everywhere else. */
.enter-hint .hint-tap { display: none; }
@media (hover: none) and (pointer: coarse) {
  .enter-hint .hint-click { display: none; }
  .enter-hint .hint-tap { display: inline; }
}
#enter-btn:focus-visible { outline: 2px solid #e6e6ea; outline-offset: 8px; }

/* Terminal typing ellipsis on the door: . -> .. -> ... -> blank, cycling
   like a shell working. Dots track tighter than the title so they read as
   an ellipsis, not spaced letters. */
.enter-cursor { letter-spacing: 0.12em; }
.enter-cursor span {
  opacity: 0;
  animation: enter-dot 2s steps(1, end) infinite;
}
.enter-cursor span:nth-child(1) { animation-name: enter-dot-1; }
.enter-cursor span:nth-child(2) { animation-name: enter-dot-2; }
.enter-cursor span:nth-child(3) { animation-name: enter-dot-3; }
/* Full pop cycle (owner 2026-08-03: the first dot must NOT be permanent):
   blank (0.35s) -> . -> .. -> ... -> all vanish, repeat. Irregular steps so
   it breathes like a shell, not a metronome. */
@keyframes enter-dot-1 { 0%, 14% { opacity: 0; } 15%, 100% { opacity: 1; } }
@keyframes enter-dot-2 { 0%, 49% { opacity: 0; } 50%, 100% { opacity: 1; } }
@keyframes enter-dot-3 { 0%, 69% { opacity: 0; } 70%, 100% { opacity: 1; } }

/* ---- circular edge fade (element-space) ----
   The soft circle lives ON the video/canvas as a mask, not on a viewport
   overlay (owner 2026-08-03: the old #edge-fade div darkened the page, not
   the video's edges — they only lined up when the video filled the screen).
   As a mask it tracks the frame at every size/orientation, and revealing
   the #0b0b0d page beneath is the same color the overlay used to paint. */
/* No edge treatment on the video (owner 2026-08-03: every feather/vignette
   attempt read as black edges — the frame ships clean, full-strength). The
   contain bars and page share #0b0b0d, so the resting boundary is already
   color-quiet without any fade. */
#edge-fade { display: none; }


/* Nav-hint animation REMOVED (owner 2026-08-03: "the crescent moves in the
   first 10 seconds"). Motion audit showed the keyframes' transform also
   stomped the button's translateX(-50%) centering — a 22.5px side-jump on
   top of the breath. The controls must be rock-still, always. */
@media (prefers-reduced-motion: reduce) {
  .enter-cursor span { animation: none; opacity: 0.6; }
}

/* Tap registered: one crescent micro-pulse (never repeats). */
#enter-gate.tapped .enter-crescent {
  animation: enter-pulse 600ms cubic-bezier(0.3, 0, 0.4, 1) 1;
}
@keyframes enter-pulse {
  0% { transform: scale(1); }
  30% { transform: scale(1.06); }
  100% { transform: scale(1); }
}

/* Threshold: the concert breathes up slowly UNDER the dissolving gate —
   a long cinematic fade that overlaps the veil, never a pop after it. */
#bg-video, #fx-canvas { transition: opacity 1100ms ease; }
body.entering #bg-video,
body.entering #fx-canvas { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  #enter-gate.tapped .enter-crescent { animation: none; }
}

/* Kill mobile double-tap zoom on the controls: tapping the crescent then a
   link read as a double-tap and pumped the viewport in/out. manipulation
   keeps taps instant and un-zoomable; pinch-zoom elsewhere unaffected. */
button, a, .glass-btn, #nav, #enter-gate {
  touch-action: manipulation;
}


/* Song timecode (review aid): top-right, unmissable but quiet. */
#vtime {
  position: fixed;
  top: max(16px, env(safe-area-inset-top));
  right: max(16px, env(safe-area-inset-right));
  z-index: 45;
  font: 600 13px/1 ui-monospace, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  color: rgba(230, 230, 234, 0.85);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}


/* Portrait: a LITTLE wide lens (owner) - the frame renders at 85% screen
   height instead of full-bleed, pulling the crop back ~15%; the slim bars
   melt into the edge fade. Scene windows in app.js still steer the ~3
   off-center passages. */
@media (orientation: portrait) {
  #bg-video, #fx-canvas {
    top: 50%;
    bottom: auto;
    height: 85svh;
    max-height: 85svh;
    transform: translateY(-50%) translateZ(0);
    -webkit-transform: translateY(-50%) translateZ(0);
    /* Edge dissolve comes from the global element-space radial mask. */
  }
}


/* Landscape = the FULL film frame, always (owner 2026-08-03: cover was
   clipping the widths on non-16:9 monitors). On an exact 16:9 screen
   contain == cover, so the common case is untouched; 16:10 / ultrawide /
   3:2 gain the cropped content back and the frame's edges melt into the
   dark via the shader feather + edge fade. Portrait keeps cover+windows. */
@media (orientation: landscape) {
  #bg-video { object-fit: contain; }
}




/* ---- WORLD MAP boot shim (app.js paints this before the lazy map chunk lands) ---- */
#map-boot {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 18px;
  background: #000;
}
#map-boot svg { animation: mapboot-breathe 2.2s ease-in-out infinite; }
#map-boot p {
  font-family: ui-monospace, 'Consolas', 'SFMono-Regular', Menlo, monospace;
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #6b6b74;
}
@keyframes mapboot-breathe {
  0%, 100% { opacity: 0.35; transform: scale(0.96); }
  50% { opacity: 0.9; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { #map-boot svg { animation: none; } }

/* ---- THE MAP AS BACKDROP ----
   Opening LINKS (or any panel) from inside the world map must not drop you back
   onto the film — the world you were in stays behind the glass and is simply
   masked (owner 2026-08-04). #panel-map keeps rendering at z-20; the wash lifts
   to z-25 and the ACTIVE panel to z-28, so the stack reads:
     map (20)  <  frosted glass (25)  <  the open panel (28)  <  nav (30)  <  buttons (40) */
/* opacity:1 is the ON state for the unfiltered degrade path; under the filtered
   path opacity is pinned at 1 anyway and the tint/blur/visibility carry it, so
   this line is inert there rather than re-arming the filter-drop. */
body.map-backdrop #overlay {
  z-index: 25;
  opacity: 1;
  background: rgba(255, 255, 255, 0.66);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  visibility: visible;
}
body.map-backdrop .panel:not(#panel-map):not([hidden]) { z-index: 28; }
/* the backdrop is scenery only — nothing in it may take a tap or a tab stop */
body.map-backdrop #panel-map { pointer-events: none; }
