/* ML AND SMILE — the landing, which is an app and not a page
 *
 * WARD is one shot, not a page of sections. Four assets carry the whole
 * identity: background.png is the plate, hero.glb is the figure standing in it,
 * title.png is the wordmark, favicon.png is the mark. Everything else in the
 * first two thirds of this file is the thin layer of interface that lets those
 * four read as one exposure.
 *
 * CHAPTER and ABOUT are the other two screens, and they are screens — they take
 * the frame over the hero and hand it back, the way the game's evidence log
 * does. Nothing here is further down a page, because there is nothing further
 * down: `html, body` are `overflow: hidden` and never move. What is longer than
 * the frame scrolls inside its own panel and says so.
 */

:root {
  --ink: #f2f5f8;
  --ink-dim: rgba(242, 245, 248, 0.68);
  --ink-mute: rgba(242, 245, 248, 0.42);
  --blue: #2f8cff;
  --cyan: #7fd0ff;
  --red: #ff2b2b;
  --void: #04060a;
  --font: "Helvetica Neue", "Inter", "Segoe UI", Roboto, system-ui, -apple-system, sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-cine: cubic-bezier(0.65, 0, 0.2, 1);
  --gutter: clamp(20px, 3.4vw, 56px);
  --mark: clamp(40px, 3.6vw, 60px);

  /* the screens */
  --panel: rgba(7, 11, 17, 0.92);
  --panel-2: rgba(13, 19, 27, 0.72);
  --line: rgba(127, 208, 255, 0.14);
  --line-2: rgba(127, 208, 255, 0.30);
  /* the notch, the home indicator and the rounded corners of a phone, once,
     so every screen clears them by naming one thing */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--void);
  color: var(--ink);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

a { color: inherit; text-decoration: none; }
img { display: block; }

/* One focus ring, on everything that can take the keyboard, and it is never
   removed without something at least as visible put in its place. Hover was
   the only feedback the navigation had, which is no feedback at all to anyone
   driving this with a keyboard. */
:where(a, button):focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
  border-radius: 3px;
}
[tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none; }

.hero {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: var(--void);
  isolation: isolate;
  transition: transform 0.6s var(--ease-cine);
}
/* A screen does not replace the hero; it comes and stands in front of it. The
   shot pushes back a little so the two read as depth rather than as one page
   swapped for another. */
body:not([data-view="ward"]) .hero { transform: scale(1.045); }

/* ── 1. plate ─────────────────────────────────────────────────────────────── */

/* The plate is fixed to the viewport. It does not follow the cursor: the only
   thing in the shot that does is the figure, inside the WebGL stage. */
.plate {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}
.plate img {
  position: absolute;
  inset: -3%;
  width: 106%;
  height: 106%;
  object-fit: cover;
  object-position: 50% 50%;
}

/* slow atmospheric drift, screened over the corridor so it reads as haze not fog */
.haze {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: 0.55;
}
.haze i {
  position: absolute;
  border-radius: 50%;
  will-change: transform;
}
.haze-a {
  left: 32%; top: 22%;
  width: 46vw; height: 32vh;
  background: radial-gradient(closest-side, rgba(70, 120, 170, 0.26), rgba(70, 120, 170, 0.10) 45%, rgba(70, 120, 170, 0));
  animation: haze-a 34s ease-in-out infinite alternate;
}
.haze-b {
  left: 8%; top: 46%;
  width: 60vw; height: 40vh;
  background: radial-gradient(closest-side, rgba(40, 70, 110, 0.16), rgba(40, 70, 110, 0.06) 45%, rgba(40, 70, 110, 0));
  animation: haze-b 47s ease-in-out infinite alternate;
}
@keyframes haze-a { from { transform: translate3d(-4%, 0, 0) scale(1); } to { transform: translate3d(5%, 3%, 0) scale(1.12); } }
@keyframes haze-b { from { transform: translate3d(3%, 2%, 0) scale(1.05); } to { transform: translate3d(-5%, -2%, 0) scale(0.96); } }

/* ── 2. stage ─────────────────────────────────────────────────────────────── */

#stage {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  touch-action: none;
}

/* ── grade: vignette, floor fade, a little lift on the left for the wordmark ─ */

.grade {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(4, 6, 10, 0.55) 0%, rgba(4, 6, 10, 0) 18%),
    linear-gradient(0deg, rgba(4, 6, 10, 0.97) 0%, rgba(4, 6, 10, 0.55) 12%, rgba(4, 6, 10, 0.18) 24%, rgba(4, 6, 10, 0) 38%),
    linear-gradient(90deg, rgba(4, 6, 10, 0.42) 0%, rgba(4, 6, 10, 0.14) 30%, rgba(4, 6, 10, 0) 50%),
    radial-gradient(120% 95% at 52% 44%, rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, 0.62) 100%);
}
.grain {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  opacity: 0.07;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.6  0 0 0 0 0.6  0 0 0 0 0.6  0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 220px 220px;
}

/* ── interface ────────────────────────────────────────────────────────────── */

.ident, .foot { z-index: 10; }
/* Above the wordmark, and not level with it. On a phone the navigation drops
   out of this header as a panel that hangs over the identity block, and at the
   same z-index the wordmark — later in the document — won: the menu opened
   *behind* it, with two of its three links unreadable and none of them
   clickable. The header outranks the shot it sits on. */
.top { z-index: 20; }

.top {
  position: absolute;
  left: 0; right: 0; top: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: calc(var(--gutter) * 0.75 + env(safe-area-inset-top, 0px)) var(--gutter) 0;
}

.brand {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  opacity: 0;
}
/* the mark's glow is a gradient behind it, not a filter on it: WebKit paints
   filtered, transformed images as opaque boxes often enough to matter */
.brand::before {
  content: "";
  position: absolute;
  left: calc(var(--mark) * -0.35);
  top: 50%;
  width: calc(var(--mark) * 1.7);
  height: calc(var(--mark) * 1.7);
  transform: translateY(-50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(47, 140, 255, 0.30), rgba(47, 140, 255, 0.08) 45%, rgba(47, 140, 255, 0) 70%);
  pointer-events: none;
}
.mark {
  position: relative;
  width: var(--mark);
  height: auto;
}
.brand-text {
  font-size: 10px;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--ink-mute);
  opacity: 0;
  transition: opacity 0.9s 0.9s, color 0.4s;
}
.hero[data-state="ready"] .brand-text { opacity: 1; }
.brand:hover .brand-text { color: var(--ink-dim); }

.nav {
  display: flex;
  gap: clamp(20px, 2.8vw, 44px);
}
.nav a {
  position: relative;
  padding: 10px 0;
  font-size: 11px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-dim);
  transition: color 0.35s;
  opacity: 0;
}
.nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 4px;
  height: 1px;
  background: linear-gradient(90deg, var(--cyan), var(--blue));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease-out);
}
.nav a:hover, .nav a:focus-visible { color: var(--ink); }
.nav a:hover::after, .nav a:focus-visible::after { transform: scaleX(1); }
/* Which screen is open is part of the navigation, not a thing to remember.
   The rule stays out and the label comes up to full strength. */
.nav a[aria-current="page"] { color: var(--ink); }
.nav a[aria-current="page"]::after { transform: scaleX(1); }

.menu {
  display: none;
  align-items: center;
  gap: 10px;
  appearance: none;
  background: rgba(4, 6, 10, 0.35);
  border: 1px solid rgba(242, 245, 248, 0.18);
  color: var(--ink-dim);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  padding: 12px 16px;
  min-height: 44px;
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: border-color 0.3s, color 0.3s;
}
.menu:hover, .menu:focus-visible { color: var(--ink); border-color: var(--line-2); }
.menu .menu-x { display: none; }
.menu[aria-expanded="true"] .menu-label { display: none; }
.menu[aria-expanded="true"] .menu-x { display: inline; }
/* Two rules that cross into an ✕. The button already says the word; the mark
   is what makes it read as a control from across the screen. */
.menu-bars {
  position: relative;
  display: block;
  width: 14px; height: 10px;
  flex: none;
}
.menu-bars i {
  position: absolute;
  left: 0; right: 0;
  height: 1px;
  background: currentColor;
  transition: transform 0.4s var(--ease-out);
}
.menu-bars i:first-child { top: 2px; }
.menu-bars i:last-child { bottom: 2px; }
.menu[aria-expanded="true"] .menu-bars i:first-child { transform: translateY(3px) rotate(45deg); }
.menu[aria-expanded="true"] .menu-bars i:last-child { transform: translateY(-3px) rotate(-45deg); }

/* The wordmark, tagline and CTA are pixel-stable: no cursor-driven transform
   here or on any ancestor. The translate is vertical centring only. */
.ident {
  position: absolute;
  left: var(--gutter);
  top: 50%;
  width: min(44vw, 760px);
  transform: translateY(-52%);
}

.kicker, .status, .meta, .view-eyebrow {
  margin: 0;
  font-size: 10px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.kicker, .view-eyebrow { display: flex; align-items: center; gap: 12px; }
.kicker { opacity: 0; }
.kicker .sep, .view-eyebrow .sep { opacity: 0.5; }

.pip {
  display: inline-block;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--cyan);
  box-shadow: 0 0 10px rgba(127, 208, 255, 0.9);
  animation: pip 3.2s ease-in-out infinite;
}
.pip-red {
  background: var(--red);
  box-shadow: 0 0 10px rgba(255, 43, 43, 0.9);
  animation: pip-red 2.4s ease-in-out infinite;
}
@keyframes pip { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
@keyframes pip-red { 0%, 88%, 100% { opacity: 0.85; } 92% { opacity: 0.2; } 96% { opacity: 0.9; } }

.title-wrap {
  position: relative;
  margin: 0;
  /* title.png carries ~31% transparent padding above and below the lettering;
     pull that in so the stack reads tight without touching the artwork. */
  margin-top: -11%;
  margin-bottom: -12%;
  transform: translateX(-2%);
}
/* bloom from the blue paint and a bed of shadow under the lettering, painted
   behind the artwork as gradients (see .brand::before for why not a filter) */
.title-wrap::before {
  content: "";
  position: absolute;
  left: -8%; right: -8%;
  top: 14%; bottom: 12%;
  background:
    radial-gradient(38% 52% at 74% 50%, rgba(47, 140, 255, 0.30), rgba(47, 140, 255, 0.10) 50%, rgba(47, 140, 255, 0) 100%),
    radial-gradient(50% 55% at 46% 58%, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.25) 55%, rgba(0, 0, 0, 0) 100%);
  opacity: 0;
  pointer-events: none;
}
.title-wrap picture { display: block; }
.title {
  position: relative;
  width: 100%;
  height: auto;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
  opacity: 0;
}

.tagline {
  margin: 0;
  max-width: 40ch;
  font-size: clamp(13px, 0.95vw, 15px);
  line-height: 1.7;
  color: var(--ink-dim);
  opacity: 0;
}

.actions {
  display: flex;
  align-items: center;
  gap: 24px;
  margin-top: clamp(22px, 2.4vw, 34px);
  opacity: 0;
}
.cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 17px 26px 17px 22px;
  border: 1px solid rgba(127, 208, 255, 0.45);
  color: var(--ink);
  font-size: 11.5px;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  background: rgba(4, 6, 10, 0.38);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  overflow: hidden;
  white-space: nowrap;
  transition: border-color 0.4s, box-shadow 0.4s;
}
.cta::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(47, 140, 255, 0.85), rgba(127, 208, 255, 0.75));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.55s var(--ease-out);
  z-index: -1;
}
.cta:hover, .cta:focus-visible {
  border-color: rgba(127, 208, 255, 0.95);
  box-shadow: 0 0 34px rgba(47, 140, 255, 0.35);
  outline: none;
}
.cta:hover::before, .cta:focus-visible::before { transform: scaleX(1); }
.cta:hover .cta-dot { background: #fff; box-shadow: 0 0 12px #fff; }
.cta-dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--cyan);
  box-shadow: 0 0 10px rgba(127, 208, 255, 0.9);
  animation: pip 2.2s ease-in-out infinite;
  transition: background 0.3s, box-shadow 0.3s;
}
.cta-arrow {
  font-weight: 300;
  transform: translateX(0);
  transition: transform 0.45s var(--ease-out);
}
.cta:hover .cta-arrow { transform: translateX(5px); }
.cta:active { transform: translateY(1px); }

/* The second action on a screen. Plainly a button, and plainly not the one:
   two identically weighted outlines side by side is two primaries and no
   hierarchy at all. */
.cta-quiet {
  border-color: rgba(242, 245, 248, 0.16);
  color: var(--ink-dim);
  background: rgba(4, 6, 10, 0.28);
  font-size: 10.5px;
  padding: 17px 22px;
}
.cta-quiet::before { background: rgba(242, 245, 248, 0.10); }
.cta-quiet:hover, .cta-quiet:focus-visible {
  border-color: rgba(242, 245, 248, 0.4);
  color: var(--ink);
  box-shadow: none;
}

.foot {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--gutter) calc(var(--gutter) * 0.7 + env(safe-area-inset-bottom, 0px));
  opacity: 0;
}
.status { display: inline-flex; align-items: center; gap: 12px; }

/* ── entrance ─────────────────────────────────────────────────────────────── */

.hero[data-state="ready"] .brand { opacity: 1; }
.hero[data-state="ready"] .nav a, .hero[data-state="ready"] .menu {
  animation: rise 1.1s var(--ease-out) both;
}
.hero[data-state="ready"] .nav a:nth-child(1) { animation-delay: 0.55s; }
.hero[data-state="ready"] .nav a:nth-child(2) { animation-delay: 0.65s; }
.hero[data-state="ready"] .nav a:nth-child(3) { animation-delay: 0.75s; }
.hero[data-state="ready"] .nav a:nth-child(4) { animation-delay: 0.85s; }
.hero[data-state="ready"] .menu { animation-delay: 0.6s; }
.hero[data-state="ready"] .kicker { animation: rise 1.2s var(--ease-out) 0.45s both; }
.hero[data-state="ready"] .title { animation: resolve 1.7s var(--ease-out) 0.55s both; }
.hero[data-state="ready"] .title-wrap::before { animation: bloom 2.4s var(--ease-out) 0.55s both; }
.hero[data-state="ready"] .tagline { animation: rise 1.3s var(--ease-out) 0.95s both; }
.hero[data-state="ready"] .actions { animation: rise 1.3s var(--ease-out) 1.15s both; }
.hero[data-state="ready"] .foot { animation: rise 1.4s var(--ease-out) 1.4s both; }

@keyframes rise {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes resolve {
  0% { opacity: 0; transform: translate3d(0, 26px, 0) scale(0.985); }
  100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes bloom {
  0% { opacity: 0; transform: scale(0.9); }
  45% { opacity: 1.0; transform: scale(1.06); }
  100% { opacity: 0.85; transform: scale(1); }
}

/* ── boot ─────────────────────────────────────────────────────────────────── */

.boot {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.boot-bg {
  position: absolute;
  inset: 0;
  background: #000;
  transition: opacity 1.1s var(--ease-cine);
}
.boot-center {
  position: relative;
  width: 72px; height: 72px;
  display: grid;
  place-items: center;
}
.boot-mark {
  position: relative;
  width: 72px; height: 72px;
  z-index: 2;
  transform-origin: center;
  animation: boot-mark-in 1.4s var(--ease-out) both;
}
.boot-glow {
  position: absolute;
  inset: -40%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(47, 140, 255, 0.40), rgba(47, 140, 255, 0.10) 45%, rgba(47, 140, 255, 0) 70%);
  animation: boot-glow 2.4s ease-in-out infinite;
}
@keyframes boot-glow { 0%, 100% { opacity: 0.55; transform: scale(0.95); } 50% { opacity: 1; transform: scale(1.08); } }
.boot-ring {
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 1px solid rgba(127, 208, 255, 0.55);
  animation: ring 2.4s cubic-bezier(0.2, 0.7, 0.3, 1) infinite;
  opacity: 0;
}
.boot-ring-2 { animation-delay: 1.2s; }
@keyframes ring {
  0% { transform: scale(0.7); opacity: 0; }
  18% { opacity: 0.9; }
  100% { transform: scale(2.3); opacity: 0; }
}
@keyframes boot-mark-in {
  from { opacity: 0; transform: scale(0.82); }
  to { opacity: 1; transform: scale(1); }
}
.boot-meter {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, 76px);
  display: grid;
  gap: 14px;
  justify-items: center;
  transition: opacity 0.5s;
}
.boot-bar {
  width: 148px; height: 1px;
  background: rgba(242, 245, 248, 0.12);
  overflow: hidden;
}
.boot-bar i {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--blue), var(--cyan));
  box-shadow: 0 0 10px rgba(127, 208, 255, 0.9);
  transition: width 0.35s var(--ease-out);
}
#boot-msg {
  font-size: 9.5px;
  letter-spacing: 0.36em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.boot-noscript {
  position: absolute;
  left: 0; right: 0; bottom: 12vh;
  text-align: center;
  font-size: 12px;
  color: var(--ink-dim);
}

/* the boot hands off: the mark travels to the corner while the black lifts */
.boot.out .boot-bg { opacity: 0; }
.boot.out .boot-ring { animation: none; opacity: 0; }
.boot.out .boot-meter { opacity: 0; }
.boot.out .boot-mark {
  animation: none;
  transition: transform 1.15s var(--ease-cine);
}
.boot.out .boot-glow { animation: none; opacity: 0; transition: opacity 0.5s; }
.boot.gone { display: none; }

/* The cut into the ward. play.html opens on black, so this page ends on black
   and the two meet on the same frame instead of flashing the browser's own
   background between them. See shell.js. */
.leaving {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.42s var(--ease-cine);
}
body.is-leaving .leaving { opacity: 1; pointer-events: auto; }

/* ── the screens ──────────────────────────────────────────────────────────── */
/* CHAPTER and ABOUT. Each takes the frame the way the game's evidence log does:
   the shot stays lit behind it, the panel arrives out of the depth, and the
   ✕, a press on the dark outside, Escape and the browser's own back gesture are
   all the same journey off it.

   The panel scrolls, not the document. That is the difference between an app
   and a page, and it is why `html, body` up at the top of this file are
   `overflow: hidden` and stay that way at every width. */

.views { position: fixed; inset: 0; z-index: 30; pointer-events: none; }

.view {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding:
    calc(var(--gutter) * 0.7 + var(--safe-t))
    calc(var(--gutter) * 0.7 + var(--safe-r))
    calc(var(--gutter) * 0.7 + var(--safe-b))
    calc(var(--gutter) * 0.7 + var(--safe-l));
  pointer-events: auto;
  background:
    radial-gradient(130% 100% at 50% 40%, rgba(3, 5, 9, 0.72) 0%, rgba(1, 2, 4, 0.93) 70%);
  backdrop-filter: blur(6px) saturate(0.85);
  -webkit-backdrop-filter: blur(6px) saturate(0.85);
  opacity: 0;
  transition: opacity 0.42s var(--ease-out);
}
.view[hidden] { display: none; }
.view.on { opacity: 1; }

.view-panel {
  position: relative;
  width: min(1080px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background:
    linear-gradient(180deg, var(--panel-2) 0%, var(--panel) 34%);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 0 rgba(190, 220, 245, 0.07) inset,
    0 40px 90px -24px rgba(0, 0, 0, 0.9);
  overflow: hidden;
  transform: translate3d(0, 22px, 0) scale(0.985);
  opacity: 0;
  transition: transform 0.52s var(--ease-out), opacity 0.42s var(--ease-out);
}
.view.on .view-panel { transform: none; opacity: 1; }
.view-panel-narrow { width: min(720px, 100%); }

/* the grabber on a sheet — a phone affordance, and only shown where it is one */
.view-grab { display: none; }

.view-head {
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gutter);
  padding: clamp(20px, 2.4vw, 30px) clamp(20px, 2.6vw, 38px) clamp(14px, 1.6vw, 20px);
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(15, 22, 31, 0.6), rgba(15, 22, 31, 0));
}
.view-titles { min-width: 0; display: grid; gap: 12px; }
.view-head h2 {
  margin: 0;
  font-size: clamp(21px, 2.6vw, 33px);
  font-weight: 300;
  letter-spacing: 0.005em;
  line-height: 1.08;
  color: var(--ink);
}

.view-close {
  flex: none;
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  margin: -6px -8px 0 0;
  padding: 0;
  appearance: none;
  border: 1px solid transparent;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-mute);
  cursor: pointer;
  transition: background 0.2s, color 0.2s, border-color 0.2s, transform 0.12s;
}
.view-close svg { width: 15px; height: 15px; display: block; }
.view-close:hover, .view-close:focus-visible {
  background: rgba(127, 208, 255, 0.12);
  border-color: var(--line-2);
  color: var(--ink);
}
.view-close:active { transform: scale(0.92); }

/* The one thing on this site that scrolls. `overscroll-behavior: contain` is
   what stops a flick past the end of the chapter from turning into the page
   behind it pulling down, which on iOS is the difference between a sheet and a
   web page in a costume. */
.view-body {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: clamp(20px, 2.2vw, 30px) clamp(20px, 2.6vw, 38px) clamp(24px, 2.6vw, 36px);
  scrollbar-width: thin;
  scrollbar-color: rgba(150, 190, 220, 0.26) transparent;
}
.view-body::-webkit-scrollbar { width: 8px; }
.view-body::-webkit-scrollbar-thumb {
  background: rgba(150, 190, 220, 0.24);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: padding-box;
}

.lede {
  margin: 0 0 clamp(22px, 2.4vw, 32px);
  max-width: 74ch;
  font-size: clamp(14px, 1.05vw, 16.5px);
  line-height: 1.7;
  color: var(--ink-dim);
}

/* ── the tiles ────────────────────────────────────────────────────────────── */
/* Six things the ward actually contains, each drawn by the piece of production
   art that depicts it. Nothing here illustrates a system this chapter does not
   have: there is no radar, no map, no loadout and no health bar, so there is no
   icon for one either. */

/* Three, then two, then one. `auto-fit` picked four across a 1080 px panel and
   left the last row two-thirds empty, which reads as a grid that ran out
   rather than as six things. */
.tiles {
  list-style: none;
  margin: 0 0 clamp(24px, 2.6vw, 36px);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(10px, 1.1vw, 16px);
}
@media (max-width: 1040px) { .tiles { grid-template-columns: repeat(2, 1fr); } }
.tile {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 16px;
  align-items: center;
  padding: 16px 18px;
  border: 1px solid rgba(127, 208, 255, 0.10);
  border-radius: 10px;
  background: linear-gradient(180deg, rgba(22, 32, 44, 0.42), rgba(9, 14, 20, 0.42));
  transition: border-color 0.35s, background 0.35s;
}
.tile:hover { border-color: var(--line); background: linear-gradient(180deg, rgba(28, 42, 58, 0.5), rgba(11, 17, 24, 0.5)); }
.tile-art {
  grid-row: 1 / 3;
  display: grid;
  place-items: center;
  width: 52px; height: 52px;
}
/* The box is fixed and the art is fitted into it. `max-height: 100%` cannot do
   this job: the grid row is sized by its content, so the percentage has nothing
   definite to resolve against and is dropped — the tall icons (the lantern is
   110×200) then overflowed the tile by half their height. */
.tile-art img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.6));
}
.tile h3 {
  margin: 0 0 5px;
  align-self: end;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink);
}
.tile p {
  margin: 0;
  align-self: start;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink-mute);
}

/* ── the controls, and the facts ──────────────────────────────────────────── */

.block-h {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 0 0 16px;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.block-h::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--line), transparent);
}

.controls-block { margin-bottom: clamp(22px, 2.4vw, 32px); }

/* Two pairs to a row where there is room for two: key, what it does, key, what
   it does. Seven controls in one long column is a list to scroll; in two it is
   a card to read. */
.keys {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr auto 1fr;
  align-items: center;
  gap: 9px clamp(14px, 1.6vw, 28px);
}
.keys dt { display: flex; gap: 4px; flex-wrap: wrap; }
.keys dd {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-mute);
}
@media (max-width: 1040px) {
  .keys { grid-template-columns: auto 1fr; }
}

/* the keycap, the same object the game draws in its pause menu */
.keycap {
  display: inline-grid;
  place-items: center;
  min-width: 22px; height: 22px;
  padding: 0 6px;
  border: 1px solid rgba(190, 214, 232, 0.34);
  border-bottom-color: rgba(190, 214, 232, 0.55);
  border-radius: 3px;
  font: 600 9.5px/1 var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #eaf2f8;
  background: linear-gradient(180deg, rgba(30, 42, 54, 0.8), rgba(8, 12, 17, 0.8));
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.5), 0 1px 0 rgba(190, 214, 232, 0.1) inset;
}

.note {
  margin: 18px 0 0;
  max-width: 70ch;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--ink-mute);
}

.facts {
  margin: 0 0 clamp(24px, 2.6vw, 34px);
  display: grid;
  grid-template-columns: minmax(96px, 132px) 1fr;
  gap: 0;
}
.facts dt {
  padding: 16px 20px 16px 0;
  border-top: 1px solid var(--line);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.facts dd {
  margin: 0;
  padding: 16px 0;
  border-top: 1px solid var(--line);
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--ink-dim);
}

/* ── the action bar ───────────────────────────────────────────────────────── */
/* Outside the scrolling body on purpose. The thing the screen is arguing for is
   the thing it must not bury: on a phone the chapter is three times the height
   of the frame, and a CTA at the end of it is a CTA nobody reaches. */

.view-foot {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  padding: clamp(14px, 1.6vw, 20px) clamp(20px, 2.6vw, 38px);
  border-top: 1px solid var(--line);
  background: linear-gradient(0deg, rgba(13, 20, 29, 0.75), rgba(13, 20, 29, 0.35));
}
.view-foot .cta { font-size: 11px; }
.foot-note {
  margin-left: auto;
  font-size: 10px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* ── the screens, narrower ────────────────────────────────────────────────── */

/* A phone gets a sheet, not a dialog: it comes up from the bottom, it keeps the
   thumb's end of the screen, and it carries a grabber because that is what
   every other sheet on the device does. */
@media (max-width: 760px), (max-height: 620px) and (max-width: 1000px) {
  .view {
    place-items: end stretch;
    padding: 0;
  }
  .view-panel, .view-panel-narrow {
    width: 100%;
    max-height: calc(100% - max(6vh, var(--safe-t)));
    border-radius: 18px 18px 0 0;
    border-bottom: 0;
    transform: translate3d(0, 100%, 0);
    transition: transform 0.46s var(--ease-cine), opacity 0.28s linear;
  }
  .view-grab {
    display: block;
    width: 38px; height: 4px;
    margin: 9px auto 0;
    border-radius: 2px;
    background: rgba(242, 245, 248, 0.22);
    flex: none;
  }
  .view-head { padding: 14px 20px 14px calc(20px + var(--safe-l)); gap: 14px; }
  .view-head h2 { font-size: 20px; }
  .view-body {
    padding: 20px calc(20px + var(--safe-r)) calc(28px + var(--safe-b)) calc(20px + var(--safe-l));
  }
  .tiles { grid-template-columns: 1fr; gap: 8px; }
  .tile { padding: 13px 14px; }
  .tile-art { width: 44px; height: 44px; }
  .keys { grid-template-columns: auto 1fr; gap: 10px 16px; }
  .facts { grid-template-columns: 1fr; }
  .facts dt { padding-bottom: 4px; }
  .facts dd { border-top: 0; padding-top: 0; padding-bottom: 18px; }
  /* opaque here, because the sheet's content passes under it as it scrolls and
     a translucent bar over moving text is a bar nobody can read */
  .view-foot {
    padding: 14px calc(20px + var(--safe-r)) calc(16px + var(--safe-b)) calc(20px + var(--safe-l));
    gap: 10px;
    background: #0a1017;
  }
  .view-foot .cta { width: 100%; justify-content: center; }
  .foot-note { display: none; }
  /* the body no longer carries the bottom safe area — the action bar under it does */
  .view-body { padding-bottom: 24px; }
}

/* A phone on its side has 390 px of height for the whole screen. A header and
   an action bar sized for a portrait sheet would leave about 150 px of chapter
   between them — one tile, and a scroll for the rest. Everything gets smaller
   and the tiles take the width the phone does have. */
@media (max-height: 520px) and (min-aspect-ratio: 1/1) {
  .view-panel, .view-panel-narrow { max-height: 100%; }
  .view-grab { margin-top: 6px; }
  .view-head { padding: 10px 20px 10px calc(20px + var(--safe-l)); }
  .view-head h2 { font-size: 17px; }
  .view-titles { gap: 7px; }
  .view-close { width: 40px; height: 40px; }
  .view-body { padding-top: 14px; padding-bottom: 16px; }
  .lede { margin-bottom: 16px; font-size: 13px; }
  .tiles { grid-template-columns: repeat(3, 1fr); margin-bottom: 18px; gap: 8px; }
  .tile { padding: 10px 12px; column-gap: 12px; }
  .tile-art { width: 38px; height: 38px; }
  .tile h3 { font-size: 9.5px; }
  .tile p { font-size: 11.5px; line-height: 1.45; }
  .controls-block { margin-bottom: 18px; }
  .keys { grid-template-columns: auto 1fr auto 1fr; gap: 7px 20px; }
  .facts { grid-template-columns: minmax(90px, 124px) 1fr; }
  .facts dt { padding: 11px 20px 11px 0; }
  .facts dd { padding: 11px 0; border-top: 1px solid var(--line); font-size: 12.5px; }
  .view-foot { padding-top: 10px; padding-bottom: calc(10px + var(--safe-b)); }
  .view-foot .cta { width: auto; }
}

/* backdrop-filter over a live WebGL canvas is recomputed while it is composited,
   and the phones that can least afford it are the ones already holding the
   plate, the wordmark and two GLBs. There the scrim is made denser instead,
   which costs nothing. */
@media (max-width: 760px), (pointer: coarse) {
  .view {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: radial-gradient(140% 100% at 50% 30%, rgba(3, 5, 9, 0.88) 0%, rgba(1, 2, 4, 0.97) 70%);
  }
}

/* ── responsive ───────────────────────────────────────────────────────────── */

/* squarer desktops: keep the wordmark clear of the raised lantern */
@media (min-aspect-ratio: 29/20) and (max-aspect-ratio: 43/25) {
  .ident { width: min(42vw, 700px); }
}
@media (min-aspect-ratio: 1/1) and (max-aspect-ratio: 29/20) {
  .ident { width: min(40vw, 640px); }
}

/* portrait tablet */
@media (max-aspect-ratio: 1/1) {
  .plate img { object-position: 48% 50%; }
  .ident {
    top: 15vh;
    width: min(62vw, 560px);
    transform: none;
  }
  .grade {
    background:
      linear-gradient(180deg, rgba(4, 6, 10, 0.6) 0%, rgba(4, 6, 10, 0) 22%),
      linear-gradient(0deg, rgba(4, 6, 10, 0.98) 0%, rgba(4, 6, 10, 0.72) 9%, rgba(4, 6, 10, 0.3) 20%, rgba(4, 6, 10, 0) 36%),
      radial-gradient(130% 100% at 50% 50%, rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, 0.6) 100%);
  }
  .meta { display: none; }
}

/* phone */
@media (max-width: 760px), (max-aspect-ratio: 18/25) {
  :root { --gutter: 20px; }
  .plate img { object-position: 46% 50%; }
  .brand-text { display: none; }
  :root { --mark: 44px; }
  .nav {
    position: absolute;
    left: var(--gutter); right: var(--gutter);
    top: calc(100% + 10px);
    flex-direction: column;
    gap: 0;
    padding: 4px 18px;
    border-radius: 10px;
    /* Nearly solid, because it hangs over the wordmark and the figure: a
       translucent panel over a photograph is a panel nobody can read, and
       backdrop-filter is not something every device will actually paint. */
    background: rgba(5, 8, 12, 0.94);
    border: 1px solid rgba(242, 245, 248, 0.14);
    box-shadow: 0 24px 50px -18px rgba(0, 0, 0, 0.9);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    transition: opacity 0.35s, transform 0.35s var(--ease-out), visibility 0s 0.35s;
  }
  .nav.open { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
  .nav a {
    padding: 15px 0;
    border-top: 1px solid rgba(242, 245, 248, 0.08);
    color: var(--ink-dim);
  }
  .nav a:first-child { border-top: 0; }
  .nav a[aria-current="page"] { color: var(--cyan); }
  .hero[data-state="ready"] .nav a { animation: none; opacity: 1; }
  .nav a::after { display: none; }
  .menu { display: inline-flex; }
  /* the wordmark and tagline stay up top; the CTA sits above the footer,
     over the soft foreground of the bed, clear of the figure in the middle */
  .ident {
    top: 12vh;
    bottom: calc(58px + env(safe-area-inset-bottom, 0px));
    width: min(88vw, 480px);
    display: flex;
    flex-direction: column;
  }
  .title-wrap { margin-top: -10%; margin-bottom: -11%; }
  .tagline { font-size: 13px; max-width: 34ch; }
  /* the primary and the way to read about it, stacked and full width — the
     shape every other app on the device puts at the bottom of a screen */
  .actions { flex-direction: column; align-items: stretch; gap: 10px; margin-top: auto; }
  .cta { padding: 15px 22px 15px 18px; font-size: 11px; justify-content: center; }
  .cta-quiet { padding: 14px 20px; }
  .foot .meta { display: none; }
  .haze { opacity: 0.4; }
}

/* very short landscape phones: everything must still fit */
@media (max-height: 460px) and (min-aspect-ratio: 1/1) {
  .ident { width: min(38vw, 420px); top: 52%; }
  .tagline, .foot { display: none; }
  .actions { margin-top: 14px; flex-direction: row; align-items: center; }
  .cta { padding: 13px 20px 13px 16px; font-size: 10.5px; }
  .cta-quiet { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .haze i, .pip, .cta-dot, .boot-ring, .boot-glow { animation: none; }
  .hero[data-state="ready"] .title-wrap::before { animation: none; opacity: 0.85; }
  .hero[data-state="ready"] .title,
  .hero[data-state="ready"] .nav a, .hero[data-state="ready"] .menu,
  .hero[data-state="ready"] .kicker, .hero[data-state="ready"] .tagline,
  .hero[data-state="ready"] .actions, .hero[data-state="ready"] .foot {
    animation-duration: 0.01s;
    animation-delay: 0s;
  }
  /* Nothing is removed that carried meaning: the screens still arrive and the
     shot still stands back. They stop travelling to get there. */
  .hero, .view, .view-panel, .nav, .tile, .cta, .cta::before, .cta-arrow, .view-close {
    transition-duration: 0.01s;
  }
  .view-panel { transform: none; }
}
