/* ML AND SMILE — behavioural inference system, Ward 04
   CSS handles the title, the two lines of in-world text, the screens the player
   opens.  Every pixel of the ward itself is rendered by
   Three.js.

   The in-game rule: at most one prompt and one fading objective line. Anything
   bigger is a screen the player asked for.

   Three voices and no more.  Mono is the hospital's own printing — labels,
   keys, headers, anything the building would have stencilled or typed. Sans is
   the game speaking to the player. Serif is a piece of paper someone left
   behind.  Every colour comes off the ramps below rather than being mixed at
   the point of use, so a screen cannot quietly drift away from the rest. */

:root {
  --bg: #04060a;

  /* Text ramp. Every value here clears 4.5:1 on --bg except --ink-faint, which
     is only ever used for text that is 14 px or larger, or for a rule. */
  --ink:       #dde5ec;   /* 14.9:1 — the thing you are reading            */
  --ink-2:     #b6c5d3;   /* 11.0:1 — secondary prose                       */
  --ink-3:     #8ea3b6;   /*  7.4:1 — labels, captions, quiet headers       */
  --ink-4:     #74889c;   /*  5.4:1 — the quietest text allowed to be small  */
  --ink-faint: #5b6b7c;   /*  3.6:1 — rules and 14 px+ decoration only       */
  --dim: var(--ink-4);

  /* Structure */
  --line:      rgba(126, 178, 219, 0.16);
  --line-2:    rgba(126, 178, 219, 0.28);
  --panel:     #070b10;
  --panel-2:   #0c1218;
  --scrim:     rgba(1, 2, 4, 0.90);

  /* Meaning. Blue is the interface; red is a warning; green is a machine
     answering you. Nothing else is allowed to be saturated. */
  --accent:     #6cbcff;
  --accent-dim: #1d5f9b;
  --accent-ink: #d6ecff;
  --warn:       #ff6a4d;
  --ok:         #7fe0ad;

  /* Paper */
  --paper:      #e2dccd;
  --paper-warm: #e6dcc6;
  --paper-ink:  #262521;
  --paper-2:    #5f5b50;

  /* One 4 px spacing scale, so nothing is spaced by feel alone. */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;

  --r-sm: 3px; --r-md: 6px; --r-lg: 10px;

  --shadow-panel:
    0 1px 0 rgba(190, 220, 245, .07) inset,
    0 24px 60px -12px rgba(0, 0, 0, .85),
    0 2px 10px rgba(0, 0, 0, .6);

  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
           Georgia, "Liberation Serif", "Times New Roman", serif;

  /* Motion. Two durations and two curves — everything on screen moves on one of
     them, which is what makes the interface feel like one object. */
  --t-fast: 130ms;
  --t-mid: 240ms;
  --ease: cubic-bezier(.2, .8, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-in-out: cubic-bezier(.65, 0, .35, 1);
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0; height: 100%;
  background: var(--bg); color: var(--ink);
  font-family: var(--sans);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  /* a two-finger pinch or a double tap on a game canvas is never a zoom, and
     on iOS the rubber-band scroll would drag the whole ward down the screen */
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}
body.touch-mode { touch-action: none; user-select: none; -webkit-user-select: none; }

/* One focus ring, on everything, and it is never removed without a replacement
   that is at least as visible. The exception is a container given tabindex=-1
   only so a screen with no controls of its own has somewhere to put the
   keyboard: it is not a target, so it does not draw a ring. */
:where(button, select, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
[tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none; }

/* ── stage ─────────────────────────────────────────────────────────────── */

#stage {
  position: fixed; inset: 0;
  display: grid; place-items: center;
  background: #000;
}

/* 100vh on mobile Safari is the viewport with the URL bar retracted, so the
   canvas is taller than the window until the bar hides and the bottom of the
   frame sits under it. dvh is the one that tracks. */
#view, #overlay {
  display: block;
  grid-area: 1 / 1;
  max-width: 100vw; max-height: 100vh;
  width: 100vw; height: 100vh;
  width: 100dvw; height: 100dvh;
  max-width: 100dvw; max-height: 100dvh;
}
#overlay { pointer-events: none; }

/* ── the in-world layer ────────────────────────────────────────────────── */

.ui { position: fixed; inset: 0; pointer-events: none; z-index: 10; }
/* the in-world layer never takes a click; the screens inside .ui put it back */
.reticle, .prompt-line, .objective, .subtitle, .stamina, .checkpoint, .fade { pointer-events: none; }

/* The reticle is a dot that opens into a ring when there is something under it.
   Two elements would be two things to keep in sync; one element with a border
   that grows out of nothing is the same read for less. */
.reticle {
  position: fixed; left: 50%; top: 50%;
  width: 6px; height: 6px; margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: rgba(226, 238, 248, 0);
  box-shadow: 0 0 0 0 rgba(226, 238, 248, 0);
  transform: scale(.7);
  transition: background var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
.reticle.on {
  background: rgba(232, 242, 250, .85);
  transform: scale(1);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, .55),
    0 0 0 5px rgba(140, 190, 235, .16),
    0 0 12px rgba(140, 190, 235, .45);
}

.prompt-line {
  position: fixed; left: 50%; top: calc(50% + 46px);
  display: flex; align-items: center; gap: var(--s2);
  font: 500 11.5px/1 var(--mono); letter-spacing: .16em;
  color: var(--ink);
  text-shadow: 0 1px 6px rgba(0, 0, 0, .95), 0 0 18px rgba(0, 0, 0, .8);
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, 4px);
  transition: opacity var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.prompt-line.on { opacity: 1; transform: translate(-50%, 0); }

/* The keycap. Defined once and worn by the prompt line and by the pause
   menu's control list, so a key looks like a key wherever it is written down. */
.prompt-line b, .keycap {
  display: inline-grid; place-items: center;
  min-width: 21px; height: 21px; padding: 0 5px;
  border: 1px solid rgba(190, 214, 232, .40);
  border-bottom-color: rgba(190, 214, 232, .62);
  border-radius: var(--r-sm);
  font: 600 10px/1 var(--mono); letter-spacing: .08em;
  color: #eaf2f8;
  background: linear-gradient(180deg, rgba(30, 42, 54, .78), rgba(8, 12, 17, .78));
  box-shadow: 0 1px 0 rgba(0, 0, 0, .5), 0 1px 0 rgba(190, 214, 232, .10) inset;
  text-shadow: none;
}

/* The one standing instruction in the game, and the only thing on the screen
   that is not either the ward or a single contextual verb. It was a line of
   grey text behind a 2 px rule, which says "a line of text" and nothing about
   what kind. The mark is the production objective glyph, drawn as a background
   so that a device which cannot fetch it is left with a line of text rather
   than a broken image over the corridor. PNG rather than WebP for the same
   reason it is a background: this is the one icon that appears without the
   player asking for anything, so it takes the format every browser has. */
.objective {
  position: fixed;
  left: calc(30px + env(safe-area-inset-left)); top: calc(28px + env(safe-area-inset-top));
  max-width: min(46ch, 42vw);
  font: 400 13px/1.55 var(--sans); letter-spacing: .012em;
  color: var(--ink-2);
  text-shadow: 0 1px 8px rgba(0, 0, 0, .95), 0 0 20px rgba(0, 0, 0, .7);
  opacity: 0; transform: translateY(-4px);
  transition: opacity .8s var(--ease-out), transform .8s var(--ease-out);
  padding-left: 30px;
  min-height: 21px;
  background: url("../assets/ui/17-objective.png") no-repeat left top 0px;
  background-size: 19px auto;
}
.objective.on { opacity: 1; transform: none; }

.subtitle {
  position: fixed; left: 50%; bottom: calc(62px + env(safe-area-inset-bottom));
  max-width: min(62ch, 78vw);
  text-align: center;
  font: 400 14px/1.62 var(--sans);
  color: var(--ink);
  /* a line of speech over a bright floor needs more than a drop shadow: this is
     a soft plate of its own, drawn by the text shadow rather than by a box that
     would flash on and off around it */
  text-shadow:
    0 1px 2px rgba(0, 0, 0, 1), 0 2px 14px rgba(0, 0, 0, .98),
    0 0 30px rgba(0, 0, 0, .9), 0 0 60px rgba(0, 0, 0, .7);
  opacity: 0; transform: translate(-50%, 4px);
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.subtitle.on { opacity: 1; transform: translate(-50%, 0); }

/* the only meter in the game, and it is only there while you are running */
.stamina {
  position: fixed; left: 50%; bottom: calc(34px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  width: 92px; height: 2px;
  background: rgba(200, 220, 235, .12);
  box-shadow: 0 0 10px rgba(0, 0, 0, .8);
  opacity: 0; transition: opacity .4s var(--ease-out);
}
.stamina.on { opacity: .85; }
.stamina i {
  display: block; height: 100%;
  background: rgba(214, 231, 243, .78);
  transition: width .12s linear;
}

/* ── the checkpoint mark ────────────────────────────────────────────────────
   The one addition to a screen that has kept itself to two lines of text, and
   it is here because the chapter saves invisibly: in a tab, with no install and
   no save menu, "will closing this cost me the last twenty minutes" is a
   question the game already knows the answer to and was not answering.

   Top right, which is the only corner nothing else uses — the objective is top
   left, the prompt and reticle are the middle, the subtitle and the stamina bar
   are bottom centre. On touch it drops below the EVIDENCE and MENU pills that
   live in that corner. It is up for two and a half seconds and it is never on
   screen during a screen. */
.checkpoint {
  position: fixed;
  right: calc(30px + env(safe-area-inset-right)); top: calc(28px + env(safe-area-inset-top));
  display: flex; align-items: center; gap: var(--s2);
  font: 600 9.5px/1 var(--mono); letter-spacing: .26em;
  color: var(--ink-3);
  text-shadow: 0 1px 8px rgba(0, 0, 0, .95), 0 0 20px rgba(0, 0, 0, .8);
  opacity: 0; transform: translateY(-4px);
  transition: opacity .45s var(--ease-out), transform .45s var(--ease-out);
}
.checkpoint.on { opacity: 1; transform: none; }
.checkpoint .ico { display: block; width: 19px; height: 19px; }
.checkpoint .ico img {
  width: 100%; height: 100%; object-fit: contain;
  filter: drop-shadow(0 1px 5px rgba(0, 0, 0, .9));
}
body.touch-mode .checkpoint { top: calc(72px + env(safe-area-inset-top)); }

.fade {
  position: fixed; inset: 0; background: #000;
  opacity: 0; transition: opacity .6s ease;
}

/* ── screens ───────────────────────────────────────────────────────────── */

.screen {
  position: fixed; inset: 0; z-index: 20;
  display: grid; place-items: center;
  pointer-events: auto;
  background:
    radial-gradient(130% 95% at 50% 42%, rgba(3, 5, 8, .78) 0%, var(--scrim) 70%);
  backdrop-filter: blur(4px) saturate(.8);
  animation: screen-in var(--t-mid) var(--ease-out) both;
}
.screen.hidden { display: none; }

/* backdrop-filter over a live WebGL canvas is recomputed every frame, and the
   phones that can least afford it are the ones running the ward at 60. There
   the scrim is made denser instead, which costs nothing. */
@media (max-width: 700px), (pointer: coarse) {
  .screen {
    backdrop-filter: none;
    background: radial-gradient(130% 95% at 50% 42%, rgba(3, 5, 8, .88) 0%, rgba(1, 2, 4, .96) 70%);
  }
}

@keyframes screen-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes panel-in {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes paper-in {
  from { opacity: 0; transform: translateY(16px) rotate(var(--tilt, 0deg)) scale(.98); }
  to   { opacity: 1; transform: translateY(0) rotate(var(--tilt, 0deg)) scale(1); }
}

/* ── the close control ─────────────────────────────────────────────────── */
/* Every screen the player can leave carries one, at a size a thumb hits, on
   the panel it closes rather than in the far corner of the window. It is the
   only way off a screen on a phone: the on-screen controls stand down while a
   screen is up — a stick under a document would take the taps meant for the
   page — and a phone has no Escape key. */

.screen-close {
  flex: none;
  display: grid; place-items: center;
  width: 44px; height: 44px;
  margin: -10px -8px -10px 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-4);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
/* [hidden] is a UA rule and this class is an author one, so the author rule
   would win and show a button that has no business being on the screen. */
.screen-close[hidden] { display: none; }
.screen-close svg { width: 14px; height: 14px; display: block; }
.screen-close:hover, .screen-close:focus-visible {
  background: rgba(108, 188, 255, .14); color: var(--ink); border-color: var(--accent-dim);
}
.screen-close:active { transform: scale(.92); }

.paper-holder { display: grid; justify-items: center; }

/* The corner of the screen, for a document — which names no slot of its own,
   because a piece of paper does not carry a button and because a ✕ inside a
   scrolling sheet scrolls away from the thumb that needs it. */
.screen-close.floating {
  position: absolute;
  top: calc(var(--s4) + env(safe-area-inset-top));
  right: calc(var(--s4) + env(safe-area-inset-right));
  margin: 0; width: 46px; height: 46px;
  border-color: var(--line-2);
  background: rgba(8, 12, 18, .82);
  color: var(--ink-2);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .55);
  backdrop-filter: blur(2px);
}
.screen-close.floating:hover, .screen-close.floating:focus-visible {
  background: rgba(28, 44, 60, .92); color: var(--ink);
}

/* ── title ─────────────────────────────────────────────────────────────── */

.title-screen {
  grid-template-rows: 1fr auto 1fr;
  align-content: center; justify-items: center;
  padding: calc(6vh + env(safe-area-inset-top)) calc(4vw + env(safe-area-inset-right))
           calc(6vh + env(safe-area-inset-bottom)) calc(4vw + env(safe-area-inset-left));
  /* A vignette rather than a flat wash: the corridor's own vanishing point is
     dead centre, so the ward stays legible behind the lockup and the edges of
     the frame close down around it. */
  background:
    radial-gradient(120% 85% at 50% 46%, rgba(4, 6, 10, .18) 0%, rgba(2, 3, 6, .88) 76%),
    linear-gradient(180deg, rgba(1, 2, 4, .72) 0%, rgba(1, 2, 4, 0) 26%,
                            rgba(1, 2, 4, 0) 62%, rgba(1, 2, 4, .82) 100%);
  backdrop-filter: none;
}
.title-mark { text-align: center; align-self: end; }

/* The wordmark is artwork and is set as artwork.
   title.png is 1672×941 with the paint occupying rows 294–654: 31.2% empty
   above the lettering and 30.4% below, which against the image's own width is
   17.6% and 17.1%. Vertical margins resolve against the containing block's
   width, so those are the numbers that would close the gap completely — and
   completely is too much. Most of it comes back and the rest is the optical
   space the lockup needs, which is why these are not 17. Set them to what the
   spacing looks like, not to what the bounding box says.
   The bloom behind the blue paint is a gradient rather than a filter on the
   image: WebKit paints filtered, transformed images as opaque boxes often
   enough to matter, and the one behind this would be a black rectangle over
   the ward. */
.title-mark .wordmark {
  position: relative;
  margin: 0 auto;
  width: min(700px, 84vw);
  margin-top: -12%;
  margin-bottom: -13.5%;
}
.title-mark .wordmark::before {
  content: '';
  position: absolute;
  left: -6%; right: -6%; top: 20%; bottom: 18%;
  background:
    radial-gradient(38% 50% at 74% 50%, rgba(47, 140, 255, .26), rgba(47, 140, 255, .08) 50%, rgba(47, 140, 255, 0) 100%),
    radial-gradient(50% 55% at 46% 56%, rgba(0, 0, 0, .55), rgba(0, 0, 0, .22) 55%, rgba(0, 0, 0, 0) 100%);
  pointer-events: none;
}
.title-mark .wordmark picture { display: block; }
.title-mark .wordmark img {
  position: relative;
  display: block;
  width: 100%; height: auto;
  user-select: none;
  -webkit-user-drag: none;
}
/* The two lines under the mark were a stack of near-identical greys. They are
   one line now, with the rule doing the separating and the second line kept for
   the one thing it says that the first does not. */
.title-mark .sub {
  margin: var(--s4) auto 0;
  font-size: clamp(9.5px, 1.3vw, 13.5px);
  letter-spacing: .44em; color: var(--ink-2); font-weight: 600; text-indent: .44em;
}
.title-mark .sub2 {
  margin: var(--s3) auto 0;
  display: flex; align-items: center; justify-content: center; gap: var(--s3);
  font-size: clamp(8.5px, 1vw, 11px);
  letter-spacing: .34em; color: var(--ink-4); font-weight: 500; text-indent: .34em;
}
/* a hairline that stops short of the text on both sides, so the lockup reads as
   one object instead of three centred paragraphs */
.title-mark .sub2::before, .title-mark .sub2::after {
  content: ''; height: 1px; width: clamp(18px, 5vw, 54px);
  background: linear-gradient(90deg, transparent, var(--line-2), transparent);
}

.title-screen .menu {
  display: flex; flex-direction: column; gap: var(--s1);
  margin-top: 7vh; align-items: stretch;
}
.title-screen .menu button {
  position: relative;
  min-width: 262px;
  background: transparent; border: 0; cursor: pointer;
  color: var(--ink-3);
  font: 600 11.5px/1 var(--mono); letter-spacing: .26em;
  padding: 15px 26px 15px 34px;
  text-align: left;
  transition: color var(--t-fast) var(--ease-out),
              letter-spacing var(--t-mid) var(--ease-out),
              background var(--t-fast) var(--ease-out);
}
/* The marker is the affordance: nothing about flat centred mono says "press
   me", and a caret that slides in from the left says which line the cursor is
   on without moving the line itself. */
.title-screen .menu button::before {
  content: ''; position: absolute;
  left: 12px; top: 50%; width: 6px; height: 6px; margin-top: -3px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0; transform: translateX(-6px) scale(.5);
  transition: opacity var(--t-fast) var(--ease-out), transform var(--t-mid) var(--ease-out);
}
/* A hard-edged box round a menu line makes it a form field. The line brightens,
   the marker arrives, and a wash falls away to nothing on the right — which
   says "this one" without drawing a control that is not there. */
.title-screen .menu button:hover, .title-screen .menu button:focus-visible {
  color: #f4f8fc; letter-spacing: .30em;
  background: linear-gradient(90deg, rgba(108, 188, 255, .13), rgba(108, 188, 255, 0) 88%);
  outline: none;
}
.title-screen .menu button:focus-visible {
  box-shadow: inset 2px 0 0 var(--accent);
}
.title-screen .menu button:hover::before,
.title-screen .menu button:focus-visible::before { opacity: 1; transform: none; }
.title-screen .menu button:active { color: #ffffff; }
/* CONTINUE is where a returning player is going; it says so quietly. */
.title-screen .menu button[data-act="continue"] { color: var(--accent-ink); }
.title-screen .menu button.hidden { display: none; }

.title-screen .chapter {
  align-self: end; margin: 0 0 2vh;
  font: 500 10px/1 var(--mono); letter-spacing: .32em; color: var(--ink-4);
}

/* The way off the last screen that had none.
   In a browser tab the back button is the way back to the site, and this would
   be a second one — so it is not there. Installed to a home screen there is no
   back button and no address bar: the launcher opens the landing, one press
   goes to the game, and from that moment the only screens are the game's own.
   Every other screen in here carries its own way off it; this is the title
   screen's, and it only exists in the one place a player would otherwise be
   shut in. */
.title-screen .leave-app { display: none; }
@media (display-mode: standalone), (display-mode: fullscreen), (display-mode: minimal-ui) {
  .title-screen .leave-app {
    display: block;
    position: absolute;
    left: 50%; transform: translateX(-50%);
    bottom: calc(var(--s3) + env(safe-area-inset-bottom));
    padding: var(--s3) var(--s4);
    font: 500 9.5px/1 var(--mono); letter-spacing: .24em; text-transform: uppercase;
    color: var(--ink-faint);
    text-decoration: none;
    transition: color var(--t-fast) var(--ease-out);
  }
  .title-screen .leave-app:hover,
  .title-screen .leave-app:focus-visible { color: var(--ink-3); }
  /* it must not sit on top of the line it is under */
  .title-screen .chapter { margin-bottom: calc(2vh + 26px); }
}

/* ── the screen shell ──────────────────────────────────────────────────── */

.modal-body {
  width: min(760px, 100%); max-height: 86vh; overflow: auto;
  padding: var(--s1) 0;
  scrollbar-width: thin;
  scrollbar-color: rgba(150, 180, 205, .28) transparent;
}
.modal-body::-webkit-scrollbar { width: 8px; }
.modal-body::-webkit-scrollbar-thumb {
  background: rgba(150, 180, 205, .25); border-radius: 4px;
  border: 2px solid transparent; background-clip: padding-box;
}

/* the panel every screen that is not a piece of paper is built out of */
.sheet {
  background: linear-gradient(180deg, var(--panel-2) 0%, var(--panel) 46%);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-panel);
  animation: panel-in var(--t-mid) var(--ease-out) both;
}
/* Sticky, and not for fashion: the ✕ lives here, and on a 320 px phone the
   evidence log is taller than the screen. A close control that scrolls off the
   top is a close control a thumb cannot reach — which is the whole bug this
   header exists to fix. The count stays legible while the list moves too. */
.sheet-head {
  position: sticky; top: 0; z-index: 3;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  padding: var(--s4) var(--s5);
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.sheet-titles {
  display: flex; align-items: baseline; gap: var(--s3);
  min-width: 0;
}
.sheet-head h2 {
  margin: 0;
  font: 700 11px/1 var(--mono); letter-spacing: .30em; color: var(--ink-3);
  text-transform: uppercase;
}
.sheet-head .count {
  font: 600 10px/1 var(--mono); letter-spacing: .18em; color: var(--ink-4);
  white-space: nowrap;
}

.hint {
  margin: var(--s5) 0 0;
  font: 500 10.5px/1.5 var(--mono); letter-spacing: .18em; color: var(--ink-4);
  text-align: center;
}
.hint .keycap { margin: 0 2px; vertical-align: -1px; }

/* ── a document ────────────────────────────────────────────────────────── */
/* Eight of these are the chapter. They are the one thing in the game the
   player is asked to sit and read, so each is dressed as the object it
   actually is: a ward form, a continuation sheet, a note folded small, or a
   plaque screwed to a wall. `data-kind` comes straight off the document. */

.paper {
  position: relative;
  --tilt: -0.25deg;
  background:
    /* the shadow the sheet casts into its own left edge, where it was folded
       back off a clipboard */
    linear-gradient(90deg, rgba(0, 0, 0, .10) 0%, rgba(0, 0, 0, 0) 3%),
    linear-gradient(180deg, rgba(0, 0, 0, .04), rgba(0, 0, 0, .11)),
    var(--paper);
  color: var(--paper-ink);
  padding: var(--s7) clamp(26px, 5vw, 56px) var(--s6);
  box-shadow:
    0 30px 70px -18px rgba(0, 0, 0, .82),
    0 2px 6px rgba(0, 0, 0, .5);
  border-radius: 2px;
  max-width: 620px; margin: 0 auto;
  transform: rotate(var(--tilt));
  animation: paper-in var(--t-mid) var(--ease-out) both;
}
.paper h2 {
  margin: 0; font: 700 15px/1.35 var(--mono); letter-spacing: .16em;
  color: #1c1b17;
}
.paper .paper-sub {
  margin: var(--s2) 0 var(--s5); font: 400 11.5px/1.4 var(--mono);
  letter-spacing: .09em; color: var(--paper-2);
  border-bottom: 1px solid rgba(0, 0, 0, .18); padding-bottom: var(--s3);
}
.paper-body p {
  margin: 0 0 var(--s4);
  font: 400 15.5px/1.72 var(--serif);
  color: #2b2924;
}
.paper-body p:last-child { margin-bottom: 0; }
/* the sign-off is set apart the way it is on the page */
.paper-body p:last-child:first-letter { }

/* The masthead: a ward form has one printed across the top, and it is what
   makes the difference between "a beige box of text" and "a thing off a
   clipboard". Drawn, not fetched. */
.paper::before {
  content: attr(data-org);
  position: absolute; left: clamp(26px, 5vw, 56px); top: var(--s5);
  font: 700 8.5px/1 var(--mono); letter-spacing: .34em;
  color: rgba(0, 0, 0, .34);
}

/* A continuation sheet is typed, and it is ruled. The ruling is drawn on each
   paragraph rather than on the block: paragraphs are separated by a margin, so
   one gradient across the whole body drifts off the baselines by the height of
   that margin and ends up striking through the lines instead of sitting under
   them. Per paragraph, the rhythm restarts where the text does. */
.paper[data-kind="log"] .paper-body p {
  font: 400 13px/1.85 var(--mono);
  letter-spacing: -.005em;
  color: #302e28;
  background-image: repeating-linear-gradient(
    180deg,
    transparent 0 calc(1.85em - 1px),
    rgba(40, 60, 90, .13) calc(1.85em - 1px) 1.85em);
  background-origin: content-box;
}

/* a note folded small: less of it, warmer, and it sits crooked */
.paper[data-kind="note"] {
  --tilt: -1.1deg;
  max-width: 460px;
  background:
    linear-gradient(180deg, rgba(0, 0, 0, .05), rgba(0, 0, 0, .13)),
    /* the crease it was folded on — enough to read as a fold, not enough to
       cut the words it runs through in half */
    linear-gradient(90deg, transparent 48.8%, rgba(0, 0, 0, .055) 49.8%,
                           rgba(255, 255, 255, .16) 50.3%, transparent 51.2%),
    var(--paper-warm);
  padding: var(--s6) clamp(24px, 4.5vw, 44px) var(--s5);
}
.paper[data-kind="note"] .paper-body p { font-size: 15px; line-height: 1.78; }

/* ── a record read off a terminal ──────────────────────────────────────── */
/* Four of the eleven records are not paper: they were on a screen, running,
   when the player walked up to them, and dressing them as a beige sheet would
   throw away the one thing that makes them frightening. So the panel is the
   screen itself — the ward's clinical dark, the system's cyan, mono set
   throughout, a scan sitting over it, and the masthead is the session the
   record was open on rather than a ward letterhead. Restraint is the whole
   brief: no glyph rain, no flicker, one accent, and type that stays type. */
.paper[data-kind="terminal"] {
  --tilt: 0deg;
  max-width: 660px;
  background:
    /* the faint horizontal structure of a panel, not scanlines you can count */
    repeating-linear-gradient(180deg,
      rgba(108, 188, 255, .030) 0 1px, transparent 1px 3px),
    linear-gradient(180deg, #071119 0%, #050c12 62%, #04090e 100%);
  color: #b9dcf3;
  border: 1px solid rgba(108, 188, 255, .22);
  border-radius: var(--r-sm);
  padding: var(--s7) clamp(24px, 4.5vw, 46px) var(--s6);
  box-shadow:
    0 0 0 1px rgba(4, 9, 14, .9),
    0 0 42px -8px rgba(60, 150, 220, .28),
    0 30px 70px -18px rgba(0, 0, 0, .85);
}
.paper[data-kind="terminal"]::before {
  color: rgba(126, 190, 232, .60);
  letter-spacing: .30em;
}
/* the link light in the corner: on, steady, and the only saturated thing here */
.paper[data-kind="terminal"]::after {
  content: '';
  position: absolute; top: var(--s5); right: clamp(24px, 4.5vw, 46px);
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px 1px rgba(108, 188, 255, .55);
}
.paper[data-kind="terminal"] h2 {
  color: #d8ecfb; letter-spacing: .18em;
}
.paper[data-kind="terminal"] .paper-sub {
  color: #6e9cbb;
  border-bottom-color: rgba(108, 188, 255, .20);
}
.paper[data-kind="terminal"] .paper-body p {
  font: 400 13px/1.75 var(--mono);
  color: #a8cfe8;
  white-space: pre-wrap;          /* the record is laid out, not reflowed */
  letter-spacing: -.002em;
}

/* a plaque is not paper at all */
.paper[data-kind="plaque"] {
  --tilt: 0deg;
  max-width: 460px;
  text-align: center;
  background: linear-gradient(158deg, #6e6f6b 0%, #43443f 34%, #5d5e59 58%, #35362f 100%);
  color: #e7e6df;
  border-radius: var(--r-sm);
  padding: var(--s7) var(--s6);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, .22) inset,
    0 -1px 0 rgba(0, 0, 0, .5) inset,
    0 30px 70px -18px rgba(0, 0, 0, .85);
}
/* four screws, which is most of what says "screwed to a wall" rather than
   "grey card" — one element, four gradients, no images */
.paper[data-kind="plaque"]::before {
  content: '';
  position: absolute; inset: 13px;
  background-image:
    radial-gradient(circle at 0 0, rgba(255, 255, 255, .30) 38%, rgba(0, 0, 0, .55) 42%, transparent 62%),
    radial-gradient(circle at 100% 0, rgba(255, 255, 255, .30) 38%, rgba(0, 0, 0, .55) 42%, transparent 62%),
    radial-gradient(circle at 0 100%, rgba(255, 255, 255, .30) 38%, rgba(0, 0, 0, .55) 42%, transparent 62%),
    radial-gradient(circle at 100% 100%, rgba(255, 255, 255, .30) 38%, rgba(0, 0, 0, .55) 42%, transparent 62%);
  background-size: 9px 9px;
  background-repeat: no-repeat;
  background-position: 0 0, 100% 0, 0 100%, 100% 100%;
  pointer-events: none;
}
.paper[data-kind="plaque"] h2,
.paper[data-kind="plaque"] .paper-sub { display: none; }
.paper[data-kind="plaque"] .paper-body p {
  margin: 0 0 var(--s5);
  font: 600 13px/1.7 var(--mono);
  letter-spacing: .26em; text-indent: .26em;
  color: #edece6;
  /* engraved: a dark cut with a light lip under it */
  text-shadow: 0 -1px 0 rgba(0, 0, 0, .65), 0 1px 0 rgba(255, 255, 255, .18);
}
.paper[data-kind="plaque"] .paper-body p:first-child {
  font-size: 15px; letter-spacing: .3em; text-indent: .3em;
}

/* ── evidence log ──────────────────────────────────────────────────────── */
/* Eight documents that only make sense against each other, so the log is the
   place the chapter is actually solved. It gets a panel, a count, and a mark
   on anything the player has picked up but not yet gone back to. */

.log { color: var(--ink); width: min(640px, 100%); margin: 0 auto; }
.log .sheet-body { padding: var(--s3) var(--s5) var(--s5); }
.log h3 {
  display: flex; align-items: center; gap: var(--s3);
  margin: var(--s5) 0 var(--s2);
  font: 600 9.5px/1 var(--mono); letter-spacing: .28em; color: var(--ink-4);
}
.log h3::after {
  content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, var(--line), transparent);
}
.log ul { list-style: none; margin: 0; padding: 0; }

/* ── the slot in the margin ────────────────────────────────────────────────
   Every row in both lists opens with one 34 px slot, and what goes in it is
   either a piece of the production icon set or the mark the log has always
   drawn. Both, at the same size and in the same place, so eight documents and
   three objects still read as one column and not as a list that ran out of
   pictures. The icons are decorative: the row says in words what it is, and a
   failed decode leaves an empty slot rather than a broken glyph. */

.log .ico {
  flex: none;
  display: grid; place-items: center;
  width: 34px; height: 34px;
  align-self: start;
  margin-top: 1px;
}
.log .ico img {
  width: 100%; height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .7));
}
/* The rows with nothing truthful to draw. A key on a split ring and a cut
   wristband are not in the set, and a padlock stamped SEALED is not either of
   them, so they keep the mark. */
.log .ico-mark {
  position: relative;
}
.log .ico-mark::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 9px; height: 9px; margin: -5px 0 0 -5px;
  border-radius: 1px;
  border: 1px solid var(--ink-faint);
  transform: rotate(45deg);
}

.log .row-text { display: grid; gap: 3px; min-width: 0; }

.log .items li {
  padding: var(--s3) 0;
  border-top: 1px solid var(--line);
  display: flex; align-items: flex-start; gap: var(--s3);
}
.log .items li:first-child { border-top: 0; }
.log .items b { font: 600 12px/1.3 var(--mono); letter-spacing: .13em; color: var(--ink); }
.log .items span { font-size: 12.5px; line-height: 1.5; color: var(--ink-4); }

.log .docs li { border-top: 1px solid var(--line); }
.log .docs li:first-child { border-top: 0; }
.log .docs button {
  position: relative;
  width: 100%; text-align: left; background: transparent; border: 0; cursor: pointer;
  padding: var(--s3) var(--s3) var(--s3) 0;
  display: flex; align-items: flex-start; gap: var(--s3);
  transition: background var(--t-fast) var(--ease-out);
}
.log .docs button:hover, .log .docs button:focus-visible {
  background: linear-gradient(90deg, rgba(108, 188, 255, .10), rgba(108, 188, 255, .02));
  outline: none;
}
.log .docs button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* the row lifts under the cursor, so the list answers a pointer moving down it */
.log .docs button .ico { transition: transform var(--t-fast) var(--ease-out); }
.log .docs button:hover .ico, .log .docs button:focus-visible .ico { transform: scale(1.08); }
.log .docs b {
  font: 600 12px/1.3 var(--mono); letter-spacing: .13em; color: var(--ink);
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
}
.log .docs span { font-size: 12.5px; line-height: 1.5; color: var(--ink-4); }
/* read once and not gone back to — the chapter expects you to go back */
.log .docs .new {
  font: 700 8px/1 var(--mono); letter-spacing: .16em;
  color: var(--accent-ink);
  border: 1px solid var(--accent-dim); border-radius: 2px;
  padding: 3px 5px 2px;
}

.log .empty {
  margin: var(--s6) 0;
  text-align: center; color: var(--ink-4); font-size: 13.5px; line-height: 1.7;
}
.log .empty b {
  display: block; margin-bottom: var(--s2);
  font: 600 11px/1 var(--mono); letter-spacing: .24em; color: var(--ink-3);
}

/* ── keypad ────────────────────────────────────────────────────────────── */
/* It is a device screwed to a wall, not a form. The bezel is what says so. */

.keypad-ui { max-width: 320px; margin: 0 auto; text-align: center; }
.keypad-ui .sheet-head { justify-content: center; padding-bottom: var(--s3); }
.keypad-ui .sheet-body { padding: var(--s4) var(--s4) var(--s5); }
/* the lock, over the pad that opens it */
.keypad-ui .sheet-titles { align-items: center; }
.keypad-ui .sheet-titles .ico { display: block; width: 22px; height: 26px; }
.keypad-ui .sheet-titles .ico img { width: 100%; height: 100%; object-fit: contain; }

.readout {
  font: 700 32px/1 var(--mono); letter-spacing: .34em;
  color: var(--ok); text-indent: .34em;
  padding: var(--s4) 0; margin-bottom: var(--s4);
  background: radial-gradient(120% 140% at 50% 0%, rgba(20, 48, 36, .95), rgba(6, 16, 12, .95));
  border: 1px solid rgba(120, 220, 170, .22);
  border-radius: var(--r-sm);
  box-shadow: 0 0 24px rgba(80, 210, 150, .10) inset, 0 1px 0 rgba(0,0,0,.6);
  text-shadow: 0 0 14px rgba(127, 224, 173, .5);
  transition: color var(--t-fast), border-color var(--t-fast), box-shadow var(--t-fast);
}
/* A wrong code is not allowed to be only a colour change: it also moves, and
   ui.js announces it. */
.readout.bad {
  color: var(--warn); border-color: rgba(255, 106, 77, .5);
  /* the whole readout goes red, not just the glyphs — a green box with red
     letters in it reads as a rendering fault rather than as a refusal */
  background: radial-gradient(120% 140% at 50% 0%, rgba(58, 20, 12, .95), rgba(20, 7, 4, .95));
  box-shadow: 0 0 24px rgba(255, 106, 77, .16) inset, 0 1px 0 rgba(0, 0, 0, .6);
  text-shadow: 0 0 14px rgba(255, 106, 77, .5);
  animation: shake 380ms var(--ease-in-out);
}
@keyframes shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-6px); }
  40%, 60% { transform: translateX(6px); }
}

.pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); }
.pad button {
  aspect-ratio: 1 / 1;
  background: linear-gradient(180deg, rgba(30, 40, 51, .95), rgba(14, 20, 27, .95));
  border: 1px solid var(--line);
  border-bottom-color: rgba(0, 0, 0, .6);
  color: var(--ink); font: 600 17px/1 var(--mono);
  border-radius: var(--r-md);
  box-shadow: 0 1px 0 rgba(190, 214, 232, .08) inset, 0 2px 4px rgba(0, 0, 0, .5);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease-out),
              transform 70ms var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}
.pad button:hover, .pad button:focus-visible {
  background: linear-gradient(180deg, rgba(46, 70, 92, .95), rgba(20, 32, 44, .95));
  border-color: var(--accent-dim);
}
.pad button:active {
  transform: translateY(1px) scale(.97);
  background: linear-gradient(180deg, rgba(58, 92, 122, .95), rgba(26, 42, 58, .95));
  box-shadow: 0 1px 3px rgba(0, 0, 0, .7) inset;
}
.pad button.clear {
  font-size: 13px; letter-spacing: .1em;
  color: var(--ink-3);
}

/* ── menus and settings ────────────────────────────────────────────────── */

.menu-panel { width: min(440px, 90vw); margin: 0 auto; }
.menu-panel .sheet-body { padding: var(--s5); }
.menu-panel .objective-note {
  margin: calc(-1 * var(--s3)) 0 var(--s5);
  font-size: 13px; line-height: 1.65; color: var(--ink-2);
  text-align: center;
  padding: 0 var(--s2);
}

/* Four identical outlined boxes made RESUME and RETURN TO TITLE the same
   weight. They are not the same thing: one is what the player came here to do
   and one throws the run away. */
button.wide {
  display: block; width: 100%;
  background: rgba(255, 255, 255, .015);
  border: 1px solid var(--line); border-radius: var(--r-md);
  color: var(--ink-2); font: 600 11px/1 var(--mono); letter-spacing: .22em;
  padding: 14px; margin-top: var(--s2); cursor: pointer;
  transition: background var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              transform 70ms var(--ease-out);
}
button.wide:hover, button.wide:focus-visible {
  background: rgba(108, 188, 255, .12); color: #eaf2f8;
  border-color: var(--accent-dim); outline: none;
}
button.wide:focus-visible { box-shadow: 0 0 0 1px var(--accent) inset; }
button.wide:active { transform: translateY(1px); }

button.wide.primary {
  background: linear-gradient(180deg, rgba(50, 116, 176, .55), rgba(26, 70, 112, .55));
  border-color: rgba(122, 190, 245, .45);
  color: #f0f7fc;
  box-shadow: 0 1px 0 rgba(190, 220, 245, .16) inset, 0 6px 18px -8px rgba(40, 110, 180, .8);
  margin-top: 0;
}
button.wide.primary:hover, button.wide.primary:focus-visible {
  background: linear-gradient(180deg, rgba(66, 141, 208, .70), rgba(32, 84, 132, .70));
  border-color: rgba(150, 210, 255, .6);
}
/* the way out of the run is set apart from the ways back into it — still
   plainly a button, just not one competing with RESUME for the same glance */
button.wide.quiet {
  margin-top: var(--s5);
  border-color: rgba(126, 178, 219, .10);
  background: transparent;
  color: var(--ink-4);
  font-size: 10px;
}
button.wide.quiet:hover, button.wide.quiet:focus-visible {
  background: rgba(255, 106, 77, .10);
  border-color: rgba(255, 106, 77, .34);
  color: #ffcbbe;
}

.menu-panel .row {
  display: grid; grid-template-columns: 1fr 1.15fr auto; align-items: center;
  gap: var(--s3); margin-bottom: var(--s4);
}
.menu-panel label {
  font: 600 9.5px/1.35 var(--mono); letter-spacing: .16em; color: var(--ink-3);
}

/* The stock slider was the loudest object on a screen made of near-blacks: a
   full-saturation system blue track with a system thumb on it. This is the same
   control, wearing the interface. */
.menu-panel input[type=range] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 22px; margin: 0;
  background: transparent; cursor: pointer;
}
.menu-panel input[type=range]::-webkit-slider-runnable-track {
  height: 3px; border-radius: 2px;
  background: linear-gradient(90deg, var(--accent-dim) 0%, var(--accent-dim) var(--fill, 50%),
                                     rgba(150, 180, 205, .16) var(--fill, 50%));
}
.menu-panel input[type=range]::-moz-range-track {
  height: 3px; border-radius: 2px; background: rgba(150, 180, 205, .16);
}
.menu-panel input[type=range]::-moz-range-progress {
  height: 3px; border-radius: 2px; background: var(--accent-dim);
}
.menu-panel input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 14px; height: 14px; margin-top: -5.5px;
  border-radius: 50%;
  background: linear-gradient(180deg, #dcebf7, #9dbdd6);
  border: 1px solid rgba(8, 14, 20, .8);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .7);
  transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out);
}
.menu-panel input[type=range]::-moz-range-thumb {
  width: 14px; height: 14px; border-radius: 50%;
  background: linear-gradient(180deg, #dcebf7, #9dbdd6);
  border: 1px solid rgba(8, 14, 20, .8);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .7);
}
.menu-panel input[type=range]:hover::-webkit-slider-thumb { transform: scale(1.12); }
.menu-panel input[type=range]:active::-webkit-slider-thumb { transform: scale(1.02); }
.menu-panel input[type=range]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-sm);
}

.menu-panel select {
  width: 100%;
  -webkit-appearance: none; appearance: none;
  background:
    /* the chevron, drawn rather than inherited from the platform */
    linear-gradient(45deg, transparent 50%, var(--ink-3) 50%) calc(100% - 15px) 50% / 5px 5px no-repeat,
    linear-gradient(135deg, var(--ink-3) 50%, transparent 50%) calc(100% - 10px) 50% / 5px 5px no-repeat,
    linear-gradient(180deg, rgba(28, 38, 48, .95), rgba(14, 20, 27, .95));
  border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink); font: 600 10.5px/1 var(--mono); letter-spacing: .14em;
  padding: 11px 30px 11px 10px;
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease-out), background-color var(--t-fast);
}
.menu-panel select:hover { border-color: var(--accent-dim); }
.menu-panel select option { background: #0e141b; color: var(--ink); }
.menu-panel .val {
  font: 600 10.5px/1 var(--mono); color: var(--ink-2); min-width: 46px; text-align: right;
  font-variant-numeric: tabular-nums;
}

.menu-panel .controls {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s2) var(--s4);
  align-items: baseline;
  margin: var(--s5) 0 0; padding-top: var(--s5); border-top: 1px solid var(--line);
}
.menu-panel .controls dt { display: flex; gap: 3px; flex-wrap: wrap; }
.menu-panel .controls dd { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--ink-4); }
/* on touch the left column is a word, not a key, so it is set as one */
.menu-panel .controls dt.word {
  font: 600 9.5px/1.5 var(--mono); letter-spacing: .14em; color: var(--ink-3);
  align-self: center;
}

/* ── chapter end ───────────────────────────────────────────────────────── */
/* The last thing the chapter does. It was a 420 px dialog with a button in it;
   it is the one moment in the game that has earned the whole frame. */

.ending-screen {
  background:
    radial-gradient(140% 100% at 50% 40%, rgba(4, 7, 11, .70) 0%, rgba(0, 0, 0, .97) 70%);
  place-items: center;
  padding: calc(8vh + env(safe-area-inset-top)) calc(6vw + env(safe-area-inset-right))
           calc(8vh + env(safe-area-inset-bottom)) calc(6vw + env(safe-area-inset-left));
}
.ending {
  width: min(760px, 100%);
  text-align: left;
  animation: ending-in 1.4s var(--ease-out) both;
}
@keyframes ending-in { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }

.ending .over {
  margin: 0 0 var(--s5);
  display: flex; align-items: center; gap: var(--s4);
  font: 600 10px/1 var(--mono); letter-spacing: .40em; color: var(--ink-4);
}
.ending .over::after {
  content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, var(--line-2), transparent);
}
.ending h2 {
  margin: 0 0 var(--s6);
  font-size: clamp(28px, 5.2vw, 58px);
  font-weight: 900; font-style: italic; text-transform: uppercase;
  letter-spacing: -.02em; line-height: .95;
  color: #f2f6fa; text-align: left;
  filter: drop-shadow(0 6px 30px rgba(0, 0, 0, .9));
}
.ending p {
  margin: 0 0 var(--s4);
  max-width: 62ch;
  font-size: 15px; line-height: 1.75; color: var(--ink-2);
}
/* each paragraph arrives after the one above it, which is how the three of
   them are meant to be read */
.ending p { animation: ending-in 1s var(--ease-out) both; }
.ending p:nth-of-type(1) { animation-delay: .35s; }
.ending p:nth-of-type(2) { animation-delay: .75s; }
.ending p:nth-of-type(3) { animation-delay: 1.15s; }
.ending .close-row { margin-top: var(--s7); animation: ending-in 1s var(--ease-out) 1.7s both; }
.ending button.wide { max-width: 300px; margin-top: 0; }

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

#boot {
  position: fixed; inset: 0; z-index: 50;
  display: grid; place-items: center;
  background:
    radial-gradient(120% 90% at 50% 45%, #0a1119 0%, #04060a 70%);
  transition: opacity .7s ease, visibility .7s;
}
#boot.done { opacity: 0; visibility: hidden; }
#boot.failed .glyph { animation: none; }
/* a failure has to be readable, not a black canvas and a console entry */
#boot.failed .boot-inner { max-width: min(620px, 88vw); }
#boot-msg .fatal-head {
  display: block; margin: var(--s1) 0 var(--s3);
  font: 700 12.5px/1.5 var(--sans); letter-spacing: .12em; color: var(--warn);
  text-transform: none;
}
#boot-msg .fatal-line {
  display: block; margin: 0 0 var(--s2);
  font: 400 12px/1.65 var(--mono); letter-spacing: .01em; color: var(--ink-3);
  text-transform: none;
}
.boot-inner { text-align: center; width: min(420px, 84vw); }
.boot-inner b {
  display: block; margin-top: var(--s5);
  font: 800 15px/1 var(--sans); letter-spacing: .34em; color: var(--ink);
}
.boot-inner span {
  display: block; margin-top: var(--s3);
  font: 400 10.5px/1.5 var(--mono); letter-spacing: .18em; color: var(--ink-4);
  min-height: 1.5em;
}

/* The spinner was a blue ring: a loading state borrowed from a web app and
   dropped into a hospital at night. This is the ward's own emergency light —
   one fixture, striking and settling, on the same 1.1 s the ring turned on.
   It is two elements and no JavaScript. */
.glyph {
  position: relative;
  width: 74px; height: 46px; margin: 0 auto;
  display: grid; place-items: end center;
}
.glyph::before {
  content: '';
  width: 52px; height: 5px;
  border-radius: 1px;
  background: linear-gradient(180deg, #cfe4f4, #8fb4cf);
  box-shadow: 0 0 18px rgba(150, 200, 240, .55);
  animation: tube 2.4s var(--ease-in-out) infinite;
}
/* the cone it throws down the wall below it */
.glyph::after {
  content: '';
  position: absolute; left: 50%; bottom: -22px;
  width: 120px; height: 40px; margin-left: -60px;
  background: radial-gradient(60% 100% at 50% 0%, rgba(160, 205, 240, .30), transparent 72%);
  animation: tube 2.4s var(--ease-in-out) infinite;
}
@keyframes tube {
  0%   { opacity: .12; }
  6%   { opacity: 1; }
  10%  { opacity: .18; }
  16%  { opacity: .92; }
  22%  { opacity: .35; }
  30%, 72% { opacity: .85; }
  78%  { opacity: .28; }
  86%  { opacity: .8; }
  100% { opacity: .12; }
}
#boot.failed .glyph::before {
  background: linear-gradient(180deg, #ff9d87, #c4452c);
  box-shadow: 0 0 18px rgba(255, 106, 77, .5);
  opacity: 1;
}
#boot.failed .glyph::after { opacity: 0; }

/* what the boot has actually got through, rather than an indeterminate spin */
.boot-bar {
  width: 100%; height: 3px; margin-top: var(--s5);
  background: rgba(150, 180, 205, .14);
  border-radius: 2px;
  overflow: hidden;
}
.boot-bar i {
  display: block; height: 100%; width: 0%;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent-dim), var(--accent));
  box-shadow: 0 0 10px rgba(108, 188, 255, .45);
  transition: width .5s var(--ease-out);
}
#boot.failed .boot-bar { display: none; }

/* ── touch controls ────────────────────────────────────────────────────── */
/* Built by src/touch.js and only present on a device that has actually been
   touched. It sits above the in-world text (which takes no input) and below
   the screens (which take all of it), so a document or the pause menu covers
   the controls rather than competing with them. */

.touch {
  position: fixed; inset: 0; z-index: 12;
  pointer-events: none;
  opacity: 0; visibility: hidden;
  transition: opacity .2s ease, visibility .2s;
}
.touch.on { opacity: 1; visibility: visible; }

.t-zone {
  position: fixed; top: 0; bottom: 0;
  pointer-events: auto;
  touch-action: none;
}
/* the left of the screen walks, the right of it looks — they do not overlap,
   so a thumb on each drives both at once with no arbitration */
.t-move { left: 0; width: 44%; }
.t-look { left: 44%; right: 0; }

.t-stick {
  position: fixed; width: 0; height: 0;
  opacity: 0; transition: opacity .12s ease;
}
.t-stick.on { opacity: 1; }
.t-stick::before {
  content: ''; position: absolute;
  left: -58px; top: -58px; width: 116px; height: 116px;
  border-radius: 50%;
  border: 1px solid rgba(180, 210, 235, .22);
  background: radial-gradient(circle, rgba(10, 16, 22, .30) 0%, rgba(10, 16, 22, .10) 70%);
}
/* Centred by the transform and by nothing else: a negative margin as well would
   put the knob half its own width up and left of the thumb holding it. */
.t-knob {
  position: absolute; left: 0; top: 0;
  width: 52px; height: 52px;
  border-radius: 50%;
  border: 1px solid rgba(200, 226, 245, .40);
  background: radial-gradient(circle at 50% 35%, rgba(52, 72, 92, .62), rgba(20, 30, 40, .58));
  box-shadow: 0 2px 14px rgba(0, 0, 0, .55);
}

/* Every target is at least 44 px, which is the smallest a thumb reliably
   hits, and the verb is the biggest because it is pressed the most. */
.t-btn {
  pointer-events: auto;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  display: grid; place-items: center;
  min-width: 62px; min-height: 62px;
  padding: 0 6px;
  border: 1px solid rgba(150, 190, 220, .30);
  border-radius: 50%;
  background: rgba(10, 16, 22, .52);
  color: var(--ink);
  font: 600 9px/1.15 var(--mono); letter-spacing: .12em;
  text-align: center;
  backdrop-filter: blur(3px);
  transition: background var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              transform 80ms var(--ease-out),
              opacity var(--t-mid) var(--ease-out);
}
.t-btn.down { background: rgba(108, 188, 255, .32); color: #f2f8fc; transform: scale(.93); }
.t-btn.latched {
  border-color: rgba(120, 200, 255, .70); color: #eaf4fb;
  background: rgba(40, 92, 132, .55);
  box-shadow: 0 0 0 1px rgba(120, 200, 255, .25);
}

/* ── the two buttons that name an object ───────────────────────────────────
   LANTERN and EVIDENCE act on a thing the player is carrying, so the thing is
   drawn on them. RUN, CROUCH, MENU and the verb name an action or a state,
   which cannot be drawn without inventing a glyph the player would have to
   learn — so those stay words. The word stays on these two as well: the icon
   is what finds the button across a dark screen, the word is what confirms it,
   and neither has to be decoded under pressure. */

.t-icon { gap: 2px; padding: 4px 6px; }
.t-icon .ico {
  display: block;
  width: 26px; height: 26px;
  pointer-events: none;
}
.t-icon .ico img {
  width: 100%; height: 100%;
  object-fit: contain;
  /* the button is the target, not the image inside it */
  pointer-events: none;
  -webkit-user-drag: none;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, .8));
  transition: filter var(--t-mid) var(--ease-out), opacity var(--t-mid) var(--ease-out);
}
.t-icon .t-word { font: inherit; }
/* Held low, the lantern is a shape on a dark button. Turned up, it is lit —
   which is the only place a thumb can read a state the ward itself has already
   changed under it. */
.t-mods .t-icon.latched .ico img { filter: drop-shadow(0 0 10px rgba(255, 180, 90, .75)); }
.t-mods .t-icon:not(.latched) .ico img { opacity: .7; }

.t-small.t-icon { padding: 0 12px; gap: 1px; }
.t-small.t-icon .ico { width: 18px; height: 18px; }

/* The verb is the button pressed most and it is the one that changes what it
   says, so it is bigger, brighter, and set apart from the three that only ever
   toggle something. */
.t-verb {
  min-width: 92px; min-height: 92px;
  font-size: 10.5px; letter-spacing: .12em;
  border-color: rgba(190, 220, 245, .5);
  background:
    radial-gradient(70% 70% at 50% 32%, rgba(48, 84, 116, .72), rgba(14, 24, 34, .68));
  color: #eef5fb;
  box-shadow: 0 4px 18px -6px rgba(0, 0, 0, .8);
}
/* something under the reticle: the rim lights, which is the same information
   the prompt line carries, put where the thumb already is */
.t-verb.ready {
  border-color: rgba(160, 216, 255, .9);
  box-shadow: 0 4px 18px -6px rgba(0, 0, 0, .8),
              0 0 0 1px rgba(120, 195, 255, .28),
              0 0 18px -2px rgba(108, 188, 255, .34);
}
/* and nothing under it: the button stays where the thumb expects it and stops
   advertising itself. Not so far down that it disappears — it is still the
   most-pressed control on the screen, and a player who cannot find it cannot
   play. */
.t-verb.idle { opacity: .42; }

/* One column of toggles, and the verb beside them — three round buttons in a
   2×2 with the verb was four equal circles competing for the same glance. */
.t-actions {
  position: fixed;
  right: calc(16px + env(safe-area-inset-right));
  bottom: calc(16px + env(safe-area-inset-bottom));
  display: grid;
  grid-template-columns: auto auto;
  grid-template-areas: "mods verb";
  gap: var(--s4);
  align-items: end;
}
.t-mods {
  grid-area: mods;
  display: flex; flex-direction: column; gap: var(--s2);
  align-items: center;
}
.t-verb { grid-area: verb; }

.t-tools {
  position: fixed;
  right: calc(16px + env(safe-area-inset-right));
  top: calc(14px + env(safe-area-inset-top));
  display: flex; gap: var(--s2);
}
.t-small {
  min-width: 0; min-height: 44px;
  padding: 0 14px;
  border-radius: 22px;
  font-size: 8.5px; letter-spacing: .18em;
  background: rgba(10, 16, 22, .44);
}

/* Menu rows are read with a thumb here, not clicked with a cursor: nothing the
   player has to hit is allowed under 44 px on a touch device. */
body.touch-mode .title-screen .menu button,
body.touch-mode button.wide,
body.touch-mode .log .docs button { min-height: 50px; }
body.touch-mode .menu-panel input[type=range] { height: 44px; }
body.touch-mode .pad button { min-height: 56px; }

/* The two lines of in-world text move out from under the thumbs. In landscape
   they pass to the left of the button cluster; in portrait there is no room
   beside it, so they go above it instead. */
body.touch-mode .subtitle { bottom: calc(104px + env(safe-area-inset-bottom)); max-width: min(46ch, 54vw); }
body.touch-mode .stamina { bottom: calc(78px + env(safe-area-inset-bottom)); }
body.touch-mode .prompt-line { top: calc(50% + 56px); }

/* and the objective stops before the evidence and menu buttons, which is a
   tighter budget on a phone held upright than the 42ch it gets on a desktop */
body.touch-mode .objective { max-width: min(42ch, calc(100vw - 210px)); }

/* Held upright there is no room beside the cluster, so the two lines go above
   it. The number is the cluster's own height (a column of three 62 px toggles
   plus its gaps, 16 px off the bottom) plus room to read. */
@media (orientation: portrait) {
  body.touch-mode .subtitle { bottom: calc(242px + env(safe-area-inset-bottom)); max-width: 86vw; }
  body.touch-mode .stamina { bottom: calc(224px + env(safe-area-inset-bottom)); }
}
/* there is no E key on a phone, and the verb button already says the verb */
body.touch-mode .prompt-line b { display: none; }

/* a phone held upright has almost no room to look: the stick and the verb
   still fit, and the rest gets out of the way */
@media (max-height: 480px) {
  .t-btn { min-width: 54px; min-height: 54px; font-size: 8px; }
  .t-verb { min-width: 76px; min-height: 76px; font-size: 9.5px; }
  .t-actions { gap: var(--s3); }
  .t-mods { gap: 6px; }
}

/* Screens have to clear the notch and the home indicator too. The title and
   the ending set their own padding further up, so they are excluded here
   rather than fought with. */
.screen:not(.title-screen):not(.ending-screen) {
  padding: calc(var(--s5) + env(safe-area-inset-top)) calc(var(--s4) + env(safe-area-inset-right))
           calc(var(--s5) + env(safe-area-inset-bottom)) calc(var(--s4) + env(safe-area-inset-left));
}

/* ── the narrow end of the range ───────────────────────────────────────── */

@media (max-width: 880px) {
  .objective { max-width: 60vw; left: calc(20px + env(safe-area-inset-left)); }
}

@media (max-width: 560px) {
  .sheet-head { padding: var(--s4) var(--s4) var(--s3); }
  .log .sheet-body, .menu-panel .sheet-body, .ending .sheet-body { padding: var(--s3) var(--s4) var(--s4); }
  .paper { padding: 46px var(--s5) var(--s5); }
  .paper-body p { font-size: 15px; }
  .screen-close { top: calc(var(--s2) + env(safe-area-inset-top)); right: calc(var(--s2) + env(safe-area-inset-right)); }
  .title-screen .menu button { min-width: 0; width: min(300px, 78vw); }
}

/* A phone on its side has 390 px of height for the whole screen, and a modal
   that centres itself in it has almost none left after the safe areas. */
@media (max-height: 500px) {
  .modal-body { max-height: 92vh; }
  .paper { padding: 38px var(--s5) var(--s4); }
  .paper::before { top: var(--s3); }
  /* a landscape phone has 390 px of height for the whole log */
  .log .docs button { padding-top: var(--s2); padding-bottom: var(--s2); }
  .log .items li { padding-top: var(--s2); padding-bottom: var(--s2); }
  .log h3 { margin-top: var(--s4); }
  .log .items li::before, .log .docs button::before { top: 15px; }
  .sheet-head { padding: var(--s4) var(--s4) var(--s3); }
  .ending h2 { font-size: clamp(22px, 5vw, 34px); margin-bottom: var(--s4); }
  .ending .close-row { margin-top: var(--s5); }
}

/* A very wide desktop must not stretch a line of prose to 140 characters — but
   it must not leave the end of the chapter as a small paragraph adrift in the
   middle of it either. The measure holds and the display type grows. */
@media (min-width: 1800px) {
  .objective { max-width: min(46ch, 30vw); }
  .subtitle { max-width: 58ch; }
  .ending { width: min(980px, 100%); }
  .ending h2 { font-size: clamp(58px, 4.2vw, 88px); }
  .ending p { font-size: 16.5px; max-width: 58ch; }
  .ending .over { font-size: 11px; }
}

/* ── reduced motion ────────────────────────────────────────────────────── */
/* Nothing is removed that carried meaning: the states still change, they just
   stop travelling. The emergency light stops flickering entirely — a strobe is
   exactly what this setting is asked for. */

@media (prefers-reduced-motion: reduce) {
  .glyph::before, .glyph::after { animation: none; opacity: .9; }
  .objective, .subtitle, .prompt-line, .checkpoint, .fade { transition-duration: .01s; }
  .touch, .t-stick, .t-btn { transition-duration: .01s; }
  .screen, .sheet, .paper, .ending, .ending p, .ending .close-row {
    animation-duration: .01s; animation-delay: 0s;
  }
  .readout.bad { animation: none; }
  .reticle { transition-duration: .01s; }
  .title-screen .menu button { transition-property: color, background; }
  .paper, .paper[data-kind="note"] { --tilt: 0deg; }
  /* the icons still change with state; they stop travelling to do it */
  .log .docs button .ico { transition-duration: .01s; }
  .log .docs button:hover .ico, .log .docs button:focus-visible .ico { transform: none; }
  .t-icon .ico img { transition-duration: .01s; }
}
