:root {
  color-scheme: dark;
  --dead: #2a3040;
  --ink: #f2ece4;
  --paper: #191c24;
}

* { box-sizing: border-box; }

/**
 * ⚠ `[hidden]` IS THE LOWEST-PRIORITY RULE IN THE BROWSER.
 *
 * `hidden` works via the UA stylesheet's `[hidden] { display: none }` — and ANY
 * author rule that sets `display` on the same element outranks it and silently
 * defeats it. `el.hidden = true` then does nothing at all: the property is set,
 * the DOM agrees, and the element stays on screen.
 *
 * This had bitten FIVE things, and nobody noticed because nothing errors:
 *
 *   .maker-extras   (display:grid) — the "change their names / sign it" disclosure
 *                   NEVER hid anything. The fields were always visible, so clicking
 *                   the button appeared to do nothing. A playtester reported exactly
 *                   this and I went looking for a JS bug.
 *   .maker-result   (display:grid) — the empty link box, "copy" and "share" showed
 *                   from the moment the maker opened, before any link existed.
 *   .panel-row      (display:flex)
 *   .nudge          (display:flex)
 *
 * (The morse chart was ALSO stuck-visible by this same trick — but that one turned
 * out to be RIGHT: the chart is the alphabet, not the answer, and gating it made
 * the puzzle a leave-the-game-and-google wall. So it is now shown deliberately and
 * never sets `hidden`. See puzzles/morse.js.)
 *
 * One rule fixes all of them, and every future one.
 */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
  background: #0d0f14;
  overflow: hidden;
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--ink);
}

body { display: grid; place-items: center; }

/* 16:9 box, as large as fits. The canvas backing store stays 480x270 forever;
   only this box changes size. */
#stage {
  position: relative;
  width: min(100vw, calc(100dvh * 16 / 9));
  height: min(100dvh, calc(100vw * 9 / 16));
  touch-action: none;
}

#game {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  image-rendering: pixelated;
  z-index: 1;
}

#tint {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--dead);
  opacity: 0.22;
  pointer-events: none;
  transition: opacity 120ms linear;
}

/* Stack: world(1) -> tint(2) -> fade(3) -> ui(4).
   The fade must cover the world but NOT the dialogue. */
#ui {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

#ui > * { pointer-events: auto; }

/* --- dialogue ------------------------------------------------------------ */

.dialogue {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 6%;
  min-height: 4.6em;
  padding: 14px 18px 16px;
  background: #12141ae8;
  border: 2px solid #3a4152;
  border-radius: 3px;
  cursor: pointer;
}

.dialogue-who {
  margin: 0 0 6px;
  font-size: clamp(9px, 1.1vw, 13px);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #7f8aa3;
}

.dialogue-text {
  margin: 0;
  font-size: clamp(12px, 1.5vw, 18px);
  line-height: 1.55;
  white-space: pre-wrap; /* line breaks WITHOUT innerHTML */
}

.dialogue.is-system .dialogue-text { color: #8f98ad; font-style: italic; }
.dialogue.is-system .dialogue-who { display: none; }

.dialogue-more {
  position: absolute;
  right: 14px;
  bottom: 8px;
  color: #7f8aa3;
  animation: nudge 1.1s ease-in-out infinite;
}

@keyframes nudge {
  0%, 100% { transform: translateY(0); opacity: .45; }
  50%      { transform: translateY(2px); opacity: 1; }
}

/* The caret spells itself out until the player has advanced ONCE. A bare ▾ is the
   genre convention and it taught nobody — testers sat in front of the first line
   not knowing it wanted them. */
.dialogue-more.teaching {
  font-size: clamp(8px, 0.95vw, 11px);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* ---------------------------------------------------------------------------
   "You can walk now."

   The world is nearly always waiting for the player to walk right — that is what
   fires every beat — and it never said so. Two testers stalled after solving a
   puzzle, with nothing on screen to tell them the game wanted them to move.

   Shown only when they CAN walk and aren't. Gone the moment they do.
   --------------------------------------------------------------------------- */
.nudge {
  position: absolute;
  left: 50%;
  bottom: 7%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  font-size: clamp(9px, 1.05vw, 12px);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #cfd6e4;
  background: #12141ae0;
  border: 2px solid #3a4152;
  pointer-events: none;   /* never eats the click that advances dialogue */
  opacity: 0;
  transition: opacity 420ms ease;
}

.nudge.is-in { opacity: 1; }

.nudge-arrow {
  color: #c8433f;                       /* the bench red — the one colour that never greys */
  animation: nudge-slide 1.15s ease-in-out infinite;
}

@keyframes nudge-slide {
  0%, 100% { transform: translateX(0); opacity: .5; }
  50%      { transform: translateX(4px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .dialogue-more, .nudge-arrow { animation: none; opacity: 1; }
}

/* Retro skip. Hard edges, no border-radius, chunky offset shadow — it should look
   stamped on rather than designed. Skips only the CURRENT run of lines. */
.dialogue-skip {
  position: absolute;
  top: 5%;
  right: 5%;
  padding: 7px 12px;
  font: inherit;
  font-size: clamp(9px, 1.05vw, 12px);
  font-weight: 700;
  letter-spacing: 0.18em;
  color: #cfd6e4;
  background: #12141a;
  border: 2px solid #6f7891;
  border-radius: 0;
  box-shadow: 3px 3px 0 #05060a;
  cursor: pointer;
  image-rendering: pixelated;
  transition: none; /* retro: things snap, they don't ease */
}

.dialogue-skip:hover,
.dialogue-skip:focus-visible {
  color: #fff;
  border-color: #c8433f;
  background: #1c1216;
  outline: none;
}

/* Pressing it should feel like a physical key going down. */
.dialogue-skip:active {
  transform: translate(3px, 3px);
  box-shadow: 0 0 0 #05060a;
}

/* --- panels --------------------------------------------------------------- */

.panel {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 14px;
  padding: 6%;
  text-align: center;
  background: #0b0d12f2;
}

.panel h1 {
  margin: 0;
  font-size: clamp(28px, 5vw, 60px);
  letter-spacing: 0.06em;
}

.panel-sub {
  margin: 0;
  font-size: clamp(11px, 1.3vw, 15px);
  color: #8f98ad;
}

.panel-ask {
  margin: 18px 0 0;
  font-size: clamp(12px, 1.4vw, 16px);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #7f8aa3;
}

.panel-row { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }

.panel-btn {
  padding: 11px 22px;
  font: inherit;
  font-size: clamp(12px, 1.4vw, 16px);
  color: var(--ink);
  background: #1c202b;
  border: 2px solid #3a4152;
  border-radius: 3px;
  cursor: pointer;
  transition: border-color 120ms, transform 120ms;
}

.panel-btn:hover,
.panel-btn:focus-visible {
  border-color: #c8433f; /* the bench red */
  transform: translateY(-1px);
  outline: none;
}

/* --- ending --------------------------------------------------------------- */

.panel-ending { background: #0b0d12fa; }

/* The message ARRIVES. It is the whole gift; it should not just blink into
   existence like a dialog box. */
.ending-message {
  margin: 0;
  max-width: 26ch;
  font-size: clamp(16px, 2.4vw, 30px);
  line-height: 1.6;
  white-space: pre-wrap; /* newlines WITHOUT innerHTML */
  overflow-wrap: anywhere; /* a 280-char word must not blow out the layout */
  animation: rise 1500ms cubic-bezier(.22,.61,.36,1) both;
}

.ending-from {
  margin: 0;
  color: #8f98ad;
  font-size: clamp(11px, 1.3vw, 15px);
  animation: rise 1200ms cubic-bezier(.22,.61,.36,1) 900ms both;
}

/* Held back a beat — putting a call-to-action under someone's love letter the
   INSTANT it lands would cheapen it. But only a beat: 3.2s felt right in theory
   and far too long in practice. It only has to arrive after the message has, not
   make you wait for it. */
.ending-offer {
  margin-top: 26px;
  animation: rise 700ms ease 1500ms both;
}

/* "again" sits beside "send this to someone" but must not compete with it. */
.panel-btn.is-quiet {
  color: #7f8aa3;
  background: none;
  border-color: #2c3240;
}

.panel-btn.is-quiet:hover {
  color: var(--ink);
  border-color: #5a637a;
}

/* --- maker: the optional half, hidden until asked for --------------------- */

/* --- the maker: you type INTO the ending ------------------------------------

   The card is the real ending's markup (.panel-ending / .ending-message /
   .ending-from) with a caret in it. Everything below exists to make a <textarea>
   and an <input> stop looking like form fields — because the moment they look
   like form fields, the illusion breaks and you are back to filling in a form
   instead of writing to someone.
   --------------------------------------------------------------------------- */

.panel.is-maker {
  position: static;          /* undo `.panel`'s `absolute; inset: 0` */
  inset: auto;
  width: min(420px, 82vw);
  min-height: 4.6em;         /* an empty card should still look like the ending */
  padding: 16px 18px;
  gap: 6px;
  border: 2px solid #2c3240;
  background: #0b0d12;
}

/* The message RISES over 1.5s at the ending and the signature follows 900ms
   later. Lovely once. Absurd on every keystroke. */
.panel.is-maker .ending-message,
.panel.is-maker .ending-from {
  animation: none;
}

/* A textarea that is not a textarea. Same font, same size, same 26ch measure and
   same wrapping as .ending-message — so the line breaks fall exactly where they
   will when she reads it. */
.maker-input {
  width: 100%;
  padding: 0;
  font: inherit;
  font-size: clamp(13px, 1.7vw, 20px);
  line-height: 1.55;
  text-align: center;
  color: inherit;
  background: none;
  border: 0;
  outline: none;
  resize: none;
  overflow: hidden;          /* height is driven from JS, so no scrollbar */
}

.maker-input::placeholder {
  color: #5c667e;
  font-style: italic;
}

/* The signature, sitting where it will sit. Invisible until it has something to
   say — no box, no border, just the words appearing under the message. */
.maker-sign {
  width: 100%;
  padding: 2px 0;
  font: inherit;
  font-size: clamp(11px, 1.3vw, 15px);
  text-align: center;
  color: #8f98ad;
  background: none;
  border: 0;
  outline: none;
}

.maker-sign::placeholder { color: #4d5670; }

/* Only on hover/focus does it admit to being editable. */
.maker-input:focus::placeholder,
.maker-sign:focus::placeholder { color: #6b7590; }

/* --- the names: two blanks in a sentence ------------------------------------

   No disclosure button, and therefore no button that can fail to disclose. An
   underlined blank in a sentence is self-evidently something you can type in.
   --------------------------------------------------------------------------- */

.maker-who {
  margin: 2px 0 0;
  font-size: clamp(9px, 1.1vw, 13px);
  color: #7f8aa3;
}

.maker-name {
  width: 6.5em;
  padding: 1px 2px;
  font: inherit;
  font-size: inherit;
  text-align: center;
  color: var(--ink);
  background: none;
  border: 0;
  border-bottom: 1px dashed #3a4152;
  outline: none;
  transition: border-color 140ms ease;
}

.maker-name:hover,
.maker-name:focus { border-bottom-color: #c8433f; }

.maker-name::placeholder { color: #7f8aa3; }

/* ⚠ Centred content in a scroll container OVERFLOWS IN BOTH DIRECTIONS, and the
   part that spills past the top edge is UNREACHABLE — you cannot scroll to it.
   That is why "copy"/"share" vanished once the card grew. `safe center` centres
   only while it fits, and falls back to start-aligned (i.e. scrollable) when it
   doesn't. The plain `center` above it is the fallback for anything that doesn't
   understand `safe`. */
.panel-maker .panel-ask { margin-top: 0; }

.panel-maker {
  overflow-y: auto;
  align-content: center;
  align-content: safe center;
}

.maker-extras {
  display: grid;
  gap: 10px;
  justify-items: center;
  animation: rise 400ms ease both;
}

@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* --- riddle --------------------------------------------------------------- */

.panel-riddle { gap: 12px; }

.riddle-who {
  margin: 0;
  font-size: clamp(9px, 1.1vw, 13px);
  letter-spacing: 0.18em;
  color: #a2704a;
}

.riddle-prompt {
  margin: 0;
  max-width: 34ch;
  font-size: clamp(13px, 1.8vw, 21px);
  line-height: 1.75;
  white-space: pre-wrap; /* the riddle's line breaks are the poem */
}

.riddle-form {
  display: flex;
  gap: 8px;
  margin-top: 10px;
  flex-wrap: wrap;
  justify-content: center;
}

.riddle-input {
  width: min(20ch, 60vw);
  padding: 10px 12px;
  font: inherit;
  font-size: clamp(12px, 1.4vw, 16px);
  text-align: center;
  color: var(--ink);
  background: #12141a;
  border: 2px solid #3a4152;
  border-radius: 3px;
}

.riddle-input:focus { border-color: #c8433f; outline: none; }
.riddle-input:disabled { opacity: 0.5; }

/* The lion's contempt, not a validation error. Never red, never an X. */
.riddle-reply {
  margin: 0;
  min-height: 1.4em;
  font-size: clamp(11px, 1.3vw, 15px);
  color: #a2704a;
  font-style: italic;
}

.riddle-companion {
  margin: 6px 0 0;
  max-width: 32ch;
  font-size: clamp(11px, 1.3vw, 15px);
  line-height: 1.6;
  color: #8fa6c4;
  white-space: pre-wrap;
}

.riddle-ask {
  margin-top: 4px;
  padding: 5px 12px;
  font: inherit;
  font-size: clamp(10px, 1.1vw, 13px);
  color: #7f8aa3;
  background: none;
  border: 1px solid #3a4152;
  border-radius: 3px;
  cursor: pointer;
}

.riddle-ask:hover:not(:disabled) { color: var(--ink); border-color: #5a637a; }
.riddle-ask:disabled { opacity: 0.35; cursor: default; }

/* Replays on every new line, so a hint you are already looking at still reads
   as NEW rather than silently changing under you. */
.is-new { animation: rise 420ms ease both; }

.panel-riddle.is-solved { animation: solved 700ms ease both; }

@keyframes solved {
  0%   { background: #0b0d12f2; }
  40%  { background: #16241cf2; }
  100% { background: #0b0d1200; }
}

/* --- morse ---------------------------------------------------------------- */

.panel-morse { gap: 12px; }

/* Scratched into wood, not printed. */
.morse-carving {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: 14px;
  max-width: 32ch;
  padding: 18px 22px;
  background: #3a2a22;
  border: 2px solid #241a15;
  border-radius: 2px;
  box-shadow: inset 0 2px 8px #0006;
}

.morse-word { display: flex; gap: 10px; }

.morse-mark {
  font-size: clamp(15px, 2vw, 24px);
  letter-spacing: 0.12em;
  color: #6b5546;              /* worn into the grain — barely legible */
  text-shadow: 0 1px 0 #241a15;
  transition: color 180ms ease, text-shadow 180ms ease;
}

/* She types a letter, and two scratches on a bench light up. This is the moment. */
.morse-mark.is-lit {
  color: #f0d9a8;
  text-shadow: 0 0 10px #e2a04e99;
}

.morse-gap {
  color: #4d3a30;
  font-size: clamp(13px, 1.6vw, 19px);
}

.morse-input { width: min(26ch, 70vw); }
.morse-input.is-wrong { border-color: #7a3a36; }

/* The chart she remembers. Without it, this puzzle is a wall. */
.morse-chart-label {
  margin: 6px 0 0;
  font-size: clamp(9px, 1.05vw, 12px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #6f7891;
}

.morse-chart {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(58px, 1fr));
  gap: 2px 10px;
  width: min(46ch, 84vw);
  max-height: 34vh;
  overflow-y: auto;
  padding: 12px 14px;
  background: #17191f;
  border: 1px solid #3a4152;
  border-radius: 3px;
  animation: rise 500ms ease both;
}

.morse-chart-row {
  display: flex;
  gap: 7px;
  align-items: baseline;
  font-size: 12px;
}

.morse-chart-row b { color: #cfd6e4; font-weight: 700; }
.morse-chart-row i { color: #7f8aa3; font-style: normal; letter-spacing: 0.08em; }

.panel-morse.is-solved .morse-carving {
  animation: carved 1600ms ease both;
}

@keyframes carved {
  0%   { box-shadow: inset 0 2px 8px #0006; }
  40%  { box-shadow: inset 0 2px 8px #0006, 0 0 40px 8px #e2a04e55; }
  100% { box-shadow: inset 0 2px 8px #0006; }
}

/* --- sliding photo -------------------------------------------------------- */

.panel-slider { gap: 14px; }

.slider-board {
  position: relative;
  border: 3px solid #c9a227;   /* the gilt frame, same as in the room */
  box-shadow: 0 0 0 2px #7a5f14, 0 10px 30px #0008;
  background: #241d1a;         /* the hole, seen through the missing piece */
}

.slider-piece {
  position: absolute;
  top: 0;
  left: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background-repeat: no-repeat;
  image-rendering: pixelated;  /* an integer upscale; keep the pixels square */
  cursor: pointer;
  /* Only the position animates. Sliding IS a change of transform. */
  transition: transform 130ms ease-out;
}

/* The missing piece. It exists — it is the two of them — but it is not shown
   until the picture is whole. */
.slider-piece.is-blank {
  visibility: hidden;
  pointer-events: none;
}

/* She points at the exact piece to move. The search makes that answer optimal. */
.slider-piece.is-hinted {
  box-shadow: inset 0 0 0 3px #f2ece4, inset 0 0 0 6px #c8433f;
  z-index: 2;
}

.panel-slider.is-solved .slider-board {
  animation: whole 1600ms ease both;
}

/* The picture comes back together. */
@keyframes whole {
  0%   { box-shadow: 0 0 0 2px #7a5f14, 0 10px 30px #0008; }
  35%  { box-shadow: 0 0 0 2px #c9a227, 0 0 40px 10px #c9a22766; }
  100% { box-shadow: 0 0 0 2px #7a5f14, 0 10px 30px #0008; }
}

/* --- maker ---------------------------------------------------------------- */

.panel-maker { gap: 8px; }

/* The link, and the little note under it. Everything else that used to live here
   — .maker-text, .maker-from, .maker-names and a SECOND .maker-name rule — was
   the old form-field maker. The second .maker-name outranked the new inline one
   (later rule, same specificity), which is why the names rendered as two big
   bordered boxes instead of blanks in a sentence. Deleted, not overridden. */

.maker-link {
  width: min(46ch, 82vw);
  padding: 8px 10px;
  font: inherit;
  font-size: clamp(10px, 1.1vw, 13px);
  color: #9fd0c4;
  background: #12141a;
  border: 2px solid #3a4152;
  border-radius: 3px;
}

.maker-link:focus { border-color: #c8433f; outline: none; }

.maker-count {
  margin: 0;
  font-size: 11px;
  color: #6f7891;
}

.maker-count.is-full { color: #c8433f; }

.maker-result {
  display: grid;
  gap: 8px;
  justify-items: center;
  animation: rise 500ms ease both;
}

.maker-note {
  margin: 0;
  font-size: 11px;
  color: #6f7891;
}

.panel-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.panel-btn:disabled:hover {
  border-color: #3a4152;
  transform: none;
}

/* --- loading -------------------------------------------------------------- */

.panel-loading { gap: 20px; background: #0b0d12; }

/* The one red thing, before there is any colour at all. */
.loading-dot {
  width: 10px;
  height: 10px;
  background: #c8433f;
  animation: pulse 1.6s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: .2; }
  50%      { opacity: 1; }
}

.panel-loading.is-done {
  opacity: 0;
  transition: opacity 400ms ease;
  pointer-events: none;
}

/* --- logo ----------------------------------------------------------------- */

.panel-logo { gap: 16px; cursor: pointer; }

/* Native 584x130. NEVER upscaled — the background remover anti-aliased the edges,
   so `pixelated` would show ragged alpha instead of clean blocks. Capped below
   native so the browser only ever scales it DOWN, which is smooth and clean. */
.logo {
  width: min(560px, 74vw);
  height: auto;
  animation: rise 1100ms cubic-bezier(.22,.61,.36,1) both;
}

.press-start {
  margin-top: 22px;
  padding: 0;
  font: inherit;
  font-size: clamp(10px, 1.2vw, 13px);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: #8f98ad;
  background: none;
  border: 0;
  cursor: pointer;
  animation: blink 1.7s steps(1) infinite 1200ms;
}

@keyframes blink {
  0%, 60%  { opacity: 1; }
  61%, 99% { opacity: 0.25; }
}

/* --- mute ----------------------------------------------------------------- */

/* Always on screen, always the same place. Someone who just opened a link should
   never feel trapped with sound they did not ask for. */
.mute {
  position: absolute;
  left: 5%;
  top: 5%;
  width: 30px;
  height: 30px;
  padding: 0;
  font: inherit;
  font-size: 14px;
  line-height: 1;
  color: #cfd6e4;
  background: #12141ac0;
  border: 2px solid #6f7891;
  border-radius: 0;              /* retro: hard edges, like the skip */
  box-shadow: 2px 2px 0 #05060a;
  cursor: pointer;
  transition: none;
}

.mute:hover { border-color: #c8433f; color: #fff; }

.mute:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 #05060a;
}

.mute.is-muted { color: #6f7891; border-color: #3a4152; }

/* --- cutscene fade -------------------------------------------------------- */

/* Sits ABOVE the world but BELOW #ui, so dialogue can play over black — which
   is what makes "they step inside" read as a cut rather than a stutter. */
#fade {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: #05060a;
  opacity: 0;
  pointer-events: none;
}
