/* =========================================================================
   Kventin Shipulin — spatial deck
   Physical pages: each screen carries its own edges and slides as one unit.
   ========================================================================= */

@font-face { font-family: "Suisse Intl"; font-weight: 400; font-style: normal; font-display: swap; src: url("/assets/fonts/suisse-regular.woff2") format("woff2"); }
@font-face { font-family: "Suisse Intl"; font-weight: 500; font-style: normal; font-display: swap; src: url("/assets/fonts/suisse-medium.woff2") format("woff2"); }
@font-face { font-family: "Suisse Intl"; font-weight: 700; font-style: normal; font-display: swap; src: url("/assets/fonts/suisse-bold.woff2") format("woff2"); }

:root {
  --bone: #f0ebe0;
  --home: #040404;
  --move: 300ms;
  --ease: cubic-bezier(.4, 0, .2, 1);
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}
/* On phones/tablets iOS Safari reports a 0 inset while its toolbar is expanded (and the
   deck never scrolls to collapse it), so guarantee a minimum to cover the chrome zones. */
@media (hover: none) and (pointer: coarse) {
  :root { --safe-top: max(env(safe-area-inset-top), 59px); --safe-bottom: max(env(safe-area-inset-bottom), 34px); }
}

* { box-sizing: border-box; }
html, body {
  height: 100%; margin: 0; overflow: hidden;
  background: var(--home); color: var(--bone);
  font-family: "Suisse Intl", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 16px; line-height: 1.55;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
  -webkit-tap-highlight-color: transparent;
  /* app-like: long-press opens the map, so suppress iOS's selection magnifier + callout */
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
/* absolute, not fixed: the page never scrolls here (html/body overflow hidden), so it's the same box —
   but Safari 26 (macOS + iOS) treats a fixed element on the top edge as a header, samples its colour
   once and keeps it, which froze the toolbar on the first page's colour. (Phones: html.flow below.) */
#app { position: absolute; top: 0; left: 0; right: 0; height: 100dvh; }
/* Solid dark strips over the status-bar / home-indicator zones. Safari samples its
   chrome colour from the page's edges (once, at load), so painting these zones dark
   gives a consistent dark toolbar on every page. Edge labels are nudged clear below. */
.safecap { position: fixed; left: 0; right: 0; z-index: 39; background: var(--home); pointer-events: none;
  transition: background-color var(--move) var(--ease), opacity .12s var(--ease); }   /* below the map (40); anchors iOS chrome colour per page */
#app.dragging .safecap { opacity: 0; }   /* during a swipe the screens' own bg fills the notch, so it slides cleanly */
.safecap--top { top: 0; height: var(--safe-top); }
.safecap--bottom { bottom: 0; height: var(--safe-bottom); }

/* ---------- Stars (a fixed layer behind the deck; shows through the home page) ---------- */
.stars { position: fixed; inset: 0; z-index: 0; background: var(--home); pointer-events: none; }
.stars canvas { width: 100%; height: 100%; display: block; }

/* ---------- Deck / screens ---------- */
.stage { position: absolute; inset: 0; overflow: hidden; z-index: 1; }
.deck  { position: absolute; inset: 0; }
.screen { position: absolute; inset: 0; transform: translate(0, 0); will-change: transform; }
.screen.is-anim { transition: transform var(--move) var(--ease); }
.screen__scroll {
  position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden;
  background: var(--world, #170505); color: var(--fg, #f0ebe0); scrollbar-width: none;
  transition: background-color .4s var(--ease);
}
.screen--home .screen__scroll { background: transparent; }   /* let the stars show through */
.screen__scroll::-webkit-scrollbar { display: none; }
/* Film grain that belongs to the page: it rides inside .screen, so it slides with
   the page on navigation and never sits as a static layer over the viewport.
   Outside the scroll container, so it doesn't repaint while content scrolls. */
.screen__grain {
  position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: .16;
  mix-blend-mode: soft-light;   /* a whisper of tooth over the colour — never crunchy or over content */
  /* Mid-frequency monochrome noise with a hair of blur: a subtle paper tooth, not
     razor-sharp deep-fried static; low opacity keeps it off the artwork and text. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='2' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeGaussianBlur stdDeviation='0.22'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 200px 200px;
}
.screen--home .screen__grain { display: none; }   /* home already has the starfield */
.screen__edges { position: absolute; inset: 0; pointer-events: none; z-index: 2; }

.inner {
  min-height: 100%; width: 100%; max-width: min(720px, 92vw); margin-inline: auto;
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(26px, 4vh, 44px);
  padding: calc(clamp(60px, 8vh, 104px) + var(--safe-top)) clamp(24px, 4vw, 44px) calc(clamp(60px, 8vh, 104px) + var(--safe-bottom));
}

/* one centred column per page — heading, prose, then media */
.copy { display: grid; gap: clamp(18px, 2.6vh, 32px); text-align: left; min-width: 0; }
.copy > * { min-width: 0; }

/* home hub — the copy block is centred on the page, but the text reads left-aligned */
.inner.hero { align-items: center; }
.hero__box { text-align: left; }
.hero__box .sub { margin-top: 4px; }
.hero__box .hint { margin-top: 24px; line-height: 1.9; }
/* the 404's way back — a plain, obvious button (this page carries no edge labels) */
.cta { margin: 4px 0 0; }
.gohome { display: inline-flex; align-items: center; gap: 9px; padding: 13px 20px; background: var(--bone); color: var(--home);
  font-family: inherit; font-size: 15px; font-weight: 700; letter-spacing: .01em; text-decoration: none; cursor: pointer;
  transition: filter .2s var(--ease), transform .2s var(--ease); }
.gohome::before { content: "◀︎"; font-size: 10px; opacity: .85; }
.gohome:hover { filter: brightness(1.08); }
.gohome:active { transform: translateY(1px); }

/* app pages: the icon sits beside the title + tagline */
.apphead { display: flex; align-items: center; margin-bottom: clamp(6px, 1.2vh, 14px); gap: clamp(18px, 2vw, 28px); }
.apphead__t { min-width: 0; }
.apphead .sub { margin-top: 6px; }
.appicon { flex: none; display: block; width: 104px; height: 104px; filter: drop-shadow(0 10px 20px rgba(0, 0, 0, .32)) drop-shadow(0 2px 4px rgba(0, 0, 0, .2)); }
.appicon img { display: block; width: 100%; height: 100%; }
@media (max-width: 520px) { .appicon { width: 76px; height: 76px; } }

/* "Available on TestFlight" badge */
.tf { display: inline-flex; align-items: center; gap: 13px; padding: 10px 24px 10px 12px; border-radius: 16px;
  background: #000; color: #fff; text-decoration: none; cursor: pointer; -webkit-tap-highlight-color: transparent;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .28), 0 6px 18px rgba(0, 0, 0, .28);
  transition: transform .2s var(--ease), box-shadow .2s var(--ease); }
.tf:hover { transform: translateY(-2px); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5), 0 10px 24px rgba(0, 0, 0, .34); }
.tf:active { transform: translateY(0); }
.tf:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
.tf__icon { flex: none; width: 44px; height: 44px; display: block; }
.tf__txt { display: grid; line-height: 1; text-align: left; }
.tf__small { font-size: 13px; font-weight: 500; letter-spacing: .01em; opacity: .85; }
.tf__big { font-size: 25px; font-weight: 700; letter-spacing: -.015em; margin-top: 3px; }

.keycap {
  display: inline-block; min-width: 1.7em; padding: 2px 7px 3px; margin-right: 3px;
  border-radius: 6px; background: var(--bone); color: var(--home);
  font-weight: 700; font-size: .92em; line-height: 1; text-align: center; vertical-align: baseline;
  border-bottom: 2px solid rgba(23, 5, 5, .28);   /* subtle keycap lip, not a drop shadow */
}
.screen--home .inner { padding: clamp(56px, 8vh, 96px) clamp(36px, 5vw, 80px); }
@media (min-width: 620px) { .hero .title { white-space: nowrap; } }

/* artwork-only pages (play) — one big centred piece */
.inner.solo { align-items: center; justify-content: center; padding: calc(clamp(64px, 9vh, 116px) + var(--safe-top)) clamp(56px, 8vw, 120px) calc(clamp(64px, 9vh, 116px) + var(--safe-bottom)); }

/* text + images: on desktop the images move into a second column on the right */
@media (min-width: 900px) {
  .inner--cols {
    max-width: min(1140px, 94vw);
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(40px, 5vw, 84px); align-content: center; align-items: center;
  }
  .inner--cols .copy { text-align: left; }
  .inner--cols .media { width: 100%; margin: 0; transform: none; }
  /* about photos on desktop: fanned polaroid stack, portrait on top */
  .inner--cols .about-figs { grid-template-columns: 1fr; gap: 0; place-items: center; padding: 9% 6%; }
  .inner--cols .about-figs figure { grid-area: 1 / 1; width: 76%; }
  .inner--cols .about-figs figure:nth-child(1) { transform: rotate(-5deg) translate(-13%, -3%); z-index: 2; }   /* me */
  .inner--cols .about-figs figure:nth-child(2) { transform: rotate(5.5deg) translate(13%, 5%); z-index: 1; }    /* behind */
}

/* ---------- Typography ---------- */
.title   { font-size: clamp(34px, 3.6vw, 54px); font-weight: 700; letter-spacing: -.02em; line-height: 1.08; margin: 0; overflow-wrap: anywhere; }
.title--sec { font-size: clamp(26px, 2.6vw, 36px); letter-spacing: -.01em; }
/* a heading for screen readers and search engines on pages that show artwork alone */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.hero .title { font-size: clamp(36px, 4.1vw, 58px); }
.sub     { font-size: clamp(18px, 1.6vw, 22px); opacity: .85; margin: 14px 0 0; font-weight: 500; }
.body    { font-size: clamp(18px, 1.35vw, 20.5px); line-height: 1.62; margin: 0; font-weight: 400; }
.body p  { margin: 0 0 1em; } .body p:last-child { margin: 0; }
.hint    { font-size: 15px; font-weight: 700; margin-top: 26px; opacity: .9; letter-spacing: .01em; }
.k       { font-weight: 700; }
.list    { list-style: none; margin: 18px 0 0; padding: 0; }
.list li { padding: 14px 0; border-top: 1px solid currentColor; font-size: 17.5px; font-weight: 400; display: flex; gap: 15px; opacity: .96; }
.list li:first-child { border-top: 0; }
.list .n { flex: none; width: 1.4em; opacity: .58; font-variant-numeric: tabular-nums; font-weight: 700; }
.listTitle { font-size: clamp(18px, 1.35vw, 20px); font-weight: 700; margin: 0; }

/* doc blocks — FAQ / policy sections (question or heading, then answer) */
.doc { display: grid; gap: clamp(16px, 2.2vh, 24px); margin-top: 22px; }
.doc__q { font-weight: 700; font-size: clamp(16px, 1.3vw, 18.5px); margin: 0 0 5px; }
.doc__a { font-size: clamp(16px, 1.15vw, 17.5px); line-height: 1.55; opacity: .84; margin: 0 0 .5em; font-weight: 400; }
.doc__a:last-child { margin: 0; }
.body a, .doc a { color: inherit; text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }

/* ---------- Sound toggle (JS adds it only to videos that carry audio) ---------- */
.media figure:has(video) { display: inline-block; position: relative; }
.snd {
  position: absolute; right: 12px; bottom: 12px; z-index: 4;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0; border: 0; cursor: pointer;
  background: rgba(11, 11, 14, .5); color: #fff;
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  opacity: 0; transition: opacity .35s var(--ease), filter .2s var(--ease);
}
.snd.ready { opacity: .8; }
.snd:hover { opacity: 1; filter: brightness(1.14); }
.snd:active { filter: brightness(.92); }
.snd:focus-visible { outline: 2px solid #fff; outline-offset: 2px; opacity: 1; }
.snd svg { width: 22px; height: 22px; display: block; }
.snd .snd__on { display: none; }
.snd.is-on .snd__on { display: block; }
.snd.is-on .snd__off { display: none; }
/* while zooming, keep the control on the focused piece usable */
.screen.zooming .zoom-focus .snd { opacity: 1; }

/* ---------- Media (breaks out of the text column to a centred band) ---------- */
.media { width: min(90vw, 900px); margin: 0 auto; margin-left: 50%; transform: translateX(-50%); text-align: center; }
.media figure { margin: 0; }
.media img, .media video { display: inline-block; width: auto; max-width: 100%; max-height: 64vh; cursor: zoom-in; }
.media figcaption { margin-top: 12px; font-size: 13.5px; letter-spacing: .03em; opacity: .72; }
.solo .media img, .solo .media video { max-height: 74vh; }
.media.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: clamp(16px, 2vw, 26px); align-items: start; }
.media.gallery img, .media.gallery video { display: block; width: 100%; height: auto; max-height: 52vh; object-fit: contain; }
/* Work case-study galleries: container-less. One big piece at a time, centred on
   the bare plane with generous space — the same clean treatment as Play artwork. */
.media.gallery:not(.about-figs) {
  display: flex; flex-direction: column; align-items: center;
  width: min(90vw, 900px); gap: clamp(56px, 11vh, 110px);
}
.media.gallery:not(.about-figs) figure { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 16px; }
.media.gallery:not(.about-figs) img,
.media.gallery:not(.about-figs) video {
  display: block; width: auto; max-width: 100%; height: auto; max-height: 74vh; object-fit: contain;
}

/* Device spread: three devices start stacked on the middle one, then slide apart
   into an even row — no overlap, no rotation — when scrolled into view. */
.media.fans { display: flex; flex-direction: column; align-items: center; gap: clamp(56px, 11vh, 120px); width: min(92vw, 1000px); }
.fan { display: flex; justify-content: center; align-items: flex-end; gap: var(--gap); --gap: 11px; }
.fan figure { margin: 0; will-change: transform; transition: transform 1.5s cubic-bezier(.16, 1, .3, 1); }
.fan img, .fan video { display: block; width: auto; height: auto; max-height: 30vh; max-width: 24vw; }
.fan figure:nth-child(2) { position: relative; z-index: 3; }   /* middle sits on top while stacked */
/* collapsed (pre-reveal): the outer two rest exactly on the middle one */
.fan:not(.in) figure:nth-child(1) { transform: translateX(calc(100% + var(--gap))); }
.fan:not(.in) figure:nth-child(3) { transform: translateX(calc(-100% - var(--gap))); }
@media (min-width: 620px) { .fan { --gap: 26px; } .fan img, .fan video { max-height: 40vh; max-width: 23vw; } }
@media (min-width: 900px) { .fan { --gap: 36px; } .fan img, .fan video { max-height: 46vh; max-width: 19vw; } }
@media (prefers-reduced-motion: reduce) { .fan figure { transition: none; } }

/* Figma prototype — interactive, no Figma chrome. The prototype already carries its
   own iPhone device frame, so match the iframe to that aspect and clip the corners so
   the embed's black canvas reads as the phone's bezel rather than a box on the plane. */
/* The Figma canvas is set to the world green (bg-color), so the whole prototype floats
   on the plane with no box. Grain is dropped on these pages so the flat green matches. */
.media.proto { width: 100%; margin-inline: auto; transform: none; text-align: center; }
.phone { height: min(82dvh, 880px); aspect-ratio: 452 / 1000; max-width: 86vw; margin: 0 auto; overflow: hidden; }
.phone__screen { width: 100%; height: 100%; }
.phone__screen iframe { width: 100%; height: 100%; border: 0; display: block; }
.screen:has(.media.proto) .screen__grain { display: none; }
/* prototype pages: trim the page padding so the enlarged phone still clears the edges */
.screen:has(.media.proto) .inner {
  padding-top: calc(clamp(20px, 3vh, 40px) + var(--safe-top));
  padding-bottom: calc(clamp(20px, 3vh, 40px) + var(--safe-bottom));
  gap: clamp(14px, 2vh, 22px);
}

/* about photos — default (mobile): a vertical stack with the portrait first. Kept a touch under
   half the viewport tall so a whole polaroid sits in view between the chrome (never clipped top
   AND bottom at once), and wider so it uses the space instead of floating with fat side margins. */
/* about photos — the same fanned stack as desktop (see .inner--cols above): both polaroids share one
   grid cell, the portrait tilted left and on top, the other tucked behind it. The padding leaves room
   for the tilt so no corner is clipped. */
.media.about-figs { width: min(440px, 92vw); grid-template-columns: 1fr; gap: 0; place-items: center; padding: 7% 7% 15%; }
.media.about-figs figure { grid-area: 1 / 1; width: 84%; margin: 0; }
.media.about-figs figure:nth-child(1) { transform: rotate(-5deg) translate(-11%, -4%); z-index: 2; }   /* me */
.media.about-figs figure:nth-child(2) { transform: rotate(5.5deg) translate(11%, 6%); z-index: 1; }    /* behind */
.media.gallery.about-figs img { max-height: 42svh; width: 100%; object-fit: contain; }
.media.embed img, .media.embed video, .embed__frame iframe { cursor: auto; }
.media.embed { width: min(90vw, 760px); }
.media.embed--audio { width: min(90vw, 400px); }
.embed__frame { position: relative; width: 100%; }
.embed__frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; border-radius: 12px; }

/* ---------- Zoom: the clicked piece scales up in place, the page dims, labels slide off ---------- */
.media figure { transition: transform .46s cubic-bezier(.34, 1.42, .5, 1), opacity .34s var(--ease); }
.screen.zooming { cursor: zoom-out; }
.screen.zooming .screen__scroll { background-color: color-mix(in srgb, var(--world, #170505), #000 60%); }
.screen.zooming .copy,
.screen.zooming .media figure:not(.zoom-focus) { opacity: .1; }
.screen.zooming .zoom-focus { position: relative; z-index: 5; transform: scale(1.32); transition-duration: .42s; }
/* Phones: zoom opens a true fullscreen lightbox — its own fixed layer (built in deck.js, above the
   breadcrumb) holding a copy of the piece. Not the figure itself turned position:fixed: iOS Safari
   keeps painting a formerly-fixed element's colour over its status bar + toolbar after it goes back
   to static, and pulling the figure out of the page shortened it and made the scroll jump. */
.lightbox { position: fixed; inset: 0; z-index: 50; display: none; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .94); padding: calc(var(--safe-top) + 16px) 14px calc(var(--safe-bottom) + 16px); cursor: zoom-out; }
.lightbox.on { display: flex; }
.lightbox img, .lightbox video { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }

/* ---------- Edge labels (slim; coloured by the page they lead to; travel with their page) ---------- */
.tab {
  position: absolute; pointer-events: auto; display: inline-flex; align-items: center; gap: 8px;
  border: 0; cursor: pointer; background: var(--tc, #333); color: #fff;
  font-family: inherit; font-size: 13.5px; font-weight: 700; letter-spacing: .04em;
  padding: 7px 13px; transition: filter .2s var(--ease), transform .46s var(--ease);
}
.tab__arrow { font-size: 10px; line-height: 1; opacity: .9; }
/* side tabs: keep the arrow an upright, correctly-pointing triangle (not rotated by the vertical text) */
.tab--left .tab__arrow, .tab--right .tab__arrow { writing-mode: horizontal-tb; }
.tab--left .tab__arrow { transform: rotate(180deg); }
.tab--top    { top: var(--safe-top);    left: 50%; transform: translateX(-50%); }
.tab--bottom { bottom: var(--safe-bottom); left: 50%; transform: translateX(-50%); }
.tab--left   { left: 0;   top: 50%;  transform: translateY(-50%) rotate(180deg); writing-mode: vertical-rl; padding: 13px 7px; }
.tab--right  { right: 0;  top: 50%;  transform: translateY(-50%); writing-mode: vertical-rl; padding: 13px 7px; }
.tab:hover  { filter: brightness(1.12); }
.tab:active { filter: brightness(.94); }
.tab:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* during zoom, the labels slide off their edge */
.screen.zooming .tab--top    { transform: translate(-50%, -160%); }
.screen.zooming .tab--bottom { transform: translate(-50%, 160%); }
.screen.zooming .tab--left   { transform: translate(-160%, -50%) rotate(180deg); }   /* keep the 180° so it slides, not spins */
.screen.zooming .tab--right  { transform: translate(160%, -50%); }

/* A label that leads to the same place on both the outgoing and incoming page
   (e.g. "home" up from every play collection) shouldn't ride the slide — a copy is
   pinned to this fixed layer so it holds still while the pages move underneath. */
.edges-fixed { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.edges-fixed .tab.tab--pinned { pointer-events: none; transition: none; }

/* home hub — same label style, but the edges run the full length of each side */
.screen--home .tab { --edge: clamp(38px, 4.4vh, 50px); }
.screen--home .tab--top,
.screen--home .tab--bottom { left: 0; right: 0; transform: none; width: 100%; height: var(--edge); justify-content: center; padding: 0; }
.screen--home .tab--right { top: var(--safe-top); bottom: calc(var(--edge) + var(--safe-bottom)); right: 0; transform: none; width: var(--edge); height: auto; justify-content: center; padding: 0; }        /* green overlaps blue (top-right) */
.screen--home .tab--left { top: calc(var(--edge) + var(--safe-top)); bottom: var(--safe-bottom); left: 0; transform: rotate(180deg); width: var(--edge); height: auto; justify-content: center; padding: 0; }   /* purple overlaps red (bottom-left) */

/* dead-direction nudge */
@keyframes nudge-x { 0%,100%{transform:translateX(0)} 50%{transform:translateX(var(--nx,0))} }
@keyframes nudge-y { 0%,100%{transform:translateY(0)} 50%{transform:translateY(var(--ny,0))} }
.deck.nudge-l { animation: nudge-x .2s var(--ease); --nx: 9px; }
.deck.nudge-r { animation: nudge-x .2s var(--ease); --nx: -9px; }
.deck.nudge-u { animation: nudge-y .2s var(--ease); --ny: 9px; }
.deck.nudge-d { animation: nudge-y .2s var(--ease); --ny: -9px; }

/* ---------- Map (M) ---------- */
.map { position: fixed; inset: 0; z-index: 40; background: rgba(11, 11, 14, .985); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); display: flex; align-items: safe center; justify-content: safe center; overflow: auto; opacity: 0; visibility: hidden; transition: opacity .3s var(--ease), visibility .3s; }
.map.open { opacity: 1; visibility: visible; }
/* Fixed design stage scaled to fit — never overlaps at any width */
.map__stage { position: relative; margin: auto; flex: none; }
.map__inner { position: absolute; top: 0; left: 0; width: 1180px; height: 560px; transform-origin: top left; transform: scale(var(--map-scale, 1)); }
.map svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.node { position: absolute; transform: translate(-50%, -50%); border: 1px solid var(--nc, #fff); background: var(--home); color: var(--bone); font-size: 10px; padding: 5px 8px; cursor: pointer; white-space: nowrap; transition: background .14s var(--ease), color .14s var(--ease); }
.node:hover, .node.here { background: var(--nc, #fff); color: var(--home); }
.node.here { font-weight: 700; }
.node--depth { font-size: 10px; opacity: .95; padding: 4px 9px; }
.map__hint { position: fixed; bottom: 18px; left: 0; right: 0; text-align: center; font-size: 10px; letter-spacing: .18em; text-transform: uppercase; opacity: .5; pointer-events: none; }

/* ---------- Map — phone: a flat, brutalist index grouped by world ---------- */
.map--m { display: block; overflow: hidden; }
.mmap__scroll { position: absolute; inset: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }   /* don't hand the scroll on to the page behind */
.mmap__close { position: absolute; top: calc(env(safe-area-inset-top) + 12px); right: 16px; z-index: 2;
  background: rgba(240, 235, 224, .1); color: var(--bone); border: 0; cursor: pointer;
  font-family: inherit; font-size: 15px; font-weight: 600; padding: 9px 16px; -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
.mmap__close:active { background: rgba(240, 235, 224, .2); }
.mmap { max-width: 540px; margin: 0 auto; display: flex; flex-direction: column; gap: 30px;
  padding: max(70px, calc(env(safe-area-inset-top) + 34px)) 26px calc(40px + env(safe-area-inset-bottom)); }
.mmap__grp { display: flex; flex-direction: column; }
/* group heading: world colour, underlined — the only chrome in the list */
.mmap__lbl { font-size: 14px; font-weight: 700; letter-spacing: -.005em;
  color: var(--c, #fff); padding-bottom: 10px; margin-bottom: 2px; border-bottom: 2px solid var(--c, #fff); }
/* rows: flat bold text on hairlines — no pills, no radius */
.mmap__row { display: block; width: 100%; text-align: left; cursor: pointer;
  background: none; border: 0; border-bottom: 1px solid rgba(240, 235, 224, .11);
  color: var(--bone); font-family: inherit; font-size: 20px; font-weight: 600; letter-spacing: -.01em;
  padding: 15px 4px; min-height: 54px; transition: color .14s var(--ease), background .14s var(--ease); }
.mmap__grp .mmap__row:last-child { border-bottom: 0; }
.mmap__row--sub { padding-left: 22px; font-size: 17px; font-weight: 400; opacity: .8; min-height: 46px; }
/* current location reads as one of the deck's solid colour tabs */
.mmap__row.here { background: var(--c, #fff); color: #0b0b0e; font-weight: 700; padding-inline: 12px; border-bottom-color: transparent; }
.mmap__row:active { color: #fff; }

/* ---------- Mobile chrome: one quiet tappable breadcrumb + map ---------- */
@media (hover: none) and (pointer: coarse) {
  :root { --move: 230ms; --ease: cubic-bezier(.22, .68, .28, 1); }   /* snappier, more satisfying swipe */
  .screen .screen__edges { display: none; }   /* no edge labels — the breadcrumb carries wayfinding */
  .edges-fixed { display: none; }
  .screen__grain { display: none; }            /* no grain on mobile: content stays flat and matches the chrome colour */
  /* Centred is the brand: every page that fits is centred (JS adds `.fits` — see applyFit), and
     cards / home / single-art always centre since they always fit. A page too tall to fit stays
     flex-start so it's fully scrollable from its top and never clips above. (JS, not CSS
     `safe center`, decides — that keyword silently degrades to plain `center` on iOS Safari.) */
  .inner { justify-content: flex-start; }
  .inner.fits, .inner--card, .screen--home .inner, .inner.solo { justify-content: center; }
  /* Symmetric top/bottom clearance (breadcrumb up top, Safari toolbar down bottom) so a centred
     page sits truly centred in the visible area. iOS keeps this fixed layout above the toolbar. */
  .inner, .inner.solo, .screen:has(.media.proto) .inner {
    padding-top: calc(var(--safe-top) + 62px);
    /* Scrolling pages run their content all the way to the bottom edge (full-bleed, like a normal
       webpage — the toolbar just floats over it), so no bottom gutter at all. */
    padding-bottom: 0;
  }
  /* A centred page that fits needs the bottom gutter back, symmetric with the top, to sit centred. */
  .inner.fits { padding-bottom: calc(var(--safe-bottom) + 62px); }
  /* a scrolling page with a page below it ends with the same gutter, so the "↓ next" hint sits under
     the content instead of over its last lines */
  .screen .screen__scroll .inner.inner--down { padding-bottom: calc(var(--safe-bottom) + 62px); }
  .screen--home .inner { padding-top: calc(var(--safe-top) + 52px); }
  /* Cap the prototype phone so title + phone + caption fit the toolbar-shortened viewport. */
  .phone { height: min(58svh, 600px); }
}
/* transparent bar: the page colour shows through and slides with the page — no mismatched strip.
   Fixed row height keeps "map" from shifting whether or not there's a breadcrumb. */
.mbar { position: fixed; top: 0; left: 0; right: 0; z-index: 30; color: var(--fg, #fff); background: none;
  padding-top: var(--safe-top); display: flex; align-items: center; justify-content: space-between; }
/* The breadcrumb stays put — it's just a quiet label of where you are. It updates its text only
   when the ancestors actually change (see updateBar), and never fades on drag, so it doesn't blink. */
.mbar__crumbs { display: flex; align-items: center; gap: 9px; min-width: 0; height: 48px; padding: 0 4px 0 20px;
  transition: opacity .2s var(--ease); }
/* the breadcrumb belongs at the top with the title — once you scroll into the body it tucks away
   (so it never sits over the text) and returns when you scroll back up. See syncEdges(). */
.mbar--tucked .mbar__crumbs { opacity: 0; }
.mbar__crumb { background: none; border: 0; padding: 0; color: inherit; font-family: inherit;
  font-size: 15px; font-weight: 500; letter-spacing: -.01em; opacity: .7; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mbar__crumb:active { opacity: 1; }
.mbar__sep { opacity: .38; font-weight: 400; }
.mbar__map { flex: none; background: none; border: 0; color: inherit; font-family: inherit;
  font-size: 15px; font-weight: 600; height: 48px; padding: 0 20px; cursor: pointer; opacity: .85; }
.mbar__map:active { opacity: .5; }
/* while dragging OR scrolling, the edge indicators fade (they overlap the moving/scrolling
   content); the breadcrumb sits clear at the top and stays put, so it doesn't flash on swipe */
#app.dragging .mdir__g, #app.scrolling .mdir__g { opacity: 0; }
/* …except the down-hint once you've reached the bottom: it shows the moment you arrive (syncDownEdge
   clears .edge-off on the scroll that lands there) instead of waiting for the scroll to settle */
#app.scrolling .mdir__down.on:not(.edge-off) { opacity: .72; }
/* down-indicator stays hidden until you've scrolled to the bottom (see syncDownEdge) */
#app .mdir__down.edge-off { opacity: 0; }
/* edge indicators — a small labelled chevron wherever a swipe leads (white; side labels run vertically like the desktop edges) */
.mdir__g { position: fixed; z-index: 25; display: flex; align-items: center; gap: 7px; pointer-events: none;
  color: var(--fg, #fff); opacity: 0; transition: opacity .22s var(--ease); }
.mdir__g.on { opacity: .72; }
.mdir__t { font-size: 12px; font-weight: 600; letter-spacing: -.01em; white-space: nowrap; }
.mdir__c { width: 8px; height: 8px; flex: none; border: 0 solid currentColor; border-right-width: 2px; border-bottom-width: 2px; }
/* The chevron is an 8px box drawn on its right+bottom edges only, so its ink sits 1.3px toward that
   corner, not in the middle of the box — rotated, that pushed each arrow off its label's centre line
   (visibly on the vertical side labels). Every rotation below cancels it with the same shift, applied
   in the chevron's own axes, so the ink lands dead centre whichever way it points. */
.mdir__down  { left: 50%; bottom: calc(var(--safe-bottom) + 13px); transform: translateX(-50%); flex-direction: column; gap: 5px; }
.mdir__down .mdir__c  { transform: rotate(45deg) translate(-1.3px, -1.3px); }
.mdir__left, .mdir__right { top: 50%; transform: translateY(-50%); flex-direction: column; gap: 9px; max-height: 60vh; }
.mdir__left  { left: 9px; }
.mdir__right { right: 9px; }
.mdir__left .mdir__t, .mdir__right .mdir__t { writing-mode: vertical-rl; letter-spacing: .03em; }
.mdir__left .mdir__t  { transform: rotate(180deg); }
.mdir__left .mdir__c  { transform: rotate(135deg) translate(-1.3px, -1.3px); }
.mdir__right .mdir__c { transform: rotate(-45deg) translate(-1.3px, -1.3px); }

/* Long-press feedback — a ring grows under the finger while the map is arming */
.lp-ring { position: fixed; z-index: 60; width: 12px; height: 12px; margin: -6px 0 0 -6px;
  border-radius: 50%; border: 2.5px solid var(--bone); pointer-events: none;
  opacity: 0; transform: scale(.5); }
.lp-ring.go { opacity: .92; transform: scale(4.4); transition: transform var(--lp, 350ms) linear, opacity .16s var(--ease); }

/* ---------- Phones: the page scrolls the DOCUMENT, so it runs full-bleed under Safari's chrome ----------
   iOS Safari only draws page content behind its status bar and glass toolbar when that content
   belongs to the document's own scroll. Anything in a position:fixed layer (the desktop stage)
   is clipped to the band between the two, however tall it is, and Safari paints flat colour above
   and below it: that was the "cropped top and bottom" on long pages. So on touch devices deck.js
   sets html.flow: the page at rest sits in normal flow and scrolls the window, like any webpage.
   Only a neighbour sliding in during a swipe is lifted out and pinned over the current view
   (placeOverlay in deck.js); it rejoins the flow when it lands. Desktop keeps the fixed stage. */
html.flow, html.flow body { height: auto; overflow: visible; }
html.flow #app { position: relative; height: auto; }
html.flow .safecap { display: none; }   /* opaque strips over a scrolling page ARE the crop; theme-color tints the chrome */
/* Safari treats ANY full-width fixed element touching the top edge as a header (even a transparent
   one): it latches the colour behind it and paints that over the status bar, hiding the page that
   should scroll under it. So the bar starts below the edge rather than padding down from it — the
   same spot on screen. (The map + zoom overlays DO touch the edges, and that's what we want there:
   Safari extends their dark into the chrome while they're open.) */
html.flow .mbar { top: var(--safe-top); padding-top: 0; }
html.flow .stage { position: relative; inset: auto; overflow: clip; }   /* the page's own height; a sliding neighbour never extends the scroll */
html.flow .deck { position: relative; inset: auto; }
/* no transform at rest: it would become the containing block and trap the fixed zoom lightbox */
html.flow .screen--flow { position: relative; inset: auto; transform: none; will-change: auto; }
html.flow .screen--flow .screen__scroll { position: relative; inset: auto; overflow: visible; }
/* fill the view with the toolbar expanded — svh, so a page doesn't flip between fitting (centred)
   and scrolling as the toolbar resizes */
html.flow .inner { min-height: 100svh; }
/* About ends with the tilted polaroid stack; without this its lower corner finishes under Safari's
   floating toolbar and reads as cropped. Other pages keep running to the bottom edge. */
html.flow .media.about-figs { margin-bottom: calc(var(--safe-bottom) + 30px); }

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) { .screen.is-anim { transition: opacity .14s ease; } .deck[class*="nudge"] { animation: none; } }

/* ---------- Small screens ---------- */
@media (max-width: 560px) {
  .media.gallery { grid-template-columns: 1fr; }
  .tab { font-size: 12px; padding: 6px 11px; }
  .tab--left, .tab--right { padding: 11px 6px; }
  /* clear the fixed side pills so body text never slides under them */
  .inner { padding-left: 40px; padding-right: 40px; }
  .inner.solo { padding-left: 44px; padding-right: 44px; }
  .screen--home .inner { padding-left: 44px; padding-right: 44px; }
  .title { font-size: clamp(26px, 7.4vw, 36px); }
  .hero .title { font-size: clamp(23px, 6.8vw, 36px); white-space: nowrap; }   /* name stays one line */
  .body { font-size: 17px; line-height: 1.6; }
  /* keep media inside the side pills so the labels never cover it */
  .media { width: min(82vw, 900px); }
  .media.embed { width: min(84vw, 760px); }
  .media.embed--audio { width: min(82vw, 380px); }
  /* full-height media is sized to the SMALL viewport (toolbar expanded), so it fits on arrival and
     doesn't resize as Safari's toolbar shrinks and grows while the page scrolls (dvh would) */
  .media img, .media video { max-height: 62svh; }
  .solo .media img, .solo .media video { max-height: 72svh; }
  .media.gallery img, .media.gallery video { max-height: 60svh; }
  /* shorter proto phone so title + phone + caption clear the bottom nav pill */
  .phone { height: min(56svh, 560px); max-width: 78vw; }
}
