:root {
  --aqua-950: #073d4a;
  --aqua-800: #0a6672;
  --aqua-500: #32b6bd;
  --water: #c7f4ee;
  --paper: #fffaf0;
  --leaf: #4fa878;
  --brass: #b4873d;
  --ink: #173f46;
  --mono: "IBM Plex Mono", ui-monospace, monospace;
  --round: "Quicksand", ui-rounded, system-ui, sans-serif;
  --body: "Source Sans 3", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --storybook: "Cormorant Infant", Georgia, "Times New Roman", serif;
  --storybook-zh: "Cormorant Infant", "Source Han Serif SC", "Noto Serif CJK SC", "Songti SC", "STSong", "PingFang SC", serif;
  --condensed: "Barlow Condensed", "Arial Narrow", sans-serif;
  --stage-height: 100dvh;
  --title-font: "Quicksand", var(--body-font);
  --body-font: "Source Sans 3", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --google-text-highlight-color: #44b9ee;
}

* { box-sizing: border-box; }
html { scroll-behavior: auto; }
/* HanziPen belongs only to the handwritten About notebook (about-test.css).
   Story Mode's shared display type must remain a conventional Chinese serif. */
html:lang(zh-CN),
html:lang(zh-Hans) { --storybook: var(--storybook-zh); }
html, body { margin: 0; min-height: 100%; }
body {
  color: var(--ink);
  background: #72d9dd;
  font-family: var(--round);
  overscroll-behavior-y: none;
}
.mobile-orientation-landing { display:none; }
/* iOS has a purpose-built, unrotated escape hatch while the interactive
   landscape runtime is being repaired. It deliberately exists outside that
   runtime, so no route texture or frame sequence begins loading here. */
.ios-fallback { display:none; }
html.is-ios-fallback,
html.is-ios-fallback body {
  width:100%; min-height:100%; overflow:hidden; background:#55c8da;
}
html.is-ios-fallback .skip-link,
html.is-ios-fallback .mobile-landscape-viewport,
html.is-ios-fallback .scroll-space { display:none !important; }
html.is-ios-fallback .ios-fallback {
  position:fixed; z-index:2147483647; inset:0; display:block; overflow:hidden;
  color:#f8ffff; background:
    radial-gradient(ellipse 82% 48% at 50% 52%, rgba(163,244,242,.22), transparent 68%),
    linear-gradient(180deg, #67d8e1 0%, #40b9ce 100%);
}
.ios-fallback__logo {
  position:absolute; top:max(22px, env(safe-area-inset-top)); left:50%;
  width:min(36vw,154px); height:auto; transform:translateX(-50%);
  filter:drop-shadow(0 2px 10px rgba(5,72,92,.14));
}
.ios-fallback__language {
  position:absolute; top:max(16px, env(safe-area-inset-top)); right:max(16px, env(safe-area-inset-right));
  min-width:48px; min-height:42px; padding:0 12px; border:1px solid rgba(255,255,255,.7);
  border-radius:999px; color:#f9ffff; background:rgba(7,94,113,.18);
  box-shadow:0 7px 20px rgba(4,78,99,.14); font:600 14px/1 var(--mono); letter-spacing:.04em;
}
.ios-fallback__message {
  position:absolute; left:50%; top:50%; width:min(82vw,430px); transform:translate(-50%,-47%);
  display:grid; justify-items:center; text-align:center;
}
.ios-fallback__skill {
  width:min(61vw,286px); height:auto; margin-bottom:clamp(24px,5vh,42px);
  filter:drop-shadow(0 17px 23px rgba(4,81,100,.2));
}
.ios-fallback__copy { display:grid; gap:11px; }
.ios-fallback__copy strong { font:500 clamp(28px,7.2vw,41px)/1.1 var(--storybook); letter-spacing:-.02em; }
.ios-fallback__copy span {
  color:rgba(248,255,255,.72);
  font:400 clamp(17px,4.5vw,23px)/1.52 var(--body);
  letter-spacing:0;
}
.ios-fallback__copy--zh { display:none; }
html.is-ios-fallback:lang(zh-CN) .ios-fallback__copy--en { display:none; }
html.is-ios-fallback:lang(zh-CN) .ios-fallback__copy--zh { display:grid; }
html.is-mobile-storymode .mobile-orientation-landing {
  position:fixed; z-index:10000; inset:0; display:grid; place-items:center;
  padding:32px; color:#195861; background:#67d4dc; text-align:center;
  transition:opacity .36s ease, visibility .36s ease;
}
html.is-mobile-storymode .mobile-orientation-landing.is-done {
  opacity:0; visibility:hidden; pointer-events:none;
}
html.is-mobile-storymode .mobile-orientation-landing .mobile-orientation-guide { display:grid; }
html.is-mobile-landscape-preview .mobile-orientation-landing {
  left:calc(50vw - 195px); top:max(18px,calc(50vh - 422px)); right:auto; bottom:auto;
  width:390px; height:844px; border-radius:28px;
  box-shadow:0 28px 90px rgba(0,0,0,.42),0 0 0 8px #071b1f;
  overflow:hidden;
}

/* Mobile browsers do not reliably honor the Screen Orientation API outside
   installed/fullscreen experiences. In portrait, rotate the complete fixed
   Story Mode coordinate system and give it the phone's swapped dimensions.
   Every overlay stays registered with the WebGL world, including when the
   device's system auto-rotate setting is disabled. */
html.is-mobile-storymode.is-virtual-landscape {
  width: 100%;
  min-height: 100%;
  overflow-x: hidden;
  overscroll-behavior: none;
}
html.is-mobile-storymode.is-virtual-landscape .mobile-landscape-viewport {
  position: fixed;
  z-index: 1;
  left: 0;
  top: 0;
  width: 100dvh;
  height: 100vw;
  transform: translateX(100vw) rotate(90deg);
  transform-origin: 0 0;
}
/* This node already lives inside the rotated logical-landscape viewport.
   Physical 100vw/100dvh swapped its axes a second time and made every mobile
   teleport cover overflow or leave a strip uncovered. */
html.is-mobile-storymode .loading-card.is-route-loading {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  gap: 10px;
}
html.is-mobile-storymode .loading-card.is-route-loading .teleport-loading-icon {
  width: clamp(120px, 16vw, 150px);
}
html.is-mobile-storymode .loading-card.is-route-loading p:not(.loading-substatus) {
  font-size: clamp(32px, 4.8vw, 42px);
  letter-spacing: .065em;
}
html.is-mobile-storymode .loading-card.is-route-loading .loading-substatus {
  margin-top: .25em;
  font-size: clamp(16px, 2.4vw, 21px);
  line-height: 1.4;
  letter-spacing: .04em;
}
html.is-mobile-storymode.is-virtual-landscape .scroll-space {
  height: 6124vw;
}
/* The in-app preview surface restores its own large pane after handoff, so a
   browser-level viewport override is not a durable phone simulator. The
   explicit preview query renders a fixed 390×844 device viewport inside that
   pane; real phones never receive this class and continue using dvh/vw. */
html.is-mobile-landscape-preview {
  overflow-x: hidden;
  background: #13292d;
}
html.is-mobile-landscape-preview body {
  background:
    radial-gradient(circle at 50% 45%, rgba(103,212,220,.16), transparent 48%),
    #13292d;
}
html.is-mobile-storymode.is-virtual-landscape.is-mobile-landscape-preview .mobile-landscape-viewport {
  left: calc(50vw - 195px);
  top: max(18px, calc(50vh - 422px));
  width: 844px;
  height: 390px;
  transform: translateX(390px) rotate(90deg);
  border-radius: 28px;
  box-shadow: 0 28px 90px rgba(0,0,0,.42), 0 0 0 8px #071b1f;
  overflow: hidden;
}
html.is-mobile-storymode.is-virtual-landscape.is-mobile-landscape-preview .scroll-space {
  height: 23884px;
}
button, a { font: inherit; }
button { color: inherit; }
.skip-link {
  position: fixed;
  z-index: 100;
  left: 1rem;
  top: 1rem;
  padding: .7rem 1rem;
  border-radius: 999px;
  background: white;
  transform: translateY(-180%);
}
.skip-link:focus { transform: translateY(0); }

.journey-header {
  position: fixed;
  z-index: 40;
  inset: 0 0 auto;
  height: 68px;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 0 clamp(18px, 3vw, 52px);
  color: rgba(255,255,255,.96);
  text-shadow: 0 1px 14px rgba(4,65,72,.34);
  pointer-events: none;
}

/* The map is another layer of this site, not a replacement for its chrome. */
html.is-story-map-open .journey-header {
  z-index: 88;
}
html.is-story-content-open .journey-header {
  z-index: 82;
}
body.is-about-overlay-open .journey-header {
  z-index: 1202;
}
html.is-mobile-storymode.is-story-map-open .journey-header,
html.is-mobile-storymode body.is-about-overlay-open .journey-header {
  z-index:40;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
}
/* Keep the original wordmark in its grid cell so the centered status never
   reflows on Reflection. A separate, decorative signature sits in the scene. */
html.is-mobile-storymode.is-reflection-scene .journey-brand {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.journey-reflection-signature {
  display: none;
}
html.is-mobile-storymode.is-reflection-scene .journey-reflection-signature {
  display: block;
  position: fixed;
  right: 18px;
  bottom: 15px;
  width: clamp(83px, 7vw, 114px);
  height: auto;
  z-index: 41;
  pointer-events: none;
  filter: drop-shadow(0 1px 10px rgba(4,65,72,.2));
}
html:is(.is-story-map-open, .is-story-content-open) .journey-status,
body.is-about-overlay-open .journey-status {
  opacity: 0;
  visibility: hidden;
}
.journey-header a,
.journey-header button { pointer-events: auto; }
.journey-brand {
  display: inline-flex;
  align-items: center;
  color: inherit;
  text-decoration: none;
}
.journey-brand-logo {
  display: block;
  width: clamp(104px, 8.8vw, 142px);
  height: auto;
  filter: drop-shadow(0 1px 10px rgba(4,65,72,.2));
}
.journey-status {
  display: flex;
  align-items: center;
  gap: .55rem;
  font: 500 10px/1 var(--mono);
  letter-spacing: .13em;
  text-transform: uppercase;
}
.journey-status-dot {
  width: 6px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: #d0fff3;
  box-shadow: 0 0 0 5px rgba(197,255,240,.14), 0 0 16px rgba(197,255,240,.9);
}
.journey-actions {
  position: relative;   /* the sound nudge hangs off this, without joining it */
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 18px;
  font-size: 12px;
}
.journey-actions a { color: inherit; text-decoration: none; }
.journey-control {
  border: 1px solid rgba(255,255,255,.42);
  color: inherit;
  background: rgba(8,83,91,.18);
  border-radius: 999px;
  padding: .45rem .72rem;
  cursor: pointer;
  backdrop-filter: blur(12px);
  white-space: nowrap;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.journey-control[aria-pressed="true"] { background: rgba(255,255,255,.28); }
/* Motion now follows the saved/system preference without occupying permanent
   header chrome. Keep the node for the existing preference machinery; the
   visible journey controls are intentionally only Sound and Language. */
#motionButton { display:none; }
.sound-control {
  width: 32px;
  height: 32px;
  display: inline-grid;
  place-items: center;
  padding: 0;
  border-radius: 50%;
}
.sound-icon {
  width: 15px;
  height: 13px;
  fill: currentColor;
  opacity: .96;
  transition: opacity 180ms ease, filter 180ms ease;
}
.sound-control:not(.is-muted) .sound-icon {
  filter: drop-shadow(0 0 4px rgba(255,255,255,.65));
}
.sound-control.is-muted .sound-icon {
  opacity: .46;
  filter: none;
}
.journey-control:focus-visible,
.journey-actions a:focus-visible,
.journey-route button:focus-visible,
.world-hotspot:focus-visible,
.airship-link:focus-visible {
  outline: 2px solid white;
  outline-offset: 3px;
}

/* The airship carries About and Map, so it has to stay clickable wherever it
   drifts. At 24 it sat under the journey progress and hover card (30), the
   route nav (32), the header (40) and the floating callout (62), any of which
   could land on top of a flag. It now sits above every in-scene layer, but
   still below the teleport cover (70) and the modal band (80+) -- those are
   meant to cover it. The finale hides it outright: that scene carries its own
   About and Map plaques. */
.airship-controls {
  position: absolute;
  z-index: 64;
  inset: 0;
  pointer-events: none;
}
.airship-link {
  position: absolute;
  display: grid;
  place-items: center;
  min-width: 44px;
  min-height: 36px;
  border: 0;
  padding: 0;
  color: #286a70;
  background: transparent;
  text-decoration: none;
  cursor: pointer;
  pointer-events: auto;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, -50%);
}
.airship-link.is-visible { opacity: 1; visibility: visible; }
.airship-link span {
  display: none;
}

.cv-gate-modal[hidden] { display: none; }
.cv-gate-modal {
  position: fixed;
  z-index: 90;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 22px;
  opacity: 0;
  transition: opacity 220ms ease-out;
}
.cv-gate-modal.is-open { opacity: 1; }
.cv-gate-overlay { position: absolute; inset: 0; background: rgba(4,51,61,.46); backdrop-filter: blur(10px); }
.cv-gate-card {
  position: relative;
  width: min(92vw, 520px);
  padding: 34px;
  border: 1px solid rgba(255,255,255,.7);
  border-radius: 5px 38px 38px 38px;
  background: rgba(255,252,241,.97);
  box-shadow: 0 38px 110px rgba(4,52,60,.3);
  transform: translateY(14px) scale(.98);
  transition: transform 220ms ease-out;
}
.cv-gate-modal.is-open .cv-gate-card { transform: translateY(0) scale(1); }
.cv-gate-card.is-shake { animation: gateShake 260ms ease-out; }
.cv-gate-kicker { margin: 0; color: #388c8e; font: 500 10px/1.4 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.cv-gate-card h2 { margin: 12px 42px 13px 0; font: 500 clamp(30px, 5vw, 48px)/.98 var(--storybook); letter-spacing: -.025em; }
.cv-gate-body { color: rgba(23,63,70,.72); line-height: 1.65; }
.cv-gate-form { display: flex; gap: 10px; margin-top: 22px; }
.cv-gate-input {
  min-width: 0;
  flex: 1;
  padding: 12px 15px;
  border: 1px solid rgba(31,128,124,.28);
  border-radius: 999px;
  color: var(--ink);
  background: rgba(108,211,194,.1);
  outline: none;
}
.cv-gate-input:focus { border-color: rgba(31,128,124,.68); box-shadow: 0 0 0 3px rgba(71,181,171,.12); }
.cv-gate-btn, .cv-gate-cancel {
  padding: 11px 18px;
  border: 1px solid rgba(31,128,124,.28);
  border-radius: 999px;
  background: rgba(108,211,194,.16);
  cursor: pointer;
}
.cv-gate-error { margin: 12px 0 0; color: #b35f53; font-size: 13px; }
.cv-gate-cancel { margin-top: 15px; background: transparent; }
.cv-gate-close {
  position: absolute;
  top: 16px;
  right: 18px;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: rgba(65,157,149,.1);
  cursor: pointer;
}
@keyframes gateShake { 0%,100% { transform: translateX(0); } 30% { transform: translateX(-7px); } 65% { transform: translateX(6px); } }

.journey-stage {
  position: fixed;
  inset: 0;
  width: 100%;
  height: var(--stage-height);
  overflow: hidden;
  isolation: isolate;
  background: #67d4dc;
}
#worldCanvas {
  position: fixed;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  touch-action: pan-y;
  cursor: grab;
}
#worldCanvas:active { cursor: grabbing; }
html.is-mobile-storymode #worldCanvas { touch-action:none; }
.cover-title-video,.sunny-intro-video { position:absolute; pointer-events:none; object-fit:contain; will-change:transform,opacity; }
.cover-title-video { z-index:5; left:50%; top:10%; width:min(84vw,1180px); height:56vh; transform:translate3d(-50%,0,0); }
.sunny-intro-video { z-index:11; left:50%; bottom:calc(21vh - 18px); width:min(78vw,760px); height:min(44vh,460px); transform:translate3d(-57%,0,0); transition:opacity 160ms ease-out; }
.sunny-code-shadow { display:none; }
.sunny-chat-bubble { position:absolute; z-index:12; left:calc(50% + clamp(65px,7vw,118px)); bottom:clamp(43vh,455px,52vh); padding:10px 16px; border:1px solid rgba(255,255,255,.84); border-radius:52% 48% 47% 53%/56% 46% 54% 44%; color:#1f666b; background:rgba(252,255,241,.91); box-shadow:0 10px 28px rgba(15,92,87,.16),inset 0 0 0 3px rgba(117,211,195,.13); font-size:13px; opacity:0; transform:translate3d(-8px,8px,0) scale(.92); transition:opacity 180ms ease-out,transform 220ms cubic-bezier(.215,.61,.355,1); pointer-events:none; }
.sunny-chat-bubble::after { content:""; position:absolute; left:18px; bottom:-8px; width:14px; height:14px; background:inherit; border-right:1px solid rgba(255,255,255,.84); border-bottom:1px solid rgba(255,255,255,.84); transform:rotate(45deg); }
.sunny-chat-bubble.is-visible { opacity:1; transform:translate3d(0,0,0) scale(1); pointer-events:auto; }
.route-preparation-cue {
  position:absolute;
  z-index:205;
  left:var(--route-cue-x,50%);
  right:auto;
  top:var(--route-cue-y,50%);
  display:flex;
  align-items:center;
  gap:clamp(2px,.45vw,6px);
  opacity:0;
  pointer-events:none;
  transition:opacity 120ms ease-out;
}
.route-preparation-cue.on-left { left:auto; right:var(--route-cue-right,50%); }
.route-preparation-cue.is-visible { opacity:1; }
.route-preparation-cue i {
  width:clamp(18px,2.2vw,34px);
  height:clamp(18px,2.2vw,34px);
  border-top:clamp(3px,.38vw,6px) solid;
  border-right:clamp(3px,.38vw,6px) solid;
  border-radius:2px;
  transform:rotate(45deg);
  opacity:.5;
  filter:drop-shadow(0 0 3px rgba(31,122,158,.85))
    drop-shadow(0 2px 9px rgba(31,122,158,.7))
    drop-shadow(0 0 18px rgba(214,244,255,.8));
  animation:start-cue-drift 1.55s ease-in-out infinite;
}
.route-preparation-cue i:nth-child(1) { border-color:#fff; }
.route-preparation-cue i:nth-child(2) { border-color:#dcf1fa; animation-delay:.16s; }
.route-preparation-cue i:nth-child(3) { border-color:#9fd9ec; animation-delay:.32s; }
.route-preparation-cue.points-back i {
  transform:rotate(-135deg);
  animation-name:crossing-ready-drift-back;
}
.route-preparation-cue.is-vertical {
  left:var(--route-cue-x,50%);
  right:auto;
  flex-direction:column;
  transform:translateX(-50%);
}
.route-preparation-cue.is-vertical i { animation-name:route-cue-drift-up; }
.route-preparation-cue.is-vertical.points-up i { transform:rotate(-45deg); }
.route-preparation-cue.is-vertical.points-down i {
  transform:rotate(135deg);
  animation-name:route-cue-drift-down;
}
@keyframes route-cue-drift-up {
  0%,100% { opacity:.34; translate:0 5px; }
  50% { opacity:1; translate:0 -5px; }
}
@keyframes route-cue-drift-down {
  0%,100% { opacity:.34; translate:0 -5px; }
  50% { opacity:1; translate:0 5px; }
}
.portal-transition {
  position: absolute;
  z-index: 14;
  inset: -3%;
  opacity: 0;
  pointer-events: none;
  transform: scale(.985);
  transform-origin: 50% 50%;
  will-change: opacity, transform;
  background:
    radial-gradient(ellipse 48% 72% at 46% 52%, rgba(250,255,218,.88) 0 12%, rgba(181,238,179,.55) 34%, transparent 72%),
    radial-gradient(ellipse 54% 42% at 18% 72%, rgba(101,213,185,.66), transparent 72%),
    radial-gradient(ellipse 46% 64% at 82% 28%, rgba(167,236,190,.62), transparent 70%),
    linear-gradient(135deg, rgba(20,108,87,.93), rgba(64,155,110,.91) 48%, rgba(26,119,109,.94));
  backdrop-filter: blur(15px) saturate(1.2) brightness(1.09);
}
.portal-transition::before,
.portal-transition::after {
  content: "";
  position: absolute;
  inset: -14%;
  will-change: transform, opacity;
}

/* AI Lab → Thoughts is a covered boundary handoff. A flat travel colour owns
   the discontinuity so neither world is ever half-visible under the other.
   Static paint only: never a live backdrop-filter over the running stage.

   The colour is the river's ink, so the field is that ink in water: a drop
   landing and its rings spreading, over the laid fibre of wet paper. Nothing
   here is a mechanism — no wipe, no shutter, no edge that travels. The camera
   is following Sunny through the boundary, so the passage has to be a place she
   is inside of, not a device closing around a camera that stayed behind.
   `--thoughts-travel-phase` runs 0 → 1 across the whole band and only ever
   drives the spread, which is what makes the field read as loading. */
.thoughts-travel-cover {
  position: fixed;
  z-index: 200;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  opacity: var(--thoughts-travel-cover, 0);
  background:
    radial-gradient(ellipse 118% 88% at 50% 50%, rgba(150,170,222,.15), transparent 62%),
    repeating-linear-gradient(101deg, rgba(214,226,255,.016) 0 2px, transparent 2px 7px),
    repeating-linear-gradient(11deg, rgba(214,226,255,.012) 0 1px, transparent 1px 6px),
    linear-gradient(180deg, #1b2344 0%, #232c53 52%, #2b3560 100%);
}
/* The rings. They start tight and open outwards as the crossing runs, thinning
   as they go the way a ripple loses its edge — the single flat ellipse this
   replaces had the right colour and no behaviour. */
.thoughts-travel-cover::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(172vh, 148vw);
  aspect-ratio: 1;
  border-radius: 50%;
  opacity: calc(.9 - var(--thoughts-travel-phase, 0) * .34);
  transform:
    translate3d(-50%, -50%, 0)
    scale(calc(.4 + var(--thoughts-travel-phase, 0) * .78));
  will-change: transform, opacity;
  background: repeating-radial-gradient(circle at 50% 50%,
    rgba(196,214,255,.19) 0 2px,
    rgba(150,172,226,.07) 2px 3.5px,
    transparent 3.5px 38px);
  -webkit-mask: radial-gradient(circle, #000 0 46%, rgba(0,0,0,.55) 68%, transparent 84%);
  mask: radial-gradient(circle, #000 0 46%, rgba(0,0,0,.55) 68%, transparent 84%);
}
/* The drop itself, small and bright at the centre of the rings. It tightens as
   the rings open, so the eye has one still point to hold through the crossing. */
.thoughts-travel-cover::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(26vh, 19vw);
  aspect-ratio: 1;
  border-radius: 50%;
  transform:
    translate3d(-50%, -50%, 0)
    scale(calc(1.16 - var(--thoughts-travel-phase, 0) * .4));
  will-change: transform;
  background: radial-gradient(circle at 50% 46%,
    rgba(232,240,255,.17) 0 16%,
    rgba(170,192,242,.09) 42%,
    transparent 72%);
}
.art-fall-transition {
  position: fixed;
  z-index: 199;
  inset: 0;
  overflow: hidden;
  opacity: var(--art-fall-active, 0);
  pointer-events: none;
}
.art-fall-transition::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 50% 50%, rgba(32,80,116,.26), transparent 42%),
    #061426;
  opacity: var(--art-void-opacity, 0);
}
.is-art-ai-fall .art-fall-transition::before {
  background:
    radial-gradient(ellipse at 50% 46%, rgba(132,236,248,.2), transparent 44%),
    linear-gradient(180deg, #36b9d5 0%, #249fca 48%, #1687b6 100%);
}
.art-fall-portal {
  position: absolute;
  z-index:1;
  left: 50%;
  top: 50%;
  width: clamp(150px, 20vw, 330px);
  aspect-ratio: 2.35 / 1;
  border: clamp(3px, .32vw, 6px) solid rgba(189,249,255,.92);
  border-radius: 50%;
  background: radial-gradient(ellipse, #000711 0 58%, rgba(14,53,78,.94) 64%, rgba(105,220,232,.2) 76%, transparent 78%);
  box-shadow: 0 0 16px rgba(146,241,250,.86), 0 0 54px rgba(59,180,207,.42), inset 0 0 22px rgba(128,235,246,.38);
  opacity: var(--art-portal-opacity, 0);
  transform: translate(-50%, -50%) scale(var(--art-portal-scale, .72));
}
.is-art-ai-fall .art-fall-portal::before,
.is-art-ai-fall .art-fall-portal::after {
  content:"";
  position:absolute;
  inset:-8%;
  border:clamp(2px,.18vw,4px) solid rgba(205,250,255,.56);
  border-radius:50%;
  opacity:0;
  animation:art-water-ring 1.8s ease-out infinite;
}
.is-art-ai-fall .art-fall-portal::after { animation-delay:.6s; }
@keyframes art-water-ring {
  0% { opacity:.68; transform:scale(.9); }
  82%, 100% { opacity:0; transform:scale(2.65); }
}
@media (prefers-reduced-motion: reduce) {
  .is-art-ai-fall .art-fall-portal::before,
  .is-art-ai-fall .art-fall-portal::after { animation:none; opacity:.22; transform:scale(1.45); }
}
.art-fall-clip-defs { position:absolute; width:0; height:0; overflow:hidden; }
.art-fall-character-clip {
  position:absolute;
  z-index:2;
  inset:0;
  clip-path:url(#artFallCharacterClip);
}
.art-fall-glow {
  position: absolute;
  left: 50%;
  width: clamp(300px, 27vw, 430px);
  height: auto;
  filter: drop-shadow(0 0 7px rgba(143,245,255,.72)) drop-shadow(0 0 22px rgba(80,222,238,.44));
  transform-origin: 50% 0;
}
.art-fall-glow--airbnb {
  top: var(--airbnb-art-fall-y, -650px);
  opacity: calc(var(--art-fall-glow-opacity, 0) * var(--airbnb-art-fall-image, 1));
  transform: translateX(-50%) scale(var(--airbnb-art-fall-scale, .96));
}
.art-fall-glow--ai {
  top: var(--art-ai-fall-y, -650px);
  opacity: calc(var(--art-fall-glow-opacity, 0) * var(--art-ai-fall-image, 0));
  transform: translateX(-50%) scale(var(--art-ai-fall-scale, 1.8));
}
.portal-transition::before {
  opacity: .82;
  background:
    radial-gradient(ellipse 19% 25% at 17% 30%, rgba(255,250,203,.82) 0 28%, rgba(206,255,224,.26) 46%, transparent 72%),
    radial-gradient(ellipse 28% 21% at 76% 18%, rgba(221,255,225,.72) 0 25%, rgba(124,231,195,.22) 50%, transparent 72%),
    radial-gradient(ellipse 24% 34% at 58% 76%, rgba(255,244,176,.64) 0 23%, rgba(185,249,204,.18) 48%, transparent 71%),
    radial-gradient(ellipse 18% 30% at 91% 63%, rgba(190,248,221,.69) 0 24%, transparent 68%);
  filter: blur(11px);
  animation: portal-bokeh-drift-a 7.5s ease-in-out infinite alternate;
}
.portal-transition::after {
  opacity: .64;
  background:
    radial-gradient(ellipse 31% 19% at 31% 16%, rgba(241,255,220,.7) 0 22%, transparent 70%),
    radial-gradient(ellipse 22% 32% at 48% 48%, rgba(255,248,191,.57) 0 24%, rgba(131,230,198,.12) 53%, transparent 72%),
    radial-gradient(ellipse 26% 20% at 84% 82%, rgba(214,255,229,.68) 0 26%, transparent 69%),
    radial-gradient(ellipse 17% 26% at 7% 84%, rgba(153,242,212,.52) 0 23%, transparent 67%);
  filter: blur(15px);
  animation: portal-bokeh-drift-b 9s ease-in-out infinite alternate;
}
.falling-particles { position: absolute; z-index: 13; inset: 0; overflow: hidden; pointer-events: none; transition: opacity 220ms ease; }
.falling-particle {
  position: absolute;
  top: -14%;
  left: var(--x);
  width: var(--size);
  height: auto;
  opacity: 1;
  filter: drop-shadow(0 5px 7px rgba(35,121,103,.12));
  will-change: transform, opacity;
  animation: forest-particle-fall var(--duration) linear var(--delay) infinite;
}
.scroll-space { height: 5978vh; }

.loading-card {
  position: absolute;
  z-index: 70;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 10px;
  background: #67d4dc;
  color: rgba(14,83,88,.8);
  transition: opacity .45s ease, visibility .45s ease;
}
.loading-card.is-done { opacity: 0; visibility: hidden; }
.loading-card.is-route-loading {
  position: fixed;
  z-index: 200;
  inset: auto;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100dvh;
  transform: none !important;
  translate: none !important;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: none;
  background: #67d4dc;
}
.loading-card.is-route-loading::before {
  display: none;
}
.loading-card.is-route-loading .loading-thread { display: none; }

/* A second line under the teleport label. Silent unless the destination is
   actually still assembling, then it says so and says when it is done — the
   same contract the scroll crossings use, so a jump and a walk explain
   themselves the same way. */
.loading-substatus {
  margin: .55em 0 0;
  min-height: 1.2em;
  color: rgba(255, 255, 255, .66);
  font: 400 12px/1.35 var(--mono, "IBM Plex Mono", ui-monospace, monospace);
  letter-spacing: .05em;
  opacity: 0;
  transition: opacity 300ms ease;
}
.loading-card.is-waiting-world .loading-substatus { opacity: 1; }
/* Needs this much specificity: `.loading-card.is-route-loading p` sets 24–48px
   for the teleport label and was catching this line too. Matched to the scroll
   crossing's own status type, so both waits speak in the same voice — the tone
   just follows whichever cover it is sitting on. */
.loading-card.is-route-loading .loading-substatus {
  margin-top: .35em;
  font: 400 clamp(16px, 1.5vw, 25px)/1.5 Georgia, serif;
  letter-spacing: .06em;
  text-transform: none;
  color: rgba(255, 255, 255, .82);
}
.loading-card.is-route-loading.is-world-ready .loading-substatus { color: #fff; }
.teleport-loading-icon {
  display:none;
  width:clamp(92px,10vw,148px);
  height:auto;
  object-fit:contain;
  filter:drop-shadow(0 15px 24px rgba(14,83,88,.18));
}
.loading-card.is-route-loading .teleport-loading-icon {
  display:block;
  animation:teleportDeviceFloat 2.4s ease-in-out infinite;
}
.loading-card.is-route-loading p {
  font-size:clamp(24px,3.2vw,48px);
  letter-spacing:.08em;
  text-transform:none;
}

.is-loading .journey-header,
.is-loading #worldCanvas,
.is-loading .sunny-code-shadow,
.is-loading .falling-particles,
.is-loading .portal-transition,
.is-loading .opening-instruction,
.is-loading .chapter-card,
.is-loading .hotspot-layer,
.is-loading .journey-progress,
.is-loading .journey-route {
  opacity: 0 !important;
  visibility: hidden;
}

.is-loading .falling-particle { animation-play-state: paused; }
.loading-card p {
  margin: 0;
  font: 500 10px/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
}
.loading-thread {
  width: 84px;
  height: 2px;
  background: rgba(255,255,255,.42);
  border-radius: 99px;
  overflow: hidden;
}
/* The sweeping bead, shown only while there is nothing to report. The first
   file to land adds .is-measured and the real fill takes over -- an empty track
   in that first moment would read as broken rather than as honest. */
.loading-thread::after {
  content: "";
  display: block;
  width: 34%;
  height: 100%;
  background: var(--aqua-500);
  animation: threadLoad 1.1s ease-in-out infinite;
}
.loading-thread.is-measured::after { display: none; }
/* Byte-weighted progress across the first batch. scaleX from the left edge, so
   growing it is a composite step rather than a layout, and the transition
   smooths the jump a twenty-megabyte atlas makes when it finally lands. */
.loading-thread-fill {
  display: none;
  width: 100%;
  height: 100%;
  background: var(--aqua-500);
  border-radius: inherit;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 260ms ease-out;
}
.loading-thread.is-measured .loading-thread-fill { display: block; }
.mobile-orientation-guide { display: none; }
html.is-mobile-storymode .loading-card:not(.is-route-loading) {
  gap:10px;
}
html.is-mobile-storymode .mobile-orientation-guide {
  display: grid;
  max-width: min(78vw, 560px);
  justify-items: center;
  gap: 9px;
  margin-top: clamp(8px, 2vh, 18px);
  color: rgba(8, 70, 76, .92);
  text-align: center;
}
.mobile-orientation-guide strong {
  font: 500 clamp(20px, 3.5vw, 32px)/1.02 var(--storybook);
  letter-spacing: -.025em;
}
.mobile-orientation-guide > span:last-child {
  max-width: 35em;
  color: rgba(8, 70, 76, .72);
  font: 500 clamp(11px, 1.65vw, 14px)/1.35 var(--round);
  white-space: nowrap;
}
.mobile-orientation-icon {
  position: relative;
  display: block;
  width: 94px;
  height: 68px;
}
.mobile-orientation-phone {
  position: absolute;
  left: 34px;
  top: 5px;
  width: 30px;
  height: 54px;
  border: 3px solid rgba(8, 70, 76, .88);
  border-radius: 8px;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,.2), 0 8px 20px rgba(8,70,76,.12);
  animation: mobilePhoneTurn 2.2s cubic-bezier(.55,.05,.25,1) infinite;
}
.mobile-orientation-phone::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 3px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: rgba(8,70,76,.72);
  transform: translateX(-50%);
}
/* Drawn rather than typed. The glyph available for this was clockwise, and the
   phone beside it turns counter-clockwise -- an instruction that contradicts
   its own illustration is worse than no illustration. This arc sweeps the way
   the phone actually goes. */
.mobile-orientation-arrow {
  position: absolute;
  right: -4px;
  top: 0;
  width: 44px;
  height: 44px;
  color: rgba(255,255,255,.96);
  filter: drop-shadow(0 3px 12px rgba(8,70,76,.28));
  animation: mobileTurnArrow 2.2s ease-in-out infinite;
}
@keyframes mobilePhoneTurn {
  0%, 18% { transform: rotate(0deg); }
  58%, 88% { transform: rotate(-90deg); }
  100% { transform: rotate(0deg); }
}
@keyframes mobileTurnArrow {
  0%, 18%, 100% { opacity: .25; transform: rotate(20deg) scale(.82); }
  45%, 82% { opacity: 1; transform: rotate(-18deg) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .mobile-orientation-phone { animation: none; transform: rotate(-90deg); }
  .mobile-orientation-arrow { animation: none; opacity: .9; transform: rotate(-18deg); }
}

.opening-instruction {
  position: absolute;
  z-index: 8;
  left: 50%;
  bottom: clamp(20px, 4.5vh, 50px);
  display:flex;
  align-items:center;
  gap:8px;
  color: #184e55;
  text-shadow: 0 1px 8px rgba(255,255,255,.7);
  transform: translateX(-50%);
  transition: opacity .2s ease;
  pointer-events: auto;
}
.opening-instruction span {
  font: 500 10px/1 var(--mono);
  letter-spacing: .22em;
  text-transform: uppercase;
  /* The chevrons are glyphs whose ink sits below their box centre, so centring
     the boxes left the word riding high above the line they draw. Matched to
     the ink rather than to the box. */
  transform: translateY(2px);
}
.scroll-arrows { display:contents; }
.scroll-nudge {
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  color: inherit;
  background: transparent;
  font: 500 22px/1 var(--round);
  cursor: pointer;
}
html.is-mobile-storymode .mobile-project-lightbox[hidden] { display:none; }
html.is-mobile-storymode .mobile-project-lightbox {
  position:fixed; z-index:9999; inset:0; display:grid; place-items:center;
  padding:20px; pointer-events:auto;
  background:rgba(7,34,61,.78);
  backdrop-filter:blur(8px) saturate(.82);
}
html.is-mobile-storymode .mobile-project-card {
  position:relative; width:min(500px,calc(100% - 48px));
  margin:0; padding:clamp(22px,5vw,40px); pointer-events:auto;
  color:#163f49; text-align:center;
  border:1px solid rgba(255,255,255,.65); border-radius:20px;
  background:linear-gradient(145deg,rgba(255,251,236,.98),rgba(221,247,240,.97));
  box-shadow:0 24px 80px rgba(0,14,31,.5),inset 0 0 0 1px rgba(28,139,149,.12);
}
.mobile-project-message {
  margin:0; font:300 clamp(19px,3vw,25px)/1.45 var(--storybook);
  text-wrap:balance;
}
.mobile-project-domain {
  color:#174d8c;
  font-weight:500;
  text-shadow:0 1px 0 rgba(255,255,255,.72);
}
html.is-mobile-storymode .world-hotspot:hover .hotspot-callout,
html.is-mobile-storymode .world-hotspot.is-hovered .hotspot-callout,
html.is-mobile-storymode .world-hotspot:focus-visible .hotspot-callout,
html.is-mobile-storymode .hotspot-callout-floating,
html.is-mobile-storymode .thoughts-hover-card { display:none !important; }
.scroll-nudge-back { animation: arrowOpenLeft 1.8s ease-in-out infinite; }
.scroll-nudge-forward { animation: arrowOpenRight 1.8s ease-in-out infinite; }
.scroll-nudge:focus-visible { outline: 2px solid rgba(255,255,255,.95); outline-offset: -2px; border-radius: 50%; }

.chapter-card {
  display: none;
  position: absolute;
  z-index: 24;
  left: 0;
  top: 0;
  width: clamp(380px, 38vw, 620px);
  aspect-ratio: 1.79;
  padding: 15% 10% 9%;
  /* The road sign now lives in Three.js world units; this DOM shell is legacy. */
  flex-direction: column;
  justify-content: center;
  border: 0;
  background: url("assets/storymode-game/road-sign-inspiring-city-en.webp") center/contain no-repeat;
  opacity: 0;
  transform: translate3d(-50%,-100%,0);
  pointer-events: none;
  color: #174d55;
  text-align: center;
}
.chapter-card span {
  display: block;
  margin-bottom: 7px;
  color: #267982;
  font: 500 clamp(11px,.95vw,14px)/1.2 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  transform: translateY(7px);
}
.chapter-card strong { display:block; width:100%; font-family:var(--storybook); font-size:clamp(30px,3vw,48px); font-weight:400; line-height:.98; letter-spacing:-.035em; transform:translateY(7px); }

/* Above the airship at 65. The airship is deliberately the highest thing in the
   scene so About and Map are never unclickable, but a hover card carries the
   information the reader asked for by pointing at something -- it losing that
   argument means the answer arrives behind the question. The layer itself stays
   pointer-transparent, so the airship is still clickable everywhere a card is
   not actually drawn. */
.hotspot-layer { position: fixed; z-index: 70; inset: 0; pointer-events: none; }
.world-hotspot {
  --hotspot-accent: #73d6cf;
  --hotspot-visible-opacity: .9;
  --hotspot-callout-offset: clamp(128px, 16vh, 220px);
  position: absolute;
  left: -100px;
  top: -100px;
  width: 80px;
  height: 120px;
  padding: 0;
  border: 0;
  border-radius: 32px;
  background: transparent;
  opacity: 0;
  transform: translate(-50%, -50%) scale(.75);
  transition: opacity .22s ease, transform .22s ease;
  pointer-events: none;
  cursor: default;
}
.world-hotspot.is-visible { opacity: 1; transform: translate(-50%, -50%) scale(1); pointer-events: auto; }
.hotspot-callout {
  position: absolute;
  left: 50%;
  top: calc(42% - var(--hotspot-callout-offset) - 10px + var(--hotspot-callout-top-guard, 0px));
  width: min(380px, 78vw);
  padding: 19px 22px 21px;
  border: 1px solid rgba(255,255,255,.86);
  border-radius: 5px 34px 34px 34px;
  background: rgba(247,252,239,.96);
  box-shadow: 0 16px 38px rgba(7,83,85,.18);
  color: #1e555c;
  text-align: left;
  backdrop-filter: blur(13px) saturate(1.08);
  transform: translate(-50%, -100%) translateY(8px) scale(.97);
  transform-origin: 50% 100%;
  opacity: 0;
  transition: opacity 180ms ease-out, transform 220ms cubic-bezier(.215,.61,.355,1);
  pointer-events: none;
}
.hotspot-callout::before { display:none; }
.hotspot-kicker { display: block; margin-bottom: 7px; color: #388c8e; font: 500 10px/1.35 var(--mono); letter-spacing: .11em; text-transform: uppercase; }
.hotspot-title { display: block; font-family: var(--storybook); font-size: clamp(29px, 2.65vw, 38px); font-weight: 500; line-height: .98; letter-spacing: -.015em; }
[data-project="bupt"] .hotspot-title {
  font-size: clamp(17px, 1.3vw, 23px);
  line-height: 1.02;
}
.hotspot-role { display: block; margin-top: 8px; font-family: var(--storybook); font-size: clamp(20px, 1.8vw, 25px); font-weight: 400; line-height: 1.12; letter-spacing: -.005em; color: rgba(23,63,70,.86); }
[data-project="bupt"] .hotspot-role { font-size: clamp(14px, 1.05vw, 18px); line-height: 1.16; }
.hotspot-body { display: block; margin-top: 12px; padding-top: 11px; border-top: 1px solid rgba(71,145,142,.17); color: rgba(23,63,70,.7); font-size: clamp(12px, 1.05vw, 14px); line-height: 1.52; }
/* A slot with nothing to say should not reserve a line for it. The vault says
   one thing on purpose, and its three empty slots were still holding the card
   tall enough that the viewport-top guard shoved the whole thing into the sky,
   nowhere near the chest it belongs to. */
.hotspot-kicker:empty,
.hotspot-title:empty,
.hotspot-role:empty,
.hotspot-body:empty { display: none; }


.world-hotspot.is-hovered .hotspot-callout,
.world-hotspot:focus-visible .hotspot-callout {
  opacity: var(--hotspot-visible-opacity);
  transform: translate(-50%, -100%) translateY(0) scale(1);
  pointer-events: auto;
}

.trulia-hotspot .hotspot-callout {
  --hotspot-callout-offset: clamp(92px, 11vh, 145px);
  border: 1px solid rgba(219,143,127,.58);
  border-radius: 5px 34px 34px 34px;
  background: rgba(255,245,225,.96);
  box-shadow: 0 16px 38px rgba(14,76,123,.18);
  color: #075c82;
  backdrop-filter: blur(7px) saturate(1.06);
}
.trulia-hotspot { cursor:var(--trulia-detail-cursor, zoom-in); }
.google-hotspot { cursor:var(--google-detail-cursor, zoom-in); }
.google-hotspot .hotspot-callout {
  border:1px solid rgba(67,139,143,.48);
  border-radius:5px 34px 34px 34px;
  background:rgba(231,246,233,.96);
  box-shadow:0 16px 38px rgba(27,83,91,.19);
  color:#245d65;
  backdrop-filter:blur(9px) saturate(1.04);
}
.google-hotspot .hotspot-kicker { color:#3c7e7d; }
.google-hotspot .hotspot-title { color:#087f86; }
.google-hotspot .hotspot-role { color:#c66b4f; }
.google-hotspot .hotspot-body {
  border-top-color:rgba(57,133,139,.24);
  color:rgba(36,93,101,.76);
}
.google-hotspot .hotspot-detail-cta {
  border-color:rgba(45,121,126,.45);
  background:rgba(255,255,255,.68);
  color:#2b666d;
}
.trulia-hotspot .hotspot-callout::before { display:none; }
.trulia-hotspot .hotspot-title { color:#075d89; font-size:clamp(25px,2vw,34px); }
.trulia-hotspot .hotspot-role { color:#bd5e62; }
.trulia-hotspot .hotspot-body { border-top-color:rgba(224,132,136,.3); color:rgba(22,72,100,.78); }
.trulia-hotspot.google-hotspot .hotspot-kicker { color:#3c7e7d; }
.trulia-hotspot.google-hotspot .hotspot-title { color:#087f86; }
.trulia-hotspot.google-hotspot .hotspot-role { color:#c66b4f; }
.trulia-hotspot.google-hotspot .hotspot-body {
  border-top-color:rgba(57,133,139,.24);
  color:rgba(36,93,101,.76);
}
.trulia-hotspot.google-hotspot .hotspot-detail-cta {
  border-color:rgba(45,121,126,.52);
  background:#fff;
  color:#2b666d;
  box-shadow:none;
}
.airbnb-hotspot {
  --hotspot-callout-offset:clamp(88px,10.5vh,138px);
  cursor:var(--airbnb-detail-cursor,default);
}
.airbnb-hotspot.is-decorative { cursor:default; }
.airbnb-hotspot:not(.is-decorative) { cursor:none; }
.airbnb-detail-cursor {
  position:fixed;
  z-index:9999;
  left:0;
  top:0;
  width:184px;
  height:184px;
  object-fit:contain;
  pointer-events:none;
  opacity:0;
  transform:translate3d(-100px,-100px,0);
  filter:drop-shadow(0 12px 18px rgba(4,20,38,.22));
  transition:opacity 90ms ease;
  will-change:transform,opacity;
}
.airbnb-detail-cursor.is-visible { opacity:1; }
.is-art-fall-transition .airbnb-detail-cursor { opacity:0 !important; }
.is-art-fall-transition .airbnb-hotspot { pointer-events:none; }
.airbnb-detail-cursor.is-art {
  width:112px;
  height:112px;
  border-radius:50%;
  background:radial-gradient(circle at 38% 36%,rgba(255,255,255,.42),rgba(178,236,255,.16) 42%,rgba(128,202,246,.06) 62%,transparent 76%);
  filter:drop-shadow(0 0 6px rgba(235,253,255,.48)) drop-shadow(0 8px 16px rgba(8,75,125,.18));
}
.airbnb-hotspot .hotspot-callout {
  top:var(--airbnb-callout-anchor-y,-14px);
  width:min(360px,82vw);
  padding:18px 21px 20px;
  border:1px solid rgba(202,160,83,.72);
  border-radius:7px 30px 30px 30px;
  background:
    linear-gradient(135deg,rgba(255,255,255,.1),transparent 34%),
    linear-gradient(150deg,rgba(249,241,216,.96),rgba(225,210,173,.94));
  box-shadow:
    0 18px 44px rgba(2,17,46,.34),
    inset 0 0 0 2px rgba(102,63,29,.12),
    inset 0 1px 0 rgba(255,255,255,.72);
  color:#15264b;
  backdrop-filter:blur(9px) saturate(1.03);
}
.airbnb-hotspot[data-project="airbnb-intro"] { cursor:var(--airbnb-detail-cursor,zoom-in); }
.airbnb-hotspot .hotspot-callout::after {
  content:"";
  position:absolute;
  inset:7px;
  z-index:-1;
  border:1px solid rgba(120,79,37,.16);
  border-radius:3px 23px 23px 23px;
  pointer-events:none;
}
/* Thoughts hover card. Structure, geometry and type scale are the shared
   .hotspot-callout contract; only the colour skin and the free positioning
   (there is no DOM hotspot to anchor to in this world) differ. */
.thoughts-hover-card {
  position: fixed;
  left: 0;
  top: 0;
  transform: none;
  opacity: 1;
  z-index: 30;
  pointer-events: none;
  transition: opacity 180ms ease-out;
}
.thoughts-hover-card[hidden] { display: none; }
.thoughts-hover-card .hotspot-callout-cta {
  pointer-events:auto;
  cursor:pointer;
}
/* Colour skin only, through the shared tokens — the contract a few blocks down
   says scene selectors may set colour tokens and nothing else. The compound
   selector is what makes them stick: the token defaults live on a plain
   `.hotspot-callout`, and this card wears that class itself rather than sitting
   inside a `.world-hotspot`, so a single-class skin ties on specificity and
   loses on order. It did, which is why this world's card was still wearing the
   default teal.
   Hues are sampled from this world's own art: the handmade-paper base runs
   #f0e0d0 → #e0d0c0, the river is a deep indigo #203050, and the island
   markers are stamped in rust. */
.hotspot-callout.thoughts-hover-card {
  --hover-card-border: rgba(244,233,216,.92);
  --hover-card-background: linear-gradient(145deg, rgba(250,243,233,.97),
    rgba(238,226,209,.94) 54%, rgba(226,211,193,.93));
  --hover-card-shadow: 0 18px 42px rgba(74,56,42,.26);
  --hover-card-color: #3b3226;
  --hover-card-kicker: #8a6a52;
  --hover-card-title: #22304f;
  --hover-card-role: #a9614a;
  --hover-card-rule: rgba(140,112,88,.26);
  --hover-card-body: rgba(74,60,45,.8);
}
.art-hotspot .hotspot-callout {
  --hotspot-visible-opacity:.9;
  border-color:rgba(181,239,255,.76);
  border-radius:26px 8px 28px 8px;
  background:
    radial-gradient(circle at 18% 12%,rgba(255,255,255,.78),transparent 27%),
    linear-gradient(145deg,rgba(224,249,255,.92),rgba(183,224,242,.88) 52%,rgba(214,202,247,.88));
  box-shadow:0 20px 48px rgba(11,82,125,.3),inset 0 0 0 2px rgba(255,255,255,.32),
    inset 0 1px 0 rgba(255,255,255,.82);
  color:#153e70;
  opacity:0;
}
.art-hotspot .hotspot-callout::after {
  border-color:rgba(255,255,255,.58);
  border-radius:20px 4px 22px 4px;
}
.art-hotspot .hotspot-kicker { color:#4877a4; }
.art-hotspot .hotspot-title { color:#174d8c; }
.art-hotspot .hotspot-role { color:rgba(43,91,143,.88); }
.art-hotspot .hotspot-body { border-top-color:rgba(61,119,166,.22); color:rgba(29,76,119,.78); }
.art-hotspot .hotspot-detail-cta {
  border-color:rgba(56,112,171,.5); background:rgba(245,253,255,.88); color:#245d9b;
  box-shadow:0 5px 0 rgba(39,89,143,.18),0 10px 22px rgba(36,94,149,.16);
}
.art-hotspot .hotspot-detail-cta:hover { transform:translateY(-2px); box-shadow:0 7px 0 rgba(39,89,143,.16),0 14px 26px rgba(36,94,149,.2); }
.art-hotspot .hotspot-detail-cta:active { transform:translateY(3px); box-shadow:0 2px 0 rgba(39,89,143,.2),0 5px 12px rgba(36,94,149,.14); }
.airbnb-hotspot .hotspot-kicker {
  color:#8d642f;
  font-weight:600;
}
.airbnb-hotspot .hotspot-title {
  color:#14264b;
  font-size:clamp(25px,2vw,34px);
  font-weight:500;
}
.airbnb-hotspot .hotspot-role {
  color:#8a4d41;
  font-size:clamp(18px,1.55vw,23px);
}
.airbnb-hotspot .hotspot-body {
  border-top-color:rgba(139,96,45,.25);
  color:rgba(20,38,75,.78);
}

/* One hover-card contract for every world. Scene selectors below this point
   may set color tokens only; dimensions, type hierarchy and material stay
   identical so a project never changes UI grammar when its world changes. */
.hotspot-callout,
.hotspot-callout-floating {
  --hover-card-border:rgba(255,255,255,.86);
  --hover-card-background:rgba(247,252,239,.96);
  --hover-card-shadow:0 16px 38px rgba(7,83,85,.18);
  --hover-card-color:#1e555c;
  --hover-card-kicker:#388c8e;
  --hover-card-title:#1e555c;
  --hover-card-role:rgba(23,63,70,.86);
  --hover-card-rule:rgba(71,145,142,.17);
  --hover-card-body:rgba(23,63,70,.7);
  --hover-card-cta-border:rgba(45,121,126,.42);
  --hover-card-cta-color:#2b666d;
  display:flex;
  flex-direction:column;
  width:min(380px,calc(100vw - 32px));
  padding:19px 22px 21px;
  border:1px solid var(--hover-card-border);
  border-radius:5px 34px 34px 34px;
  background:var(--hover-card-background);
  box-shadow:var(--hover-card-shadow);
  color:var(--hover-card-color);
  text-align:left;
  -webkit-backdrop-filter:blur(13px) saturate(1.08);
  backdrop-filter:blur(13px) saturate(1.08);
}
.hotspot-callout .hotspot-kicker,
.hotspot-callout-floating .hotspot-kicker {
  display:block;
  margin:0 0 7px;
  color:var(--hover-card-kicker);
  font:500 10px/1.35 var(--mono);
  letter-spacing:.11em;
  text-transform:uppercase;
}
.hotspot-callout .hotspot-title,
.hotspot-callout-floating .hotspot-title {
  display:block;
  margin:0;
  color:var(--hover-card-title);
  /* A short name earns a bigger line. The ceiling is set by the longest
     title the card has to hold: BUPT still lands in two lines and MSRA
     still lands in one. */
  font:500 clamp(24px,2.05vw,38px)/.98 var(--storybook);
  letter-spacing:-.015em;
}
.hotspot-callout .hotspot-role,
.hotspot-callout-floating .hotspot-role {
  display:block;
  margin:8px 0 0;
  color:var(--hover-card-role);
  font:400 clamp(17px,1.3vw,21px)/1.12 var(--storybook);
  letter-spacing:-.005em;
}
/* Education names keep their deliberate wrapping behavior while inheriting
   the same restrained CMU type ruler now shared by every project card. */
.world-hotspot[data-project="bupt"] .hotspot-title,
.world-hotspot[data-project="hci"] .hotspot-title,
.world-hotspot[data-project="msra"] .hotspot-title {
  font-size:clamp(24px, 1.75vw, 32px);
  line-height:.98;
}
.world-hotspot[data-project="hci"] .hotspot-title,
.world-hotspot[data-project="msra"] .hotspot-title { white-space:nowrap; }
.world-hotspot[data-project="bupt"] .hotspot-role,
.world-hotspot[data-project="hci"] .hotspot-role,
.world-hotspot[data-project="msra"] .hotspot-role {
  font-size:clamp(17px, 1.3vw, 21px);
  line-height:1.12;
}
.hotspot-callout .hotspot-body,
.hotspot-callout-floating .hotspot-body {
  display:block;
  margin:12px 0 0;
  padding:11px 0 0;
  border-top:1px solid var(--hover-card-rule);
  color:var(--hover-card-body);
  font:400 clamp(12px,1.05vw,14px)/1.52 var(--body);
  letter-spacing:0;
}
.hotspot-callout .hotspot-detail-cta,
.hotspot-callout-floating .hotspot-detail-cta {
  align-self:flex-start;
  margin:14px 0 0;
  padding:8px 13px;
  border:1px solid var(--hover-card-cta-border);
  border-radius:999px;
  background:rgba(255,255,255,.72);
  box-shadow:none;
  color:var(--hover-card-cta-color);
  font:600 10px/1 var(--mono);
  letter-spacing:.08em;
  text-transform:uppercase;
}
.trulia-hotspot .hotspot-callout {
  --hover-card-border:rgba(219,143,127,.58);
  --hover-card-background:rgba(255,245,225,.96);
  --hover-card-shadow:0 16px 38px rgba(14,76,123,.18);
  --hover-card-color:#075c82;
  --hover-card-kicker:#388c8e;
  --hover-card-title:#075d89;
  --hover-card-role:#bd5e62;
  --hover-card-rule:rgba(224,132,136,.3);
  --hover-card-body:rgba(22,72,100,.78);
}
.google-hotspot .hotspot-callout,
.trulia-hotspot.google-hotspot .hotspot-callout {
  --hover-card-border:rgba(67,139,143,.48);
  --hover-card-background:rgba(231,246,233,.96);
  --hover-card-shadow:0 16px 38px rgba(27,83,91,.19);
  --hover-card-color:#245d65;
  --hover-card-kicker:#3c7e7d;
  --hover-card-title:#087f86;
  --hover-card-role:#c66b4f;
  --hover-card-rule:rgba(57,133,139,.24);
  --hover-card-body:rgba(36,93,101,.76);
}
.airbnb-hotspot .hotspot-callout {
  --hover-card-border:rgba(202,160,83,.72);
  --hover-card-background:linear-gradient(150deg,rgba(249,241,216,.96),rgba(225,210,173,.94));
  --hover-card-shadow:0 18px 44px rgba(2,17,46,.34);
  --hover-card-color:#15264b;
  --hover-card-kicker:#8d642f;
  --hover-card-title:#14264b;
  --hover-card-role:#8a4d41;
  --hover-card-rule:rgba(139,96,45,.25);
  --hover-card-body:rgba(20,38,75,.78);
  --hover-card-cta-border:rgba(139,96,45,.35);
  --hover-card-cta-color:#15264b;
}
.art-hotspot .hotspot-callout {
  --hover-card-border:rgba(181,239,255,.76);
  --hover-card-background:linear-gradient(145deg,rgba(224,249,255,.92),rgba(183,224,242,.88) 52%,rgba(214,202,247,.88));
  --hover-card-shadow:0 20px 48px rgba(11,82,125,.3);
  --hover-card-color:#153e70;
  --hover-card-kicker:#4877a4;
  --hover-card-title:#174d8c;
  --hover-card-role:rgba(43,91,143,.88);
  --hover-card-rule:rgba(61,119,166,.22);
  --hover-card-body:rgba(29,76,119,.78);
  --hover-card-cta-border:rgba(56,112,171,.5);
  --hover-card-cta-color:#245d9b;
}
.hotspot-callout-floating {
  --hover-card-border:rgba(255,255,255,.86);
  --hover-card-background:linear-gradient(145deg,rgba(255,251,241,.94),rgba(232,244,252,.91));
  --hover-card-shadow:0 16px 38px rgba(97,104,134,.18);
  --hover-card-color:#46536b;
  --hover-card-kicker:#8a7a9a;
  --hover-card-title:#464862;
  --hover-card-role:#9b7f76;
  --hover-card-rule:rgba(137,120,160,.2);
  --hover-card-body:rgba(70,83,107,.76);
  --hover-card-cta-border:rgba(137,120,160,.28);
  --hover-card-cta-color:#675c78;
}

/* CTA ink follows the title actually painted by each world's skin. The skin
   selectors above colour some titles directly rather than through inherited
   tokens, so name the matching deep swatch explicitly here. */
.trulia-hotspot .hotspot-callout { --hover-card-cta-bg:#075d89; }
.google-hotspot .hotspot-callout,
.trulia-hotspot.google-hotspot .hotspot-callout { --hover-card-cta-bg:#087f86; }
.airbnb-hotspot .hotspot-callout { --hover-card-cta-bg:#14264b; }
.art-hotspot .hotspot-callout { --hover-card-cta-bg:#174d8c; }
.hotspot-callout.thoughts-hover-card { --hover-card-cta-bg:#22304f; }
.hotspot-callout-floating { --hover-card-cta-bg:#464862; }

@media (max-width:640px) {
  .airbnb-hotspot { --hotspot-callout-offset:clamp(72px,8vh,102px); }
  .hotspot-callout,
  .hotspot-callout-floating {
    width:min(310px,86vw);
    padding:16px 18px 18px;
  }
  .hotspot-callout .hotspot-title,
  .hotspot-callout-floating .hotspot-title { font-size:clamp(24px,1.75vw,32px); }
  .hotspot-callout .hotspot-role,
  .hotspot-callout-floating .hotspot-role { font-size:clamp(17px,1.3vw,21px); }
  .hotspot-callout .hotspot-body,
  .hotspot-callout-floating .hotspot-body { font-size:12px; line-height:1.48; }
}
.hotspot-detail-cta,
.hotspot-building-cta {
  display:inline-flex;
  align-items:center;
  justify-content:center;
  border:1px solid rgba(31,92,96,.42);
  border-radius:999px;
  background:#fff;
  box-shadow:none;
  color:#245d65;
  font:600 10px/1 var(--mono);
  letter-spacing:.08em;
  text-transform:uppercase;
  transform:translateY(0);
  transition:transform 110ms ease-out,background-color 110ms ease-out,filter 110ms ease-out;
}
.hotspot-detail-cta { margin-top:13px; padding:8px 12px; }
.hotspot-detail-cta:hover { background:rgba(255,255,255,.86); filter:brightness(1.02); transform:translateY(-1px); }
.hotspot-detail-cta:active { transform:translateY(0); box-shadow:none; }
.hotspot-callout .hotspot-detail-cta,
.hotspot-callout-floating .hotspot-detail-cta {
  margin-top:14px;
  padding:8px 13px;
  border-color:color-mix(in srgb,var(--hover-card-cta-bg,var(--hover-card-title)) 82%,black);
  background:var(--hover-card-cta-bg,var(--hover-card-title));
  box-shadow:0 5px 12px color-mix(in srgb,var(--hover-card-cta-bg,var(--hover-card-title)) 24%,transparent);
  color:#fff;
}
.hotspot-callout .hotspot-detail-cta:hover,
.hotspot-callout-floating .hotspot-detail-cta:hover {
  background:var(--hover-card-cta-bg,var(--hover-card-title));
  color:#fff;
  filter:brightness(1.13);
}
/* Google's older world-specific CTA selector is more specific than the shared
   skin above. Keep its final painted button on the same deep teal as its title. */
.google-hotspot .hotspot-callout .hotspot-detail-cta,
.trulia-hotspot.google-hotspot .hotspot-callout .hotspot-detail-cta {
  border-color:#076a70;
  background:#087f86;
  color:#fff;
}
.google-hotspot .hotspot-callout .hotspot-detail-cta:hover,
.trulia-hotspot.google-hotspot .hotspot-callout .hotspot-detail-cta:hover {
  background:#087f86;
  color:#fff;
}
.hotspot-building-cta {
  display:none;
}
.trulia-hotspot.is-hovered .hotspot-building-cta,
.trulia-hotspot:focus-visible .hotspot-building-cta { opacity:1; transform:translate(-50%,-50%) scale(1); }

/* Was a verbatim copy of the base rule, including the blur it no longer uses. */
.project-dialog.is-detail-panel .dialog-backdrop {
  /* The world's own colour over a blurred still of it -- the same glass the
     chat machine sits on, so opening a project and opening a conversation
     read as the same room. Affordable because render() early-returns while
     is-story-content-open is set, so the canvas behind is frozen and the
     blur happens once. --scene-scrim-a/b are published on <html> by
     storymode-ask.js; the literals are the fallback if it never ran.
     The Map and About keep their dark ink: they are indexes of the whole
     journey rather than a moment inside one world. */
  background:linear-gradient(145deg,
    var(--scene-scrim-a,rgba(223,247,255,.86)),
    var(--scene-scrim-b,rgba(203,236,248,.80)));
  -webkit-backdrop-filter:blur(14px) saturate(1.12);
  backdrop-filter:blur(14px) saturate(1.12);
}
.project-dialog.is-detail-panel .dialog-card {
  width:min(
    1180px,
    calc(100vw - 224px),
    calc((100vh - 192px) * 2),
    calc((100vh - clamp(120px, 9vh, 220px) - 40px) * 2)
  );
  height:auto;
  aspect-ratio:2 / 1;
  padding:0;
  overflow:hidden;
  border:1px solid rgba(255,255,255,.85);
  border-radius:10px 42px 34px 42px;
  background:
    radial-gradient(circle at 12% 7%,rgba(255,255,255,.98),transparent 28%),
    linear-gradient(145deg,rgba(250,255,239,.97),rgba(224,248,239,.96) 58%,rgba(225,242,253,.96));
  box-shadow:0 42px 120px rgba(4,52,60,.3),inset 0 0 0 5px rgba(255,255,255,.22);
  color:#225b63;
}

/* Shared responsive geometry for canonical 50/50 case panels.
   Trulia Product Launches, Trulia Thoth, and AI Lab split panels use one
   sizing contract; their content and visual skins remain independent. */
@media (min-width:2200px) {
  .project-dialog.is-detail-panel:has(.detail-panel[data-detail-type="split"]:not([hidden])),
  .project-dialog.is-detail-panel.is-split-panel {
    padding-left:15vw;
    padding-right:15vw;
  }

  .project-dialog.is-detail-panel:has(.detail-panel[data-detail-type="split"]:not([hidden])) .dialog-card,
  .project-dialog.is-detail-panel.is-split-panel .dialog-card {
    width:min(70vw,calc((100vh - 192px) * 2));
    max-width:70vw;
    height:auto;
    aspect-ratio:2 / 1;
    margin-inline:auto;
  }

}
.detail-panel {
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  width:100%;
  height:100%;
  min-height:0;
}

/* Canonical Bubble Universe split-case contract. Every image, iframe, or
   video + copy panel shares this geometry, typography, glow and media base;
   worlds may provide content and accent color only. */
.panel-split {
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  width:100%;
  height:100%;
  min-height:0;
  overflow:hidden;
  background:transparent;
}
.panel-split > .case-media,
.panel-split > .case-text-panel {
  width:100%;
  max-width:none;
  min-width:0;
  min-height:0;
}
.panel-split > .case-text-panel {
  --case-copy-width:400px;
  position:relative;
  display:flex;
  flex-direction:column;
  justify-content:safe center;
  overflow:hidden auto;
  padding:clamp(54px,6.2vw,86px) clamp(62px,7.5vw,120px) clamp(54px,6.2vw,86px) clamp(84px,8.4vw,132px);
  background:transparent;
  backdrop-filter:blur(22px) saturate(1.18);
  scrollbar-width:thin;
  scrollbar-color:rgba(86,112,130,.28) rgba(255,255,255,.1);
}
.panel-split .case-glow-base,
.panel-split .case-glow-orb {
  position:absolute;
  left:var(--case-glow-x,50%);
  top:var(--case-glow-y,50%);
  z-index:0;
  display:block;
  pointer-events:none;
  transform:translate3d(-50%,-50%,0);
  will-change:transform;
}
.panel-split .case-glow-base {
  width:100%;
  height:100%;
  background:radial-gradient(ellipse 70% 70% at 50% 50%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),white 14%) 0%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),white 42%) 34%,transparent 72%);
}
.panel-split .case-glow-orb {
  width:136%;
  height:136%;
  opacity:.86;
  background:
    radial-gradient(ellipse 48% 48% at 50% 50%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),transparent 8%),transparent 72%),
    radial-gradient(ellipse 36% 36% at 66% 59%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),transparent 34%),transparent 74%),
    radial-gradient(ellipse 30% 30% at 32% 36%,rgba(255,255,255,.76),transparent 78%);
  filter:blur(34px) saturate(1.08);
  transition:opacity 260ms ease;
}
.project-dialog.is-case-glowing .panel-split .case-glow-orb { opacity:.98; }
.panel-split .case-kicker,
.panel-split .case-title,
.panel-split .case-copy {
  position:relative;
  z-index:1;
  width:min(var(--case-copy-width),100%);
}
.panel-split .case-kicker {
  margin:0 0 24px;
  color:rgba(23,32,51,.54);
  font:400 14px/1.1 var(--body-font,var(--body));
  letter-spacing:0;
  text-transform:none;
}
.panel-split .case-title {
  margin:0 0 28px;
  color:rgba(23,32,51,.9);
  font:300 clamp(56px,5.15vw,72px)/.98 var(--title-font,var(--storybook));
  letter-spacing:0;
  white-space:pre-line;
}
.panel-split .case-copy,
.panel-split .case-copy p {
  color:rgba(23,32,51,.74);
  font:400 18px/1.52 var(--title-font,var(--storybook));
}
.panel-split .case-copy p { margin:0 0 16px; }
.panel-split .case-copy p:last-child { margin-bottom:0; }
.panel-split > .case-media {
  position:relative;
  height:100%;
  margin:0;
  overflow:hidden;
  border-radius:0;
  background:color-mix(in srgb,var(--panel-accent,#93e2fa),white 78%);
}
.panel-split > .case-media > img {
  display:block;
  width:100%;
  height:100%;
  object-fit:cover;
}
.detail-panel[hidden] { display:none; }
.longform-panel[hidden] { display:none; }
.content-module-panel[hidden] { display:none; }
.hotspot-callout-floating[hidden] { display:none; }

.ai-lab-detail-cursor {
  position:fixed;
  z-index:9999;
  left:0;
  top:0;
  width:148px;
  height:148px;
  object-fit:contain;
  pointer-events:none;
  opacity:0;
  transform:translate3d(-120px,-120px,0);
  filter:drop-shadow(0 10px 16px rgba(115,101,132,.2));
  transition:opacity 90ms ease;
  will-change:transform,opacity;
}
.ai-lab-detail-cursor.is-visible { opacity:1; }
.is-route-teleporting .ai-lab-detail-cursor { opacity:0 !important; }

/* Thoughts' paper magnifier. Same follower contract as AI Lab's — the islands
   are WebGL sprites with no DOM hotspot, so a CSS `cursor:` cannot carry art
   this size. The box is not square because the art is not: 1171x798, and the
   pointer offset is measured off the lens in storymode.js rather than assumed
   to be the box centre. */
.thoughts-detail-cursor {
  position:fixed;
  z-index:9999;
  left:0;
  top:0;
  width:164px;
  height:112px;
  object-fit:contain;
  pointer-events:none;
  opacity:0;
  transform:translate3d(-260px,-260px,0);
  filter:drop-shadow(0 10px 16px rgba(74,56,42,.24));
  transition:opacity 90ms ease;
  will-change:transform,opacity;
}
.thoughts-detail-cursor.is-visible { opacity:1; }
.is-route-teleporting .thoughts-detail-cursor { opacity:0 !important; }
/* Touch has no hover position. Never let a synthetic pointermove leave a
   desktop magnifier parked over the rotated mobile composition. */
html.is-mobile-storymode :is(.airbnb-detail-cursor,.ai-lab-detail-cursor,.thoughts-detail-cursor) {
  display:none !important;
}
html.is-mobile-storymode :is(.trulia-hotspot,.google-hotspot,.airbnb-hotspot,#worldCanvas) {
  cursor:default !important;
}
/* The finale's plate covers the canvas, so the Thoughts pointer handler stops
   firing and can never clear the magnifier on its own. */
.is-reflection-scene .thoughts-detail-cursor { opacity:0 !important; }
.is-reflection-scene .airship-controls,
.is-reflection-scene .airship-visual-overlay { display: none; }

.hotspot-callout-floating {
  --hover-card-border:rgba(255,255,255,.86);
  --hover-card-background:
    radial-gradient(circle at 18% 12%,rgba(255,255,255,.92),transparent 30%),
    linear-gradient(145deg,rgba(255,251,241,.94),rgba(232,244,252,.91));
  --hover-card-shadow:0 16px 38px rgba(97,104,134,.18);
  --hover-card-color:#46536b;
  --hover-card-kicker:#8a7a9a;
  --hover-card-title:#464862;
  --hover-card-role:#9b7f76;
  --hover-card-rule:rgba(137,120,160,.2);
  --hover-card-body:rgba(70,83,107,.76);
  --hover-card-cta-border:rgba(137,120,160,.28);
  --hover-card-cta-color:#675c78;
  position:fixed;
  z-index:62;
  cursor:pointer;
  opacity:.9;
}
.hotspot-callout-cta {
  align-self:flex-start;
}
.hotspot-callout-cta[hidden] {
  display:none;
}

/* Also worn by the vault's chest, which likewise has one thing to say. Sharing
   the rule rather than copying it: a lookalike written twice is a lookalike
   that drifts the first time either one is touched.
   Orbiting AI Lab planets only announce their name. The full information
   card—and its magnifier—belong exclusively to the planet currently held in
   focus. This modifier deliberately keeps the shared card's colour tokens but
   removes every content affordance except the project title. */
.hotspot-callout-floating.is-planet-name-tip,
.airbnb-hotspot.is-simple-label .hotspot-callout {
  border:0;
  width:max-content;
  max-width:min(240px,calc(100vw - 32px));
  padding:8px 13px 9px;
  border-radius:14px;
  cursor:default;
  pointer-events:none;
  box-shadow:0 8px 20px rgba(97,104,134,.14);
  -webkit-backdrop-filter:blur(9px) saturate(1.04);
  backdrop-filter:blur(9px) saturate(1.04);
}
.hotspot-callout-floating.is-planet-name-tip .hotspot-kicker,
.hotspot-callout-floating.is-planet-name-tip .hotspot-role,
.hotspot-callout-floating.is-planet-name-tip .hotspot-body,
.hotspot-callout-floating.is-planet-name-tip .hotspot-callout-cta,
.airbnb-hotspot.is-simple-label .hotspot-callout::before,
.airbnb-hotspot.is-simple-label .hotspot-callout::after,
.airbnb-hotspot.is-simple-label .hotspot-kicker,
.airbnb-hotspot.is-simple-label .hotspot-role,
.airbnb-hotspot.is-simple-label .hotspot-body,
.airbnb-hotspot.is-simple-label .hotspot-callout-cta {
  display:none;
}
.hotspot-callout-floating.is-planet-name-tip .hotspot-title,
.airbnb-hotspot.is-simple-label .hotspot-title {
  overflow:hidden;
  max-width:214px;
  color:var(--hover-card-title);
  font:500 16px/1.15 var(--storybook);
  letter-spacing:0;
  text-overflow:ellipsis;
  white-space:nowrap;
}

/* Long panels start below the site nav, with the same air under it that the
   nav itself has above it. The header is 68px tall and its controls sit 18px
   from the top, so clearing the header clears exactly that much again. */
.project-dialog.is-content-module-panel {
  --long-panel-top:68px;
  padding:var(--long-panel-top) 56px 0;
  align-items:flex-start;
}
.project-dialog.is-content-module-panel .dialog-card {
  --image-stack-panel-top:var(--long-panel-top);
  --image-stack-overhang:0px;
  width:min(calc(100vw - 112px),2400px);
  height:calc(100vh - var(--long-panel-top));
  height:calc(100dvh - var(--long-panel-top));
  min-height:calc(100vh - var(--long-panel-top));
  min-height:calc(100dvh - var(--long-panel-top));
  max-height:none;
  margin:0 auto;
  padding:0;
  overflow:hidden;
  border-radius:10px 42px 0 0;
  background:#fafafa;
}
.project-dialog.is-content-module-panel .dialog-close {
  z-index:4;
}
@media (min-width:2200px) {
  .project-dialog.is-content-module-panel {
    padding-left:15vw;
    padding-right:15vw;
  }
  .project-dialog.is-content-module-panel .dialog-card {
    width:70vw;
    max-width:70vw;
  }
}
.content-module-panel {
  position:relative;
  width:100%;
  height:100%;
  background:#fafafa;
  overflow:hidden;
  overscroll-behavior:contain;
}
.project-dialog.is-content-preparing .content-module-panel::after {
  content:"";
  position:absolute;
  inset:0;
  z-index:20;
  border-radius:inherit;
  background:
    linear-gradient(100deg,
      rgba(250,250,250,0) 0 38%,
      rgba(224,241,244,.72) 50%,
      rgba(250,250,250,0) 62% 100%),
    radial-gradient(circle at 50% 44%,rgba(255,255,255,.98),rgba(250,250,250,.98) 42%),
    #fafafa;
  background-size:260% 100%,100% 100%,100% 100%;
  animation:panel-door-shimmer 1.35s ease-in-out infinite;
  pointer-events:auto;
}
@keyframes panel-door-shimmer {
  0% { background-position:120% 0,0 0,0 0; }
  100% { background-position:-120% 0,0 0,0 0; }
}
/* Composite portfolio panels are a continuous sheet from their top offset to
   the live viewport bottom. Keep the scroller at the card's full height so a
   short screen never exposes the backdrop below the content surface. */
.content-module-panel > .case-body {
  width:100%;
  height:100%;
  overflow:auto;
  overscroll-behavior:contain;
}

/* The decade timeline is itself the whole case study, so give it the same
   top-to-bottom reading surface as the dedicated long panels. */
.project-dialog.is-content-module-panel.is-airbnb-decade-panel {
  --long-panel-top:40px;
}

/* Every project renderer shares one image lifecycle. The first visible images
   hold the panel's opaque door; later native-lazy images keep this shimmer in
   their own box until load + decode completes. `!important` deliberately wins
   over imported gallery rules that otherwise keep an unseen image transparent,
   which was the source of the large blank white panels on a cold cache. */
.project-dialog img.is-panel-image-loading {
  opacity: 1 !important;
  color: transparent;
  background-color: rgba(228, 236, 238, .78);
  background-image: linear-gradient(100deg,
    rgba(255,255,255,.10) 0 38%,
    rgba(255,255,255,.72) 50%,
    rgba(255,255,255,.10) 62% 100%);
  background-size: 260% 100%;
  animation: panel-image-shimmer 1.25s ease-in-out infinite;
}
.project-dialog img.is-panel-image-ready {
  animation: none;
}
@keyframes panel-image-shimmer {
  0% { background-position: 120% 0; }
  100% { background-position: -120% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .project-dialog img.is-panel-image-loading { animation: none; }
}
.content-module-panel > .case-body .airbnb-tab-section-title {
  font-family:var(--storybook) !important;
  font-weight:300 !important;
  line-height:.98 !important;
  letter-spacing:0 !important;
}
.content-module-panel > .case-body .airbnb-tab-feature h3 {
  font-family:var(--storybook) !important;
  font-size:clamp(30px,3.8vw,50px) !important;
  font-style:italic;
  font-weight:300 !important;
  letter-spacing:-.01em !important;
}
@media (max-width: 720px) {
  .project-dialog.is-content-module-panel { padding:14px 10px 0; }
  .project-dialog.is-content-module-panel .dialog-card {
    width:calc(100vw - 20px);
    height:calc(100vh - 14px);
    height:calc(100dvh - 14px);
    min-height:calc(100vh - 14px);
    min-height:calc(100dvh - 14px);
    border-radius:10px 26px 0 0;
  }
  .project-dialog.is-content-module-panel.is-airbnb-decade-panel {
    --long-panel-top:10px;
    padding-top:var(--long-panel-top);
  }
  .project-dialog.is-content-module-panel.is-airbnb-decade-panel .dialog-card {
    height:calc(100vh - var(--long-panel-top));
    height:calc(100dvh - var(--long-panel-top));
    min-height:calc(100vh - var(--long-panel-top));
    min-height:calc(100dvh - var(--long-panel-top));
  }
}
.longform-panel,
.longform-panel .case-body.is-image-stack {
  width:100%;
  height:100%;
  min-height:0;
}
.project-dialog.is-detail-panel.is-longform-panel {
  --layout-vw:100vw;
  --layout-vh:100vh;
  --layout-half-vw:50vw;
  --layout-half-vh:50vh;
  --image-stack-max-width:2400px;
  --image-stack-base-height:min(calc(var(--layout-half-vw) - 56px),calc(var(--layout-vh) - 96px),1200px);
  --image-stack-overhang:48px;
  --image-stack-panel-top:40px;
  place-items:start center;
  padding:0;
}
.project-dialog.is-detail-panel.is-longform-panel .dialog-card {
  width:min(calc(var(--layout-vw) - 112px),calc((var(--layout-vh) - 96px) * 2),var(--image-stack-max-width));
  height:calc(var(--layout-vh) - var(--image-stack-panel-top) + var(--image-stack-overhang));
  max-height:none;
  aspect-ratio:auto;
  margin-top:var(--image-stack-panel-top);
  border:1px solid rgba(255,255,255,.85);
  border-radius:10px 42px 34px 42px;
  background:#fafafa;
  box-shadow:inset 0 1px 24px rgba(255,255,255,.36),0 28px 78px rgba(61,129,177,.15);
}
@media (min-width:2200px) {
  .project-dialog.is-detail-panel.is-longform-panel {
    padding-left:15vw;
    padding-right:15vw;
  }
  .project-dialog.is-detail-panel.is-longform-panel .dialog-card {
    width:70vw;
    max-width:70vw;
  }
}
.longform-panel .case-body.is-image-stack {
  display:block;
  height:calc(100% - 48px);
  overflow:hidden auto;
  scrollbar-width:thin;
  scrollbar-color:rgba(86,112,130,.34) rgba(255,255,255,.1);
  background:#fafafa;
}
.longform-panel .case-stack-intro {
  display:grid;
  grid-template-columns:minmax(0,18fr) minmax(120px,3fr);
  column-gap:11px;
  align-items:center;
  min-height:42%;
  padding:clamp(64px,7.2vw,104px) clamp(62px,7.8vw,124px) clamp(48px,5.6vw,84px);
  background:#fafafa;
}
.longform-panel .case-stack-intro-copy {
  position:relative;
  max-width:min(980px,100%);
  --text-highlight-color:#67d4dc;
}
.longform-panel .case-stack-eyebrow {
  margin:0 0 28px;
  color:#000;
  font-family:var(--title-font);
  font-size:clamp(17px,1.56vw,20.7px);
  font-weight:300;
  line-height:1.3777;
  letter-spacing:0;
}
.longform-panel .case-stack-lede {
  max-width:1000px;
  color:#000;
  font-family:var(--title-font);
  font-size:clamp(30px,2.92vw,38.75px);
  font-weight:300;
  line-height:1.2992;
  letter-spacing:0;
}
.longform-panel .case-stack-brand-highlight,
.longform-panel .case-stack-highlight {
  position:relative;
  display:inline-block;
  white-space:nowrap;
  z-index:0;
}
.longform-panel .case-stack-brand-highlight { margin-right:20px; }
.longform-panel .case-stack-shape {
  position:absolute;
  display:block;
  pointer-events:none;
  overflow:visible;
  z-index:-1;
}
.longform-panel .case-stack-shape path {
  fill:none;
  stroke:var(--text-highlight-color);
  stroke-width:.1em;
  stroke-linecap:square;
  stroke-linejoin:bevel;
}
.longform-panel .case-stack-google-shape {
  width:3.5em;
  height:1.2727em;
  left:-.0909em;
  top:.0455em;
}
.longform-panel .case-stack-google-shape path {
  stroke-dasharray:256;
  stroke-dashoffset:256;
  animation:caseStackShapeDraw 920ms cubic-bezier(.22,1,.36,1) 260ms forwards;
}
.longform-panel .case-stack-project-shape {
  width:4.4545em;
  height:1.2727em;
  left:0;
  top:-.03em;
}
.longform-panel .case-stack-project-shape path {
  stroke-dasharray:204;
  stroke-dashoffset:204;
  animation:caseStackShapeDraw 980ms cubic-bezier(.22,1,.36,1) 620ms forwards;
}
@keyframes caseStackShapeDraw { to { stroke-dashoffset:0; } }
.longform-panel .case-image-stack { width:100%; margin:0; padding:0; font-size:0; line-height:0; }
.longform-panel .case-stack-frame {
  position:relative;
  display:block;
  width:100%;
  margin:0;
  aspect-ratio:10000 / 2150;
  border:0;
  overflow:hidden;
  background:transparent;
}
.longform-panel .case-stack-image {
  position:absolute;
  inset:0;
  display:block;
  width:100%;
  height:100%;
  border:0;
  object-fit:cover;
  object-position:var(--stack-focal-point,50% 50%);
  opacity:1;
  vertical-align:bottom;
}
/* The copyright band that closes every long panel. Bubble Universe styles
   `.case-stack-footer` unqualified (styles.css), and this was ported behind a
   `.longform-panel` qualifier — but the module-rendered panels (Thoughts, the
   Airbnb natives) live in `.content-module-panel`, so the qualifier meant the
   band never applied there: no height, no tint, no centring, left-aligned in
   the inherited body font. Unqualified, as upstream, so it closes every host. */
.case-stack-footer {
  min-height:78.656px;
  padding:0 clamp(54px,7.2vw,112px);
  display:flex;
  align-items:center;
  justify-content:center;
  background:rgba(213,242,252,.18);
  color:rgba(78,111,132,.42);
  font:300 clamp(13px,1.0866vw,14.41px)/1.8 "Poppins",var(--body);
  text-align:center;
}
.case-stack-footer .case-stack-footer-copy {
  margin:0;
  color:inherit;
  font:inherit;
  text-align:center;
}
.case-stack-footer .case-stack-footer-copy span { color:inherit; font:inherit; }
.detail-panel-media {
  position:relative;
  display:grid;
  place-items:center;
  min-width:0;
  min-height:0;
  margin:0;
  padding:0;
  overflow:hidden;
  background:#c8f5ca;
}
.detail-panel-media img {
  display:block;
  width:100%;
  height:100%;
  object-fit:cover;
  filter:none;
}
.detail-panel[data-detail-type="profile"] .detail-panel-media {
  overflow:visible;
  background:transparent;
}
.panel-profile-icon { display:none; }
.detail-panel[data-detail-type="profile"] #detailPanelImage { display:none; }
.detail-panel[data-detail-type="profile"] .panel-profile-icon {
  position:absolute;
  display:block;
  opacity:0;
  transform-origin:50% 50%;
  animation:truliaIdentityDropIn .72s cubic-bezier(.16,.88,.18,1) forwards;
}
.detail-panel[data-detail-type="profile"] .panel-profile-icon-logo {
  position:absolute;
  left:12%;
  top:11%;
  width:min(72%,560px);
  max-height:72%;
  transform:rotate(-6deg);
}
.detail-panel[data-detail-type="profile"] .panel-profile-icon-home {
  left:39%;
  top:42%;
  z-index:2;
  width:min(58%,440px);
  max-height:58%;
  transform:rotate(8deg);
  animation-delay:.12s;
}
.detail-panel[data-detail-type="profile"] .panel-profile-icon img {
  position:static;
  display:block;
  width:100%;
  height:auto;
  max-height:none;
  object-fit:contain;
  filter:drop-shadow(0 16px 30px rgba(67,98,128,.13)) drop-shadow(0 0 24px rgba(255,255,255,.26));
  animation:truliaIdentityFloatA 4.8s ease-in-out .78s infinite;
}
.detail-panel[data-detail-type="profile"] .panel-profile-icon-home img {
  animation-name:truliaIdentityFloatB;
  animation-duration:5.4s;
  animation-delay:.9s;
}
.detail-panel[data-detail-id="airbnb-intro"] .panel-profile-icon-home {
  left:42%;
  top:46%;
  width:min(46%,350px);
  max-height:50%;
}
.detail-panel[data-detail-type="split"] .detail-panel-copy {
  --case-glow-color:#c8f5ca;
}
.detail-panel[data-detail-id="trulia-thoth-dashboard"] .detail-panel-copy {
  --case-glow-color:#96f5f2;
}
.detail-panel[data-detail-id="ai-lab-sunnys-journey"] .detail-panel-copy,
.detail-panel[data-detail-id="ai-lab-bubble-universe"] .detail-panel-copy,
.detail-panel[data-detail-id="ai-lab-vibe-coding-101"] .detail-panel-copy {
  --case-glow-color:#93e2fa;
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-media-open,
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-media-open-corner {
  position:absolute;
  z-index:3;
  color:rgba(34,46,72,.68);
  border:1px solid rgba(255,255,255,.78);
  text-decoration:none;
  background:rgba(255,255,255,.82);
  box-shadow:0 14px 32px rgba(61,129,177,.15);
  backdrop-filter:blur(14px) saturate(1.12);
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-media-open {
  left:50%;
  bottom:clamp(20px,2.4vw,34px);
  display:inline-flex;
  align-items:center;
  gap:9px;
  padding:12px 20px;
  border-radius:999px;
  font:400 clamp(13px,.95vw,15px)/1 var(--body-font,var(--sans));
  white-space:nowrap;
  transform:translateX(-50%);
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-media-open-corner {
  top:16px;
  right:18px;
  display:grid;
  place-items:center;
  width:34px;
  height:34px;
  border-radius:50%;
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-media-open svg,
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-media-open-corner svg {
  width:1.25em;
  height:1.25em;
  fill:none;
  stroke:currentColor;
  stroke-width:1.35;
  stroke-linecap:round;
  stroke-linejoin:round;
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-media-open-corner svg {
  width:50%;
  height:50%;
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) [aria-disabled="true"] {
  cursor:default;
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-glow-base {
  width:72%;
  height:72%;
  opacity:.42;
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-glow-orb {
  width:92%;
  height:92%;
  opacity:.34;
  filter:blur(27px) saturate(1.02);
}
.project-dialog.is-case-glowing .detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-bubble-universe"],[data-detail-id="ai-lab-vibe-coding-101"]) .case-glow-orb {
  opacity:.46;
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-vibe-coding-101"]) .detail-panel-media {
  background:linear-gradient(145deg,#dff7f1,#d9e8ff 58%,#f4e5f7);
}
.detail-panel:is([data-detail-id="ai-lab-sunnys-journey"],[data-detail-id="ai-lab-vibe-coding-101"]) .detail-panel-media img {
  width:82%;
  height:82%;
  object-fit:contain;
}
.detail-panel[data-detail-id="ai-lab-sunnys-journey"] .detail-panel-media {
  background:#10a9d2;
}
.detail-panel[data-detail-id="ai-lab-sunnys-journey"] .detail-panel-media img {
  width:100%;
  height:100%;
  object-fit:cover;
  border:0;
  box-shadow:none;
}

.about-overlay {
  /* The loading panel's box, named once. The shimmer draws itself with these
     and the close button hangs off the same corner, so the two cannot drift
     apart -- which is exactly what happened while the button was positioned
     from the book's measured geometry instead: a different rectangle, and one
     that does not exist yet while the book is still being fetched. */
  --about-shimmer-width:min(72vw,1180px);
  --about-shimmer-height:min(76vh,760px);
  --about-book-gutter:clamp(14px,2vw,34px);
  --about-book-width:min(calc(100vw - (2 * var(--about-book-gutter))),1672px);
  --about-book-height:calc(var(--about-book-width) * 941 / 1672);
  position:fixed;
  inset:0;
  z-index:1200;
  padding:0;
}
.about-overlay[hidden] { display:none; }
.about-overlay__scrim {
  position:absolute;
  inset:0;
  border:0;
  background:rgba(4,11,24,.77);
  -webkit-backdrop-filter:none;
  backdrop-filter:none;
}
.about-overlay__stage {
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  overflow:visible;
  border:0;
  border-radius:0;
  background:transparent;
  box-shadow:none;
}
.about-overlay__stage iframe {
  display:block;
  width:100%;
  height:100%;
  border:0;
  background:transparent;
  opacity: 1;
  transition: opacity 260ms ease;
}
/* Held until the About document has finished assembling itself. The panel keeps
   the book's own footprint and breathes, so the reader is looking at something
   settled rather than watching it build. */
.about-overlay.is-loading .about-overlay__stage iframe { opacity: 0; }
.about-overlay.is-loading .about-overlay__stage::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  width: var(--about-shimmer-width);
  height: var(--about-shimmer-height);
  border-radius: 10px 16px 12px 18px / 14px 10px 18px 12px;
  background: linear-gradient(100deg,
    rgba(247,241,226,.10) 0 38%, rgba(247,241,226,.22) 50%, rgba(247,241,226,.10) 62% 100%);
  background-size: 260% 100%;
  animation: skill-slot-shimmer 1.25s ease-in-out infinite;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .about-overlay.is-loading .about-overlay__stage::after { animation: none; }
}
/* A shimmer says something is happening. It does not say what, or whether it is
   getting anywhere -- and a wait with no end in sight is the part that is hard
   to sit through, not the wait itself. This says both, in the book's own voice. */
.about-overlay__note {
  position: absolute;
  z-index: 1;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  display: grid;
  gap: 14px;
  justify-items: center;
  margin: 0;
  color: rgba(247,241,226,.86);
  font: 300 clamp(15px, 1.5vw, 19px)/1.5 var(--storybook, serif);
  letter-spacing: .02em;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 400ms ease;
}
/* Only after a beat: a book that arrives at once should never have said a word. */
.about-overlay.is-loading .about-overlay__note {
  opacity: 1;
  transition-delay: 550ms;
}
.about-overlay__bar {
  display: block;
  width: min(38vw, 260px);
  height: 2px;
  overflow: hidden;
  border-radius: 2px;
  background: rgba(247,241,226,.18);
}
.about-overlay__bar i {
  display: block;
  width: 0%;
  height: 100%;
  border-radius: inherit;
  background: rgba(247,241,226,.72);
  /* Eased, so a batch of pictures landing together reads as filling rather
     than as a jump. */
  transition: width 420ms cubic-bezier(.22,.61,.36,1);
}
/* The fallbacks are the book's own corner, not the viewport's edge.

   Those two variables are written by measuring the book inside the iframe, so
   they do not exist yet while it is loading -- and the old fallback put the
   close button against the right edge of the screen, halfway down, stranded far
   outside the panel it belongs to, for the whole of the wait. The book's box is
   known from these variables without asking the iframe anything, so the corner
   is right from the first frame and the measurement only refines it. */
.about-overlay .about-overlay__close.story-map__close {
  top:var(--about-book-top,calc(50% - (var(--about-book-height) / 2) + 30px));
  left:var(--about-book-visible-right,calc(50% + (var(--about-book-width) / 2) - 8px));
  right:auto;
  z-index:2;
  translate:-50% -50%;
}
/* While the book is being fetched there is no book to measure, and the only
   thing on screen is the shimmer panel. Hang the close on its top-right corner
   -- the same corner, from the same two values the panel is drawn from. */
.about-overlay.is-loading .about-overlay__close.story-map__close {
  top: calc(50% - (var(--about-shimmer-height) / 2) + 22px);
  left: calc(50% + (var(--about-shimmer-width) / 2) - 22px);
}
html.is-mobile-storymode .about-overlay .about-overlay__close.story-map__close {
  position:absolute;
  top:18px;
  left:auto;
  right:18px;
  translate:none;
}
/* The About loader lives inside the phone's short landscape canvas. Reusing
   the desktop vw sizing made the shimmer, status line, and progress rail read
   like a full-screen title card after the virtual viewport was rotated and
   scaled. Keep the same quiet composition, but size it from the mobile canvas
   and give Sunny's aside enough measure to avoid an orphaned final word. */
html.is-mobile-storymode .about-overlay {
  --about-shimmer-width:min(66vw,560px);
  --about-shimmer-height:min(68vh,280px);
}
html.is-mobile-storymode .about-overlay__note {
  width:min(64vw,480px);
  gap:9px;
  font-size:14px;
  line-height:1.35;
}
html.is-mobile-storymode .about-overlay__bar {
  width:min(34vw,220px);
}
html.is-mobile-storymode .about-overlay__aside {
  width:100%;
  max-width:52ch;
  margin-top:.6em;
  font-size:10px;
  line-height:1.4;
  text-wrap:balance;
}
html.is-mobile-storymode[lang="zh-CN"] .about-overlay__aside {
  max-width:34em;
}
body.is-about-overlay-open { overflow:hidden; }

/* Modal ownership is global. Three.js raycasters and document-level pointer
   listeners cannot be stopped by the scrim alone, so hide and disarm every
   scene-owned hover surface while Map or About owns the viewport. */
html.is-story-map-open .world-hotspot,
body.is-about-overlay-open .world-hotspot {
  pointer-events:none!important;
}
/* The detail cursors belong to the scene and its buildings. Every overlay
   that covers the stage has to put them away -- the chat was the one that
   still let its magnifier follow the pointer into the lightbox. */
html.is-story-map-open :is(.airbnb-detail-cursor,.ai-lab-detail-cursor,.thoughts-detail-cursor,.hotspot-callout-floating,.thoughts-hover-card),
html.is-ask-sunny-open :is(.airbnb-detail-cursor,.ai-lab-detail-cursor,.thoughts-detail-cursor,.hotspot-callout-floating,.thoughts-hover-card),
body.is-about-overlay-open :is(.airbnb-detail-cursor,.ai-lab-detail-cursor,.thoughts-detail-cursor,.hotspot-callout-floating,.thoughts-hover-card) {
  opacity:0!important;
  visibility:hidden!important;
  pointer-events:none!important;
}
.detail-panel-copy {
  position:relative;
  display:flex;
  flex-direction:column;
  justify-content:safe center;
  min-width:0;
  min-height:0;
  padding:clamp(54px,6.2vw,86px) clamp(62px,7.5vw,120px) clamp(54px,6.2vw,86px) clamp(84px,8.4vw,132px);
  overflow:hidden auto;
  background:transparent;
  color:#225b63;
  scrollbar-width:thin;
  scrollbar-color:rgba(86,112,130,.28) rgba(255,255,255,.1);
}
.detail-panel-glow-base,
.detail-panel-glow-orb {
  position:absolute;
  left:var(--case-glow-x,50%);
  top:var(--case-glow-y,50%);
  z-index:0;
  display:block;
  pointer-events:none;
  transform:translate3d(-50%,-50%,0);
  will-change:transform;
}
.detail-panel-glow-base {
  width:100%;
  height:100%;
  background:radial-gradient(ellipse 70% 70% at 50% 50%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),white 14%) 0%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),white 42%) 34%,transparent 72%);
}
.detail-panel-glow-orb {
  width:136%;
  height:136%;
  opacity:.86;
  background:
    radial-gradient(ellipse 48% 48% at 50% 50%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),transparent 8%),transparent 72%),
    radial-gradient(ellipse 36% 36% at 66% 59%,color-mix(in srgb,var(--case-glow-color,#c8f5ca),transparent 34%),transparent 74%),
    radial-gradient(ellipse 30% 30% at 32% 36%,rgba(255,255,255,.76),transparent 78%);
  filter:blur(34px) saturate(1.08);
}
.detail-panel[data-detail-type="profile"] .detail-panel-glow-base {
  width:150%;
  height:150%;
  opacity:.38;
  background:radial-gradient(ellipse 48% 44% at 50% 50%,rgba(103,212,220,.56),rgba(103,212,220,.18) 42%,transparent 76%);
  filter:blur(30px) saturate(1.04);
}
.detail-panel[data-detail-type="profile"] .detail-panel-glow-orb {
  width:170%;
  height:170%;
  opacity:.28;
  background:radial-gradient(ellipse 38% 34% at 50% 50%,rgba(103,212,220,.50),rgba(103,212,220,.14) 42%,transparent 74%);
  filter:blur(36px) saturate(1.04);
}
.project-dialog.is-detail-panel .dialog-card:has(.detail-panel[data-detail-id="airbnb-intro"]:not([hidden])) {
  background:
    radial-gradient(circle at 13% 9%,rgba(255,255,255,.94),transparent 27%),
    linear-gradient(145deg,rgba(249,242,220,.98),rgba(229,222,191,.97) 52%,rgba(211,224,224,.97));
  border-color:rgba(218,188,123,.82);
}
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-copy {
  color:#172b4d;
}
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-role {
  color:rgba(131,72,59,.78);
}
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-meta {
  color:rgba(102,73,37,.58);
}
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-body {
  color:rgba(23,43,77,.68);
}
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-glow-base {
  background:radial-gradient(ellipse 48% 44% at 50% 50%,rgba(103,212,220,.56),rgba(103,212,220,.18) 42%,transparent 76%);
}
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-glow-orb {
  background:radial-gradient(ellipse 38% 34% at 50% 50%,rgba(103,212,220,.50),rgba(103,212,220,.14) 42%,transparent 74%);
}
.detail-panel-copy > :not(.detail-panel-glow-base):not(.detail-panel-glow-orb) {
  position:relative;
  z-index:1;
}
.detail-panel-kicker {
  margin:0 0 22px;
  color:rgba(23,32,51,.54);
  font:500 clamp(11px,.85vw,15px)/1.4 var(--mono);
  letter-spacing:.13em;
  text-transform:uppercase;
}
.detail-panel[data-detail-type="split"] .detail-panel-kicker {
  color:rgba(23,32,51,.38);
  font-weight:300;
}
.detail-panel-copy h2 {
  width:min(400px,100%);
  margin:0 0 28px;
  color:rgba(23,32,51,.9);
  font-family:var(--storybook);
  font-size:clamp(56px,5.15vw,72px);
  font-weight:300;
  line-height:.98;
  letter-spacing:0;
}
.detail-panel-meta {
  width:min(400px,100%);
  margin:0 0 24px;
  color:rgba(23,32,51,.62);
  font:400 14px/1.5 var(--body);
  letter-spacing:.08em;
  white-space:pre-line;
}
.detail-panel-body {
  width:min(400px,100%);
  color:rgba(23,32,51,.74);
  font-family:var(--body);
  font-size:16px;
  font-weight:300;
  line-height:1.42;
}
.detail-panel[data-detail-type="split"] .detail-panel-body {
  color:rgba(23,32,51,.58);
  font-weight:300;
}
.detail-panel-body p { margin:0 0 16px; }
.detail-panel-body p:last-child { margin-bottom:0; }
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-body p.is-identity-heading {
  margin:24px 0 8px;
  color:rgba(23,32,51,.82);
  font-family:var(--storybook);
  font-size:18px;
  font-weight:500;
  line-height:1.32;
}
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-accordion {
  position:relative;
  margin:0 0 26px;
}
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-body p.has-identity-accordion-toggle {
  position:relative;
  margin-bottom:12px;
}
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-toggle {
  display:inline-flex;
  align-items:center;
  gap:5px;
  margin:0 0 0 7px;
  padding:0;
  border:0;
  border-radius:999px;
  color:rgba(86,112,130,.54);
  background:transparent;
  box-shadow:none;
  font:inherit;
  font-weight:300;
  cursor:pointer;
  opacity:.82;
  vertical-align:baseline;
  transition:color 180ms ease,opacity 180ms ease,text-shadow 220ms ease;
}
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-toggle svg {
  width:1.05em;
  height:1.05em;
  fill:none;
  stroke:currentColor;
  stroke-width:1.8;
  stroke-linecap:round;
  stroke-linejoin:round;
  transition:transform 220ms ease;
}
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-toggle:hover,
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-toggle:focus-visible {
  color:rgba(23,32,51,.66);
  opacity:1;
  text-shadow:0 0 16px rgba(255,255,255,.62);
}
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-toggle.is-open svg,
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-accordion.is-open .identity-bullet-toggle svg {
  transform:rotate(180deg);
}
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-list {
  display:grid;
  gap:20px;
}
.detail-panel[data-detail-id="airbnb-intro"] .identity-bullet-list[hidden] { display:none; }
.detail-panel[data-detail-id="airbnb-intro"] .detail-panel-body p.is-identity-bullet {
  width:calc(100% - 24px);
  margin:0 0 0 24px;
  padding-left:8px;
  text-indent:-8px;
  font-size:16px;
  line-height:1.54;
}

/* Identity panels deliberately inherit Bubble Universe's proven 980×620
   responsive structure. Story Mode changes only the material skin. */
.project-dialog.is-detail-panel:not(.is-longform-panel) .dialog-card:has(.detail-panel[data-detail-type="profile"]:not([hidden])) {
  width:min(980px,calc(100vw - 92px));
  height:min(620px,calc(100vh - 104px));
  aspect-ratio:auto;
  border:1px solid rgba(255,255,255,.85);
  background:
    radial-gradient(circle at 12% 7%,rgba(255,255,255,.98),transparent 28%),
    linear-gradient(145deg,rgba(250,255,239,.97),rgba(224,248,239,.96) 58%,rgba(225,242,253,.96));
  -webkit-backdrop-filter:blur(18px) saturate(1.12);
  backdrop-filter:blur(18px) saturate(1.12);
}
.detail-panel[data-detail-type="profile"] {
  position:relative;
  grid-template-columns:minmax(220px,.82fr) minmax(380px,1.18fr);
  column-gap:clamp(28px,4.6vw,64px);
  align-items:stretch;
  padding:clamp(42px,5.8vw,70px) clamp(44px,5.8vw,82px);
  overflow:hidden;
  background:transparent;
}
.detail-panel[data-detail-type="profile"] .detail-panel-copy {
  position:static;
  display:block;
  padding:clamp(6px,1vw,12px) 8px clamp(6px,1vw,12px) 0;
  overflow:hidden auto;
  background:transparent;
}
.detail-panel[data-detail-type="profile"] .detail-panel-media,
.detail-panel[data-detail-type="profile"] .detail-panel-copy > :not(.detail-panel-glow-base):not(.detail-panel-glow-orb) {
  position:relative;
  z-index:2;
}
.detail-panel[data-detail-type="profile"] .detail-panel-copy h2 {
  width:100%;
  margin:0 0 18px;
  color:rgba(23,32,51,.9);
  font-family:var(--storybook);
  font-size:clamp(44px,5vw,64px);
  font-weight:300;
  line-height:.98;
}
.detail-panel-role {
  display:none;
}
.detail-panel[data-detail-type="profile"] .detail-panel-role {
  display:block;
  width:100%;
  margin:0 0 14px;
  color:rgba(54,87,105,.58);
  font-family:var(--storybook);
  font-size:clamp(22px,2.25vw,29px);
  font-style:italic;
  font-weight:300;
  line-height:1.15;
  letter-spacing:.018em;
}
.detail-panel[data-detail-type="profile"] .detail-panel-meta {
  width:100%;
  margin:0 0 24px;
  color:rgba(86,112,130,.52);
  font-family:var(--mono);
  font-size:11px;
  font-weight:300;
  line-height:1.72;
  letter-spacing:.14em;
}
.detail-panel[data-detail-type="profile"] .detail-panel-body {
  width:min(540px,100%);
  color:rgba(23,32,51,.62);
  font-family:var(--body-font);
  font-size:17px;
  font-weight:300;
  line-height:1.56;
}
.project-dialog.is-detail-panel .dialog-close,
.project-dialog.is-detail-panel .dialog-return {
  border:0;
  background:rgba(65,157,149,.1);
  color:#225b63;
}
.project-dialog.is-detail-panel .dialog-close {
  z-index:3;
  top:16px;
  right:18px;
  width:34px;
  height:34px;
  display:grid;
  place-items:center;
  padding:0;
  font-size:21px;
  line-height:1;
  transform-origin:50% 50%;
  transition:transform 180ms cubic-bezier(.2,.8,.2,1),background-color 160ms ease;
}
.project-dialog.is-detail-panel .dialog-close:hover,
.project-dialog.is-detail-panel .dialog-close:focus-visible { transform:rotate(90deg); }

@keyframes truliaIdentityDropIn {
  from { opacity:0; translate:0 -34px; }
  to { opacity:1; translate:0 0; }
}
@keyframes truliaIdentityFloatA {
  0%,100% { transform:translate3d(0,0,0) rotate(0deg); }
  50% { transform:translate3d(5px,-10px,0) rotate(1.4deg); }
}
@keyframes truliaIdentityFloatB {
  0%,100% { transform:translate3d(0,0,0) rotate(0deg); }
  50% { transform:translate3d(-5px,-8px,0) rotate(-1.8deg); }
}

/* Final shared split-case authority. This deliberately sits after the legacy
   Trulia declarations so Product Launches, Thoth, Bubble Universe, CoStudy,
   Spiritus and Marmot Five cannot drift into near-duplicate variations. */
.panel-split[data-detail-type="split"],
.panel-split.split-panel-host {
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
}
.panel-split[data-detail-type="split"] > .case-text-panel,
.panel-split.split-panel-host > .case-text-panel {
  --case-copy-width:400px;
  padding:clamp(54px,6.2vw,86px) clamp(62px,7.5vw,120px) clamp(54px,6.2vw,86px) clamp(84px,8.4vw,132px);
  /* Bubble Universe owns the canonical right-hand glass. World accents may
     still tint the delayed glow, but never replace this base surface. */
  background:
    radial-gradient(circle at 18% 12%,rgba(255,255,255,.94),transparent 31%),
    linear-gradient(145deg,rgba(240,252,255,.97),rgba(225,248,249,.96) 58%,rgba(220,242,253,.96));
  color:rgba(23,32,51,.74);
}
.panel-split[data-detail-type="split"] .case-kicker,
.panel-split.split-panel-host .case-kicker {
  margin:0 0 24px;
  color:rgba(86,112,130,.52);
  font-family:var(--mono);
  font-size:11px;
  font-weight:300;
  line-height:1.72;
  letter-spacing:.14em;
  text-transform:none;
}
.panel-split[data-detail-type="split"] .case-title,
.panel-split.split-panel-host .case-title {
  width:min(var(--case-copy-width),100%);
  margin:0 0 24px;
  color:rgba(23,32,51,.9);
  font-family:var(--storybook);
  font-size:clamp(44px,5vw,64px);
  font-weight:300;
  line-height:.98;
  letter-spacing:0;
}
.panel-split[data-detail-type="split"] .case-copy,
.panel-split[data-detail-type="split"] .case-copy p,
.panel-split.split-panel-host .case-copy,
.panel-split.split-panel-host .case-copy p {
  width:min(var(--case-copy-width),100%);
  color:rgba(23,32,51,.62);
  font-family:var(--body-font);
  font-size:17px;
  font-weight:300;
  line-height:1.56;
}

@media (max-width:800px) {
  .project-dialog.is-detail-panel { padding:10px; }
  .project-dialog.is-detail-panel:has(.panel-split:not([hidden])) {
    padding-left:5vw;
    padding-right:5vw;
  }
  .project-dialog.is-detail-panel .dialog-card {
    width:calc(100vw - 20px);
    height:calc(100vh - 20px);
    aspect-ratio:auto;
  }
  .project-dialog.is-detail-panel:has(.panel-split:not([hidden])) .dialog-card {
    width:90vw;
  }
  .panel-split[data-detail-type="split"],
  .panel-split.split-panel-host {
    grid-template-columns:1fr;
    grid-template-rows:minmax(210px,38vh) minmax(0,1fr);
  }
  .detail-panel[data-detail-type="profile"] {
    grid-template-columns:1fr;
    grid-template-rows:minmax(190px,34vh) minmax(0,1fr);
    padding:24px;
    row-gap:18px;
  }
  .detail-panel-media { padding:24px; }
  .panel-split[data-detail-type="split"] > .case-text-panel,
  .panel-split.split-panel-host > .case-text-panel { padding:40px 34px; }
  .panel-split[data-detail-type="split"] .case-title,
  .panel-split.split-panel-host .case-title { font-size:clamp(38px,10vw,54px); }
  .panel-split[data-detail-type="split"] .case-copy,
  .panel-split[data-detail-type="split"] .case-copy p,
  .panel-split.split-panel-host .case-copy,
  .panel-split.split-panel-host .case-copy p { font-size:17px; }
  .project-dialog.is-detail-panel.is-longform-panel { padding:70px 10px 0; }
  .project-dialog.is-detail-panel.is-longform-panel .dialog-card {
    width:calc(100vw - 20px);
    height:calc(100vh - 70px + 48px);
  }
  .longform-panel .case-stack-intro {
    grid-template-columns:1fr;
    min-height:42%;
    padding:64px 36px 52px;
  }
  .longform-panel .case-stack-eyebrow { margin-bottom:24px; font-size:16px; }
  .longform-panel .case-stack-lede { font-size:clamp(28px,7vw,42px); }
}

/* CMU keeps the existing callout geometry and information hierarchy, but
   changes its material language from cool glass to cream paper, verdigris
   steel and restrained copper hardware. */
[data-project="mhci"] .hotspot-callout,
[data-project="drama"] .hotspot-callout,
[data-project="capstone"] .hotspot-callout {
  --hotspot-callout-offset: clamp(180px, 22vh, 300px);
  box-sizing: border-box;
  width: min(380px, 78vw);
  max-width: 380px;
  border-color: rgba(165,104,58,.72);
  background:
    radial-gradient(circle at 12% 15%, rgba(255,255,255,.82), transparent 28%),
    linear-gradient(145deg, rgba(255,253,235,.98), rgba(241,247,219,.97) 58%, rgba(218,239,224,.97));
  box-shadow:
    0 19px 46px rgba(22,94,84,.24),
    inset 0 0 0 2px rgba(91,151,129,.2),
    inset 0 1px 0 rgba(255,255,255,.86),
    0 0 0 4px rgba(240,182,105,.12);
  color: #174f57;
  backdrop-filter: blur(6px) saturate(1.03);
}
[data-project="mhci"] .hotspot-callout::before,
[data-project="drama"] .hotspot-callout::before,
[data-project="capstone"] .hotspot-callout::before { border-color: rgba(82,143,121,.25); }
[data-project="mhci"] .hotspot-callout::after,
[data-project="drama"] .hotspot-callout::after,
[data-project="capstone"] .hotspot-callout::after {
  content: none;
  position: absolute;
  left: 50%;
  top: calc(100% + 1px);
  width: 1px;
  height: calc(var(--hotspot-callout-offset) - 30px);
  background: linear-gradient(to bottom, rgba(164,104,57,.72), rgba(101,168,145,.56) 72%, rgba(235,255,245,.18));
  box-shadow: 0 0 5px rgba(220,255,241,.42);
  transform: translateX(-50%);
  pointer-events: none;
}
[data-project="mhci"] .hotspot-kicker,
[data-project="drama"] .hotspot-kicker,
[data-project="capstone"] .hotspot-kicker { color: #8b623d; }
[data-project="mhci"] .hotspot-title,
[data-project="drama"] .hotspot-title,
[data-project="capstone"] .hotspot-title {
  color: #15536a;
  font-size: clamp(24px, 1.75vw, 32px);
  white-space: nowrap;
}
[data-project="mhci"] .hotspot-role,
[data-project="drama"] .hotspot-role,
[data-project="capstone"] .hotspot-role {
  color: rgba(39,91,79,.9);
  font-size: clamp(17px, 1.3vw, 21px);
  white-space: nowrap;
}
[data-project="mhci"] .hotspot-body,
[data-project="drama"] .hotspot-body,
[data-project="capstone"] .hotspot-body {
  border-top-color: rgba(157,104,59,.25);
  color: rgba(36,77,69,.78);
}

@media (max-width: 620px) {
  [data-project="mhci"] .hotspot-title,
  [data-project="drama"] .hotspot-title,
  [data-project="capstone"] .hotspot-title,
  [data-project="mhci"] .hotspot-role,
  [data-project="drama"] .hotspot-role,
  [data-project="capstone"] .hotspot-role {
    white-space: normal;
  }
}

.sunny-bag-hotspot {
  position: absolute;
  z-index: 8;
  left: 50%;
  top: 62%;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  opacity: 0;
  transform: translate(-50%, -50%) scale(.82);
  pointer-events: none;
  cursor: pointer;
  transition: opacity 180ms ease, transform 220ms cubic-bezier(.2,.8,.2,1);
}
.sunny-bag-hotspot.is-visible { opacity: 1; transform: translate(-50%, -50%) scale(1); pointer-events: auto; }
.bag-hotspot-dot {
  display: block;
  position: absolute;
  left: 50%;
  top: 50%;
  width: 20px;
  height: 20px;
  border: 2px solid rgba(255,255,255,.92);
  border-radius: 50%;
  background: rgba(157,220,224,.16);
  box-shadow: 0 0 0 4px rgba(255,255,255,.1), 0 0 12px rgba(220,255,255,.72);
  transform: translate(-50%, -50%);
  animation: hotspotPulse 2.25s ease-in-out infinite;
}
.bag-hotspot-label {
  position: absolute;
  left: 50%;
  bottom: 48px;
  width: max-content;
  max-width: 140px;
  padding: 6px 10px;
  border: 1px solid rgba(255,255,255,.8);
  border-radius: 999px;
  color: #174f58;
  background: rgba(247,255,247,.92);
  box-shadow: 0 7px 20px rgba(7,75,80,.14);
  font: 600 10px/1.2 var(--mono);
  letter-spacing: .04em;
  opacity: 0;
  transform: translate(-50%, 5px);
  transition: opacity 150ms ease, transform 180ms ease;
  pointer-events: none;
}
.sunny-bag-hotspot:hover .bag-hotspot-label,
.sunny-bag-hotspot:focus-visible .bag-hotspot-label { opacity: 1; transform: translate(-50%, 0); }

.skill-toast {
  position: absolute;
  /* The pill starts at Sunny's satchel, but sits behind the interactive bag
     marker so the pickup still reads as being gathered into that same place. */
  z-index: 7;
  left: 50%;
  top: 24%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: max-content;
  max-width: min(320px, 76vw);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.skill-toast.is-behind-left {
  align-items:flex-end;
  transform:translate(calc(-100% - 42px),-50%);
}
.skill-toast.is-behind-right {
  align-items:flex-start;
  transform:translate(16px,-50%);
}
.skill-toast-item {
  display: flex;
  align-items: center;
  gap: 7px;
  width: max-content;
  max-width: min(320px, 76vw);
  padding: 7px 13px 7px 8px;
  border: 1px solid rgba(255,255,255,.74);
  border-radius: 999px;
  color: #174f58;
  background: rgba(249,255,238,.66);
  box-shadow: 0 9px 25px rgba(7,79,82,.14);
  backdrop-filter: blur(10px) saturate(1.08);
  -webkit-backdrop-filter: blur(10px) saturate(1.08);
  font: 600 10.5px/1.35 var(--mono);
  text-align: center;
  opacity: 0;
  transform: translateY(8px) scale(.94);
  transition: opacity 180ms ease-out, transform 240ms cubic-bezier(.215,.61,.355,1);
}
.skill-toast-item.is-visible { opacity: 1; transform: translateY(0) scale(1); }
.skill-toast-check { display: grid; flex: 0 0 auto; width: 20px; height: 20px; place-items: center; border-radius: 50%; color: white; background: rgba(48,151,142,.8); box-shadow: inset 0 0 0 1px rgba(255,255,255,.46); font: 600 12px/1 var(--mono); }
.skill-fly-layer { position: absolute; z-index: 3; inset: 0; overflow: hidden; pointer-events: none; }
.skill-fly-icon { position: absolute; width: 76px; height: 76px; object-fit: contain; filter: drop-shadow(0 8px 9px rgba(7,80,84,.2)); will-change: transform, opacity; }

.skill-inventory[hidden] { display: none; }
.skill-inventory { position: fixed; z-index: 84; inset: 0; display: grid; place-items: center; padding: 22px; }
/* The close button sits outside the scrolling card, on a frame that is exactly
   the card's box. Inside it, no amount of cancelling keeps up -- a scroll
   handler is always a frame behind, which is visible as jitter the whole way
   down. Outside, the browser never scrolls it and there is nothing to correct. */
.skill-inventory-frame {
  /* Shared with the card so the frame's overlays line up with its content
     rather than guessing at a fallback. */
  --inventory-pad: clamp(30px, 4vw, 50px);
  --inventory-header-inset: 10px;
  /* The close button's own box, named, because the small heading has to share
     its centreline and guessing at it is how they ended up 18px apart. */
  --inventory-close-top: 16px;
  --inventory-close-size: 34px;
  position: relative;
  width: min(790px, 100%);
  max-width: 100%;
}
.skill-inventory-frame > .dialog-close {
  z-index: 5;
  top: var(--inventory-close-top);
  height: var(--inventory-close-size);
}
/* The small heading lives on the frame beside the close button, for the same
   reason: outside the card nothing scrolls it. It takes the band the stuck row
   opens above the pills, which was empty, and leaves the pills flush with the
   left edge the big heading uses. */
.skill-inventory-sticky-label {
  position: absolute;
  z-index: 5;
  /* Same top and same height as the close button, centred inside it: the two
     share one centreline by construction rather than by a tuned offset. */
  top: var(--inventory-close-top);
  left: calc(var(--inventory-pad) + var(--inventory-header-inset));
  display: flex;
  align-items: center;
  height: var(--inventory-close-size);
  opacity: 0;
  color: rgba(34,91,99,.72);
  font: 600 11px/1 var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  pointer-events: none;
  transition: opacity 220ms ease;
}
.skill-inventory-frame.is-stuck .skill-inventory-sticky-label { opacity: 1; }

.skill-inventory-card {
  --inventory-header-inset: 10px;
  --inventory-pad: clamp(30px, 4vw, 50px);
  position: relative;
  width: 100%;
  max-height: min(780px, calc(100vh - 44px));
  overflow: auto;
  padding: var(--inventory-pad);
  border: 1px solid rgba(255,255,255,.85);
  border-radius: 10px 42px 34px 42px;
  background: radial-gradient(circle at 12% 7%, rgba(255,255,255,.98), transparent 28%), linear-gradient(145deg, rgba(250,255,239,.97), rgba(224,248,239,.96) 58%, rgba(225,242,253,.96));
  box-shadow: 0 42px 120px rgba(4,52,60,.3), inset 0 0 0 5px rgba(255,255,255,.22);
}
.skill-inventory-card .dialog-kicker { margin-left: var(--inventory-header-inset); }
.skill-inventory-card h2 { margin: 9px 0 5px var(--inventory-header-inset); font: 500 clamp(35px, 6vw, 58px)/.95 var(--storybook); letter-spacing: -.025em; }
.skill-inventory-count { display: flex; align-items: center; gap: 7px; margin: 18px 0 22px var(--inventory-header-inset); color: rgba(23,79,88,.68); font: 500 11px/1.4 var(--mono); letter-spacing: .07em; }
.skill-inventory-count-check { display: grid; width: 19px; height: 19px; place-items: center; border-radius: 50%; color: white; background: rgba(48,151,142,.8); font: 600 11px/1 var(--mono); }
/* The pills follow the reader down the bag. Scrolled past the heading there was
   nothing left saying which screen this is or how to filter it, just a field of
   icons.
   The row itself sticks -- no wrapper, no extra element. A sticky box takes no
   space until it sticks, so at rest this is the same row in the same place; the
   negative margins are cancelled exactly by the padding, so the pills do not
   move a pixel either. Only the background and the small label arrive, and only
   once there is a heading gone to explain. */
.skill-inventory-filters {
  position: sticky;
  /* top:0 resolves to the content box here, which is one card-padding below the
     card's own edge -- that gap is where a band of scrolling icons used to show
     above the row. Lifted by exactly that padding so it lands on the edge. */
  top: calc(var(--inventory-pad) * -1);
  z-index: 3;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 8px;
  margin: -10px calc(var(--inventory-pad) * -1) 0;
  padding: 10px calc(var(--inventory-pad) + 44px) 20px calc(var(--inventory-pad) + var(--inventory-header-inset));
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  transition: background-color 220ms ease;
}

.skill-inventory-filters.is-stuck {
  background: rgba(240,251,244,.96);
  padding-top: calc(var(--inventory-pad) + 10px);
}

.skill-inventory-filters::-webkit-scrollbar { display: none; }
.skill-inventory-filter { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 7px; padding: 8px 11px 8px 13px; border: 1px solid rgba(42,125,126,.18); border-radius: 999px; color: rgba(25,76,84,.7); background: rgba(255,255,255,.43); box-shadow: 0 4px 12px rgba(7,80,84,.04); font: 600 10px/1 var(--mono); letter-spacing: .035em; cursor: pointer; transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease; }
.skill-inventory-filter small { display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center; border-radius: 999px; color: rgba(29,101,104,.72); background: rgba(46,143,137,.1); font: 600 9px/1 var(--mono); }
.skill-inventory-filter.is-active { border-color: rgba(43,139,135,.55); color: white; background: rgba(42,139,134,.86); box-shadow: 0 7px 18px rgba(7,80,84,.16); }
.skill-inventory-filter.is-active small { color: #1e6769; background: rgba(255,255,255,.86); }
.skill-inventory-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(12px, 2vw, 20px); padding-top: 18px; }
.skill-slot { position: relative; display: grid; align-content: start; justify-items: center; min-height: 214px; padding: 14px 12px 16px; overflow: visible; border: 1px solid rgba(50,132,132,.15); border-radius: 18px 24px 20px 27px / 22px 17px 26px 20px; background: rgba(255,255,255,.56); box-shadow: 0 7px 18px rgba(7,80,84,.05); text-align: center; cursor: default; transform: translateY(0); transition: transform 140ms cubic-bezier(.215,.61,.355,1), box-shadow 150ms ease-out, background-color 150ms ease-out; }
.skill-slot img { position: relative; z-index: 1; width: min(154px, 100%); aspect-ratio: 1; object-fit: contain; filter: drop-shadow(0 8px 9px rgba(7,80,84,.12)); transform: translateY(0) scale(1); transform-origin: 50% 70%; transition: transform 120ms cubic-bezier(.215,.61,.355,1); will-change: transform; }
.skill-slot strong { position: relative; z-index: 2; margin-top: -2px; color: #225b63; font: 600 18px/1.05 var(--condensed); letter-spacing: .005em; }
.skill-slot small { margin-top: 4px; color: rgba(34,91,99,.58); font: 500 8px/1.25 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
/* Only the fallback path needs dimming. The locked drawing is meant to be seen
   in full colour -- it is the picture of a locked slot, not a faded version of
   something else. */
.skill-slot.is-locked:not(.has-locked-art) { filter: saturate(.2); opacity: .42; }
/* Only the fallback needs greying. The locked drawing is already a locked
   drawing -- desaturating it a second time would just make it muddy. */
.skill-slot.is-locked:not(.has-locked-art) img { filter: grayscale(1); opacity: .32; }
/* Until the picture has actually decoded, the slot holds its own shape and
   breathes, so nothing pops into place from nowhere. */
.skill-slot.is-loading img { opacity: 0; }
.skill-slot.is-loading::before {
  content: "";
  position: absolute;
  top: 14px;
  left: 50%;
  translate: -50% 0;
  width: min(154px, calc(100% - 24px));
  aspect-ratio: 1;
  border-radius: 12px;
  background: linear-gradient(100deg,
    rgba(42,139,134,.05) 0 38%, rgba(42,139,134,.13) 50%, rgba(42,139,134,.05) 62% 100%);
  background-size: 260% 100%;
  animation: skill-slot-shimmer 1.25s ease-in-out infinite;
}
@keyframes skill-slot-shimmer {
  0% { background-position: 130% 0; }
  100% { background-position: -30% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .skill-slot.is-loading::before { animation: none; }
}
.skill-inventory-card .dialog-return { margin-top: 32px; }

/* Mobile gets a purpose-built landscape inventory: filters become a left
   rail and the collection remains a true three-column, vertically scrollable
   grid. This is responsive layout, not a scaled desktop screenshot. */
html.is-mobile-storymode .skill-inventory { padding:18px 30px; overflow:hidden; }
html.is-mobile-storymode .skill-inventory-card {
  --inventory-header-inset:0;
  display:grid;
  grid-template-columns:122px minmax(0,1fr);
  grid-template-rows:46px minmax(0,1fr);
  column-gap:18px;
  width:min(780px,calc(100% - 8px));
  /* A percentage inside min() becomes indefinite for this transformed fixed
     grid in Mobile Safari, so the card expands to the full 69-slot content
     (3,000px+) and its child never acquires overflow. Give the authored
     landscape card a definite block size; the overlay remains its clip. */
  height:344px;
  max-height:calc(100% - 4px);
  padding:20px 22px 18px;
  overflow:hidden;
  border-radius:18px 34px 28px 34px;
  transform:none;
}
html.is-mobile-storymode .skill-inventory-frame > .dialog-close {
  /* The button belongs to the 8px-wider frame while this 34px arc belongs to
     the card. Its 30px circle is concentric with that card corner at
     top: 34 - 15 = 19px and right: 8 + 34 - 15 = 27px. */
  top:19px;
  right:27px;
  width:30px;
  height:30px;
}
html.is-mobile-storymode .skill-inventory-card .dialog-kicker {
  display:none;
}
html.is-mobile-storymode .skill-inventory-card h2 {
  grid-column:2;
  grid-row:1;
  align-self:start;
  margin:0;
  font-size:34px;
  line-height:.92;
}
html.is-mobile-storymode .skill-inventory-count {
  position:absolute;
  top:19px;
  right:58px;
  height:30px;
  margin:0;
  font-size:8px;
}
html.is-mobile-storymode .skill-inventory-count-check {
  width:16px;
  height:16px;
  font-size:9px;
}
html.is-mobile-storymode .skill-inventory-filters {
  grid-column:1;
  grid-row:1 / 3;
  align-self:start;
  display:flex;
  flex-direction:column;
  gap:8px;
  margin:0;
  padding:0;
  overflow:visible;
}
html.is-mobile-storymode .skill-inventory-filter {
  justify-content:space-between;
  width:100%;
  padding:7px 8px 7px 11px;
  font-size:8px;
}
html.is-mobile-storymode .skill-inventory-filter small {
  min-width:18px;
  height:18px;
  font-size:8px;
}
html.is-mobile-storymode .skill-inventory-grid {
  grid-column:2;
  grid-row:2;
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:9px;
  min-height:0;
  /* The selected card grows beyond its resting box. Give the last column a
     real landing strip so its icon/title do not disappear under the right
     edge, and keep this grid as the inventory's native touch scrollport. */
  padding:24px 30px 18px 0;
  overflow-x:hidden;
  overflow-y:scroll;
  overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;
  /* The whole authored viewport may be rotated 90deg on a portrait phone.
     JS maps that physical gesture back into logical landscape scrollTop. */
  touch-action:none;
  scrollbar-width:thin;
}
html.is-mobile-storymode .skill-slot {
  min-height:132px;
  padding:7px 6px 9px;
  border-radius:14px 18px 15px 19px;
}
html.is-mobile-storymode .skill-slot:not(.is-locked) { cursor:pointer; }
html.is-mobile-storymode .skill-slot:not(.is-locked).is-selected {
  transform:translateY(-6px);
  background:rgba(255,255,255,.76);
  box-shadow:0 18px 34px rgba(7,80,84,.15);
}
html.is-mobile-storymode .skill-slot:not(.is-locked).is-selected img {
  transform:translateY(-14px) scale(1.28);
}
html.is-mobile-storymode .skill-slot img { width:min(82px,100%); }
html.is-mobile-storymode .skill-slot strong { margin-top:-3px; font-size:12px; }
html.is-mobile-storymode .skill-slot small { margin-top:2px; font-size:6px; }
html.is-mobile-storymode .skill-inventory-card .dialog-return { display:none; }

@media (hover: hover) and (pointer: fine) {
  .skill-inventory-filter:hover { transform: translateY(-2px); border-color: rgba(42,139,134,.42); box-shadow: 0 8px 18px rgba(7,80,84,.1); }
  .skill-slot:not(.is-locked):hover { transform: translateY(-6px); background: rgba(255,255,255,.76); box-shadow: 0 18px 34px rgba(7,80,84,.15); }
  .skill-slot:not(.is-locked):hover img { transform: translateY(-14px) scale(1.28); }
}

.journey-progress {
  position: absolute;
  z-index: 30;
  inset: auto 0 0;
  height: 3px;
  background: rgba(255,255,255,.22);
}
.journey-progress-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, #fff7cf, #5ce1d1, #57a8db);
  box-shadow: 0 0 10px rgba(221,255,246,.7);
  transform: scaleX(0);
  transform-origin: left;
}
.journey-route {
  position: absolute;
  z-index: 32;
  right: clamp(18px, 3vw, 42px);
  bottom: clamp(18px, 3vw, 34px);
  display: flex;
  max-width: calc(100vw - 36px);
  gap: clamp(7px, .9vw, 14px);
  padding-top: 28px;
  overflow-x: auto;
  scrollbar-width: none;
}
.journey-route button {
  position: relative;
  display: grid;
  grid-template-columns: auto auto;
  align-items: center;
  gap: 7px;
  padding: 0;
  border: 0;
  color: rgba(255,255,255,.7);
  background: transparent;
  text-shadow: 0 1px 8px rgba(5,68,74,.3);
  font: 500 clamp(7px,.62vw,9px)/1 var(--mono);
  letter-spacing: .06em;
  cursor: pointer;
  transition: transform .18s ease, color .18s ease;
}
.journey-route button:not(:disabled):hover,
.journey-route button:not(:disabled):focus-visible {
  transform: scale(1.08);
}
.journey-route button b {
  position:relative;
  font:inherit;
  font-weight:500;
}
.journey-route button span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 0 0 rgba(255,255,255,0);
  transition: box-shadow .2s ease, transform .2s ease;
}
.journey-route button.is-active { color: white; }
.is-ai-lab-scene .journey-route button[data-node="ai-lab"] {
  color: #fff;
  opacity: 1;
}
.journey-route button.is-active::before {
  content:none;
}
.journey-route button.is-active::after {
  content:none;
}
.journey-route button.is-active b::before { content:none; }
.journey-route-plane {
  position:absolute;
  left:0;
  top:5px;
  width:18px;
  height:18px;
  color:#fff;
  pointer-events:none;
  transform:translateX(-50%);
  transition:left .48s cubic-bezier(.22,.8,.28,1), transform .24s ease;
  filter:drop-shadow(0 1px 5px rgba(4,28,64,.42));
}
.journey-route-plane svg { width:100%; height:100%; overflow:visible; }
.journey-route-plane path { fill:rgba(255,255,255,.18); stroke:currentColor; stroke-width:1.65; stroke-linecap:round; stroke-linejoin:round; }
.journey-route-plane.is-jump { transition:none; }
.journey-route-plane.is-flying { animation:route-plane-flight .48s ease both; }
@keyframes route-plane-flight {
  0%,100% { transform:translateX(-50%) translateY(0) rotate(0deg); }
  45% { transform:translateX(-50%) translateY(-10px) rotate(-8deg) scale(1.12); }
}
.journey-route button.is-active span { transform: scale(1.25); box-shadow: 0 0 0 5px rgba(255,255,255,.16); }
.journey-route.is-map-highlighted button:not(:disabled) span {
  animation: route-map-pulse .52s ease-out both;
}
.journey-route.is-map-highlighted button:nth-child(2) span { animation-delay: .05s; }
.journey-route.is-map-highlighted button:nth-child(3) span { animation-delay: .1s; }
.journey-route.is-map-highlighted button:nth-child(4) span { animation-delay: .15s; }
.journey-route.is-map-highlighted button:nth-child(5) span { animation-delay: .2s; }
@keyframes route-map-pulse {
  0% { transform: scale(1); }
  45% { transform: scale(1.55); box-shadow: 0 0 0 6px rgba(255,255,255,.16); }
  100% { transform: scale(1); }
}
.journey-route button:disabled { opacity:.34; cursor:default; }

html.is-mobile-storymode .journey-route {
  left:50%;
  right:auto;
  bottom:10px;
  width:max-content;
  max-width:calc(100% - 32px);
  justify-content:center;
  padding-top:18px;
  overflow:visible;
  transform:translateX(-50%);
}
html.is-mobile-storymode .journey-route-plane {
  top:2px;
  width:12px;
  height:12px;
}
/* The hold chevrons are the journey's primary controls; a station hotspot
   that drifts under a thumb must never win the tap. The hotspot layer sits at
   z-index 70, so on phones the header (pointer-events:none shell, only its
   controls are interactive) rides above it. */
html.is-mobile-journey-controls .journey-header { z-index: 90; }
html.is-mobile-storymode .opening-instruction { bottom:29px; }
html.is-mobile-storymode .opening-instruction span {
  display:block;
  transform:translateY(1px);
}
html.is-mobile-storymode .scroll-nudge {
  display:grid;
  place-items:center;
  padding:0 0 1px;
}
/* Phones get game-style hold controls instead of dragging the enormous
   backing scroll document. The document remains the world coordinate, but it
   only moves programmatically while either thumb is held. */
html.is-mobile-journey-controls .opening-instruction {
  position:absolute;
  inset:0;
  transform:none;
  pointer-events:none;
}
/* Android building hotspots can overlap the visible thumb controls even when
   their painted architecture is far away. Navigation is the primary mobile
   action, so its whole control layer must win hit-testing over hotspot-layer. */
html.is-android-story-runtime .opening-instruction {
  z-index:2147483100;
}
/* Crossings create their own full-stage compositing layers. Keep Android's
   primary travel controls above those layers without exposing them over the
   separate boot/orientation cover. */
html.is-android-story-runtime.is-journey-transition .opening-instruction {
  z-index:2147483647;
  opacity:1 !important;
  visibility:visible !important;
}
html.is-mobile-journey-controls .opening-instruction span {
  position:absolute;
  left:50%;
  bottom:29px;
  transform:translateX(-50%);
  white-space:nowrap;
}
html.is-mobile-journey-controls .scroll-nudge {
  position:absolute;
  /* Centre in the portrait phone's own coordinate system first. The entire
     phone is rotated afterwards, exactly like the header controls. */
  top:50%;
  width:clamp(64px,8cqw,104px);
  height:clamp(64px,8cqw,104px);
  border:1px solid rgba(255,255,255,.74);
  border-radius:50%;
  color:#fff;
  background:rgba(238,244,245,.72);
  box-shadow:0 8px 28px rgba(5,60,75,.18), inset 0 0 0 1px rgba(255,255,255,.28);
  font-size:0;
  padding:0;
  text-shadow:0 2px 10px rgba(5,60,75,.18);
  transform:translateY(-50%);
  pointer-events:auto;
  touch-action:none;
  -webkit-user-select:none;
  user-select:none;
  transition:transform 120ms ease,background 120ms ease,box-shadow 120ms ease;
}
html.is-mobile-journey-controls .scroll-nudge::before {
  content:"";
  position:absolute;
  inset:0;
  background-position:50% 50%;
  background-repeat:no-repeat;
  background-size:38% 38%;
  pointer-events:none;
}
html.is-mobile-journey-controls .scroll-nudge-back { left:clamp(4px,.8cqw,10px); }
html.is-mobile-journey-controls .scroll-nudge-back::before {
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M21 4L9 16l12 12' fill='none' stroke='white' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
html.is-mobile-journey-controls .scroll-nudge-forward { right:clamp(4px,.8cqw,10px); }
html.is-mobile-journey-controls .scroll-nudge-forward::before {
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M11 4l12 12-12 12' fill='none' stroke='white' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
html.is-mobile-journey-controls .scroll-nudge.is-pressed {
  transform:translateY(-50%) scale(.92);
  background:rgba(255,255,255,.9);
  box-shadow:0 4px 18px rgba(5,60,75,.26),0 0 24px rgba(210,248,255,.68);
}
html.is-mobile-storymode .airship-controls { z-index:2147483000; }

/* Airbnb's blue-hour worlds need an explicit high-contrast navigation state. */
.is-airbnb-scene .journey-header {
  color: #fff;
  text-shadow: 0 2px 13px rgba(0,12,42,.92), 0 0 3px rgba(0,12,42,.9);
}
.is-airbnb-scene .opening-instruction {
  color: #fff;
  text-shadow: 0 2px 12px rgba(0,12,42,.96), 0 0 4px rgba(0,12,42,.9);
}
.is-airbnb-scene .journey-route button {
  color: rgba(255,255,255,.82);
  text-shadow: 0 2px 9px rgba(0,12,42,.98), 0 0 3px rgba(0,12,42,.9);
}
.is-airbnb-scene .journey-route button.is-active { color: #fff; }
.is-art-scene .journey-header {
  color: #fff;
  text-shadow: 0 2px 13px rgba(0,38,62,.88), 0 0 3px rgba(0,38,62,.82);
}
.is-art-scene .opening-instruction {
  color: #fff;
  text-shadow: 0 2px 12px rgba(0,38,62,.9), 0 0 4px rgba(0,38,62,.82);
}
.is-art-scene .journey-route button {
  color: rgba(255,255,255,.82);
  text-shadow: 0 2px 9px rgba(0,38,62,.94), 0 0 3px rgba(0,38,62,.82);
}
.is-art-scene .journey-route button.is-active { color:#fff; }

.cmu-trulia-transit {
  position: fixed;
  z-index: 27;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  background:
    radial-gradient(circle at 50% 50%, rgba(8,26,23,.98) 0 8%, #020504 48%, #000 100%);
  pointer-events: none;
  will-change: opacity;
}
/* Route jumps belong exclusively to the global blue teleport layer. The
   destination's authored transition must never leak through that loader. */
.is-route-teleporting .cmu-trulia-transit {
  opacity: 0 !important;
  visibility: hidden !important;
}
.is-route-teleporting .portal-transition,
.is-route-teleporting .art-fall-transition {
  opacity: 0 !important;
  visibility: hidden !important;
}
.cmu-trulia-transit::before,
.cmu-trulia-transit::after {
  content: "";
  position: absolute;
  inset: -22%;
  pointer-events: none;
  will-change: transform, opacity;
}
.cmu-trulia-transit::before {
  opacity: .64;
  background:
    radial-gradient(circle at 18% 24%, rgba(127,255,185,.72) 0 1.2%, transparent 4.8%),
    radial-gradient(circle at 77% 19%, rgba(202,255,174,.58) 0 1%, transparent 4.2%),
    radial-gradient(circle at 84% 72%, rgba(74,235,180,.66) 0 1.4%, transparent 5.2%),
    radial-gradient(circle at 26% 79%, rgba(193,255,208,.5) 0 .8%, transparent 3.8%),
    radial-gradient(circle at 55% 13%, rgba(109,240,175,.48) 0 .7%, transparent 3.5%),
    radial-gradient(circle at 48% 86%, rgba(86,224,166,.5) 0 .9%, transparent 4%);
  transform: translate3d(var(--tunnel-shift, 0),0,0)
    rotate(var(--tunnel-wait-turn, 0deg)) scale(var(--tunnel-scale, .9));
}
.cmu-trulia-transit::after {
  opacity: .32;
  background:
    repeating-conic-gradient(
      from 2deg at 50% 50%,
      transparent 0 7deg,
      rgba(120,255,190,.22) 7.4deg 8deg,
      transparent 8.6deg 18deg
    );
  -webkit-mask: radial-gradient(circle, transparent 0 12%, rgba(0,0,0,.35) 24%, #000 72%);
  mask: radial-gradient(circle, transparent 0 12%, rgba(0,0,0,.35) 24%, #000 72%);
  transform: rotate(calc(var(--tunnel-rotation, 0deg) + var(--tunnel-wait-turn, 0deg) * .42))
    scale(var(--tunnel-scale, .9));
}
.cmu-trulia-streaks {
  position: absolute;
  inset: -18%;
  opacity: 0;
  background:
    radial-gradient(circle at 50% 50%, rgba(157,255,204,.22), transparent 20%),
    repeating-conic-gradient(from 0deg at 50% 50%, transparent 0 10deg, rgba(102,245,177,.2) 10.4deg 10.9deg, transparent 11.4deg 24deg);
  -webkit-mask: radial-gradient(circle, transparent 0 9%, #000 34%);
  mask: radial-gradient(circle, transparent 0 9%, #000 34%);
  transform: rotate(calc(var(--tunnel-wait-turn, 0deg) * -.26)) scale(var(--streak-scale, .82));
  transform-origin: center;
  will-change: transform, opacity;
}

.airbnb-curtain-valance {
  position: absolute;
  display: none;
  pointer-events: none;
}
.cmu-trulia-transit.is-airbnb-curtain {
  background:
    radial-gradient(ellipse at 50% 58%, rgba(28,35,75,.96), rgba(7,9,28,.99) 58%, #02030c 100%);
}
.cmu-trulia-transit.is-airbnb-curtain::before,
.cmu-trulia-transit.is-airbnb-curtain::after,
.cmu-trulia-transit.is-airbnb-curtain .cmu-trulia-streaks {
  opacity: 0 !important;
}
.cmu-trulia-transit.is-airbnb-curtain .airbnb-curtain-valance {
  display: block;
  z-index: 3;
  inset: 0 0 auto;
  height: 18%;
  background:
    radial-gradient(ellipse at 8% -5%, #8f3b4f 0 58%, transparent 60%),
    radial-gradient(ellipse at 25% -5%, #551832 0 58%, transparent 60%),
    radial-gradient(ellipse at 42% -5%, #913d50 0 58%, transparent 60%),
    radial-gradient(ellipse at 59% -5%, #51162f 0 58%, transparent 60%),
    radial-gradient(ellipse at 76% -5%, #913d50 0 58%, transparent 60%),
    radial-gradient(ellipse at 93% -5%, #52172f 0 58%, transparent 60%);
  border-bottom: 3px solid rgba(218,164,82,.7);
  filter: drop-shadow(0 12px 18px rgba(0,0,0,.42));
  transform: translateY(var(--curtain-valance-y, -100%));
  will-change: transform;
}

.airbnb-curtain-svg {
  position: absolute;
  z-index: 2;
  inset: 0;
  display: none;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.cmu-trulia-transit.is-airbnb-curtain .airbnb-curtain-svg { display: block; }
/* The curtain's own waiting loop, the counterpart to the tunnel's turn. It
   breathes as one drape rather than per-panel, so the folds and the hem stay
   in register — a draft behind the fabric, not a tear in it. Scale only ever
   grows, so no edge can pull away from the frame. */
.cmu-trulia-transit.is-airbnb-curtain.is-waiting .airbnb-curtain-svg {
  transform-origin: 50% 0;
  animation: airbnb-curtain-drift 7.4s ease-in-out infinite;
}
@keyframes airbnb-curtain-drift {
  0%, 100% { transform: scaleX(1) translateX(0); }
  34% { transform: scaleX(1.013) translateX(-.35%); }
  68% { transform: scaleX(1.008) translateX(.3%); }
}
.cmu-trulia-transit.is-airbnb-curtain.is-waiting .airbnb-curtain-valance {
  animation: airbnb-valance-glow 7.4s ease-in-out infinite;
}
@keyframes airbnb-valance-glow {
  0%, 100% { filter: drop-shadow(0 12px 18px rgba(0,0,0,.42)) brightness(1); }
  40% { filter: drop-shadow(0 14px 22px rgba(0,0,0,.46)) brightness(1.09); }
}
@media (prefers-reduced-motion: reduce) {
  .cmu-trulia-transit.is-airbnb-curtain.is-waiting .airbnb-curtain-svg,
  .cmu-trulia-transit.is-airbnb-curtain.is-waiting .airbnb-curtain-valance {
    animation: none;
  }
}
.airbnb-curtain-shape {
  stroke: rgba(218,164,82,.78);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  filter: drop-shadow(0 0 24px rgba(0,0,0,.72));
}
.airbnb-curtain-folds path {
  fill: none;
  stroke: rgba(31,5,28,.28);
  stroke-width: 30;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.airbnb-curtain-folds-left path {
  filter: drop-shadow(13px 0 9px rgba(255,183,180,.24));
}
.airbnb-curtain-folds-right path {
  filter: drop-shadow(-13px 0 9px rgba(255,183,180,.24));
}

.project-dialog[hidden] { display: none; }
.project-dialog { position: fixed; z-index: 80; inset: 0; display: grid; place-items: center; padding: 22px; }
/* A plain translucent sheet, not live frosted glass. backdrop-filter recomputes
   the blur every frame, and what it is blurring here is the journey -- water
   moving, particles falling, a canvas that never stops. Over a full-screen panel
   that is a per-frame cost for an effect nobody can see through anyway, and on
   some machines the recomputation itself shimmers. The gradient is carrying a
   little more of the separation now that the blur is not helping. */
.dialog-backdrop {
  position:absolute;
  inset:0;
  background:linear-gradient(145deg,rgba(223,247,255,.86),rgba(203,236,248,.80));
}
.dialog-card {
  position: relative;
  width: min(510px, 100%);
  padding: clamp(28px, 5vw, 54px);
  border: 1px solid rgba(255,255,255,.76);
  border-radius: 4px 36px 36px 36px;
  background: rgba(249,255,249,.94);
  box-shadow: 0 40px 110px rgba(4,52,60,.28);
}
.dialog-close {
  position: absolute;
  top: 16px;
  right: 18px;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(65,157,149,.1);
  font-size: 21px;
  line-height: 1;
  cursor: pointer;
  transform-origin: 50% 50%;
  transition: transform 180ms cubic-bezier(.2,.8,.2,1), background-color 160ms ease;
}
.dialog-close:hover,
.dialog-close:focus-visible { transform: rotate(90deg); }
.dialog-kicker { color: #388c8e; font: 500 10px/1.4 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.dialog-card h2 { margin: 12px 0 15px; font-size: clamp(31px, 6vw, 54px); line-height: .95; letter-spacing: -.055em; }
.dialog-card > p:not(.dialog-kicker) { color: rgba(23,63,70,.72); line-height: 1.65; }
.dialog-note { padding-top: 14px; border-top: 1px solid rgba(28,111,110,.15); font-size: 12px; }
.dialog-return {
  margin-top: 10px;
  padding: 11px 17px;
  border: 1px solid rgba(31,128,124,.28);
  border-radius: 999px;
  background: rgba(108,211,194,.14);
  cursor: pointer;
}

@keyframes threadLoad { 0% { transform: translateX(-100%); } 55%, 100% { transform: translateX(300%); } }
@keyframes teleportDeviceFloat {
  0%,100% { transform:translateY(0) rotate(-1deg); }
  50% { transform:translateY(-9px) rotate(1deg); }
}
@keyframes arrowOpenLeft { 0%, 100% { transform: translateX(2px); } 50% { transform: translateX(-3px); } }
@keyframes arrowOpenRight { 0%, 100% { transform: translateX(-2px); } 50% { transform: translateX(3px); } }
@keyframes hotspotPulse {
  0%, 100% { transform: translate(-50%, -50%) scale(.94); }
  50% { transform: translate(-50%, -50%) scale(1.07); }
}
@keyframes portal-bokeh-drift-a {
  from { transform: translate3d(-2.5%, 1.5%, 0) scale(.96) rotate(-2deg); opacity: .68; }
  to { transform: translate3d(3.5%, -2%, 0) scale(1.08) rotate(2.5deg); opacity: .92; }
}
@keyframes portal-bokeh-drift-b {
  from { transform: translate3d(3%, -2%, 0) scale(1.04) rotate(2deg); opacity: .72; }
  to { transform: translate3d(-3%, 3%, 0) scale(.95) rotate(-3deg); opacity: .5; }
}
@keyframes forest-particle-fall {
  0% { transform: translate3d(8vw, -10vh, 0) rotate(0deg) scale(.76); opacity: 1; }
  38% { transform: translate3d(-1vw, 38vh, 0) rotate(112deg) scale(.9); opacity: 1; }
  72% { transform: translate3d(-7vw, 79vh, 0) rotate(224deg) scale(.96); opacity: 1; }
  100% { transform: translate3d(var(--drift), 116vh, 0) rotate(var(--spin)) scale(1); opacity: 1; }
}

@media (max-width: 760px) {
  .journey-header { height: 58px; grid-template-columns: 1fr auto; }
  .journey-status { display: none; }
  .journey-actions { gap: 6px; }
  .journey-actions a { display: none; }
  .journey-control { padding: .38rem .55rem; font-size: 10px; }
  /* The nudge belongs under Sound, but on a narrow header its sentence may be
     wider than the controls row. Anchor its right edge to that row's safe
     edge: it can grow leftward, never beyond the window's right margin. */
  .sound-nudge { left: auto; right: 0; }
  .scroll-space { height: 5326vh; }
  .chapter-card { width: min(82vw, 460px); }
  .journey-route { left: 16px; right: 16px; justify-content: space-between; }
  .journey-route button { font-size: 8px; }
  .opening-instruction { bottom: 67px; }
  .airship-link span { max-width: 82%; font-size: 7px; padding: .2rem .34rem; }
  .cv-gate-form { flex-direction: column; }
  .skill-inventory-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .skill-inventory-filters { margin-left: 0; }
  .skill-slot { min-height: 184px; }
}

@media (max-width: 430px) {
  .journey-brand-logo { width: 92px; }
  .journey-header { padding-inline: 14px; }
  .journey-route button { gap: 4px; }
}

/* Final ultrawide authority comes after every legacy Trulia and AI Lab base
   rule. Font and copy measures grow continuously with the 70vw shell. */
@media (min-width:2200px) {
  .panel-split[data-detail-type="split"] > .case-text-panel,
  .panel-split.split-panel-host > .case-text-panel {
    --case-copy-width:clamp(400px,18vw,700px);
    padding:
      clamp(86px,4.48vw,172px)
      clamp(120px,6.25vw,240px)
      clamp(86px,4.48vw,172px)
      clamp(132px,6.88vw,264px);
  }
  .panel-split[data-detail-type="split"] .case-kicker,
  .panel-split.split-panel-host .case-kicker {
    margin-bottom:clamp(24px,1.3vw,40px);
    font-size:clamp(11px,.48vw,17px);
  }
  .panel-split[data-detail-type="split"] .case-title,
  .panel-split.split-panel-host .case-title {
    margin-bottom:clamp(24px,1.25vw,40px);
    font-size:clamp(64px,2.85vw,96px);
  }
  .panel-split[data-detail-type="split"] .case-copy,
  .panel-split[data-detail-type="split"] .case-copy p,
  .panel-split.split-panel-host .case-copy,
  .panel-split.split-panel-host .case-copy p {
    font-size:clamp(17px,.76vw,29px);
    line-height:1.56;
  }
}

@media (min-width:3000px) and (min-height:1700px) {
  .project-dialog.is-detail-panel.is-longform-panel {
    --image-stack-base-height:min(calc(50vw - 112px),calc(100vh - 192px));
    --image-stack-max-width:1750px;
    padding-inline:clamp(240px,16vw,720px);
  }
  .longform-panel .case-stack-intro { padding:210px 260px 160px; }
  .longform-panel .case-stack-intro-copy { max-width:min(1680px,100%); }
  .longform-panel .case-stack-eyebrow {
    margin-bottom:44px;
    font-size:28px;
    line-height:1.38;
  }
  .longform-panel .case-stack-lede {
    max-width:1680px;
    font-size:68px;
    line-height:1.28;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; }
  .hotspot-callout { transition: none; }
  .portal-transition { backdrop-filter: none; transform: none; background: rgba(224,252,247,.72); }
  .portal-transition::before, .portal-transition::after, .falling-particle { animation: none; }
  .falling-particles { display: none; }
  .cover-title-video { display: none; }
  .sunny-intro-video { display: none; }
  .airship-link span, .cv-gate-modal, .cv-gate-card { transition: none; }
  .cv-gate-card.is-shake { animation: none; }
  .skill-toast-item, .skill-slot, .skill-slot img, .sunny-bag-hotspot { transition: none; }
  .cmu-trulia-streaks { display: none; }
  .dialog-close { transition: none !important; }
  .dialog-close:hover,
  .dialog-close:focus-visible { transform: none; }
}

/* The waiting ripple for AI Lab → Thoughts. The cover's own rings are scrolled,
   so they freeze the moment the crossing is held; these two are on a clock and
   keep opening outwards, staggered half a period apart so there is always one
   mid-flight. Sized off the same min(172vh, 148vw) as the rings they join. */
.thoughts-travel-ripple { display: none; }
.thoughts-travel-cover.is-waiting .thoughts-travel-ripple { display: block; }
.thoughts-travel-ripple::before,
.thoughts-travel-ripple::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(172vh, 148vw);
  aspect-ratio: 1;
  margin: 0;
  border: 1.5px solid rgba(198,216,255,.42);
  border-radius: 50%;
  opacity: 0;
  transform: translate3d(-50%, -50%, 0) scale(.16);
  will-change: transform, opacity;
  animation: thoughts-travel-ripple 4.4s cubic-bezier(.22,.61,.36,1) infinite;
}
.thoughts-travel-ripple::after { animation-delay: 2.2s; }
/* Brightest just after it leaves the source and thinning the whole way out, so
   it spends itself against the edge instead of being switched off there. The
   border thins with it — a ring that only faded still read as a hard circle. */
@keyframes thoughts-travel-ripple {
  0% { opacity: 0; border-width: 2px; transform: translate3d(-50%, -50%, 0) scale(.12); }
  9% { opacity: .58; }
  34% { opacity: .42; border-width: 1.5px; }
  62% { opacity: .2; border-width: 1px; }
  84% { opacity: .06; border-width: .75px; }
  100% { opacity: 0; border-width: .5px; transform: translate3d(-50%, -50%, 0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .thoughts-travel-cover.is-waiting .thoughts-travel-ripple { display: none; }
}

/* The scene's own CSS animations are not driven by the render loop, so freezing
   that still leaves them running behind a panel that covers them -- particles
   falling, the portal drifting, a hotspot pulsing, none of it visible.
   
   Everything inside the stage, wholesale: every overlay in this site lives
   outside .journey-stage -- checked, all of them -- so a panel's own shimmer
   and transitions are never caught by this. */
html.is-story-content-open .journey-stage *,
html.is-story-map-open .journey-stage *,
body.is-about-overlay-open .journey-stage * {
  animation-play-state: paused;
}

/* Content panels deliberately draw their hand-made circles and underlines on
   entry.  They may be mounted by a host inside the frozen journey subtree, so
   the blanket scene pause above used to stop every path partway through its
   dash animation.  The panel owns these marks, not the background journey. */
html.is-story-content-open .project-dialog:not([hidden]) .case-stack-shape path {
  animation-play-state: running !important;
}

/* The loading card's thread keeps drawing itself for the whole visit. The card
   is marked done within seconds and hidden behind visibility and zero opacity,
   and the animation goes on asking the compositor for a frame every frame,
   forever, for something nobody can see.

   This first landed on .loading-thread, which never had an animation to stop --
   the sweep is declared on its ::after, and animation is not inherited. The
   rule was real, the target was not, and the bead went on sweeping. A value in
   this codebase is often measured against the wrong thing; so, it turns out, is
   a selector. */
.loading-card.is-done .loading-thread::after { animation: none; }

/* The cue in front of Sunny at the very start.

   Ahead of her rather than beside her: she is about to walk to the right and
   the whole job of this is to say so, which a mark behind her shoulder cannot
   do. Large, because it competes with a painted world rather than sitting on
   chrome, and white with a glow because the water it stands over is bright and
   busy -- the aqua that reads well on the loading card disappears into it. */
.start-scroll-cue {
  position: absolute;
  z-index: 8;
  /* Out in the open water ahead of her, level with her stride rather than her
     head -- at head height it reads as a thought, and it collides with the
     painted title. */
  /* Measured off the marked-up reference: the marks sit about four tenths of
     her own height ahead of her, level with her waist, on the open water
     between her and the machinery -- clear of the painted title above and of
     the jetty below. */
  left: calc(50% + clamp(134px, 12vw, 244px));
  top: 71%;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Air between the marks and the words: they are two statements, not one
     label with an ornament on top. */
  gap: clamp(17px, 1.55vw, 25px);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -50%);
  transition: opacity 520ms ease;
}
/* Shown only while the reader has not moved yet. Anything else on the journey
   -- a panel, the map, About, the loading card -- means this is not the moment. */
.start-scroll-cue.is-visible { opacity: 1; }
.is-loading .start-scroll-cue { opacity: 0; }

.start-scroll-chevrons {
  display: flex;
  align-items: center;
  gap: clamp(2px, .5vw, 6px);
}
.start-scroll-chevrons i {
  width: clamp(22px, 2.9vw, 44px);
  height: clamp(22px, 2.9vw, 44px);
  border-top: clamp(4px, .5vw, 8px) solid;
  border-right: clamp(4px, .5vw, 8px) solid;
  border-radius: 2px;
  transform: rotate(45deg);
  opacity: .5;
  /* The halo is the water's own blue, not black -- a black shadow over this
     lagoon reads as dirt on the glass rather than as light. Tight first so the
     mark keeps an edge, then a soft bloom so it carries across the bright
     places. */
  filter: drop-shadow(0 0 3px rgba(31, 122, 158, .85))
          drop-shadow(0 2px 9px rgba(31, 122, 158, .7))
          drop-shadow(0 0 18px rgba(214, 244, 255, .8));
  animation: start-cue-drift 1.55s ease-in-out infinite;
}
/* One colour each, running white into the water: the run of colour across the
   set is what carries the eye along it, so it lives between the marks rather
   than inside any one of them. */
.start-scroll-chevrons i:nth-child(1) { border-color: #ffffff; }
.start-scroll-chevrons i:nth-child(2) { border-color: #dcf1fa; animation-delay: .16s; }
.start-scroll-chevrons i:nth-child(3) { border-color: #9fd9ec; animation-delay: .32s; }

.start-scroll-copy {
  color: #fff;
  font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
  font-size: clamp(10px, .86vw, 14px);
  font-weight: 600;
  line-height: 1.34;
  letter-spacing: .18em;
  text-align: center;
  white-space: pre-line;
  text-shadow: 0 1px 5px rgba(17, 95, 126, .95),
               0 0 14px rgba(218, 247, 255, .9);
}

/* The glow travels along the row rather than each mark blinking on its own, so
   it reads as a direction and not as three separate lights. */
@keyframes start-cue-drift {
  0%, 62%, 100% { opacity: .5; transform: rotate(45deg) translate(0, 0); }
  28% { opacity: 1; transform: rotate(45deg) translate(4px, -4px); }
}

@media (prefers-reduced-motion: reduce) {
  .start-scroll-chevrons i { animation: none; opacity: .85; }
}

/* First-visit controls card. It is intentionally larger than the ambient
   SCROLL cue: readers who have not yet inferred that this is interactive need
   an instruction with nouns, not another decorative arrow. */
.journey-onboarding-toast {
  position:absolute;
  left:clamp(22px,3vw,58px);
  bottom:calc(clamp(20px,4.5vh,50px) - 9px);
  z-index:2147483646;
  width:min(440px,calc(100vw - 44px));
  /* 16px sides: the title, the method cards and the close chip all share the
   * same frame the Ask Sunny panel uses. */
  padding:clamp(22px,2vw,30px) 16px 16px;
  border:1px solid rgba(255,255,255,.82);
  border-radius:28px 28px 28px 6px;
  color:#174e5a;
  background:rgba(244,252,246,.78);
  box-shadow:0 24px 70px rgba(8,65,78,.23),inset 0 1px 0 rgba(255,255,255,.82);
  backdrop-filter:blur(20px) saturate(1.06);
  opacity:0;
  transform:scale(.109,.2);
  pointer-events:none;
  transform-origin:0 100%;
  will-change:transform,border-radius,opacity;
}
.journey-onboarding-toast.is-visible { opacity:0; }
.is-loading .journey-onboarding-toast { opacity:0; }
.journey-onboarding-toast.is-visible.is-open,
.journey-onboarding-toast.is-visible.is-morphing-open,
.journey-onboarding-toast.is-visible.is-collapsing {
  opacity:1;
}
.journey-onboarding-toast.is-open { transform:none; pointer-events:auto; }
.journey-onboarding-toast.is-morphing-open {
  pointer-events:none;
  animation:journey-toast-morph-open .62s cubic-bezier(.18,.82,.2,1) both;
}
.journey-onboarding-toast.is-collapsing {
  pointer-events:none;
  animation:journey-toast-morph-close .5s cubic-bezier(.55,.04,.22,1) both;
}
.journey-onboarding-toast.is-morphing-open > strong,
.journey-onboarding-toast.is-morphing-open > .journey-onboarding-close,
.journey-onboarding-toast.is-morphing-open > .journey-onboarding-methods {
  animation:journey-toast-content-fade .3s .27s ease both;
}
.journey-onboarding-toast.is-collapsing > strong,
.journey-onboarding-toast.is-collapsing > .journey-onboarding-close,
.journey-onboarding-toast.is-collapsing > .journey-onboarding-methods {
  animation:journey-toast-content-hide .16s ease both;
}
.journey-onboarding-toast > strong {
  display:block;
  padding-right:34px;
  /* 500 is the heaviest Cormorant Infant weight index.html actually loads;
   * asking for 600 dropped this one title all the way back to Georgia. */
  font:500 clamp(20px,1.55vw,27px)/1.15 "Cormorant Infant",Georgia,serif;
  letter-spacing:-.015em;
  /* Share the close button's row: its chip sits 16px from the top edge, so
   * the title climbs out of the toast's larger padding to meet it. */
  margin-top:calc(19px - clamp(22px,2vw,30px));
  margin-bottom:26px;
}
.journey-onboarding-close {
  position:absolute;
  top:16px;
  right:16px;
  color:rgba(23,78,90,.62);
}
.journey-onboarding-close:hover,.journey-onboarding-close:focus-visible {
  color:#174e5a;
  background:rgba(51,137,145,.2);
  transform:rotate(90deg);
}
.journey-onboarding-methods { display:flex; align-items:stretch; gap:12px; }
.journey-onboarding-method {
  flex:1 1 0;
  min-width:0;
  padding:15px 12px 13px;
  border-radius:17px;
  border:1px solid rgba(255,255,255,.84);
  background:rgba(249,253,255,.88);
  box-shadow:inset 0 1px 0 #fff,0 8px 22px rgba(18,91,105,.06);
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:10px;
}
.journey-onboarding-method em {
  color:#155567;
  font:600 10px/1.3 "IBM Plex Mono",monospace;
  letter-spacing:.035em;
  text-align:center;
  font-style:normal;
}
.journey-onboarding-method em span { display:block; }
.journey-onboarding-method em small {
  display:block;
  margin-top:5px;
  color:rgba(23,78,90,.48);
  font:500 8px/1.35 "Quicksand",system-ui,sans-serif;
  letter-spacing:.015em;
}
.journey-onboarding-or {
  align-self:center;
  color:rgba(23,78,90,.48);
  font:600 9px/1 "IBM Plex Mono",monospace;
}
.journey-input-mouse {
  position:relative;
  width:27px;
  height:39px;
  border:2px solid #258a98;
  border-radius:15px;
}
.journey-input-mouse::after {
  content:"";
  position:absolute;
  left:50%;
  top:7px;
  width:3px;
  height:8px;
  border-radius:3px;
  background:#258a98;
  transform:translateX(-50%);
  animation:journey-wheel-guide 1.25s ease-in-out infinite;
}
.journey-input-keys { display:flex; gap:7px; }
.journey-input-keys b {
  display:grid;
  place-items:center;
  width:36px;
  height:36px;
  border:1px solid rgba(26,111,125,.36);
  border-bottom-width:4px;
  border-radius:9px;
  color:#187687;
  background:#fff;
  font:600 25px/1 system-ui,sans-serif;
  box-shadow:0 5px 12px rgba(23,78,90,.1);
  animation:journey-key-guide 1.5s ease-in-out infinite;
}
.journey-input-keys b:last-child { animation-delay:.24s; }
@keyframes journey-wheel-guide {
  0%,100% { opacity:.35; transform:translate(-50%,0); }
  45% { opacity:1; transform:translate(-50%,10px); }
}
@keyframes journey-key-guide {
  0%,68%,100% { transform:translateY(0); border-bottom-width:4px; }
  78% { transform:translateY(3px); border-bottom-width:1px; }
}
@keyframes journey-toast-morph-open {
  0% {
    opacity:1;
    transform:scale(.109,.2);
    border-radius:50%;
    background:rgba(24,84,98,.46);
    box-shadow:0 8px 24px rgba(5,48,61,.24),inset 0 1px 0 rgba(255,255,255,.36);
  }
  100% {
    opacity:1;
    transform:none;
    border-radius:28px 28px 28px 6px;
    background:rgba(244,252,246,.78);
    box-shadow:0 24px 70px rgba(8,65,78,.23),inset 0 1px 0 rgba(255,255,255,.82);
  }
}
@keyframes journey-toast-morph-close {
  0% {
    opacity:1;
    transform:none;
    border-radius:28px 28px 28px 6px;
    background:rgba(244,252,246,.78);
    box-shadow:0 24px 70px rgba(8,65,78,.23),inset 0 1px 0 rgba(255,255,255,.82);
  }
  100% {
    opacity:1;
    transform:scale(.109,.2);
    border-radius:50%;
    background:rgba(24,84,98,.46);
    box-shadow:0 8px 24px rgba(5,48,61,.24),inset 0 1px 0 rgba(255,255,255,.36);
  }
}
@keyframes journey-toast-content-fade {
  from { opacity:0; }
  to { opacity:1; }
}
@keyframes journey-toast-content-hide {
  from { opacity:1; }
  to { opacity:0; }
}
.journey-onboarding-launcher {
  position:absolute;
  left:clamp(22px,3vw,58px);
  bottom:calc(clamp(20px,4.5vh,50px) - 9px);
  z-index:2147483646;
  display:grid;
  place-items:center;
  width:48px;
  height:48px;
  padding:0;
  border:1px solid rgba(255,255,255,.72);
  border-radius:50%;
  color:#eafcff;
  /* Match the header's language pill exactly; this is one system control,
     merely resting at the lower-left corner. */
  background:rgba(8,83,91,.18);
  box-shadow:0 8px 24px rgba(5,48,61,.24),inset 0 1px 0 rgba(255,255,255,.36);
  backdrop-filter:blur(12px) saturate(1.08);
  cursor:pointer;
  pointer-events:none;
  opacity:0;
  transform:translateY(10px) scale(.82);
  transition:opacity .3s ease,transform .38s cubic-bezier(.2,.86,.22,1),background-color .18s ease;
}
.journey-onboarding-launcher.is-visible {
  pointer-events:auto;
  opacity:1;
  transform:none;
}
.journey-onboarding-launcher:hover,.journey-onboarding-launcher:focus-visible {
  background:rgba(255,255,255,.28);
  transform:scale(1.06);
}
.journey-onboarding-launcher svg {
  width:32px;
  height:28px;
  fill:none;
  stroke:currentColor;
  stroke-width:1.8;
  stroke-linecap:round;
  stroke-linejoin:round;
}
@media (max-width:760px) {
  .journey-onboarding-toast { left:16px; width:min(390px,calc(100vw - 32px)); padding:17px; }
  .journey-onboarding-method { padding:10px 9px; flex-direction:row; }
  .journey-onboarding-method em { text-align:left; }
  .journey-onboarding-launcher { left:16px; }
}
html.is-mobile-storymode .journey-onboarding-toast,
html.is-mobile-storymode .journey-onboarding-launcher { display:none !important; }
@media (prefers-reduced-motion:reduce) {
  .journey-input-mouse::after,.journey-input-keys b { animation:none; }
  .journey-onboarding-toast.is-morphing-open,
  .journey-onboarding-toast.is-collapsing,
  .journey-onboarding-toast.is-morphing-open > strong,
  .journey-onboarding-toast.is-morphing-open > .journey-onboarding-close,
  .journey-onboarding-toast.is-morphing-open > .journey-onboarding-methods,
  .journey-onboarding-toast.is-collapsing > strong,
  .journey-onboarding-toast.is-collapsing > .journey-onboarding-close,
  .journey-onboarding-toast.is-collapsing > .journey-onboarding-methods { animation:none; }
}

/* Sunny's aside under the bar. Quieter than the line above it -- it is company,
   not status -- and it fades between lines rather than snapping, so a new one
   reads as being said rather than as the panel changing. */
.about-overlay__aside {
  display: block;
  margin-top: .85em;
  /* Wide enough that these lines are lines, not paragraphs. Sunny saying one
     thing that wrapped onto two looked like a notice rather than a remark. */
  max-width: min(88%, 46ch);
  font: 400 12px/1.5 var(--round, "Quicksand", system-ui, sans-serif);
  color: rgba(247, 241, 226, .58);
  text-align: center;
  white-space: pre-line;
  opacity: 0;
  transition: opacity 400ms ease;
}
.about-overlay__aside.is-visible { opacity: 1; }
html[lang="zh-CN"] .about-overlay__aside { max-width: min(88%, 30em); letter-spacing: .02em; }

/* The About-ready halo is drawn in WebGL from the flag's alpha outline so the
   canvas artwork stays singular.  Keep the DOM target visually inert: it is
   only the projected click/focus surface. */
.airship-about.is-book-ready::after { content: none; }

/* "psst -- there's music."

   Scrolling is this site's whole interaction, and a wheel is not a user gesture
   as far as the browser is concerned: no amount of scrolling will ever let
   sound start. Someone who only ever scrolls could finish the journey without
   learning it was scored. This is Sunny mentioning it -- and because it is a
   button, pressing it is itself the gesture that lets the sound in.

   Hung under the speaker rather than set beside it. In the row it read as a
   fourth control, shouldered the language and motion buttons out of position,
   and looked like something to operate rather than something being said. Taken
   out of the flow it points at the one button it is about, and the header keeps
   the shape it had. */
.sound-nudge {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  border: 0;
  padding: 6px 12px 7px;
  border-radius: 99px;
  color: #184e55;
  background: rgba(255, 255, 255, .9);
  box-shadow: 0 3px 14px rgba(6, 62, 74, .3);
  font: 500 11px/1 var(--round, "Quicksand", system-ui, sans-serif);
  letter-spacing: .04em;
  white-space: nowrap;
  text-shadow: none;
  cursor: pointer;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 460ms ease, transform 460ms ease;
}
/* A tail, so it is plainly about the speaker above it and not a label floating
   on its own. */
.sound-nudge::before {
  content: "";
  position: absolute;
  left: 14px;
  top: -4px;
  width: 9px;
  height: 9px;
  background: inherit;
  transform: rotate(45deg);
  border-radius: 2px;
}
.sound-nudge.is-visible { opacity: 1; transform: none; }
.sound-nudge:hover { background: #fff; }
.sound-nudge:focus-visible { outline: 2px solid rgba(255,255,255,.95); outline-offset: 3px; }
html[lang="zh-CN"] .sound-nudge { letter-spacing: .06em; }

/* The two notes drift, because that is what the sentence is claiming.
   
   They rise and settle out of step with each other, the way a pair of notes
   would if they had actually got loose. Only ever on screen while the nudge is
   -- and the nudge goes for good the moment sound is on -- so this is never an
   animation left running on something nobody can see. */
.sound-nudge-notes {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  margin-left: 6px;
  font-style: normal;
}
.sound-nudge-notes b {
  display: inline-block;
  font-weight: 500;
  animation: sound-note-drift 2.6s ease-in-out infinite;
}
.sound-nudge-notes b:nth-child(2) {
  font-size: .88em;
  animation-delay: .55s;
}
@keyframes sound-note-drift {
  0%, 100% { transform: translateY(0) rotate(0deg); opacity: .75; }
  40%      { transform: translateY(-3px) rotate(-7deg); opacity: 1; }
  70%      { transform: translateY(-1px) rotate(3deg); opacity: .9; }
}
@media (prefers-reduced-motion: reduce) {
  .sound-nudge-notes b { animation: none; opacity: 1; }
}

/* Local route-coordinate QA (`?ab-test=1`). Flags are projected from the same
   world-space x values as the camera, so their stems cross screen centre at
   the exact B entry and A resting frames. Completely absent in normal visits. */
.ab-test-layer {
  position: fixed;
  inset: 0;
  z-index: 140;
  overflow: hidden;
  pointer-events: none;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
}
.ab-test-readout {
  position: absolute;
  top: 70px;
  left: 50%;
  translate: -50% 0;
  padding: 6px 10px;
  border: 1px solid rgba(255,255,255,.72);
  border-radius: 5px;
  color: #fff;
  background: rgba(8,18,28,.78);
  box-shadow: 0 3px 14px rgba(0,0,0,.28);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .09em;
  white-space: nowrap;
}
.ab-test-centre-guide {
  position: absolute;
  inset: 0 auto 0 calc(50% - 1px);
  z-index: 3;
  width: 2px;
  pointer-events: none;
}
.ab-test-centre-guide > i {
  position: absolute;
  top: 58px;
  bottom: 24px;
  left: 0;
  width: 2px;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255,255,255,.94) 0 9px,
    rgba(255,255,255,.14) 9px 16px
  );
  box-shadow: 0 0 1px #06101a, 0 0 8px rgba(255,255,255,.72);
}
.ab-test-centre-guide > i::before,
.ab-test-centre-guide > i::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  background: #fff;
  box-shadow: 0 0 1px #06101a, 0 0 8px rgba(255,255,255,.86);
  transform: translate(-50%, -50%);
}
.ab-test-centre-guide > i::before { width: 25px; height: 2px; }
.ab-test-centre-guide > i::after {
  width: 9px;
  height: 9px;
  border: 2px solid #fff;
  border-radius: 50%;
  background: rgba(6,13,22,.78);
}
.ab-test-centre-guide > span {
  position: absolute;
  left: 9px;
  bottom: 30px;
  display: grid;
  gap: 2px;
  padding: 5px 7px;
  border: 1px solid rgba(255,255,255,.82);
  border-radius: 4px;
  color: #fff;
  background: rgba(6,13,22,.82);
  box-shadow: 0 3px 12px rgba(0,0,0,.3);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .08em;
  white-space: nowrap;
}
.ab-test-centre-guide small {
  color: rgba(255,255,255,.65);
  font: 500 8px/1.1 "IBM Plex Mono", ui-monospace, monospace;
  letter-spacing: .02em;
}
.ab-test-marker {
  --ab-color: #ffcf4a;
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  opacity: 0;
  transition: opacity 120ms linear;
  pointer-events: auto;
  cursor: ew-resize;
  touch-action: none;
}
.ab-test-marker.is-point-b { --ab-color: #54e8ff; }
.ab-test-marker.is-point-c { --ab-color: #ff57c8; }
.ab-test-marker.is-point-d { --ab-color: #72f27a; }
.ab-test-marker.is-point-x { --ab-color: #ff664f; }
.ab-test-marker.is-point-globe-l { --ab-color: #ff9d55; --ab-point-offset: 235; }
.ab-test-marker.is-point-globe-r { --ab-color: #c98cff; --ab-point-offset: 235; }
.ab-test-marker.is-visible { opacity: 1; }
.ab-test-marker.is-dragging { z-index: 4; opacity: 1; }
.ab-test-marker.is-dragging > i { width: 2px; box-shadow: 0 0 13px var(--ab-color); }
.ab-test-marker.is-dragging > span { background: rgba(6,13,22,.96); transform: scale(1.04); transform-origin: left center; }
.ab-test-marker > i {
  position: absolute;
  top: 54px;
  bottom: 25px;
  left: 0;
  width: 1px;
  background: linear-gradient(to bottom, transparent, var(--ab-color) 12%, var(--ab-color) 88%, transparent);
  box-shadow: 0 0 7px var(--ab-color);
}
.ab-test-marker > i::after {
  content: "";
  position: absolute;
  left: -4px;
  top: 50%;
  width: 9px;
  height: 9px;
  border: 2px solid var(--ab-color);
  border-radius: 50%;
  background: rgba(6,13,22,.82);
  transform: translateY(-50%);
}
.ab-test-marker > span {
  position: absolute;
  top: calc(96px + var(--ab-row) * 43px + (var(--ab-point-offset, 0) * 1px));
  left: 7px;
  display: grid;
  grid-template-columns: auto auto;
  gap: 1px 7px;
  min-width: 122px;
  padding: 6px 8px;
  border: 1px solid color-mix(in srgb, var(--ab-color) 76%, white);
  border-radius: 5px;
  color: #fff;
  background: rgba(6,13,22,.86);
  box-shadow: 0 3px 13px rgba(0,0,0,.32), inset 3px 0 0 var(--ab-color);
  white-space: nowrap;
}
.ab-test-marker.is-point-a > span { margin-top: 19px; }
.ab-test-marker.is-point-c > span { margin-top: 38px; }
.ab-test-marker.is-point-d > span { margin-top: 57px; }
.ab-test-marker.is-point-x > span { margin-top: 76px; }
.ab-test-marker b {
  grid-row: 1 / span 2;
  align-self: center;
  color: var(--ab-color);
  font-size: 20px;
  line-height: 1;
}
.ab-test-marker strong { font-size: 10px; letter-spacing: .03em; }
.ab-test-marker em { color: rgba(255,255,255,.72); font-size: 9px; font-style: normal; }
.ab-test-google-loop {
  --loop-color: #ff74d4;
  position: absolute;
  inset: 0;
  z-index: 6;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms linear;
}
.ab-test-google-loop.is-visible { opacity: 1; }
.ab-test-google-loop > svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.ab-test-google-loop path {
  fill: none;
  stroke: var(--loop-color);
  stroke-width: 2;
  stroke-dasharray: 8 5;
  vector-effect: non-scaling-stroke;
  filter: drop-shadow(0 0 5px rgba(255,116,212,.95));
}
.ab-test-google-loop > strong {
  position: absolute;
  top: 112px;
  left: 50%;
  padding: 5px 8px;
  border: 1px solid var(--loop-color);
  border-radius: 5px;
  color: #fff;
  background: rgba(6,13,22,.88);
  box-shadow: 0 3px 13px rgba(0,0,0,.32);
  font-size: 9px;
  letter-spacing: .08em;
  translate: -50% 0;
}
.ab-test-google-loop-node {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  padding: 0;
  border: 0;
  color: #fff;
  background: none;
  pointer-events: auto;
  cursor: move;
  touch-action: none;
}
.ab-test-google-loop-node > i {
  position: absolute;
  left: 0;
  top: 0;
  display: grid;
  width: 24px;
  height: 24px;
  place-items: center;
  border: 2px solid var(--loop-color);
  border-radius: 50%;
  color: #fff;
  background: rgba(6,13,22,.9);
  box-shadow: 0 0 0 1px rgba(255,255,255,.7), 0 0 13px var(--loop-color);
  font: 700 9px/1 "IBM Plex Mono", ui-monospace, monospace;
  transform: translate(-50%, -50%);
}
.ab-test-google-loop-node > span {
  position: absolute;
  left: 15px;
  top: 12px;
  padding: 3px 5px;
  border: 1px solid rgba(255,116,212,.72);
  border-radius: 3px;
  color: rgba(255,255,255,.82);
  background: rgba(6,13,22,.82);
  font: 600 8px/1 "IBM Plex Mono", ui-monospace, monospace;
  white-space: nowrap;
}
.ab-test-google-loop-node.is-dragging { z-index: 2; }
.ab-test-google-loop-node.is-dragging > i { transform: translate(-50%, -50%) scale(1.2); }
.ab-test-google-loop-node.is-dragging > span { background: rgba(6,13,22,.98); }
.ab-test-google-loop-handle-line {
  stroke: rgba(255,255,255,.78);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}
.ab-test-google-loop-handle {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  padding: 0;
  border: 0;
  background: none;
  pointer-events: auto;
  cursor: crosshair;
  touch-action: none;
}
.ab-test-google-loop-handle > i {
  position: absolute;
  left: 0;
  top: 0;
  width: 12px;
  height: 12px;
  border: 2px solid #fff;
  border-radius: 2px;
  background: var(--loop-color);
  box-shadow: 0 0 0 1px rgba(6,13,22,.75), 0 0 8px rgba(255,116,212,.9);
  transform: translate(-50%, -50%) rotate(45deg);
}
.ab-test-google-loop-handle.is-in > i { background: #67e9ff; }
.ab-test-google-loop-handle.is-dragging { z-index: 3; }
.ab-test-google-loop-handle.is-dragging > i {
  transform: translate(-50%, -50%) rotate(45deg) scale(1.35);
}
.ab-test-foreground-handle {
  --ab-color: #a8ffcf;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 5;
  width: 0;
  height: 0;
  opacity: 0;
  pointer-events: none;
  cursor: move;
  touch-action: none;
  transition: opacity 120ms linear;
}
.ab-test-foreground-handle.is-side-right { --ab-color: #ffd39a; }
.ab-test-foreground-handle.is-visible { opacity: 1; pointer-events: auto; }
.ab-test-foreground-handle.is-dragging { z-index: 7; }
.ab-test-foreground-handle > i {
  position: absolute;
  width: 28px;
  height: 28px;
  border: 2px solid var(--ab-color);
  border-radius: 50%;
  background: rgba(6,13,22,.76);
  box-shadow: 0 0 0 1px rgba(255,255,255,.7), 0 0 16px var(--ab-color);
  transform: translate(-50%, -50%);
}
.ab-test-foreground-handle > i::before,
.ab-test-foreground-handle > i::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  background: var(--ab-color);
  transform: translate(-50%, -50%);
}
.ab-test-foreground-handle > i::before { width: 18px; height: 1px; }
.ab-test-foreground-handle > i::after { width: 1px; height: 18px; }
.ab-test-foreground-handle > span {
  position: absolute;
  left: 20px;
  top: -23px;
  display: grid;
  gap: 2px;
  min-width: 208px;
  padding: 6px 8px;
  border: 1px solid var(--ab-color);
  border-radius: 5px;
  color: #fff;
  background: rgba(6,13,22,.9);
  box-shadow: 0 3px 13px rgba(0,0,0,.34), inset 3px 0 0 var(--ab-color);
  white-space: nowrap;
}
.ab-test-foreground-handle.is-side-right > span { left: auto; right: 20px; text-align: right; }
.ab-test-foreground-handle strong { font-size: 10px; letter-spacing: .05em; }
.ab-test-foreground-handle em { color: var(--ab-color); font-size: 9px; font-style: normal; }
.ab-test-foreground-handle small { color: rgba(255,255,255,.6); font-size: 7px; letter-spacing: .04em; }
.ab-test-foreground-handle.is-dragging > span { background: rgba(6,13,22,.98); }
