/* home.css: the homepage v2 (site/index.html).
   The page ships as the stills page, <html class="fallback">: the opening's poster with the H1 over it as
   the first screen, then each chapter's poster, full screen, and its copy on a band beneath, all on
   ordinary scroll. No-JS and reduced-motion visitors keep it. boot.js swaps the class to html.journey only
   when it starts the engine (plan Task 11), which builds the journey layout over these rules.
   - Unscoped: tokens, type, links, the brand mark, the phones' home bar and menu, the footer, and the
     opening's first screen. The opening's poster box and its H1 sit in the same place on both pages, so
     the switch at boot moves nothing (rulings J).
   - html.fallback: the stills layout of the five chapters after the opening.
   - html[data-pre="journey"] (index.html predicts the journey, until boot's verdict or the first scroll): those
     chapters' posters wait, unrequested.
   - html.journey: the canvas boot.js creates, fixed over the page; the page as long as the engine's score;
     every copy block a fixed layer the engine shows and hides (.is-shown); a chapter jump's cover (html[data-cover]).
   - #rail: the chapter rail, and .toggles, the sound's toggle, which only the journey unhides.
   - .player: the hackathons' video player, which the journey lays over a print once a visitor asks for its demo.
   Every colour pair here meets WCAG AA for its size; the bands carry their chapter's palette. */

:root {
  --night: #07090d;
  --cream: #f2ebdf;
  --cream-soft: rgba(242, 235, 223, 0.8);
  --halo: rgba(5, 7, 11, 0.55);
  --serif: 'Newsreader Display', 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
  --serif-zh: 'Noto Serif SC Home', 'Songti SC', 'STSong', 'Noto Serif SC', serif;
  --sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --edge: clamp(24px, 6vw, 100px);
  --bar: 56px;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }
html { background: var(--night); -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--night);
  color: var(--cream);
  font: 400 16px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img { display: block; }
a { color: inherit; }
h1, h2, h3, p { margin: 0; }
/* every Chinese run on the page is drawn from the home CJK subset (check-glyphs pins its coverage) */
[lang="zh-Hans"] { font-family: var(--serif-zh); }
[hidden] { display: none !important; }
:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
/* a phrase that must not break at its hyphens ("2,000-year-old") */
.nw { white-space: nowrap; }
.vh {
  position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---- the brand mark: the seal and the name. A small corner link on desktop; in the bar on phones ---- */
.brand {
  display: inline-flex; align-items: center; gap: 0.6em;
  font: 500 12px/1 var(--sans);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--cream);
}
.brand .seal { width: 15px; height: 15px; flex: none; background: url(/assets/luqlabs/favicon.svg) center / contain no-repeat; }
.corner {
  position: absolute; z-index: 3;
  left: var(--edge);
  top: calc(env(safe-area-inset-top, 0px) + clamp(18px, 3.4vh, 32px));
  padding: 8px 0;
  text-shadow: 0 0 14px var(--halo), 0 0 3px var(--halo);
}

/* ---- phones: the home bar, with the site's menu (a <details>, so it opens with JS off too) ----------- */
.home-bar { display: none; }
@media (max-width: 900px) {
  .corner { display: none; }
  .home-bar {
    position: fixed; z-index: 20; left: 0; right: 0; top: 0;
    height: calc(env(safe-area-inset-top, 0px) + var(--bar));
    padding: env(safe-area-inset-top, 0px) 8px 0 20px;
    display: flex; align-items: center; justify-content: space-between;
    border-bottom: 1px solid rgba(242, 235, 223, 0.08);
  }
  /* the frosted ground sits on a pseudo-element: a backdrop-filter on the bar itself would make the bar
     the containing block of the menu's fixed panel */
  .home-bar::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: rgba(7, 9, 13, 0.74);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
  }
  .menu-btn {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px;
    list-style: none; cursor: pointer;
    color: var(--cream);
  }
  .menu-btn::-webkit-details-marker { display: none; }
  .menu-btn svg { width: 22px; height: 22px; }
  .menu .icon-close, .menu[open] .icon-menu { display: none; }
  .menu[open] .icon-close { display: block; }
  .mobile-menu {
    position: fixed; z-index: 19; left: 0; right: 0; bottom: 0;
    top: calc(env(safe-area-inset-top, 0px) + var(--bar));
    overflow-y: auto;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 28px;
    padding: 32px 24px calc(env(safe-area-inset-bottom, 0px) + 40px);
    background: var(--night);
  }
  /* a closed <details> only hides its content from paint; take the links out of layout too */
  .menu:not([open]) > .mobile-menu { display: none; }
  .mobile-menu a {
    font: 500 13px/1.4 var(--sans);
    letter-spacing: 0.2em;
    text-transform: uppercase;
    text-decoration: none;
    text-align: center;
    color: var(--cream);
    padding: 6px 4px;
  }
  .mm-label {
    margin-top: 8px;
    font: 500 10.5px/1 var(--sans);
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: #c9a96a;
  }
  .mobile-menu .mm-cta {
    margin-top: 6px;
    padding: 13px 26px;
    border-radius: 999px;
    border: 1px solid rgba(241, 215, 166, 0.6);
  }
  html:has(.home-bar .menu[open]) { overflow: hidden; }
}

/* ---- type ------------------------------------------------------------------------------------------ */
.display {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(42px, 4.9vw, 74px);
  line-height: 1.02;
  letter-spacing: -0.012em;
}
.display .line { display: block; }
.display em { font-style: italic; }
.sub, .eyebrow {
  font: 500 12px/1.5 var(--sans);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
/* "Our iOS apps" keeps its own case, as the ink-to-rain spike set it: "iOS" is Apple's casing (the eyebrows' tracking
   kept: the spike's tighter 0.03em puts the stone's crack glint under a larger share of it, 3.71:1 at 360 x 780) */
#apps-title { text-transform: none; }

/* ---- the opening: the poster with the H1 over it, the first screen on both pages ------------------- */
/* 100lvh: the large viewport, as the journey's canvas, so a collapsing mobile toolbar never resizes it */
#opening {
  position: relative;
  height: 100vh; height: 100lvh;
  overflow: hidden;
  background: linear-gradient(180deg, #27303e 0%, #202734 54%, #080b0f 56%, #06080b 100%);
}
#opening .still { position: absolute; inset: 0; }
/* until the journey's canvas has faded in over it (data-faded-in, about 1 s after the first frame: journey.js) the poster
   stays put, fixed as the headline and the cue are, so a visitor who scrolls while a slow link loads the journey never
   uncovers the bare night, before the first frame or under the fade (the same box as at the top of the page; a jump's cover,
   once the journey runs, is fixed the same way: below) */
.journey:not([data-faded-in]) #opening .still { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100vh; height: 100lvh; }
/* each bucket's poster is cut for its aspect; the two wide ones ('w', and '240' from 21:10) keep the gate at its optical
   centre, 60% across */
#poster { width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
@media (min-aspect-ratio: 29/20) { #poster { object-position: 60% 50%; } }
/* its foot stays clear of a phone's toolbar and of the cue: at most 56 px above the small viewport's foot
   (lvh - svh: the toolbar's height; 0 without one, where this is the clamp alone) */
#opening-copy {
  position: absolute; z-index: 2;
  left: var(--edge);
  bottom: clamp(56px, 13vh, 132px); bottom: max(clamp(56px, 13vh, 132px), calc(100lvh - 100svh + 56px));
  max-width: min(36rem, 42vw);
  text-shadow: 0 1px 24px var(--halo), 0 0 3px var(--halo);
}
#opening-copy .sub { margin-top: 1.15rem; color: var(--cream-soft); text-wrap: balance; }

/* one rise on load, staggered; nothing waits for the scene */
@keyframes rise { from { opacity: 0; transform: translate3d(0, 14px, 0); } to { opacity: 1; transform: none; } }
#opening-copy .line, #opening-copy .sub { animation: rise 1s var(--ease-out) both; }
#opening-copy .line:nth-child(2) { animation-delay: 0.11s; }
#opening-copy .sub { animation-delay: 0.25s; }

/* a quiet scroll cue: a hairline with a slow bead of light running down it */
.cue {
  position: absolute; z-index: 2;
  left: var(--edge);
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 30px));
  bottom: calc(100lvh - 100svh + env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 30px));
  display: flex; align-items: center; gap: 12px;
  font: 500 11px/1 var(--sans);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cream-soft);
}
.cue i, .cue span { animation: rise 1s var(--ease-out) 0.5s both; }
.cue i { position: relative; display: block; width: 1px; height: 30px; background: rgba(242, 235, 223, 0.22); overflow: hidden; }
.cue i::after {
  content: ""; position: absolute; left: 0; top: -40%; width: 1px; height: 40%;
  background: linear-gradient(180deg, rgba(242, 235, 223, 0), rgba(242, 235, 223, 0.95));
  animation: bead 2.4s cubic-bezier(0.45, 0, 0.25, 1) 1.2s infinite;
}
@keyframes bead { from { top: -40%; } to { top: 100%; } }
/* it stops while the journey's clock is paused (the session pause, ui/motion.js); the stills page runs it once, so it is
   over 3.6 s after load (WCAG 2.2.2: moving content that lasts more than 5 s needs a pause) */
html[data-motion="paused"] .cue i::after { animation-play-state: paused; }
.fallback .cue i::after { animation-iteration-count: 1; }

/* With JavaScript off (index.html's <noscript>, so the stills page only): one quiet line at the foot of the opening, over
   the dark pool, saying the chapters are below as stills and where to start a project. Wide screens: at the right, level
   with the cue across the frame; phones and tall screens: centred above the cue (below). Kept above a phone's toolbar as
   the cue is (lvh - svh). */
.nojs {
  position: absolute; z-index: 2;
  right: var(--edge);
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 30px));
  bottom: calc(100lvh - 100svh + env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 30px));
  max-width: min(24rem, 40vw);
  font: 400 12.5px/1.5 var(--sans);
  text-align: right;
  color: var(--cream-soft);
  text-shadow: 0 0 14px var(--halo), 0 0 3px var(--halo);
}
.nojs a { color: var(--cream); text-underline-offset: 3px; }

/* phones and tall screens: the gate and its double make one vertical figure, the lockup above it, close
   under the bar (the spike's lockup sits 54 px from the top; the bar takes 56). The line is in full cream,
   not the desktop's softer one: here the moonlit wall behind it is lighter, and on narrow phones (360 px
   and less) it wraps toward the gate's warm halo; so every line of it keeps AA contrast. */
@media (max-aspect-ratio: 4/5) {
  #opening-copy {
    left: 24px; right: 24px;
    top: calc(env(safe-area-inset-top, 0px) + var(--bar) + 16px); bottom: auto;
    max-width: none;
    text-align: center;
  }
  #opening-copy .display { font-size: 40px; }
  #opening-copy .sub { margin-top: 0.9rem; font-size: 11.5px; letter-spacing: 0.14em; color: var(--cream); }
  .cue { left: 0; right: 0; flex-direction: column; justify-content: center; gap: 8px; }
  .cue span { order: -1; }
  .nojs {
    left: 24px; right: 24px; max-width: none; text-align: center;
    bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 30px) + 64px);
    bottom: calc(100lvh - 100svh + env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 30px) + 64px);
  }
}
/* short phones (320x568 to 375x667): a smaller lockup, so the line stays clear of the gate's lit rim,
   which sits about a third of the way down the phone poster */
@media (max-aspect-ratio: 4/5) and (max-height: 680px) {
  #opening-copy .display { font-size: 32px; }
  #opening-copy .sub { margin-top: 0.6rem; font-size: 11px; }
}

/* ---- the stills page: each chapter's poster, full screen, then its copy on a band -------------------- */
/* --band, --ink, --ink-soft and --rule are the chapter's: its band continues the still's palette */
#ten-thousand-suns { --band: #0c0908; --ink: #f6e6c6; --ink-soft: rgba(246, 230, 198, 0.8); --rule: rgba(241, 215, 166, 0.55); }
#lu-xun { --band: #110a04; --ink: #efe3cd; --ink-soft: rgba(239, 227, 205, 0.8); --rule: rgba(237, 222, 196, 0.5); }
#apps { --band: #0b1016; --ink: #dfe7f1; --ink-soft: rgba(223, 231, 241, 0.8); --rule: rgba(214, 224, 236, 0.45); }
#hackathons { --band: #e3e7ec; --ink: #0b0f1a; --ink-soft: rgba(11, 15, 26, 0.76); --rule: rgba(11, 15, 26, 0.5); }
#contact { --band: #ece0c9; --ink: #1c140c; --ink-soft: rgba(28, 20, 12, 0.8); --rule: rgba(28, 20, 12, 0.5); }

.fallback .chapter:not(#opening) { background: var(--band); color: var(--ink); }
.fallback .chapter:not(#opening) > .still { display: block; height: 100vh; height: 100svh; overflow: hidden; }
.fallback .chapter:not(#opening) > .still img { width: 100%; height: 100%; object-fit: cover; }
/* While the journey is predicted and not yet decided (index.html's pre-boot script: <html data-pre="journey">),
   these posters are not requested: a lazy image with no box never nears the viewport. Its fixed-height .still
   keeps its place, so nothing moves when the mark goes (a stills verdict, or the visitor's first scroll). */
[data-pre="journey"] .chapter:not(#opening) > .still img { display: none; }
.fallback .chapter > .copy:not(#opening-copy) {
  padding: clamp(56px, 10vh, 112px) var(--edge) clamp(72px, 14vh, 152px);
}

/* the chapter titles and the ending's headline: a lockup, the title then its line and pill */
.lockup .title { font-family: var(--serif); font-weight: 400; color: var(--ink); }
.lockup .title .en, .lockup .title .zh { display: block; }
.lockup .en { font-size: clamp(32px, 3.3vw, 52px); line-height: 1.05; letter-spacing: 0.002em; }
.lockup .zh { font-family: var(--serif-zh); letter-spacing: 0.24em; line-height: 1.25; }
#tts-copy .zh { margin-top: 10px; font-size: clamp(22px, 2.05vw, 32px); color: var(--ink-soft); }
#luxun-copy .zh { font-size: clamp(34px, 3.3vw, 52px); letter-spacing: 0.16em; }
#luxun-copy .en { margin-top: 0.45em; font-size: clamp(18px, 1.5vw, 23px); font-style: italic; color: var(--ink-soft); }
#contact-copy .title { font-size: clamp(38px, 4.4vw, 66px); line-height: 1.04; letter-spacing: -0.012em; max-width: 12em; }
.lockup .line { max-width: 34em; font-size: 16px; line-height: 1.65; color: var(--ink-soft); text-wrap: pretty; }
#contact-copy .line { font: italic 400 19px/1.5 var(--serif); letter-spacing: 0.005em; }
.lockup .body { margin-top: 26px; }
.lockup .pill { margin-top: 30px; }

.pill {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 13px 22px 13px 24px;
  border-radius: 999px;
  border: 1px solid var(--rule);
  font: 500 13.5px/1 var(--sans);
  letter-spacing: 0.03em;
  text-decoration: none;
  color: var(--ink);
  transition: border-color 0.25s ease, background-color 0.25s ease, color 0.25s ease;
}
.pill:hover { border-color: var(--ink); }
.pill.solid { background: var(--ink); border-color: var(--ink); color: var(--band); }
.pill.solid:hover { background: transparent; color: var(--ink); }
.arrow { display: inline-block; transition: transform 0.35s var(--ease-out); }
a:hover .arrow { transform: translateX(3px); }
.out:hover .arrow { transform: translate(2px, -2px); }

/* the apps and the projects: an eyebrow over a row of items */
.set .eyebrow { color: var(--ink-soft); padding-bottom: 18px; border-bottom: 1px solid var(--rule); }
.set .items { display: grid; gap: 44px; margin-top: 36px; }
/* each item a column with its links at the foot, so the link rows line up across a row of items */
.set .item { display: flex; flex-direction: column; }
.set .links { margin-top: auto; padding-top: 20px; }
.set .name { font: 400 clamp(30px, 2.6vw, 40px)/1.06 var(--serif); letter-spacing: 0.002em; color: var(--ink); }
.set .line { margin-top: 12px; max-width: 30em; font-size: 15.5px; line-height: 1.6; color: var(--ink-soft); text-wrap: pretty; }
.links { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 26px; }
.links a:not(.watch) {
  padding: 8px 0 6px;
  font: 500 14px/1.2 var(--sans);
  text-decoration: none;
  color: var(--ink);
  border-bottom: 1px solid var(--rule);
  transition: border-color 0.25s ease;
}
.links a:not(.watch):hover { border-bottom-color: var(--ink); }
.links .meta { margin-left: 4px; font-weight: 400; color: var(--ink-soft); }
.watch {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 19px 11px 16px;
  border-radius: 999px;
  border: 1px solid var(--ink);
  font: 500 13.5px/1 var(--sans);
  letter-spacing: 0.01em;
  text-decoration: none;
  color: var(--ink);
  transition: background-color 0.25s ease, color 0.25s ease;
}
.watch::before {
  content: ""; width: 0; height: 0;
  border-left: 7px solid currentColor; border-top: 4.5px solid transparent; border-bottom: 4.5px solid transparent;
}
.watch:hover { background: var(--ink); color: var(--band); }
/* the focus ring takes currentColor, which on these two is the band's own colour: draw it in the ink */
.pill.solid:focus-visible, .watch:hover:focus-visible { outline-color: var(--ink); }

@media (min-width: 901px) {
  .lockup { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); column-gap: clamp(32px, 6vw, 112px); align-items: end; }
  .lockup .body { margin-top: 0; padding-bottom: 6px; }
  #apps-copy .items { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: clamp(32px, 6vw, 112px); }
  #hack-copy .items { grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: clamp(28px, 4vw, 72px); }
}

/* ---- the footer: the company links, the studio's address, the owner ---------------------------------- */
.site-footer {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 18px 40px;
  padding: clamp(40px, 7vh, 64px) var(--edge) calc(env(safe-area-inset-bottom, 0px) + clamp(40px, 7vh, 64px));
  border-top: 1px solid rgba(242, 235, 223, 0.1);
  font-size: 13.5px;
  color: var(--cream-soft);
}
.f-links { display: flex; flex-wrap: wrap; gap: 6px 28px; }
.site-footer a { padding: 6px 0; text-decoration: none; color: var(--cream-soft); transition: color 0.2s ease; }
.site-footer a:hover { color: var(--cream); text-decoration: underline; text-underline-offset: 4px; }
.f-copy { margin-left: auto; font-size: 12px; letter-spacing: 0.08em; }
@media (max-width: 900px) {
  .site-footer { flex-direction: column; align-items: flex-start; gap: 18px; }
  .f-links { flex-direction: column; gap: 4px; }
  .f-copy { margin-left: 0; }
}

/* ---- the journey (html.journey, while the engine runs) ----------------------------------------------- */
/* The canvas boot.js creates is fixed over the page, and fades in over the opening's poster once the engine
   has drawn its first frame (the engine adds .on). The page is as long as the score plus one screen (the
   engine sets --journey-vh, in vh); the footer follows it, above the canvas. The later chapters' stills
   wait, hidden, for chapter jumps. Every copy block is a fixed layer over the canvas, shown (.is-shown)
   while the engine's timeline is in its range; the opening's sits exactly where it sits on the stills page,
   so the switch at boot moves nothing. */
.journey #gl {
  position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100vh; height: 100lvh;
  display: block; pointer-events: none;
  opacity: 0; transition: opacity 0.9s ease;
}
.journey #gl.on { opacity: 1; }
/* a hackathon print at its hold at the screen's own pixels, on a screen denser than the canvas (worlds/fog.js: the
   source resampled by the browser to the rectangle it fills, laid over the canvas only while the camera stands at that
   hold and the hackathons world is the one drawn). It comes in over 0.3 s once the camera stands there (the same pixels
   at the screen's own density: only the sharpness changes, never in one frame) and goes at once when it moves on */
.journey .hold-ov {
  position: fixed; z-index: 1; left: 0; top: 0; transform-origin: 0 0;
  visibility: hidden; opacity: 0; pointer-events: none;
}
.journey[data-world="fog"] .hold-ov.on { visibility: visible; opacity: 1; transition: opacity 0.3s linear; }
.journey main { height: calc(var(--journey-vh, 100) * 1vh); height: calc(var(--journey-vh, 100) * 1lvh); }
.journey .chapter:not(#opening) > .still { display: none; }
.journey .copy {
  position: fixed; z-index: 2;
  opacity: 0; visibility: hidden;
  transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}
.journey .copy.is-shown { opacity: var(--fade, 1); visibility: visible; transition: opacity 0.45s ease, visibility 0s; }
/* the opening's lockup and its cue keep their stills-page place (fixed in the viewport is the same place at
   the top of the page) and go with the timeline, not the scroll: the cue leaves with the headline */
.journey #opening-copy { position: fixed; }
/* Copy over the canvas anchors from the top, in lvh (worlds-rulings §3). #gl is fixed at top 0, 100lvh tall;
   a fixed element's `bottom` follows the visible viewport instead, so on a phone an expanded toolbar would lift
   a bottom-anchored block by lvh - svh against the picture (and the H1 would jump from its stills-page place,
   which is measured from the top of the 100lvh #opening, when the engine starts). So the lockups hang from a
   top anchor: the block is a column from the canvas's top, 100lvh tall, with the same clamp as padding at its
   foot, its content packed there (flex-end); the column itself lets pointers through. So the type is laid out
   and painted exactly where it was without a toolbar (the same arithmetic, no transform). The opening's foot
   also keeps 56 px above the small viewport's foot (100svh: a phone's height with its toolbar out, static like
   lvh), as on the stills page, so neither the toolbar nor the cue covers it. The phones' opening keeps its own
   top anchor under the bar (max-aspect-ratio: 4/5). */
@media not all and (max-aspect-ratio: 4/5) {
  .journey #opening-copy {
    top: 0; bottom: auto;
    height: min(100lvh, calc(100svh - 56px + clamp(56px, 13vh, 132px))); padding-bottom: clamp(56px, 13vh, 132px);
    display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; pointer-events: none;
  }
  .journey #opening-copy:not([data-fade="out"]) > * { pointer-events: auto; }
}
.journey .cue {
  position: fixed;
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 30px));
  opacity: var(--fade, 1);
  transition: opacity 0.45s ease;
}
.journey #opening-copy:not(.is-shown) ~ .cue { opacity: 0; }
/* Copy that leaves with the picture (worlds-rulings §3; core/score.js TEXT_RANGES fade): the engine's timeline
   sets its opacity (ui/copy.js writes --fade and marks it data-fade), so no timed transition may lag it, or it
   would ghost over the connection in a fast scroll or a fling. The block is hidden (is-shown off) where --fade
   reaches 0, so it leaves at once. At fade 0 (data-fade="out") it takes no pointer events either: invisible, it
   never stands in a click's way. */
.journey [data-fade], .journey .copy.is-shown[data-fade] { transition: none; }
.journey [data-fade="out"] { pointer-events: none; }
/* the later chapters' blocks: a lockup at the foot of the frame (each world's own task places its own) */
.journey .copy:not(#opening-copy) {
  left: var(--edge); right: var(--edge); bottom: clamp(56px, 13vh, 132px);
  max-width: 44rem;
  text-shadow: 0 1px 24px var(--halo), 0 0 3px var(--halo);
}
/* Ten Thousand Suns at the bird hold: the crossing spike's lockup, title over line over pill, stacked, kept
   off the bird and the bright centre of the blades. The frame scales with the canvas's height (lvh). At the
   hold the bird's tail ends at about 60-61% of that height upright (64% in landscape), the ladder of light on
   the floor starts at about 84% (88.5%), and the wing tips are 17lvh either side of the centre; so the block is
   placed and sized in lvh against those. Every band anchors from the canvas's top, in lvh, never from
   `bottom`: the bands placed by their foot are a column from the top down to that line, the lockup packed at
   its foot, like the opening's (above); its foot also stays 12 px inside a phone's small viewport (100svh,
   static), so a short landscape phone never hides the pill under the toolbar. The block's own rules name the
   chapter, to outrank `.journey .copy:not(#opening-copy)`.
   - Wide screens (wider than 29:20): at the foot of the frame's dark left side (the spike's d-d0), as laid out
     at 1440x900; on a shorter frame (a laptop's browser window) the whole lockup scales down with the frame,
     so the title stays as far below the bird and the line as far off the blades as at 1440x900. On narrower
     wide frames the title's width is held 25.5lvh short of the centre and the line's 22lvh (5lvh outside the
     wing tips), its size stepping down to 12 px where that keeps it on three lines.
   - Squarer frames down to 19:20 (taller, where the block sits at or below the tail's level, clear of the
     bird): the same corner, as laid out in pixels.
   - Near 4:3 (13:10 to 29:20), and short landscape (phones held sideways): the same corner, sized in lvh, its
     foot just above the ladder (or 12 px inside a phone's small viewport, if that is higher), its width capped
     6lvh short of the wing tips (the title scaled down to keep to one line within it), so it stays beside the
     bird and off the blades' bright centre.
   - Upright tablets: centred, its top at 65lvh, or 37 px under 61.4lvh where that is lower (short tablets and
     foldables keep 40 px under the tail), the line on two lines so the pill clears the ladder.
   - Upright phones (600 px wide and less): the spike's own lockup (m-m0), title, zh and pill, its top at
     69.5lvh, with the spike's air under the tail. The line can't fit that band with 40 px of air (the 12 px
     floor), so it is visually hidden here: screen readers still read it, and the stills page shows it. */
.journey #ten-thousand-suns #tts-copy {
  top: 0; bottom: auto;
  height: min(100lvh, calc(100svh - 12px + clamp(56px, 13lvh, 132px))); padding-bottom: clamp(56px, 13lvh, 132px);
  max-width: 30rem;
  display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; pointer-events: none;
}
.journey #tts-copy > *:not([data-fade="out"]) { pointer-events: auto; }
.journey #tts-title, .journey #tts-copy .body { opacity: var(--fade, 1); }
.journey #tts-copy .en { font-size: clamp(30px, 3.1vw, 48px); }
.journey #tts-copy .body { margin-top: 22px; padding-bottom: 0; }
.journey #tts-copy .line { max-width: 25em; font-size: 15px; }
.journey #tts-copy .pill { margin-top: 26px; }
/* each length is the smaller of its px size and its size at 1440x900 in lvh (a hair over it, so 1440x900 and
   anything taller keep the px sizes exactly), with floors for the line and the pill */
@media (min-aspect-ratio: 29/20) {
  .journey #tts-copy .en { font-size: min(clamp(30px, 3.1vw, 48px), 4.97lvh, (50vw - 25.5lvh - var(--edge)) / 9); }
  .journey #tts-copy .zh { margin-top: min(10px, 1.12lvh); font-size: min(clamp(22px, 2.05vw, 32px), 3.29lvh); }
  .journey #tts-copy .body { margin-top: min(22px, 2.45lvh); }
  .journey #tts-copy .line {
    max-width: min(25em, 50vw - 22lvh - var(--edge));
    font-size: max(12px, min(15px, max(13px, 1.67lvh), (50vw - 22lvh - var(--edge)) / 24.2));
  }
  .journey #tts-copy .pill {
    margin-top: min(26px, 2.9lvh); padding: min(13px, 1.45lvh) 22px min(13px, 1.45lvh) 24px;
    font-size: max(12px, min(13.5px, 1.51lvh));
  }
}
@media (min-aspect-ratio: 13/10) and (max-aspect-ratio: 29/20) {
  .journey #ten-thousand-suns #tts-copy {
    height: min(100lvh, calc(100svh - 12px + 13lvh)); padding-bottom: 13lvh; max-width: calc(50vw - 23lvh - var(--edge));
  }
  .journey #tts-copy .en { font-size: min(3.7lvh, (50vw - 23lvh - var(--edge)) / 9); }
  .journey #tts-copy .zh { margin-top: 0.9lvh; font-size: 2.6lvh; }
  .journey #tts-copy .body { margin-top: 2.2lvh; }
  .journey #tts-copy .line { max-width: none; font-size: max(13px, 1.75lvh); line-height: 1.55; }
  .journey #tts-copy .pill { margin-top: 2.6lvh; padding: 1.45lvh 20px 1.45lvh 22px; font-size: max(12px, 1.6lvh); white-space: nowrap; }
}
@media (min-aspect-ratio: 13/10) and (max-height: 520px) {
  .journey #ten-thousand-suns #tts-copy {
    height: min(100lvh, calc(100svh - 12px + 13lvh)); padding-bottom: 13lvh; max-width: calc(50vw - 23lvh - var(--edge));
  }
  .journey #tts-copy .en { font-size: min(6.1lvh, (50vw - 23lvh - var(--edge)) / 9); }
  .journey #tts-copy .zh { margin-top: 1lvh; font-size: 4.6lvh; }
  .journey #tts-copy .body { margin-top: 3lvh; }
  .journey #tts-copy .line { max-width: none; font-size: max(12px, 3.3lvh); line-height: 1.4; }
  .journey #tts-copy .pill { margin-top: 3.2lvh; padding: 2.2lvh 18px 2.2lvh 20px; font-size: max(12px, 3.5lvh); white-space: nowrap; }
}
@media (max-aspect-ratio: 19/20) {
  .journey #ten-thousand-suns #tts-copy {
    left: 24px; right: 24px; top: max(65lvh, calc(61.4lvh + 37px)); height: auto; padding-bottom: 0; max-width: none;
    display: block; text-align: center;
  }
  .journey #tts-copy .en { font-size: 3.2lvh; }
  .journey #tts-copy .zh { margin-top: 0.45lvh; font-size: 2.2lvh; padding-left: 0.24em; }
  .journey #tts-copy .body { margin-top: 1.2lvh; }
  .journey #tts-copy .line { max-width: 35em; margin: 0 auto; font-size: max(12px, 1.5lvh); line-height: 1.4; text-wrap: balance; }
  .journey #tts-copy .pill { margin-top: 1.1lvh; padding: 1.3lvh 20px 1.3lvh 22px; font-size: max(12px, 1.6lvh); }
}
@media (max-aspect-ratio: 19/20) and (max-width: 600px) {
  .journey #ten-thousand-suns #tts-copy { top: 69.5lvh; }
  .journey #tts-copy .en { font-size: 3.55lvh; }
  .journey #tts-copy .zh { margin-top: 1.7lvh; font-size: 2.5lvh; }
  .journey #tts-copy .body { margin-top: 1.95lvh; }
  .journey #tts-copy .line {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  .journey #tts-copy .pill { margin-top: 0; padding: 1.42lvh 20px 1.42lvh 22px; }
}
/* Lu Xun at the chamber's arrival (worlds/chamber.js: the camera stands back from the stele while the torch rakes
   the guardian, left, over the top, to the right): the spikes' lockup (crossing d-d5, ink-to-rain d-d0), the title
   over its English, the line in spaced capitals, the pill, at the foot of the frame's dark left side, clear of the
   stele's face and of the flame. Anchored from the canvas's top in lvh, as the hall's: a column from the top down to
   its foot line, the lockup packed at its foot, which also stays 12 px inside a phone's small viewport (100svh,
   static), so a toolbar never covers the pill. Its width keeps 24 px short of the stele, whose near edge is at
   about 40% of a landscape frame's width (the line wraps there on a 4:3 screen). The block comes and goes with its
   timed fade at the start of the hold; its title and body leave with the picture as the camera starts in toward the
   stele (TEXT_RANGES fade, on the two children: no timed transition lags them).
   - Upright frames (19:20 and narrower): the frame is lifted by the rail's lens shift, the stele and its flame above
     the lockup, which is sized in lvh so it keeps under the stele's foot on short phones too. Wider than the bar's
     widths (901 px and more), it keeps the left margin, where the rail and the toggles stand.
   - Short landscape (phones held sideways): the same corner, sized in lvh, its width kept left of the stele. */
.journey #lu-xun #luxun-copy {
  top: 0; bottom: auto;
  height: min(100lvh, calc(100svh - 12px + clamp(32px, 9lvh, 104px))); padding-bottom: clamp(32px, 9lvh, 104px);
  max-width: min(34rem, 40vw - var(--edge) - 24px);
  display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; pointer-events: none;
}
.journey #luxun-copy > *:not([data-fade="out"]) { pointer-events: auto; }
.journey #luxun-title, .journey #luxun-copy .body { opacity: var(--fade, 1); }
.journey #luxun-copy .zh { font-size: clamp(34px, 3.3vw, 52px); line-height: 1.1; }
.journey #luxun-copy .en { margin-top: 0.5em; font-size: clamp(16px, 1.22vw, 19px); }
.journey #luxun-copy .body { margin-top: 20px; padding-bottom: 0; }
.journey #luxun-copy .line { max-width: none; font: 500 11.5px/1.5 var(--sans); letter-spacing: 0.14em; text-transform: uppercase; }
.journey #luxun-copy .pill { margin-top: 18px; }
@media (max-aspect-ratio: 19/20) {
  .journey #lu-xun #luxun-copy {
    left: 24px; right: 24px; max-width: none;
    height: min(100lvh, calc(100svh - 12px + 7lvh)); padding-bottom: 7lvh;
  }
  .journey #luxun-copy .zh { font-size: 4lvh; }
  .journey #luxun-copy .en { font-size: 1.9lvh; }
  .journey #luxun-copy .body { margin-top: 2.2lvh; }
  .journey #luxun-copy .line { font-size: max(11px, 1.36lvh); }
  .journey #luxun-copy .pill { margin-top: 2lvh; padding: 1.4lvh 20px 1.4lvh 22px; font-size: max(12px, 1.55lvh); }
}
@media (max-aspect-ratio: 19/20) and (min-width: 901px) {
  .journey #lu-xun #luxun-copy { left: var(--edge); right: var(--edge); }
}
@media (min-aspect-ratio: 13/10) and (max-height: 520px) {
  .journey #lu-xun #luxun-copy {
    height: min(100lvh, calc(100svh - 12px + 9lvh)); padding-bottom: 9lvh; max-width: calc(41vw - var(--edge) - 4lvh);
  }
  .journey #luxun-copy .zh { font-size: 7.5lvh; }
  .journey #luxun-copy .en { font-size: max(12px, 3.6lvh); }
  .journey #luxun-copy .body { margin-top: 3.5lvh; }
  .journey #luxun-copy .line { font-size: 11px; }
  .journey #luxun-copy .pill { margin-top: 3lvh; padding: 2.2lvh 18px 2.2lvh 20px; font-size: 12px; }
}
/* The apps over the rain world (worlds/rain.js, its two holds): the ink-to-rain spike's quiet set, the eyebrow over the
   app that is up (one at a time: both apps in one grid cell, their links lined up at its foot), at the foot of the
   frame's dark left side, the phone lying to its right (the rail's lens shift). Anchored from the canvas's top in lvh,
   as the lockups: a column from the top down to its foot line, the set packed at its foot, which also stays 12 px
   inside a phone's small viewport (100svh, static), so a toolbar never covers the links. Each app comes and goes with
   its timed fade where it is shown and hidden; its name, line and links, and the eyebrow, leave with the picture
   (TEXT_RANGES fade, on the children: the wash, and the connections at either end).
   - Upright frames (4:5 and narrower): the phone is lifted above the set (the rail's lens shift), which spans the frame;
     wider than the bar's widths (901 px and more), it keeps the left margin, where the rail and the toggles stand.
   - Short landscape (phones held sideways): the same corner, sized in lvh. */
.journey #apps #apps-copy {
  top: 0; bottom: auto;
  height: min(100lvh, calc(100svh - 12px + clamp(32px, 9lvh, 104px))); padding-bottom: clamp(32px, 9lvh, 104px);
  max-width: min(25rem, 34vw);
  display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; pointer-events: none;
}
.journey #apps-copy .eyebrow { opacity: var(--fade, 1); padding-bottom: 0; border-bottom: 0; }
.journey #apps-copy .items { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; margin-top: 12px; }
.journey #apps-copy .item {
  grid-area: 1 / 1; align-self: end;
  opacity: 0; visibility: hidden;
  transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}
.journey #apps-copy .item.is-shown { opacity: 1; visibility: visible; transition: opacity 0.45s ease, visibility 0s; }
.journey #apps-copy .item > * { opacity: var(--fade, 1); }
.journey #apps-copy .item.is-shown > *:not([data-fade="out"]) { pointer-events: auto; }
.journey #apps-copy .name { font-size: clamp(28px, 2.35vw, 36px); line-height: 1.08; }
.journey #apps-copy .line { max-width: none; margin-top: 12px; font-size: 15px; line-height: 1.5; }
.journey #apps-copy .links { padding-top: 18px; gap: 8px 24px; }
@media (max-aspect-ratio: 4/5) {
  .journey #apps #apps-copy {
    left: 24px; right: 24px; max-width: none;
    height: min(100lvh, calc(100svh - 12px + max(30px, 4.5lvh))); padding-bottom: max(30px, 4.5lvh);
  }
  .journey #apps-copy .items { margin-top: 9px; }
  .journey #apps-copy .name { font-size: 28px; }
  .journey #apps-copy .line { margin-top: 9px; font-size: 14.5px; }
  .journey #apps-copy .links { padding-top: 14px; }
}
@media (max-aspect-ratio: 4/5) and (min-width: 901px) {
  .journey #apps #apps-copy { left: var(--edge); right: var(--edge); }
}
@media (min-aspect-ratio: 13/10) and (max-height: 520px) {
  .journey #apps #apps-copy {
    height: min(100lvh, calc(100svh - 12px + 7lvh)); padding-bottom: 7lvh; max-width: min(22rem, 38vw);
  }
  .journey #apps-copy .items { margin-top: 1.6lvh; }
  .journey #apps-copy .name { font-size: max(20px, 6.4lvh); }
  .journey #apps-copy .line { margin-top: 1.8lvh; font-size: max(12px, 3.4lvh); line-height: 1.4; }
  .journey #apps-copy .links { padding-top: 2.4lvh; }
  .journey #apps-copy .links a:not(.watch) { padding: 1.6lvh 0 1.2lvh; font-size: max(12px, 3.3lvh); }
}
/* The hackathon projects over the still water (worlds/fog.js, its three holds): the hack-lantern-mirror spike's words,
   ink on the pale morning, the eyebrow over the project that is up (one at a time: the three in one grid cell), low at
   the frame's left, the print standing right of them (the rail's lens shift). They sit low over the deep blue near water,
   so a breath of the morning's pale haze lies behind them (the name's ::before, under the whole set, the eyebrow over
   the project included: it comes and goes with the project), so the ink reads; on a wide screen the haze fades out over
   the last 290 px before the print's left edge at its hold (fog.js sets --fog-print-l-<i> on <html>), so a hold stays
   the source's exact pixels and its double stays clear. Anchored from the canvas's top in lvh, as the apps: a column
   from the top down to its foot line, the set packed at its foot, which also stays 12 px inside a phone's small viewport
   (100svh), so a toolbar never covers the links. Each project comes and goes with its timed fade where it is shown and
   hidden; its name, line and links, and the eyebrow, leave with the picture (TEXT_RANGES fade, on the children).
   - Upright, square and near-square frames (narrower than 13:10; on a shorter frame, wider: to 1.33 at 860 px tall and
     less, to 29:20 at 760 px and less, to 3:2 at 560 px and less): the scroll stands high (the rail's lens shift), the
     words in the lower third, across the frame; the haze is not cut (the print is above it). From 4:5 to 1:1 the print at
     its hold stands too far left for the corner (it came over the eyebrow and the name, the line a word a row); beyond,
     the corner's line and links sit low on the deep water, under 4.5:1 with no haze behind them (QuotePilot's long line
     the last: to 1135 x 900, 1048 x 800, 1006 x 768, 933 x 700 and 800 x 600; CocktailSteps' to 762 x 540 and 762 x 524;
     at 741 px, just past 1.33, 4.51). The short landscape below (13:10 and wider, 520 px tall and less) never meets this.
     Here the line may run over the edge of a print's double, under the haze. Wider than the bar's widths (901 px and
     more), the words keep the left margin, where the rail and the toggles stand.
   - Short landscape (phones held sideways): the same corner, sized in lvh, narrower (the scroll stands right of it in the
     16:10 framing there: worlds/fog.js framing). */
.journey #hackathons #hack-copy {
  --hl: clamp(24px, 6.2vw, 104px); --hw: min(31rem, 40vw); --hpad: clamp(36px, 10lvh, 110px);
  top: 0; bottom: auto; left: var(--hl); right: auto; width: var(--hw); max-width: none;
  height: min(100lvh, calc(100svh - 12px + var(--hpad))); padding-bottom: var(--hpad);
  display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; pointer-events: none;
  text-shadow: none;
}
.journey #hack-copy .eyebrow {
  position: relative; z-index: 1; opacity: var(--fade, 1); padding-bottom: 0; border-bottom: 0;
  font-size: 11.5px; color: var(--ink);
}
.journey #hack-copy .items { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; margin-top: 14px; }
.journey #hack-copy .item {
  grid-area: 1 / 1; align-self: start; position: relative;   /* (each right under the eyebrow, whatever its height) */
  opacity: 0; visibility: hidden;
  transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}
.journey #hack-copy .item.is-shown { opacity: 1; visibility: visible; transition: opacity 0.45s ease, visibility 0s; }
.journey #hack-copy .item > * { opacity: var(--fade, 1); }
.journey #hack-copy .item.is-shown > *:not([data-fade="out"]) { pointer-events: auto; }
.journey #hack-copy .name { font-size: clamp(34px, 3.2vw, 50px); line-height: 1.04; }
.journey #hack-copy .line {
  /* (at least 28 px short of the print's left edge at its hold: --pl, below) */
  max-width: min(27em, calc(var(--pl) - var(--hl) - 28px)); margin-top: 12px; font-size: 15.5px; line-height: 1.5;
}
.journey #hack-copy .links { padding-top: 22px; gap: 10px 22px; }
#hack-cocktailsteps { --pl: var(--fog-print-l-0, 200vw); }
#hack-quotepilot { --pl: var(--fog-print-l-1, 200vw); }
#hack-linecanary { --pl: var(--fog-print-l-2, 200vw); }
.journey #hack-copy .name::before {
  /* (the haze leads the words out: its own opacity follows their fade twice over, so it is gone well before they are) */
  content: ""; position: absolute; z-index: -1; pointer-events: none; opacity: calc(var(--fade, 1) * var(--fade, 1));
  left: calc(var(--hw) * -0.4); right: calc(var(--hw) * -0.7); top: -95%; bottom: -55%;
  /* its left edge is at var(--hl) - 0.4 var(--hw) in the viewport: the fade's two stops, in its own x, are 300 and 12 px
     short of the print (a long, soft fall: no edge reads as the words leave and the print moves off) */
  --hz-x: calc(var(--pl) - var(--hl) + 0.4 * var(--hw));
  -webkit-mask-image: linear-gradient(to right, #000 calc(var(--hz-x) - 300px), transparent calc(var(--hz-x) - 12px));
  mask-image: linear-gradient(to right, #000 calc(var(--hz-x) - 300px), transparent calc(var(--hz-x) - 12px));
  background: radial-gradient(closest-side, rgba(234, 238, 242, 0.86) 22%, rgba(234, 238, 242, 0.7) 46%, rgba(234, 238, 242, 0.32) 72%,
    rgba(234, 238, 242, 0.1) 88%, rgba(234, 238, 242, 0) 100%);
}
@media not all and (min-aspect-ratio: 13/10), (min-height: 520.02px) and (max-height: 860px) and (max-aspect-ratio: 133/100),
  (min-height: 520.02px) and (max-height: 760px) and (max-aspect-ratio: 29/20), (min-height: 520.02px) and (max-height: 560px) and (max-aspect-ratio: 3/2) {
  .journey #hackathons #hack-copy {
    --hl: 24px; --hw: calc(100vw - 48px); --hpad: max(30px, 4.5lvh);
  }
  .journey #hack-copy .items { margin-top: 3px; }
  .journey #hack-copy .name { font-size: 26px; }
  .journey #hack-copy .line { position: relative; max-width: none; margin-top: 5px; font-size: 14.5px; line-height: 1.4; }
  .journey #hack-copy .links { padding-top: 7px; }
  .journey #hack-copy .watch { padding: 8px 17px 8px 14px; }
  .journey #hack-copy .name::before {
    left: -38%; right: -38%; top: -90%; bottom: -62%;
    -webkit-mask-image: none; mask-image: none;
  }
  /* (and a second breath under the line and the links: the morning's pale low over the water, as the spike's words lay) */
  .journey #hack-copy .line::before {
    content: ""; position: absolute; z-index: -1; pointer-events: none; opacity: var(--fade, 1);
    left: -38%; right: -38%; top: -40%; bottom: -170%;
    background: radial-gradient(closest-side, rgba(234, 238, 242, 0.62) 30%, rgba(234, 238, 242, 0.34) 66%, rgba(234, 238, 242, 0.1) 88%,
      rgba(234, 238, 242, 0) 100%);
  }
  @media (min-width: 901px) {
    .journey #hackathons #hack-copy { --hl: clamp(24px, 6.2vw, 104px); --hw: calc(100vw - 2 * var(--hl)); }
  }
}
@media (min-aspect-ratio: 13/10) and (max-height: 520px) {
  .journey #hackathons #hack-copy { --hw: min(20rem, 35vw); --hpad: 7lvh; }
  .journey #hack-copy .items { margin-top: 1.6lvh; }
  .journey #hack-copy .name { font-size: max(20px, 6.4lvh); }
  .journey #hack-copy .line { margin-top: 1.8lvh; font-size: max(12px, 3.4lvh); line-height: 1.4; }
  .journey #hack-copy .links { padding-top: 2.4lvh; }
  .journey #hack-copy .name::before {
    /* (the words stand close to the print here: the haze's fall stays short, 172 to 12 px) */
    -webkit-mask-image: linear-gradient(to right, #000 calc(var(--hz-x) - 172px), transparent calc(var(--hz-x) - 12px));
    mask-image: linear-gradient(to right, #000 calc(var(--hz-x) - 172px), transparent calc(var(--hz-x) - 12px));
  }
}
/* The ending at dusk (worlds/ending.js): the bookends spike's lockup on the sunlit wall. At night the wall is dark and the
   type is light; at dusk the light is on the wall, so the type is the project form's own ink (/make-yours), with a faint
   warm halo, and the one way through is an ink pill with the form's paper type. The headline breaks as the spike's does,
   "Tell us what / you want built." (its measure, 6.75em, lies between the two lines' widths and "Tell us what you"'s).
   Wide screens: left of the gate, level with it, the waterline only ever moving away below: the lockup is the spike's at
   1440 x 900 and, on a shorter frame, scales with the canvas's height (each size the smaller of the spike's and its lvh
   share there), so its foot stays at about 48 lvh, clear of the waterline, which the first frame puts at 55. Anchored
   from the canvas's top in lvh (worlds-rulings §3), so a toolbar never moves it against the picture; nothing here is
   anchored from the foot. The title and the body come in with the picture over the ending's first frame and stay to the end of the page
   (TEXT_RANGES fade, on the two children, so the block keeps its timed fade where it is shown: a chapter jump landing
   on the rest).
   - Upright frames (4:5 and narrower): the lockup centred above the gate and its double, under the bar, sized in lvh as
     the frame is (the gate's top stands at about a third of the canvas's height through the whole walk: ending.js
     PORTRAIT), so it keeps clear of the gate on every phone of that shape; shorter phones (760 px and less, 600 and
     less: the bar takes more of their height) a little smaller and tighter.
   - Short landscape (phones held sideways): the same corner, close under the bar, sized in lvh and narrower (the gate's
     left edge at rest is at 48 to 50vw there: ending.js RAIL.short), so its foot stays on the wall above the first frame's
     waterline.
   - Frames wider than 2:1, and short landscape: the line in the full ink. There the lockup stands at the wall's far left,
     where the vignette and the dusk's fall-off darken the lime, and the soft ink falls under 4.5:1. */
.journey #contact #contact-copy {
  --ink: #0b0f1a; --ink-soft: rgba(11, 15, 26, 0.84); --band: #f7f5ef; --rule: #0b0f1a;
  display: block;
  top: clamp(120px, 21lvh, 260px); bottom: auto; left: var(--edge); right: auto;
  max-width: min(36rem, 42vw);
  text-shadow: 0 0 18px rgba(250, 238, 214, 0.32);
  pointer-events: none;
}
.journey #contact-title, .journey #contact-copy .body { opacity: var(--fade, 1); }
.journey #contact-copy > *:not([data-fade="out"]) { pointer-events: auto; }
.journey #contact-copy .title { max-width: 6.75em; font-size: clamp(30px, min(4.4vw, 7.05lvh), 66px); line-height: 1.02; text-wrap: balance; }
.journey #contact-copy .body { margin-top: min(1.15rem, 2.05lvh); padding-bottom: 0; }
.journey #contact-copy .line { max-width: min(24em, 30vw); font-size: clamp(12px, 1.89lvh, 17px); text-wrap: balance; }
.journey #contact-copy .pill {
  margin-top: min(1.9rem, 3.38lvh); gap: 0.6em; padding: 0.95em 1.55em 0.95em 1.65em;
  font: 600 clamp(12px, 1.67lvh, 15px)/1 var(--sans); letter-spacing: 0.01em;
  text-shadow: none; box-shadow: 0 10px 30px -14px rgba(11, 15, 26, 0.7);
  transition: box-shadow 0.35s var(--ease-out), background-color 0.35s var(--ease-out);
}
.journey #contact-copy .pill:hover { background: #121827; border-color: #121827; color: var(--band); box-shadow: 0 14px 36px -12px rgba(11, 15, 26, 0.8); }
.journey #contact-copy .pill:hover .arrow { transform: translateX(4px); }
/* (squarer frames: the rail turns toward the phones' framing, the gate larger and further left, its left edge at rest
   at 41vw at 4:3, 39vw at 5:4, 34vw at 11:10 and 27vw at 9:10; the lockup narrows ahead of it) */
@media (max-aspect-ratio: 13/10) {
  .journey #contact-copy .title { font-size: clamp(30px, min(3.6vw, 7.05lvh), 66px); }
  .journey #contact-copy .line { max-width: min(24em, 24vw); }
}
@media (max-aspect-ratio: 11/10) {
  .journey #contact-copy .title { font-size: clamp(26px, min(2.8vw, 7.05lvh), 66px); }
  .journey #contact-copy .line { max-width: 18vw; font-size: clamp(12px, 1.89lvh, 16px); }
}
@media (min-aspect-ratio: 2/1) {
  .journey #contact-copy .line { color: var(--ink); }
}
@media (max-aspect-ratio: 4/5) {
  .journey #contact #contact-copy {
    left: 24px; right: 24px; max-width: none;
    top: calc(env(safe-area-inset-top, 0px) + var(--bar) + 1.6lvh);
    text-align: center;
  }
  .journey #contact-copy .title { margin: 0 auto; font-size: max(24px, 4lvh); }
  .journey #contact-copy .body { margin-top: 1.5lvh; }
  .journey #contact-copy .line { max-width: 19em; margin: 0 auto; font-size: max(13px, 1.78lvh); line-height: 1.45; }
  .journey #contact-copy .pill { margin-top: 2.1lvh; padding: 0.9em 1.45em 0.9em 1.55em; font-size: max(13px, 1.78lvh); }
}
@media (max-aspect-ratio: 4/5) and (max-height: 760px) {
  .journey #contact #contact-copy { top: calc(env(safe-area-inset-top, 0px) + var(--bar) + 0.8lvh); }
  .journey #contact-copy .title { font-size: 3.7lvh; }
  .journey #contact-copy .body { margin-top: 1.2lvh; }
  .journey #contact-copy .line { font-size: max(12px, 1.95lvh); line-height: 1.4; }
  .journey #contact-copy .pill { margin-top: 1.6lvh; padding: 0.75em 1.35em 0.75em 1.45em; font-size: max(12px, 1.95lvh); }
}
@media (max-aspect-ratio: 4/5) and (max-height: 600px) {
  .journey #contact-copy .body { margin-top: 0.6lvh; }
  .journey #contact-copy .line { line-height: 1.3; }
  .journey #contact-copy .pill { margin-top: 0.8lvh; }
}
@media (min-aspect-ratio: 13/10) and (max-height: 520px) {
  .journey #contact #contact-copy { top: calc(var(--bar) + 1.6lvh); max-width: 40vw; }
  .journey #contact-copy .title { font-size: max(20px, 5.6lvh); }
  .journey #contact-copy .body { margin-top: 1.6lvh; }
  .journey #contact-copy .line { max-width: 38vw; font-size: 12px; line-height: 1.4; color: var(--ink); }
  .journey #contact-copy .pill { margin-top: 2.4lvh; padding: 0.85em 1.4em 0.85em 1.5em; font-size: 12px; }
}
@media (min-aspect-ratio: 13/10) and (max-height: 360px) {
  .journey #contact-copy .title { font-size: max(18px, 5lvh); }
  .journey #contact-copy .body { margin-top: 1lvh; }
  .journey #contact-copy .line { line-height: 1.3; }
  .journey #contact-copy .pill { margin-top: 1.6lvh; }
}
.journey .site-footer { position: relative; z-index: 2; background: var(--night); }

/* A distant chapter jump (ui/chapters.js mountCover): the #poster <img>, the opening's still in its <picture>, takes the
   destination's poster for this viewport and covers the canvas, fixed over it (above a hold's overlay, under the copy),
   until the world there is warm; then it crossfades out over the live frame (data-cover-out). The opening's two wide
   posters ('w' and '240') keep the gate at 60% across (above); the other worlds' stills are centred. */
.journey[data-cover] #opening .still {
  position: fixed; z-index: 2; inset: 0 auto auto 0; width: 100%; height: 100vh; height: 100lvh;
  background: var(--night);
}
.journey[data-cover]:not([data-cover="opening"]) #poster { object-position: 50% 50%; }
.journey[data-cover-out] #opening .still { opacity: 0; pointer-events: none; transition: opacity 0.6s ease; }

/* ---- the chapter rail (ui/chapters.js fills #rail and unhides it once the journey's first frame is drawn) -----------
   Six quiet marks, one per chapter, the current one drawn long (aria-current); a click, or Enter, takes the journey there.
   Each target is 24 x 24 px (WCAG 2.5.8). The marks are cream on a ground at least 0.66 night (the capsule, or the
   phones' bar), so they keep 3:1 over the palest frame, and the labels sit on chips of 0.78 night (4.5:1 and more); the
   focus ring is drawn inside the target, on that ground.
   - Desktop: a small capsule in the left margin (centred in --edge, beside the copy, never on it), centred on the
     canvas's height (in lvh, as the copy: a toolbar never moves it against the picture). Every hold keeps its print,
     screen and artwork right of the copy, so that margin stays clear at every aspect; the right edge does not (from 4:3 to
     3:2 the hackathons' prints run to it). Its labels come out to its right while a pointer is over it or a key has
     focused one of its links (all of them where :has() is supported, else the focused one's).
   - Phones and tablets (the home bar's widths): a row in the home bar, beside the menu button, on the bar's frosted
     ground. The label of a link a key has focused (or a pointer hovers) shows in the bar itself, on a night chip over
     the brand mark (from 12 px in to 6 px short of the row, centred on the bar's height), so it never covers the
     picture: a hung rubbing reaches the top of the frame at the chamber's peel and end holds. Its type is trimmed to
     fit "Ten Thousand Suns" on one line at 360 px; narrower, it wraps inside the bar. Hidden while the menu is open.
   - Only the label of the link hovered or focused takes pointer events, so a click on copy under another label's chip
     is the copy's. Esc quiets the labels (#rail[data-quiet], ui/chapters.js) until the pointer or focus leaves the rail
     (WCAG 1.4.13). */
#rail {
  position: fixed; z-index: 21;
  left: max(8px, var(--edge) / 2 - 16px); top: 50vh; top: 50lvh; transform: translateY(-50%);
  animation: rail-in 0.9s ease both;
}
@keyframes rail-in { from { opacity: 0; } to { opacity: 1; } }
#rail ol {
  display: flex; flex-direction: column; margin: 0; padding: 4px; list-style: none;
  border-radius: 999px; background: rgba(7, 9, 13, 0.66);
}
#rail a {
  position: relative; display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 50%;
  color: var(--cream); text-decoration: none;
}
#rail .dot {
  width: 6px; height: 6px; border-radius: 3px; background: rgba(242, 235, 223, 0.6);
  transition: width 0.3s var(--ease-out), height 0.3s var(--ease-out), background-color 0.2s ease;
}
#rail a:hover .dot, #rail [aria-current] .dot { background: var(--cream); }
#rail [aria-current] .dot { height: 14px; }
#rail a:focus-visible { outline: 2px solid var(--cream); outline-offset: -2px; }
#rail .label {
  position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%);
  padding: 5px 10px; border-radius: 4px; background: rgba(7, 9, 13, 0.78);
  font: 500 11px/1.2 var(--sans); letter-spacing: 0.16em; text-transform: uppercase; white-space: nowrap;
  opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
}
#rail a:focus-visible .label { opacity: 1; pointer-events: auto; }
@media (min-width: 901px) {
  #rail:has(a:focus-visible) .label { opacity: 1; }
}
@media (min-width: 901px) and (hover: hover) {
  #rail:hover .label { opacity: 1; }
  #rail a:hover .label { pointer-events: auto; }
}
@media (max-width: 900px) {
  #rail { left: auto; right: 56px; top: calc(env(safe-area-inset-top, 0px) + (var(--bar) - 24px) / 2); transform: none; }
  #rail ol { flex-direction: row; padding: 0; background: none; }
  #rail [aria-current] .dot { width: 14px; height: 6px; }
  /* (fixed: #rail has no transform here, so the viewport places it) */
  #rail .label {
    position: fixed; left: 12px; top: calc(env(safe-area-inset-top, 0px) + var(--bar) / 2); transform: translateY(-50%);
    min-width: 110px; max-width: calc(100% - 218px); padding: 4px 7px; background: var(--night);
    font-size: 10.5px; letter-spacing: 0.06em; white-space: normal; text-align: center;
  }
  html:has(.home-bar .menu[open]) #rail { visibility: hidden; }
  /* a label shown by a key sits over the brand mark, far from its link: it takes no pointer, so a tap there is the brand's */
  #rail a:focus-visible .label { pointer-events: none; }
}
/* (a pointer's hover only: a tap leaves :hover on what it touched until the next one) */
@media (max-width: 900px) and (hover: hover) {
  #rail a:hover .label { opacity: 1; pointer-events: auto; }
}
#rail[data-quiet] .label { opacity: 0 !important; pointer-events: none !important; }

/* ---- the toggles: the sound's, alone in its quiet group (ui/sound.js unhides it once the journey's first frame is
   drawn) -------------------------------------------------------------------------------------------------------------
   A small round button that keeps its state in aria-pressed and its name in visually hidden text: the speaker, "Sound"
   (waves when on; a muted mark when off, the default). A 32 x 32 px target on a ground of 0.72 night, as the rail's marks
   (the icon keeps 3:1 over the palest frame); the focus ring is drawn inside it, on that ground. The group is placed; the
   toggle is not.
   - Desktop: at the foot of the left margin, under the rail and centred with it in --edge, never on the copy: every hold
     keeps its print, screen and artwork right of the copy, so that margin stays clear at every aspect. Anchored to the
     visible viewport's foot, so a tablet's toolbar never hides it (it moves up with the toolbar, in the clear margin).
   - Phones and tablets (the home bar's widths): just under the bar at its right, centred under the menu button, where no
     hold's copy, print, screen or artwork reaches (the bar holds the brand and the rail's row; held sideways, the
     hackathons' hanging line can pass behind them). Fixed from the top, so a toolbar never moves it against the
     picture. Hidden while the menu is open, as the rail. Here it is 24 x 24 px (2.5.8's minimum, as the rail's marks),
     4 px under the bar, so it ends 84 px down: the hackathons' LineCanary print at its hold starts about 0.151 of the
     frame's height down (118 px at 360 x 780), and on tall phones reaches under the toggle's column (to 27 px from the
     right edge at 360 x 800, its top there 121); wider, the opening's headline and the chamber's stele leave no room
     beside it. */
.toggles {
  position: fixed; z-index: 21;
  left: max(8px, var(--edge) / 2 - 16px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(18px, 3.4vh, 32px));
}
#sound-toggle {
  display: grid; place-items: center;
  width: 32px; height: 32px; margin: 0; padding: 0; border: 0; border-radius: 50%;
  background: rgba(7, 9, 13, 0.72); color: var(--cream); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.2s ease;
  animation: rail-in 0.9s ease both;
}
#sound-toggle svg { width: 18px; height: 18px; }
#sound-toggle[aria-pressed="false"] .waves, #sound-toggle[aria-pressed="true"] .mute { display: none; }
#sound-toggle:focus-visible { outline: 2px solid var(--cream); outline-offset: -2px; }
@media (hover: hover) {
  #sound-toggle:hover { background: rgba(7, 9, 13, 0.88); }
}
@media (max-width: 900px) {
  .toggles { left: auto; right: 18px; bottom: auto; top: calc(env(safe-area-inset-top, 0px) + var(--bar) + 4px); }
  #sound-toggle { width: 24px; height: 24px; }
  #sound-toggle svg { width: 15px; height: 15px; }
}

/* ---- the hackathons' video player (ui/player.js: made in the journey, laid over a print at its hold once a visitor asks
   for its demo) ---------------------------------------------------------------------------------------------------------
   The YouTube iframe fills the print's rectangle (fitted into the visible viewport, clear of the rail and the toggles),
   on the night, which shows while the player loads; ui/player.js sets its size and place. Its close button sits outside
   one of its corners (ui/player.js layout): the toggles' chip, a 32 x 32 px target on a ground of 0.72 night, so the
   cross keeps 3:1 and more over the palest frame, the focus ring inside it. Above the canvas and the copy, under the rail,
   the toggles and the bar; hidden with them while the phones' menu is open (below). */
.player {
  position: fixed; z-index: 3; left: 0; top: 0;
  background: var(--night); box-shadow: 0 18px 50px rgba(20, 26, 32, 0.22);
}
.player iframe { display: block; width: 100%; height: 100%; border: 0; }
.player-close {
  position: absolute; display: grid; place-items: center;
  width: 32px; height: 32px; margin: 0; padding: 0; border: 0; border-radius: 50%;
  background: rgba(7, 9, 13, 0.72); color: var(--cream); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.2s ease;
}
.player-close svg { width: 18px; height: 18px; }
.player-close:focus-visible { outline: 2px solid var(--cream); outline-offset: -2px; }
@media (hover: hover) {
  .player-close:hover { background: rgba(7, 9, 13, 0.88); }
}

/* ---- keyboard focus and the phones' menu (WCAG 2.4.7, 2.4.11) --------------------------------------------------------
   - Keyboard focus never moves the journey (ui/focus.js puts back the scroll a focus would cause), so the two focusable
     things in the page's flow are pinned in view while they hold it: the brand mark at its corner, on a night chip (legible
     over any world; its ring inside the chip, as the rail's), and the footer, stuck to the viewport's foot (sticky keeps its
     box in the flow, so the page's length, and the journey's scroll with it, never changes; at the page's end it is where
     it always is). Keyboard focus only (:focus-visible), as the brand's: a click or a tap that leaves focus on a footer link
     (the mail link, a new tab) must not leave the footer stuck over every world. At the home bar's widths the pinned footer
     keeps to the room under the bar and scrolls itself: on a short viewport (320 x 256, 1280 x 1024 at 400 %; a phone held
     sideways with its bars shown) its column is taller than that room, and a focused link under the bar or off the top
     would be hidden. The browser then scrolls the footer's own box to the focus, which the guard leaves alone (it watches
     only the page's scroll). It stays one column (its column wraps otherwise, and at 320-336 px the second one ran under the
     toggles), scrolled to the focus with 12 px to spare, so every ring stays inside its box.
   - Phones: the open menu's panel covers the page, so what is under it leaves the Tab order rather than take focus unseen
     (the copy, the footer, the toggles, the video player; the rail above). A link focused on the stills page scrolls in below the bar. */
.journey .corner:focus-visible {
  position: fixed; background: var(--night); padding: 8px 12px; margin-left: -12px; border-radius: 4px; outline-offset: -2px;
}
.journey .site-footer:has(:focus-visible) { position: sticky; bottom: 0; }
@media (max-width: 900px) {
  .journey .site-footer:has(:focus-visible) {
    max-height: calc(100vh - env(safe-area-inset-top, 0px) - var(--bar));
    max-height: calc(100dvh - env(safe-area-inset-top, 0px) - var(--bar));
    overflow-y: auto; flex-wrap: nowrap; scroll-padding-block: 12px;
  }
  html { scroll-padding-top: calc(env(safe-area-inset-top, 0px) + var(--bar) + 8px); }
  html:has(.home-bar .menu[open]) .copy, html:has(.home-bar .menu[open]) .copy * { visibility: hidden !important; }
  html:has(.home-bar .menu[open]) .site-footer, html:has(.home-bar .menu[open]) .toggles, html:has(.home-bar .menu[open]) .player {
    visibility: hidden;
  }
}

@media (prefers-reduced-motion: reduce) {
  #opening-copy .line, #opening-copy .sub, .cue i, .cue span { animation: none; }
  .journey #gl, .journey .copy, .journey .copy.is-shown, .journey #apps-copy .item, .journey #hack-copy .item { transition: none; }
  .cue i::after { animation: none; top: 30%; }
  .pill, .watch, .arrow, .links a, .site-footer a { transition: none; }
}
