/* FANOUT — presentation layer.
   Dark ground, thin bright lines, nothing drawn that the player has not earned.
   Sized for a phone first: every tap target is at least 48px tall. */

:root {
  --ground:   #04060b;
  --ground-2: #090d16;
  --panel:    #0d1320;
  --line:     #1b2740;
  --ink:      #e6edfb;
  --ink-dim:  #8ea0c4;
  --ink-far:  #4a5a7d;
  --glow:     #6fe8ff;
  --glow-dim: #2d6d80;
  --relic:    #ffcf6f;
  --warn:     #ffc45c;
  --win:      #7dffa8;
  --lose:     #ff6f8a;
  --boss:     #ff9d5c;

  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --tap: 48px;
  --pad: clamp(12px, 3.5vw, 22px);
  --radius: 10px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  height: 100%;
  overflow-x: hidden;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  overscroll-behavior: none;
}

body {
  /* Radial lift off pure black so the map's darkness still reads as a place. */
  background:
    radial-gradient(120% 70% at 50% -10%, #0d1524 0%, var(--ground) 60%),
    var(--ground);
}

#app {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.boot {
  padding: var(--pad);
  max-width: 34rem;
  margin: 10vh auto;
  color: var(--ink-dim);
  line-height: 1.5;
}
.boot__title { font-family: var(--mono); letter-spacing: .4em; color: var(--glow); }
.boot pre {
  background: var(--panel); border: 1px solid var(--line);
  padding: 12px; border-radius: var(--radius); color: var(--ink);
  font-family: var(--mono); overflow-x: auto;
}
code { font-family: var(--mono); }

/* ---------- generic chrome ---------- */

.screen {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  width: 100%;
  max-width: 640px;   /* phone-shaped on a desktop rather than stretched thin */
  margin-inline: auto;
  padding: max(var(--pad), env(safe-area-inset-top)) var(--pad)
           max(var(--pad), env(safe-area-inset-bottom));
  gap: var(--pad);
}
.screen--flush { padding: 0; gap: 0; max-width: none; }

.scroll { scrollbar-width: thin; scrollbar-color: var(--line) transparent; overflow-y: auto; -webkit-overflow-scrolling: touch; min-height: 0; flex: 1 1 auto; }

.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink-dim);
  letter-spacing: .08em;
}
.topbar__spacer { flex: 1 1 auto; }
.topbar { min-width: 0; }
.topbar .tag { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topbar .btn { flex: 0 0 auto; }

.tag {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-far);
  white-space: nowrap;
}

.insight {
  font-family: var(--mono);
  color: var(--glow);
  white-space: nowrap;
}
.insight::before { content: "◈ "; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

.btn {
  min-height: var(--tap);
  padding: 12px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 14px;
  letter-spacing: .1em;
  text-transform: uppercase;
  transition: border-color .12s, color .12s, background .12s;
}
.btn:hover:not(:disabled), .btn:focus-visible:not(:disabled) {
  border-color: var(--glow-dim);
  color: var(--glow);
  outline: none;
}
.btn:disabled { opacity: .35; cursor: default; }
.btn--ghost { background: transparent; }
.btn--primary { border-color: var(--glow-dim); color: var(--glow); }
.btn--wide { width: 100%; }
.btn--sm { min-height: 40px; padding: 8px 12px; font-size: 12px; }

.row { display: flex; gap: 10px; flex-wrap: wrap; }
.row--tight { gap: 6px; }

.hint { color: var(--ink-far); font-size: 13px; line-height: 1.5; }
.hint--mono { font-family: var(--mono); }

/* ---------- title / level select ---------- */

.title {
  text-align: center;
  flex: 0 0 auto;
  padding-top: clamp(8px, 4vh, 40px);
}
.title__word {
  margin: 0;
  font-family: var(--mono);
  font-size: clamp(28px, 10vw, 58px);
  letter-spacing: .2em;
  padding-left: .2em;
  color: var(--ink);
  text-shadow: 0 0 28px rgba(111, 232, 255, .35);
}
.title__sub {
  margin: 6px 0 0;
  color: var(--ink-far);
  font-size: 13px;
  letter-spacing: .18em;
  text-transform: uppercase;
  font-family: var(--mono);
}

.levels { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }

.level-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-height: 72px;
  padding: 14px 16px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,30,50,.5), rgba(9,13,22,.5));
  transition: border-color .12s, transform .08s;
}
.level-card:hover:not(:disabled), .level-card:focus-visible:not(:disabled) {
  border-color: var(--glow-dim);
  outline: none;
}
.level-card:active:not(:disabled) { transform: translateY(1px); }
.level-card:disabled { opacity: .4; cursor: default; }

.level-card__n {
  font-family: var(--mono);
  font-size: 26px;
  color: var(--glow);
  width: 1.6em;
  text-align: center;
}
.level-card--locked .level-card__n,
.level-card--locked .level-card__name { color: var(--ink-far); }
.level-card__label { min-width: 0; }
.level-card__name { display: block; font-family: var(--mono); letter-spacing: .12em; }
.level-card__meta {
  display: block;
  margin-top: 4px;
  overflow-wrap: anywhere;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-far);
  letter-spacing: .04em;
}
.level-card__marks { display: flex; gap: 6px; font-size: 15px; }
.mark--cleared { color: var(--win); }
.mark--relic { color: var(--relic); }
.mark--locked { color: var(--ink-far); }

/* ---------- run view ---------- */

.run { justify-content: flex-start; }

.run__stage {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 2px;
  min-height: 0;
}
.run__power {
  font-family: var(--mono);
  font-size: clamp(64px, 26vw, 150px);
  line-height: .95;
  font-weight: 500;
  color: var(--ink);
  text-shadow: 0 0 40px rgba(111, 232, 255, .45);
  font-variant-numeric: tabular-nums;
}
/* Below zero is a *state*, not a typo: the sign gets its own glyph box so a
   minus can never read as a stray dash, the number turns, and the stage behind
   it turns with it (r3-spec §6.3). */
.run__power { display: flex; align-items: baseline; justify-content: center; gap: .06em; }
.run__sign {
  font-size: .62em;
  line-height: 1;
  padding: 0 .16em;
  border-radius: 6px;
  background: rgba(255, 111, 138, .16);
  border: 1px solid rgba(255, 111, 138, .5);
  transform: translateY(-.14em);
}
.run__power--zero { color: var(--warn); text-shadow: 0 0 40px rgba(255, 196, 92, .35); }
.run__power--neg { color: var(--lose); text-shadow: 0 0 44px rgba(255, 111, 138, .45); }

.run__stage--neg .run__powerlabel { color: var(--lose); }
.run__stage--neg { position: relative; }
/* A cold red wash behind the whole stage, so the state is readable from arm's
   length and not only by reading the digits. */
.run__stage--neg::before {
  content: "";
  position: absolute; inset: -6% -20% -6% -20%;
  background: radial-gradient(60% 50% at 50% 42%, rgba(255, 111, 138, .13), rgba(255,111,138,0) 70%);
  pointer-events: none;
}
.run__stage--neg > * { position: relative; }

/* ---------- the runes inscribed on the power bar ----------
   The player's amendment: nothing is picked up at a door, the door's rune is
   inscribed on you as you pass through it. So this row is the run itself, written
   down — one glyph per step, coloured by what that step did to you at the moment you
   took it, so a row of red with one amber in it is a run that crossed zero and never
   came back. It is drawn by src/ui/runes.js from `state.pathEdgeIds`, which means it
   cannot disagree with the route core recorded. */

.runes {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: 2px 3px;
  margin-top: 8px;
  padding: 5px 9px;
  min-height: 30px;
  max-width: 100%;
  border-radius: 999px;
  border: 1px solid rgba(111, 232, 255, .12);
  background: linear-gradient(180deg, rgba(20, 32, 54, .5), rgba(9, 13, 22, .35));
}
/* Empty, but the same height: the doors must not jump down the screen when the
   first rune lands. */
.runes--empty {
  border-color: rgba(74, 90, 125, .22);
  background: none;
}
.runes__none {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-far);
  line-height: 18px;
}
.runes__slot {
  display: block;
  width: 11px;
  height: 18px;
  color: var(--glow);
}
.runes__slot .rune { display: block; width: 100%; height: 100%; }
.runes__slot--up { color: var(--glow); }
.runes__slot--down { color: var(--lose); }
.runes__slot--flat { color: var(--ink-far); }
/* The step that changed which side of zero you are on — the one worth finding again
   when you look back at the row. */
.runes__slot--flip { color: var(--warn); }
.runes__slot--fresh {
  /* Inscribed on arrival. 100 ms, inside the §10.7 budget, and it blocks nothing. */
  animation: rune-inscribe .1s ease-out backwards;
  filter: drop-shadow(0 0 6px currentColor);
}
@keyframes rune-inscribe {
  from { opacity: 0; transform: scale(1.9); }
  to   { opacity: 1; transform: none; }
}
/* Long runs wrap rather than shrink: a rune the size of a full stop is not a rune.
   The cap exists so a long trail cannot push the doors off a short screen — but a cap
   with `overflow: hidden` has to choose an end to lose, and flex wraps downward, so it
   used to hide the *last* rows: the newest runes, including the one still carrying its
   `--fresh` flash (QA §4). `align-content: flex-end` anchors the stack to the bottom
   of the row instead, so the overflow leaves at the top and what falls off is the
   oldest — history the player has already read. Three rows of room rather than the two
   a 24-rune trail exactly fills, because that had no headroom at all: a 25th rune, a
   narrower phone or a bigger default size clipped immediately. `max-height` reserves
   nothing, so the two-row trail sits at the same height it always did. */
@media (max-height: 620px) {
  .runes {
    max-height: 70px;
    overflow: hidden;
    align-content: flex-end;
  }
}

.run__belowzero {
  margin-top: 10px;
  font-family: var(--mono);
  font-size: clamp(10px, 3vw, 12px);
  letter-spacing: .16em;
  color: var(--lose);
  border: 1px solid rgba(255, 111, 138, .35);
  background: rgba(255, 111, 138, .08);
  border-radius: 999px;
  padding: 5px 12px;
}
.run__belowzero--zero {
  color: var(--warn);
  border-color: rgba(255, 196, 92, .35);
  background: rgba(255, 196, 92, .08);
}

.run__powerlabel {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .34em;
  text-indent: .34em;
  text-transform: uppercase;
  color: var(--ink-far);
}
.run__distance {
  margin-top: 14px;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .1em;
  color: var(--ink-dim);
}
.run__distance b { color: var(--boss); font-weight: 500; }
.run__target { color: var(--boss); }

/* ---------- the doors (r3-spec §10.3) ----------
   A choice is a door: a frame with light behind it, a leaf on a hinge, and the op
   standing in front of it as the relic you pick up. Tapping it opens the leaf,
   takes the relic and walks you through. Drawn entirely in borders, gradients and
   transforms — no images, no generated art. Everything that moves is a transform or
   an opacity, so a phone composites it instead of repainting.

   Colour still carries the arithmetic: the light beyond a door, the gem in front of
   it and its pool on the floor are all one per-door variable, so a door that takes
   you down glows red and one that carries you across zero glows amber. */

.doors {
  position: relative;
  flex: 0 0 auto;
  display: grid;
  gap: 10px;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  align-items: stretch;
}
/* The floor the doors stand on. Absolute, so it is not a grid item — without it
   the doorways read as three cards floating in a void rather than as a wall. */
.doors::before {
  content: "";
  position: absolute;
  left: -6%; right: -6%; bottom: -8px;
  height: 74%;
  background: radial-gradient(58% 100% at 50% 100%, rgba(126, 158, 214, .09), transparent 72%);
  pointer-events: none;
}

.door {
  /* --door-ink: the accent. --door-glow: the light through the doorway. */
  --door-ink: var(--glow);
  --door-glow: rgba(111, 232, 255, .55);
  --door-gem-hi: rgba(141, 240, 255, .85);
  --door-gem-lo: rgba(30, 96, 122, .5);

  /* How wide the doorway itself is allowed to get. The button stays full width —
     it is the tap target — but a doorway wider than about this reads as a lit
     alcove rather than as a door, which is exactly what happens with two doors on
     a narrow phone. */
  --doorway: min(100%, 124px);

  position: relative;
  min-height: clamp(148px, 29vh, 226px);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  transition: opacity .2s ease-out, transform .2s ease-out;
}
.door--down {
  --door-ink: var(--lose);
  --door-glow: rgba(255, 111, 138, .5);
  --door-gem-hi: rgba(255, 150, 172, .85);
  --door-gem-lo: rgba(122, 32, 52, .5);
}
.door--flat {
  --door-ink: var(--ink-dim);
  --door-glow: rgba(142, 160, 196, .34);
  --door-gem-hi: rgba(180, 196, 224, .7);
  --door-gem-lo: rgba(52, 66, 94, .5);
}
/* A door that carries you across zero — the one move whose meaning inverts. */
.door--flip {
  --door-ink: var(--warn);
  --door-glow: rgba(255, 196, 92, .55);
  --door-gem-hi: rgba(255, 215, 140, .88);
  --door-gem-lo: rgba(124, 84, 18, .5);
}

/* ---- the doorway ---- */

.door__frame {
  position: absolute;
  top: 0; bottom: 34px; left: 50%;
  width: var(--doorway);
  transform: translateX(-50%);
  border: 1px solid var(--line);
  border-bottom-color: rgba(0, 0, 0, .6);
  border-radius: 10px 10px 3px 3px;
  background: linear-gradient(180deg, #0a1120, #06090f);
  box-shadow: inset 0 0 22px rgba(0, 0, 0, .7);
  overflow: hidden;
  transition: border-color .18s ease-out;
}
/* The lintel across the top, which is what makes it read as a doorway in a wall
   rather than as a rounded rectangle. */
.door__frame::before {
  content: "";
  position: absolute;
  left: -1px; right: -1px; top: -1px;
  height: 9px;
  border-radius: 10px 10px 0 0;
  background: linear-gradient(180deg, #2a3a5c, #121b2c);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .55);
}
/* The threshold. A door has to stand on something or it floats. */
.door__frame::after {
  content: "";
  position: absolute;
  left: -1px; right: -1px; bottom: -1px;
  height: 5px;
  background: linear-gradient(180deg, rgba(200, 220, 255, .16), rgba(0, 0, 0, .7));
  box-shadow: 0 -4px 10px rgba(0, 0, 0, .5);
}

/* What is on the other side. Barely visible under a closed door, full on as it
   opens — a door you have not opened should still look like it leads somewhere. */
.door__beyond {
  position: absolute;
  inset: 8px 3px 0;
  border-radius: 4px 4px 0 0;
  background:
    radial-gradient(130% 95% at 50% 62%, var(--door-glow), transparent 76%),
    linear-gradient(180deg, rgba(255, 255, 255, .07), transparent 58%);
  opacity: .16;
  transition: opacity .22s ease-out;
}

.door__leaf {
  position: absolute;
  inset: 8px 3px 0;
  border-radius: 4px 4px 0 0;
  border: 1px solid rgba(255, 255, 255, .05);
  background:
    linear-gradient(97deg, #1b2641 0%, #151e35 46%, #0e1423 100%),
    var(--panel);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .4), 2px 0 12px rgba(0, 0, 0, .5);
  transform-origin: left center;
  transform: perspective(620px) rotateY(0deg);
  /* 130 ms, not the 260 this shipped with: §10.7 put a 250 ms ceiling on the whole
     walk, and the leaf alone was over it. */
  transition: transform .13s cubic-bezier(.32, .72, .18, 1), filter .13s ease-out;
  backface-visibility: hidden;
}
/* Two recessed panels, kept in the upper two thirds so the relic stands clear. */
.door__leaf::before,
.door__leaf::after {
  content: "";
  position: absolute;
  left: 20%; right: 20%;
  border: 1px solid rgba(255, 255, 255, .055);
  border-radius: 2px;
  background: linear-gradient(180deg, rgba(255, 255, 255, .03), rgba(0, 0, 0, .16));
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .5);
}
.door__leaf::before { top: 7%; height: 25%; }
.door__leaf::after { top: 37%; height: 25%; }

.door__handle {
  position: absolute;
  right: 9%; top: 72%;
  width: 7px; height: 7px;
  margin-top: -3.5px;
  border-radius: 50%;
  background: var(--relic);
  box-shadow: 0 0 9px rgba(255, 207, 111, .7);
}

/* ---- the rune the door is marked with ---- */

/* The op is engraved on the leaf. Nothing stands in front of the door to be picked
   up any more (player amendment): the labyrinth keeps its marks, and walking through
   inscribes the same glyph on the power bar. */
.door__rune {
  position: absolute;
  left: 50%;
  top: 44%;
  width: calc(var(--doorway) * .40);
  height: auto;
  transform: translate(-50%, -50%);
  color: var(--door-ink);
  filter: drop-shadow(0 0 7px var(--door-glow));
  opacity: .92;
  transition: opacity .12s ease-out, transform .12s ease-out;
}
/* The same rune, showing through the empty doorway of a door already standing open,
   so an open door still says which one it is. Hidden on a shut door, where the leaf
   carries it. */
.door__rune--beyond { opacity: 0; }
.door--open .door__rune--beyond { opacity: .5; }

.door__op {
  font-family: var(--mono);
  font-size: clamp(19px, 6.2vw, 27px);
  line-height: 1.05;
  color: var(--door-ink);
  text-shadow: 0 0 16px var(--door-glow);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---- the arithmetic under the doorway ---- */

.door__meta {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}
.door__result {
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
.door__seen {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-far);
  min-height: 12px;
}
.door__seen--flip { color: var(--warn); }

/* ---- reaching for it: the leaf cracks open before you commit ---- */

.door:focus-visible { outline: none; }
.door:hover .door__leaf,
.door:focus-visible .door__leaf { transform: perspective(620px) rotateY(-14deg); }
.door:hover .door__beyond,
.door:focus-visible .door__beyond { opacity: .55; }
.door:hover .door__frame,
.door:focus-visible .door__frame { border-color: var(--door-ink); }
.door:hover .door__rune,
.door:focus-visible .door__rune { opacity: 1; transform: translate(-50%, -50%) scale(1.06); }
.door:active .door__leaf { transform: perspective(620px) rotateY(-22deg); }

/* ---- a door already walked stands open (r3-spec §10.7) ----
   `choice.known` used to print the word `mapped` under the door. The open door says
   it instead: no swing to sit through, so the retry is quicker than the first
   attempt — which is the point, in a game whose loop is dying and trying again. */

.door--open .door__leaf {
  transform: perspective(620px) rotateY(-74deg);
  filter: brightness(.5);
}
.door--open .door__beyond { opacity: .62; }
/* Light is already coming out of this one, in its own colour. */
.door--open .door__frame { box-shadow: inset 0 0 22px rgba(0, 0, 0, .55), 0 0 12px var(--door-glow); }
/* The leaf's own rune goes with the leaf: edge-on it is a smear, and the rune
   showing through the opening is the one that still reads. */
.door--open .door__leaf .door__rune { opacity: 0; }
.door--open:hover .door__leaf,
.door--open:focus-visible .door__leaf { transform: perspective(620px) rotateY(-79deg); }
.door--open:hover .door__beyond,
.door--open:focus-visible .door__beyond { opacity: .85; }

/* ---- the light a door throws as it opens (r3-spec §11.3) ----

   *"get rid of the red/blue flash on the click, because that strobe is slightly
   disorienting."*

   It used to be the door's own state colour thrown at full strength and then taken
   away: red out of a `−` door, cyan out of an up door, with the screen dropping to
   black in between. Alternating signs made that red, black, cyan, black — a strobe,
   and worse on a phone held close. So the light a door throws is no longer tinted by
   what that door does: one warm, low-saturation light for every door, with intensity
   doing the work. The *sign* is still said by everything that is not a flash — the
   door's resting ink, its rune, its numbers, the red wash below zero.

   Every value the light uses is here, in one block, because the likely next note is
   "that one, but less" and it should cost a number rather than a rewrite:

     --open-light      the flood through the opening, and the doorway behind the leaf
     --open-halo       the same light landing on the door's own frame
     --open-beyond     how far the doorway itself lights up   (was 1, at full colour)
     --open-dim        how far the rest of the screen steps back  (was 0 — black)
     --open-dim-doors  …and the doors the player did not take  (was .2)

   The last two are the luminance swing: the screen no longer goes out between one
   room and the next, it dims. */
.run {
  --open-light: rgba(255, 234, 208, .34);
  --open-halo: rgba(255, 232, 202, .3);
  --open-beyond: .78;
  --open-dim: .62;
  --open-dim-doors: .4;
}

/* ---- going through ----
   The whole walk is 140 ms shut, 90 ms already open, and the arrival on the far side
   is another 100 ms that blocks nothing — inside the player's 250 ms ceiling with
   room to spare, because at 13 steps on level 1 and 25 on level 5 every 100 ms here
   is seconds off a run. */

.door--opening .door__leaf {
  /* Not a full 90°: a sliver stays visible, so the door reads as standing open
     rather than as having vanished. */
  transform: perspective(620px) rotateY(-79deg);
  filter: brightness(.45);
  transition: transform .13s cubic-bezier(.3, .8, .3, 1), filter .13s ease-out;
}
/* What is beyond the door lights up as it opens — in the one warm light, not in the
   colour of what this door does to you (§11.3). */
.door--opening .door__beyond {
  opacity: var(--open-beyond);
  background:
    radial-gradient(130% 95% at 50% 62%, var(--open-light), transparent 76%),
    linear-gradient(180deg, rgba(255, 255, 255, .07), transparent 58%);
  transition: opacity .1s ease-out;
}
/* Light pours out of an open door — onto its own frame, and past it. The frame keeps
   its resting ink on the border, which is the door still saying what it is; what came
   off is the 30px coloured halo that made the tap flash. */
.door--opening .door__frame {
  border-color: var(--door-ink);
  box-shadow: inset 0 0 22px rgba(0, 0, 0, .35), 0 0 18px var(--open-halo);
}
/* The rune does not fade off the door any more: it leaves. `.runeflight` starts
   exactly on top of this glyph, at this size and in this colour, so hiding this one
   at take-off is what makes the two read as one object instead of a cross-fade
   (§11.2.3). */
.door--opening .door__rune { opacity: 0; transition: opacity .05s linear; }

/* Everything that is not the door you chose steps back. It used to go out: `opacity:
   0` on the stage and the topbar, which is the black half of the strobe (§11.3). */
.run--going .door:not(.door--opening) {
  opacity: var(--open-dim-doors);
  transform: translateY(4px) scale(.98);
}
.run--going .run__stage,
.run--going .topbar,
.run--going .relic,
.run--going .reveal {
  opacity: var(--open-dim);
  transform: scale(1.01);
  transition: opacity .12s ease-in, transform .12s ease-in;
}

/* The light through the opening, grown out of the doorway itself. Its size and
   position come from that door's rect in run.js, so it starts exactly on the door
   the player tapped. Its colour used to come from there too — the door's own
   `--door-glow` — and that is the flash §11.3 removed: every door now throws the
   same warm light and run.js reads nothing off the door. */
.walkthrough {
  position: fixed;
  z-index: 40;
  border-radius: 6px;
  pointer-events: none;
  opacity: 0;
  transform: scale(1);
  transform-origin: 50% 50%;
  background: radial-gradient(58% 58% at 50% 50%, var(--open-light), transparent 74%);
  /* Shorter than the walk itself, so the light has reached its full spread by the
     time the next screen replaces it rather than being cut off mid-grow. */
  transition: transform .12s cubic-bezier(.4, 0, .7, .5), opacity .12s ease-in;
  will-change: transform, opacity;
}

/* ---- arriving somewhere ---- */

/* `backwards`, not `both`: the fill has to be released when the animation ends, or
   it would go on overriding the recede transform on the next tap.

   Both arrivals start from the value the screen *left off at* rather than from zero
   (§11.3). Coming back from `opacity: 0` is the second half of the strobe: the room
   you walk into cannot start blacked out, or every step is flare, blank, re-enter. */
.run--arrive .door { animation: door-arrive .1s cubic-bezier(.2, .85, .3, 1) backwards; }
@keyframes door-arrive {
  from { opacity: var(--open-dim-doors); transform: translateY(8px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
.run--arrive .run__stage { animation: stage-arrive .1s ease-out backwards; }
@keyframes stage-arrive {
  from { opacity: var(--open-dim); transform: scale(1.025); }
  to   { opacity: 1; transform: none; }
}

/* ---- the rune's flight from the door to the bar (r3-spec §11.2) ----

   *"I would actually accept a slightly slower transition if we could somehow make the
   rune move from off the door into its location in the rune bar."*

   One glyph crossing the real distance between the two boxes, both measured at
   take-off in run.js. It lives on `<body>`, not inside the screen, because the screen
   it left is replaced 140 ms into a 300 ms flight. Transform, colour and nothing else
   — no layout while it is in the air. z-index 45 puts it over the walk-through light
   (40) and under a dialog (60), so a rune passes behind an open dialog. */
.runeflight {
  position: fixed;
  z-index: 45;
  pointer-events: none;
  transform-origin: 50% 50%;
  will-change: transform;
  filter: drop-shadow(0 0 6px currentColor);
  transition:
    transform var(--flight-ms, 300ms) cubic-bezier(.36, .06, .2, 1),
    color var(--flight-ms, 300ms) ease-out,
    opacity var(--strand-ms, 120ms) ease-out;
}
.runeflight .rune { display: block; width: 100%; height: 100%; }

/* The bar went out from under it: the step ended on a screen that has no rune row,
   which is the boss. The glyph keeps the travel it is in the middle of — the transform
   transition above is untouched — and dissolves instead of popping out of existence
   over the reveal (r4c; `STRAND_MS` in run.js must match). */
.runeflight--stranded { opacity: 0; }

/* The landing slot, reserved before take-off so the bar cannot move under a rune in
   the air (§11.2.4): it is in the row, holding its width, unpainted. */
.runes__slot--reserved { visibility: hidden; }
/* The rune has arrived in the *state* — the screen re-rendered 140 ms in — but not
   yet on the bar. It is blank until the glyph lands, which is when `landFlight` turns
   it into `--fresh` and the inscribe flash plays. */
.runes__slot--incoming { opacity: 0; }

/* ---------- the sacrifice relic, as a decision ---------- */

.relic {
  flex: 0 0 auto;
  border: 1px solid rgba(255, 207, 111, .45);
  border-radius: var(--radius);
  background:
    radial-gradient(90% 120% at 50% 0%, rgba(255, 207, 111, .14), rgba(255, 207, 111, .04) 70%),
    var(--panel);
  padding: 12px 12px 12px;
  font-family: var(--mono);
  animation: relic-in 320ms ease-out both;
}
.relic__head { display: flex; align-items: flex-start; gap: 12px; }
.relic__mark {
  font-size: 22px; color: var(--relic); line-height: 1.1;
  text-shadow: 0 0 18px rgba(255, 207, 111, .7);
}
.relic__head b { color: var(--relic); font-size: 13px; letter-spacing: .14em; font-weight: 500; }
.relic__head small {
  display: block; margin-top: 3px;
  color: var(--ink-dim); font-size: 11.5px; letter-spacing: 0; line-height: 1.45;
}
.relic__arms { display: grid; gap: 8px; margin-top: 12px; grid-template-columns: 1fr 1fr; }
@media (max-width: 420px) { .relic__arms { grid-template-columns: 1fr; } }
.relic__take, .relic__leave {
  min-height: var(--tap);
  padding: 10px 12px;
  text-align: left;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: rgba(9, 13, 22, .6);
  font-family: var(--mono);
}
.relic__take { border-color: rgba(255, 207, 111, .6); background: rgba(255, 207, 111, .1); }
.relic__take b { color: var(--relic); }
.relic__leave b { color: var(--ink); }
.relic__take b, .relic__leave b { display: block; font-size: 12.5px; letter-spacing: .08em; font-weight: 500; }
.relic__take small, .relic__leave small {
  display: block; margin-top: 3px; color: var(--ink-dim); font-size: 11px; line-height: 1.4;
}
.relic__take:hover, .relic__take:focus-visible { border-color: var(--relic); outline: none; }
.relic__leave:hover, .relic__leave:focus-visible { border-color: var(--glow-dim); outline: none; }

/* The heat band (r3-spec §9.3): a price tag on the run being given up, sitting
   on the arm that keeps it. It describes the path already walked, never the
   doors ahead, and it exists on this screen only. */
.relic__heat {
  display: flex; align-items: baseline; gap: 6px;
  margin-top: 8px; padding-top: 7px;
  border-top: 1px solid var(--line);
  font-size: 10.5px; line-height: 1.4; letter-spacing: .04em;
  color: var(--ink-dim);
}
.relic__heat i { font-style: normal; font-size: 9px; }
.relic__heat--hot { color: var(--win); }
.relic__heat--cold { color: #7fb0d8; }
.relic__leave--hot { border-color: rgba(125, 255, 168, .45); background: rgba(125, 255, 168, .05); }
.relic__leave--cold { border-color: rgba(127, 176, 216, .35); }

@keyframes relic-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* reveal ladder */

.reveal { flex: 0 0 auto; border-top: 1px solid var(--line); padding-top: 10px; }
.reveal__head {
  display: flex; align-items: center; gap: 10px;
  width: 100%; min-height: 40px;
  font-family: var(--mono); font-size: 12px;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-dim);
}
.reveal__head .topbar__spacer { flex: 1 1 auto; }
.topbar { min-width: 0; }
.topbar .tag { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topbar .btn { flex: 0 0 auto; }
.reveal__body { display: grid; gap: 8px; padding-top: 8px; }
.reveal__ladder { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
.reveal__buy {
  min-height: var(--tap);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel);
  font-family: var(--mono);
}
.reveal__buy:hover:not(:disabled), .reveal__buy:focus-visible:not(:disabled) {
  border-color: var(--glow-dim); outline: none;
}
.reveal__buy:disabled { opacity: .3; cursor: default; }
/* a tier that would reach past the boss: closed, not merely unaffordable */
.reveal__buy--past { border-style: dashed; }
.reveal__buy--past b { color: var(--ink-far); }
.reveal__buy b { font-size: 16px; font-weight: 500; color: var(--ink); }
.reveal__buy span { font-size: 11px; color: var(--glow); }

/* resume-a-run call to action on the title screen */

.resume {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: var(--tap);
  padding: 12px 16px;
  text-align: left;
  border: 1px solid var(--glow-dim);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,60,76,.45), rgba(9,13,22,.5));
  font-family: var(--mono);
  color: var(--glow);
}
.resume:hover, .resume:focus-visible { border-color: var(--glow); outline: none; }
.resume__mark { font-size: 18px; }
.resume__text b { font-weight: 500; letter-spacing: .1em; font-size: 13px; }
.resume__text small { display: block; margin-top: 2px; color: var(--ink-dim); font-size: 11.5px; }

/* ---------- map ---------- */

.map { position: relative; flex: 1 1 auto; min-height: 0; }
#map-canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; }
#map-canvas:active { cursor: grabbing; }

.map__overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  padding: max(var(--pad), env(safe-area-inset-top)) var(--pad)
           max(var(--pad), env(safe-area-inset-bottom));
  gap: 10px;
}
.map__overlay > * { pointer-events: auto; }
.map__bar {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--mono); font-size: 12px; color: var(--ink-dim);
  flex-wrap: wrap;
}
/* The canvas is framed to keep content clear of this bar; the scrim is the
   second line of defence at awkward zoom levels. */
.map__overlay::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0; height: 96px;
  background: linear-gradient(180deg, rgba(4,6,11,.92) 0%, rgba(4,6,11,.75) 55%, rgba(4,6,11,0) 100%);
  pointer-events: none;
}
.map__overlay > * { position: relative; }
.map__bar .btn { background: rgba(9,13,22,.82); backdrop-filter: blur(6px); }
.map__foot { margin-top: auto; display: flex; align-items: flex-end; gap: 10px; }
.map__legend {
  font-family: var(--mono); font-size: 11px; line-height: 1.7;
  color: var(--ink-far);
  background: rgba(4,6,11,.72);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 8px 10px;
  backdrop-filter: blur(6px);
}
.map__legend i { font-style: normal; }

/* "You are here", as a DOM element rather than a canvas frame: one dot breathing
   must not cost a full map redraw sixty times a second (r3-spec §6 perf floor).
   The canvas draws the dot itself; this is only the halo around it. */
.map__here {
  position: absolute; left: 0; top: 0;
  width: 46px; height: 46px; margin: -23px 0 0 -23px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.45);
  pointer-events: none;
  will-change: transform;
  animation: here-pulse 2.4s ease-out infinite;
}
@keyframes here-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(255,255,255,.28), inset 0 0 12px rgba(255,255,255,.12); opacity: .95; }
  70%  { box-shadow: 0 0 0 16px rgba(255,255,255,0), inset 0 0 12px rgba(255,255,255,0); opacity: .35; }
  100% { box-shadow: 0 0 0 0 rgba(255,255,255,0), inset 0 0 12px rgba(255,255,255,0); opacity: .95; }
}

/* The depth rail — the whole level as one strip, so 26 layers can be navigated. */
.rail {
  position: absolute;
  right: calc(var(--pad) - 2px);
  top: 86px; bottom: 120px;
  width: 16px;
  pointer-events: none;
}
.rail::before {
  content: ""; position: absolute; left: 7px; top: 0; bottom: 0; width: 2px;
  background: linear-gradient(180deg, rgba(111,232,255,.06), rgba(111,232,255,.18));
  border-radius: 2px;
}
.rail__tick {
  position: absolute; left: 4px; width: 8px; height: 1px;
  background: var(--glow); opacity: .3;
}
.rail__mark { position: absolute; left: 1px; width: 14px; height: 2px; margin-top: -1px; border-radius: 2px; }
.rail__mark--you { background: #fff; box-shadow: 0 0 10px rgba(255,255,255,.9); height: 3px; }
.rail__mark--relic { background: var(--relic); box-shadow: 0 0 10px rgba(255,207,111,.8); }
.rail__mark--boss { background: var(--boss); box-shadow: 0 0 10px rgba(255,157,92,.8); }
.rail__window {
  position: absolute; left: 2px; right: 2px;
  border: 1px solid rgba(111,232,255,.55);
  border-radius: 3px;
  background: rgba(111,232,255,.07);
}

/* ---------- building (covers the first, synchronous level build) ---------- */

.building {
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Held back for a beat: a 9ms build must not flash this on screen at all.
     The delay is mirrored by BUILD_HINT_AFTER in app.js. */
  animation: building-in 200ms ease-out 110ms both;
}
.building__box { display: grid; gap: 14px; justify-items: center; }

.building__word {
  font-family: var(--mono);
  font-size: clamp(13px, 4vw, 16px);
  letter-spacing: .26em;
  padding-left: .26em;
  text-transform: uppercase;
  color: var(--ink);
}
.building__sub {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-far);
}

/* Five bars fanning out and back — the shape of the level being built.
   Transform and opacity only, so the compositor keeps it alive while the
   generator holds the main thread. */
.building__bars { display: flex; align-items: flex-end; gap: 7px; height: 46px; }
.building__bars i {
  display: block;
  width: 3px;
  height: 46px;
  border-radius: 2px;
  background: var(--glow);
  transform-origin: 50% 100%;
  animation: building-bar 1.15s ease-in-out infinite;
}
.building__bars i:nth-child(1) { animation-delay: 0ms; }
.building__bars i:nth-child(2) { animation-delay: 110ms; }
.building__bars i:nth-child(3) { animation-delay: 220ms; }
.building__bars i:nth-child(4) { animation-delay: 330ms; }
.building__bars i:nth-child(5) { animation-delay: 440ms; }

@keyframes building-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes building-bar {
  0%, 100% { transform: scaleY(.18); opacity: .35; }
  50%      { transform: scaleY(1);   opacity: 1; }
}

/* ---------- boss reveal ----------
   Four beats, sequenced with animation-delay so nothing re-renders and
   prefers-reduced-motion (which kills every animation below) simply shows the
   finished screen: the demand, your number, the verdict, the earned line. */

.boss { text-align: center; gap: 0; }
.boss__scroll { display: flex; flex-direction: column; }
/* Centres the stack vertically without ever clipping its top when it overflows. */
.boss__scroll > :first-child { margin-top: auto; }
.boss__scroll > :last-child { margin-bottom: auto; }

.boss__where { animation: boss-rise .35s ease-out both; }

.boss__demand { margin: clamp(10px, 3vh, 26px) 0 0; }
.boss__lead {
  margin: 0;
  font-family: var(--mono); font-size: 11px; letter-spacing: .3em; text-indent: .3em;
  text-transform: uppercase; color: var(--ink-far);
  animation: boss-rise .4s ease-out 120ms both;
}
.boss__target {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: clamp(56px, 20vw, 110px);
  line-height: 1.05;
  color: var(--boss);
  text-shadow: 0 0 46px rgba(255,157,92,.45);
  animation: boss-slam .5s cubic-bezier(.16,.9,.3,1) 220ms both;
}

.boss__mine {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  margin-top: clamp(10px, 3vh, 22px);
  animation: boss-rise .4s ease-out 620ms both;
}
.boss__num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.boss__num b {
  display: block;
  font-size: clamp(34px, 11vw, 60px);
  font-weight: 500;
  line-height: 1;
}
.boss__num--you b { color: var(--glow); text-shadow: 0 0 30px rgba(111,232,255,.4); }
.boss__num span {
  display: block; margin-top: 6px;
  font-size: 11px; letter-spacing: .2em; text-transform: uppercase; color: var(--ink-far);
}
.boss__vs { font-family: var(--mono); color: var(--ink-far); font-size: 26px; }

.boss__verdict {
  font-family: var(--mono);
  font-size: clamp(24px, 8.6vw, 48px);
  letter-spacing: .2em;
  padding-left: .2em;
  margin: clamp(14px, 4vh, 28px) 0 6px;
  animation: boss-verdict .45s ease-out 900ms both;
}
.boss__verdict--win { color: var(--win); text-shadow: 0 0 34px rgba(125,255,168,.4); }
.boss__verdict--lose { color: var(--lose); text-shadow: 0 0 34px rgba(255,111,138,.35); }

.boss__earned {
  font-family: var(--mono); font-size: 13px; color: var(--ink-dim); min-height: 1.4em;
  animation: boss-rise .4s ease-out 1350ms both;
}
.boss__earned b { color: var(--glow); font-weight: 500; }
.boss__after {
  max-width: 28rem; margin: 10px auto 0;
  animation: boss-rise .4s ease-out 1350ms both;
}

/* "You have walked this exact route before" (r3-spec §9.4) — the one line that
   turns "this seed is impossible" into "I have to try something else". */
.boss__repeat {
  max-width: 30rem;
  margin: clamp(14px, 3vh, 20px) auto 0;
  padding: 10px 14px;
  text-align: left;
  border: 1px solid var(--line);
  border-left: 2px solid var(--boss);
  border-radius: var(--radius);
  background: rgba(255, 157, 92, .06);
  font-size: 12.5px; line-height: 1.5; color: var(--ink-dim);
  animation: boss-rise .4s ease-out 1250ms both;
}
.boss__repeat b { display: block; color: var(--boss); font-weight: 500; margin-bottom: 3px; }
.boss__actions {
  margin-top: clamp(20px, 6vh, 44px); justify-content: center;
  animation: boss-rise .4s ease-out 1400ms both;
}

@keyframes boss-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes boss-slam {
  from { opacity: 0; transform: scale(1.7); }
  to   { opacity: 1; transform: none; }
}
@keyframes boss-verdict {
  0%   { opacity: 0; letter-spacing: .5em; }
  100% { opacity: 1; letter-spacing: .2em; }
}

/* ---------- the run ended on purpose ---------- */

.sacrifice { text-align: center; gap: 0; }
.sacrifice__mark {
  font-size: clamp(40px, 13vw, 64px); color: var(--relic); line-height: 1;
  text-shadow: 0 0 40px rgba(255,207,111,.6);
  animation: boss-slam .5s cubic-bezier(.16,.9,.3,1) 80ms both;
}
.sacrifice__verdict {
  font-family: var(--mono);
  font-size: clamp(22px, 7.6vw, 40px); letter-spacing: .22em; padding-left: .22em;
  margin: 10px 0 4px; color: var(--relic);
  animation: boss-verdict .45s ease-out 380ms both;
}
.sacrifice__lead {
  margin: 0; font-family: var(--mono); font-size: 12.5px; color: var(--ink-dim);
  animation: boss-rise .4s ease-out 560ms both;
}
.sacrifice__step {
  display: flex; align-items: center; justify-content: center; gap: 16px;
  margin: clamp(14px, 4vh, 30px) 0 6px;
  animation: boss-rise .45s ease-out 700ms both;
}
.sacrifice__num {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: clamp(34px, 12vw, 64px); color: var(--ink-far); line-height: 1;
}
.sacrifice__num--to { color: var(--relic); text-shadow: 0 0 34px rgba(255,207,111,.45); }
.sacrifice__arrow { font-family: var(--mono); color: var(--ink-far); font-size: 20px; }
.sacrifice__caption {
  margin: 0; font-family: var(--mono); font-size: 11px; letter-spacing: .06em; color: var(--ink-dim);
  animation: boss-rise .4s ease-out 820ms both;
}

/* ---------- the rank shop ---------- */

.ranks {
  margin-top: 14px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--glow-dim);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,30,50,.45), rgba(9,13,22,.45));
  padding: 12px 14px 14px;
}
.ranks--bought { animation: rank-flash 900ms ease-out both; }
.ranks__head { display: flex; align-items: center; gap: 10px; }
.ranks__title {
  font-family: var(--mono); font-size: 11px; letter-spacing: .2em; color: var(--ink-dim);
}
.ranks__pips { display: flex; align-items: center; gap: 4px; margin-top: 10px; }
.ranks__pip { font-style: normal; color: var(--ink-far); opacity: .5; }
.ranks__pip--on { color: var(--glow); opacity: 1; text-shadow: 0 0 12px rgba(111,232,255,.6); }
.ranks__count {
  margin-left: 8px; font-family: var(--mono); font-size: 10.5px;
  letter-spacing: .1em; color: var(--ink-far); text-transform: uppercase;
}
.ranks__base { margin: 8px 0 0; font-family: var(--mono); font-size: 12px; color: var(--ink-dim); }
.ranks__base b { color: var(--ink); font-weight: 500; }
.ranks__buy {
  display: flex; align-items: center; gap: 12px;
  width: 100%; min-height: var(--tap); margin-top: 10px;
  padding: 10px 14px;
  border: 1px solid var(--glow-dim); border-radius: var(--radius);
  background: rgba(20, 60, 76, .35);
  font-family: var(--mono); text-align: left;
}
.ranks__buy b { flex: 1 1 auto; color: var(--glow); font-size: 12.5px; letter-spacing: .06em; font-weight: 500; }
.ranks__price { color: var(--glow); font-size: 15px; white-space: nowrap; }
.ranks__buy:hover:not(:disabled), .ranks__buy:focus-visible:not(:disabled) {
  border-color: var(--glow); outline: none;
}
.ranks__buy:disabled { opacity: .45; cursor: default; border-color: var(--line); }
.ranks__buy:disabled b, .ranks__buy:disabled .ranks__price { color: var(--ink-far); }
.ranks__note { margin: 9px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--ink-far); }

@keyframes rank-flash {
  0%   { border-left-color: var(--glow); box-shadow: 0 0 0 1px rgba(111,232,255,.5), 0 0 34px rgba(111,232,255,.35); }
  100% { border-left-color: var(--glow-dim); box-shadow: none; }
}

/* ---------- records ---------- */

.recs { display: grid; gap: 10px; }

.rec {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,30,50,.4), rgba(9,13,22,.4));
  padding: 12px 14px;
}
.rec__head {
  display: flex; align-items: baseline; gap: 8px;
  font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
}
.rec__name { color: var(--ink); }
.rec__head .mark--cleared, .rec__head .mark--relic { font-size: 11px; white-space: nowrap; }

.rec__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 8px 10px;
  margin-top: 10px;
}
.stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stat__v {
  font-family: var(--mono); font-size: 19px; line-height: 1.1;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.stat__k {
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-far);
  overflow-wrap: anywhere;
}
.dim { color: var(--ink-far); }

.danger { color: var(--lose); border-color: rgba(255,111,138,.4); }

.notice {
  border: 1px solid var(--line);
  border-left: 2px solid var(--boss);
  border-radius: var(--radius);
  padding: 12px 14px;
  background: var(--panel);
  color: var(--ink-dim);
  font-size: 13px;
  line-height: 1.55;
}
.notice b { color: var(--ink); font-weight: 500; }
.notice pre { margin: 8px 0 0; white-space: pre-wrap; font-family: var(--mono);
              font-size: 12px; color: var(--ink-far); }

/* ---------- the confirm dialog (r3-spec §11.1) ----------
   Replaces window.confirm at all four sites. It is a sibling of #app, not a child,
   because render() clears the app root — see the head of src/ui/modal.js.

   z-index sits above `.walkthrough` (40), which is the only other thing that covers
   the viewport, so a dialog raised at the tail of a walk is never behind the flood. */

.modal {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: var(--pad);
  /* The keyboard never gets under the card on a short phone. */
  overflow-y: auto;
}

.modal__scrim {
  position: fixed; inset: 0;
  background: rgba(2, 4, 8, .72);
  backdrop-filter: blur(3px);
  animation: modal-scrim .12s ease-out both;
}

.modal__card {
  position: relative;
  width: min(30rem, 100%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    linear-gradient(180deg, rgba(22, 32, 54, .96), rgba(9, 13, 22, .98)),
    var(--panel);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .6);
  padding: 18px 18px 16px;
  animation: modal-card .14s cubic-bezier(.16, .9, .3, 1) both;
}

.modal__title {
  margin: 0;
  font-family: var(--mono);
  font-size: 15px; font-weight: 500; line-height: 1.35;
  letter-spacing: .02em;
  color: var(--ink);
}

.modal__body {
  margin: 10px 0 0;
  font-size: 13px; line-height: 1.55;
  color: var(--ink-dim);
}

/* The arms are at the foot of a centred card, away from the control that raised the
   dialog. Cancel is always first in the DOM, so it is first in tab order and is what
   focus and a blind Enter find (§11.1.2).

   Stacked is the phone case and it is `column-reverse` on purpose: the label "Take
   it · end the run" will not sit beside another button at 390px, and when they stack
   the bottom one is the one the thumb reaches without moving. That has to be Cancel.
   Above 30rem they sit in a row, cancel left, the way a desktop dialog reads. */
.modal__arms {
  display: flex; flex-direction: column-reverse; gap: 10px;
  margin-top: 16px;
}
.modal__arm { flex: 1 1 auto; min-width: 8.5rem; }

@media (min-width: 30rem) {
  .modal__arms { flex-direction: row; }
}
.modal__confirm--primary { border-color: var(--glow-dim); color: var(--glow); }
.modal__confirm--primary:hover:not(:disabled),
.modal__confirm--primary:focus-visible:not(:disabled) { border-color: var(--glow); }
.modal__confirm--danger { border-color: rgba(255, 111, 138, .4); color: var(--lose); }
.modal__confirm--danger:hover:not(:disabled),
.modal__confirm--danger:focus-visible:not(:disabled) { border-color: var(--lose); }

@keyframes modal-scrim { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-card {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* ---------- accounts (r5-spec §11) ----------

   Everything in this section is drawn only when `GET /api/config` reported sign-in.
   None of it restyles anything that existed before: there is not a single selector
   below that can match a node the title screen already had, which is the CSS half of
   "a guest sees exactly today's game".

   It is deliberately the quietest panel in the build — no glow, no left rule in the
   accent colour, nothing that competes with a level card. Signing in is not the point
   of the screen and must not look like it is. */

.account {
  margin: 0 0 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,30,50,.3), rgba(9,13,22,.3));
  padding: 12px 14px;
}
.account__note { margin: 10px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--ink-far); }
.account__gis:empty { min-height: 44px; }   /* holds the row while Google's frame loads */

/* The one line §11 allows about a sign-in: what happened to their progress. */
.account__line {
  margin: 10px 0 0;
  border-left: 2px solid var(--glow-dim);
  padding-left: 10px;
  font-size: 12px; line-height: 1.5; color: var(--ink-dim);
}

/* r5d — the same line when it is carrying bad news. A warmer rule and slightly brighter
   text, and nothing else: it is still a line on a panel, not a dialog and not an error
   page, because a sign-in that did not work leaves a playable game behind it. */
.account__line--problem {
  border-left-color: var(--warn);
  color: var(--ink);
}

.account--in { padding: 10px 14px; }
.account__row { display: flex; align-items: center; gap: 10px; }
.account__who {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  font-family: var(--mono); font-size: 12.5px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.account__mark { font-style: normal; color: var(--relic); }

/* synced / syncing / offline. A word and a dot, and it never moves anything: the
   widths are close enough that a state change does not reflow the row. */
.account__status {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-far); white-space: nowrap;
}
.account__dot { font-style: normal; font-size: 8px; line-height: 1; }
.account__status--synced .account__dot { color: var(--win); }
.account__status--syncing .account__dot { color: var(--glow); animation: sync-pulse 1.1s ease-in-out infinite; }
.account__status--offline .account__dot { color: var(--warn); }

@keyframes sync-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

/* ---------- the account screen ---------- */

.acct {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,30,50,.4), rgba(9,13,22,.4));
  padding: 14px;
}
.acct__head { display: flex; align-items: center; gap: 10px; }
.acct__who {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 14px; color: var(--ink);
}
.acct__note { margin: 12px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--ink-dim); }
.acct__note b { color: var(--ink); font-weight: 500; }
.acct__note--warn { color: var(--warn); }
.acct__note--warn b { color: var(--warn); }
.acct__note--small { font-size: 11.5px; color: var(--ink-far); }

.acct__facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 10px; margin: 16px 0 0;
}
.acct__fact { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.acct__k {
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-far);
}
.acct__v { margin: 0; font-family: var(--mono); font-size: 13px; color: var(--ink); }

/* ---------- the Insight store (r5-spec §9) ----------

   Same rule as the account section above: nothing here can match a node that existed
   before this run, and none of it is drawn at all unless `/api/config` said the server
   sells Insight.

   It is deliberately the least shouty screen in the game. No gradient on the buy button,
   no accent on the largest bundle beyond one hairline, no badge that says anything about
   the player ("most popular", "recommended"). The only comparative claim on the screen is
   the per-Insight rate, which is arithmetic. */

.store__head {
  margin: 0 0 14px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--glow-dim);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,30,50,.3), rgba(9,13,22,.3));
  padding: 12px 14px;
}
.store__head--guest { border-left-color: var(--warn); }
.store__balance { margin: 0; font-family: var(--mono); font-size: 13px; color: var(--ink-dim); }
.store__balance b { color: var(--ink); font-size: 15px; font-weight: 500; }
.store__note { margin: 9px 0 0; font-size: 11.5px; line-height: 1.55; color: var(--ink-far); }
.store__note b { color: var(--ink-dim); font-weight: 500; }
.store__note--small { font-size: 11px; }
.store__head .account__gis { margin-top: 12px; }
.store__buys { margin-top: 16px; }

.bundles { display: grid; gap: 10px; }

.bundle {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(20,30,50,.4), rgba(9,13,22,.4));
  padding: 12px 14px 14px;
}
/* The best per-Insight rate, marked with a hairline and a word. Not a colour wash: the
   largest bundle is the cheapest per Insight and saying so once is enough. */
.bundle--best { border-color: var(--glow-dim); }

.bundle__top { display: flex; align-items: baseline; gap: 7px; }
.bundle__n {
  font-family: var(--mono); font-size: 22px; line-height: 1;
  color: var(--glow); font-variant-numeric: tabular-nums;
}
.bundle__unit {
  font-family: var(--mono); font-size: 10px; letter-spacing: .18em; color: var(--ink-far);
}
.bundle__price { font-family: var(--mono); font-size: 16px; color: var(--ink); white-space: nowrap; }

.bundle__rate {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 8px 0 0;
  font-family: var(--mono); font-size: 11.5px; color: var(--ink-dim);
}
.bundle__save { color: var(--win); }
.bundle__mark {
  border: 1px solid var(--glow-dim); border-radius: 999px;
  padding: 1px 8px;
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--glow);
}

.bundle__buy {
  display: block; width: 100%; min-height: var(--tap); margin-top: 12px;
  padding: 10px 14px;
  border: 1px solid var(--glow-dim); border-radius: var(--radius);
  background: rgba(20, 60, 76, .3);
  font-family: var(--mono); font-size: 12.5px; letter-spacing: .06em; color: var(--glow);
}
.bundle__buy:hover:not(:disabled), .bundle__buy:focus-visible:not(:disabled) {
  border-color: var(--glow); outline: none;
}
.bundle__buy:disabled { opacity: .45; cursor: default; border-color: var(--line); color: var(--ink-far); }

/* The store link at the foot of the rank shop. A line of text, not a second buy button. */
.ranks__store {
  display: block; width: 100%; min-height: 40px; margin-top: 10px;
  padding: 8px 0 0;
  border: 0; border-top: 1px solid var(--line);
  background: none; text-align: left;
  font-family: var(--mono); font-size: 11.5px; color: var(--ink-dim);
}
.ranks__store:hover, .ranks__store:focus-visible { color: var(--glow); outline: none; }

/* ---------- the purchase receipt (§9.2) ----------
   One block, never a modal, and it is the only thing on the screen that can say a
   purchase did not land. Its colour is the answer, not the mood. */

.receipt {
  margin: 0 0 14px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--glow-dim);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 12px 14px;
}
.receipt--win { border-left-color: var(--win); }
.receipt--warn { border-left-color: var(--warn); }
.receipt--lose { border-left-color: var(--lose); }
.receipt__head { margin: 0; font-family: var(--mono); font-size: 13px; color: var(--ink); }
.receipt__body { margin: 8px 0 0; font-size: 12.5px; line-height: 1.55; color: var(--ink-dim); }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}
