/* ============ REMOTE — visual system ============
   One dark ground, two player accents, everything else is grey.
   P1 = amber, P2 = cyan. Objects tinted with the PARTNER accent are
   the ones the partner controls. That single rule teaches the game. */

:root {
  --bg:        #090c12;
  --bg-2:      #0e131c;
  --panel:     #121926;
  --line:      #1f2836;
  --ink:       #e7eefb;
  --ink-dim:   #8895ab;
  --ink-faint: #7b8aa3;   /* 4.7:1 on --bg-2; do not darken past this */

  --p1:        #ffb03a;
  --p2:        #35e0d8;
  --bad:       #ff5470;
  --good:      #7bf59a;

  --me:        var(--p1);   /* you: always warm */
  --them:      var(--p2);   /* anything your partner controls: always cyan */
  --slot:      var(--p1);   /* which player you are -- badge only */

  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);

  /* 1 while the player's equipment is racked up the right-hand edge. The
     stylesheet decides; main.js reads this rather than guessing from geometry,
     because a console with a single key is taller than it is wide either way. */
  --rail: 0;

  /* The size of the intercom key, declared once. The console subtracts it to
     find its own track, so the two pieces of equipment cannot be bolted on top
     of each other however many channels a level has. Every breakpoint that
     resizes .mic must resize these with it. */
  --mic-w: 68px;
  --mic-h: 68px;
  /* The partner-muted annunciator sits directly above the intercom, and the
     rack's track subtracts it too. Reserved whether or not it is lit, because
     a rack that resizes when your partner mutes their microphone is worse
     than 24px of dark panel. */
  --pm-h: 24px;
  --gap: 12px;

  /* How much of the strip under the rail the code and the objective push the
     things below them down by. Zero on a wide screen, where the code sits far
     left and the objective is centred and they never meet; the real heights
     on anything narrower, where they do. Costing the room 40px of height to
     avoid a collision that is not happening is not a trade worth making. */
  --stack-r: 0px;
  --stack-t: 0px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
[hidden] { display: none !important; }  /* our .veil/.join-form/.stick set display, which would win otherwise */

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

#app { position: fixed; inset: 0; }

/* ---------- screens ---------- */
.screen {
  position: absolute; inset: 0;
  display: none;
  /* Deliberately NOT align-items:center. A centred flex child that is taller
     than its parent overflows equally in both directions, and the half above
     the top cannot be scrolled to -- on a landscape phone that put the logo
     and the JOIN button off opposite edges of the screen with no way back.
     margin:auto on the child centres it while it fits and gives up cleanly
     when it does not. */
  align-items: flex-start; justify-content: center;
  overflow-y: auto; overscroll-behavior: contain;
  padding: calc(24px + var(--safe-t)) 24px calc(24px + var(--safe-b));
  z-index: 1;
  /* the menus are read against the darkened bay behind them, not a flat panel */
  background: radial-gradient(115% 80% at 50% 46%, rgba(9,12,14,.70) 0%, rgba(8,10,12,.9) 76%);
}
.screen[data-active] { display: flex; animation: fade .35s ease both; }
.screen-game[data-active] { display: block; padding: 0; background: none; }

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

.menu-inner {
  width: min(380px, 100%); margin: auto;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center;
}

/* ---------- type ----------
   Everything on these screens is signage or equipment. Stencilled labels,
   engraved plates and keys that travel -- the same vocabulary as the in-game
   console, so the menu belongs to the building rather than to a website. */
.logo {
  margin: 0; font-family: var(--mono);
  font-size: clamp(44px, 15vw, 80px);
  font-weight: 700; letter-spacing: .2em; text-indent: .2em;
  color: #e4e7e0;
  text-shadow: 0 2px 0 rgba(0,0,0,.6);
}
.logo-rule {
  width: 100%; height: 0; margin: 2px 0 0;
  border-top: 2px solid rgba(228,231,224,.5);
  border-bottom: 1px solid rgba(0,0,0,.6);
}
.tagline {
  margin: 6px 0 12px; font-family: var(--mono);
  font-size: 11px; letter-spacing: .3em; text-transform: uppercase;
  color: #8b95a0;
}
.eyebrow {
  margin: 0; font-family: var(--mono); font-size: 10px;
  letter-spacing: .34em; color: #7f8a94; text-transform: uppercase;
}
.menu-note {
  margin: 16px 0 0; max-width: 46ch; font-family: var(--mono);
  font-size: 11px; line-height: 1.8; letter-spacing: .04em;
  color: #79838d;
}
.error { min-height: 18px; margin: 4px 0 0; font-family: var(--mono); font-size: 12px; color: var(--bad); }

/* ---------- keys ----------
   The same piece of hardware as the in-game console: a plate with a lit top
   edge standing 3px proud, which goes down when you push it. */
.menu-buttons { display: flex; flex-direction: column; gap: 9px; width: 100%; margin-top: 6px; }

.btn {
  appearance: none; cursor: pointer;
  min-height: 44px; padding: 0 20px; width: 100%;
  border: 1px solid #1a1f23; border-radius: 2px;
  background: linear-gradient(180deg, #5d646b 0%, #454b52 44%, #383e44 100%);
  box-shadow: 0 3px 0 #14181b, inset 0 1px 0 rgba(255,255,255,.26);
  color: #e6eaee;
  font-family: var(--mono); font-size: 12px; font-weight: 700;
  letter-spacing: .18em;
  transition: transform .06s linear, box-shadow .06s linear, filter .15s ease;
}
.btn:hover { filter: brightness(1.1); }
.btn:active { transform: translateY(3px); box-shadow: 0 0 0 #14181b, inset 0 2px 4px rgba(0,0,0,.5); }
.btn:focus-visible { outline: 2px solid var(--p2); outline-offset: 2px; }

/* the lit key: the one you are meant to press */
.btn-primary {
  background: linear-gradient(180deg, #e8a94a 0%, #d99a34 48%, #c2851f 100%);
  border-color: #5a3c11; color: #2a1a04;
  box-shadow: 0 3px 0 #4b330d, inset 0 1px 0 rgba(255,255,255,.4);
}
.btn-primary:active { box-shadow: 0 0 0 #4b330d, inset 0 2px 4px rgba(0,0,0,.35); }
/* an unlit key: present, but not what the building is asking you to do */
.btn-ghost {
  background: linear-gradient(180deg, #333b41 0%, #262c31 100%);
  color: #97a1a8; box-shadow: 0 2px 0 #14181b, inset 0 1px 0 rgba(255,255,255,.12);
}
.btn-ghost:active { box-shadow: 0 0 0 #14181b, inset 0 2px 4px rgba(0,0,0,.5); }
.btn-small { min-height: 38px; width: auto; font-size: 10.5px; padding: 0 16px; }
@media (pointer: coarse) { .btn-small { min-height: 44px; } }

/* ---------- the code field: an engraved plate ---------- */
.join-form { width: 100%; display: flex; flex-direction: column; gap: 9px; align-items: stretch; }
.join-form label {
  font-family: var(--mono); font-size: 10px; letter-spacing: .32em; color: #7f8a94;
}
#code-input {
  width: 100%; height: 66px; text-align: center;
  font-family: var(--mono); font-size: 32px; font-weight: 700; letter-spacing: .34em; text-indent: .34em;
  color: #e9ecef; background: #14181b;
  border: 1px solid #0a0d0f; border-top-color: #2c343a; border-radius: 2px;
  box-shadow: inset 0 3px 7px rgba(0,0,0,.75);
  text-transform: uppercase;
}
#code-input:focus { outline: none; box-shadow: inset 0 3px 7px rgba(0,0,0,.75), 0 0 0 2px var(--p2); }
#code-input::placeholder { color: #2c3339; }
.join-row { display: flex; gap: 9px; justify-content: center; }

/* ---------- lobby ---------- */
.code-display {
  font-family: var(--mono); font-size: clamp(46px, 16vw, 76px); font-weight: 700;
  letter-spacing: .22em; text-indent: .22em; color: #e9ecef;
  margin: 2px 0 6px;
  text-shadow: 0 2px 0 rgba(0,0,0,.6);
}
/* an annunciator: one row per seat, each with its own lamp */
.slots {
  display: flex; flex-direction: column; gap: 1px; width: 100%; margin: 16px 0 6px;
  background: #12171b; border: 1px solid #12171b;
}
.slot {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px;
  background: linear-gradient(180deg, #333e46 0%, #232c33 100%);
  font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
}
.slot .dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: #1e2427; box-shadow: inset 0 1px 1px rgba(0,0,0,.8);
}
.slot .slot-label { color: #a8b2ba; }
.slot .slot-state { margin-left: auto; color: #9fa9b3; font-size: 10px; }
.slot[data-here] .dot { box-shadow: 0 0 5px currentColor; }
.slot[data-here] .slot-state { color: var(--good); }
#slot-p1[data-here] .dot { background: var(--p1); color: var(--p1); }
#slot-p2[data-here] .dot { background: var(--p2); color: var(--p2); }
.slot[data-you] .slot-label::after { content: " (YOU)"; color: #79838d; }

.progress-line {
  margin: 2px 0 0; font-family: var(--mono); font-size: 10.5px;
  letter-spacing: .14em; color: #79838d;
}
.lobby-hint {
  margin: 8px 0; font-family: var(--mono); font-size: 11px; line-height: 1.8;
  letter-spacing: .04em; color: #79838d; max-width: 36ch;
}

/* ---------- game ---------- */
#stage { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; display: block; }

/* ---------- the head rail ----------
   One piece of signage screwed across the top of the frame, divided into cells
   by hairlines. The HUD is not five cards over the room; it is a strip of the
   equipment, and the room starts underneath it. */
.rail {
  position: absolute; top: 0; left: 0; right: 0;
  padding-top: var(--safe-t);
  display: flex; align-items: stretch;
  background: linear-gradient(180deg, #333e46 0%, #232c33 100%);
  border-bottom: 1px solid #0e1215;
  box-shadow: 0 2px 6px -3px #000, inset 0 1px 0 #4d5b65;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .18em;
  color: #a0abb3;
  pointer-events: none;
  z-index: 4;
}
.cell {
  display: flex; align-items: center; gap: 7px;
  padding: 7px 12px; min-height: 30px;
  border-right: 1px solid #0e1215;
  box-shadow: 1px 0 0 rgba(255,255,255,.05);
  white-space: nowrap;
}
.cell-gap { flex: 1 1 auto; min-width: 8px; }
.cell-level { color: #a0abb3; gap: 8px; }
.cell-level .lv-n::before { content: "LVL "; color: #7c8792; }
.cell-level .lv-name { overflow: hidden; text-overflow: ellipsis; }
.cell-timer { font-variant-numeric: tabular-nums; letter-spacing: .12em; color: #dfe6ec; }
.cell-timer[data-low] { color: var(--bad); animation: blink 1s infinite; }
.cell-timer[data-held] { color: #a0abb3; }
/* the two cells you can press behave like the rest of the equipment */
.cell-btn {
  pointer-events: auto; cursor: pointer; background: none;
  border: 0; border-right: 1px solid #0e1215;
  font: inherit; color: #b6c0c7; letter-spacing: .18em;
}
/* A finger needs a target a mouse does not. The rail keeps its printed height;
   the pressable cells grow to meet the thumb. */
@media (pointer: coarse) {
  .cell { min-height: 44px; }
  .cell-btn { padding-left: 16px; padding-right: 16px; }
}
.cell-btn:hover { background: rgba(255,255,255,.05); }
.cell-btn:active { background: rgba(0,0,0,.28); }
.cell-btn:focus-visible { outline: 2px solid var(--p2); outline-offset: -3px; }
.cell-btn[aria-pressed="true"] { color: #79838d; }
.cell-status { border-right: 0; box-shadow: none; }

.spk { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.spk path:first-child { fill: currentColor; stroke-width: 1.2; }
.spk-off { opacity: 0; }
.cell-btn[aria-pressed="true"] .spk-wave { opacity: 0; }
.cell-btn[aria-pressed="true"] .spk-off { opacity: 1; }

.you-badge {
  padding: 1px 5px; border-radius: 1px; font-weight: 700; font-size: 10px;
  background: var(--slot); color: #0b0e14;
}
.cell-status .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--good); box-shadow: 0 0 5px rgba(123,245,154,.8); }
.cell-status[data-off] .dot { background: var(--bad); box-shadow: 0 0 0 3px rgba(255,84,112,.22); animation: blink 1s infinite; }
.cell-status[data-off] { color: var(--bad); }
@keyframes blink { 50% { opacity: .25; } }

/* the printed slip, hanging off the left end of the rail */
.readout {
  position: absolute; left: 10px; top: calc(var(--hud-h, 30px) + 6px);
  display: flex; align-items: center; gap: 8px;
  padding: 3px 8px; border-radius: 1px;
  background: linear-gradient(180deg, #a9a28c 0%, #948d78 100%);
  border: 1px solid #12171b; border-top-color: #c2bba4;
  box-shadow: 0 2px 0 rgba(0,0,0,.45);
  font-family: var(--mono); color: #23251f;
  z-index: 3;
}
.readout-head { font-size: 7.5px; letter-spacing: .18em; color: #252420; }
.readout-body { font-size: 12.5px; font-weight: 700; letter-spacing: .14em; }
/* The header says WHOSE code this is, and on the crossed-sign levels that is
   the whole puzzle -- it never gets hidden, only smaller. */
@media (max-width: 480px) {
  .readout { padding: 3px 6px; gap: 6px; }
  .readout-head { font-size: 6.5px; letter-spacing: .1em; }
  .readout-body { font-size: 11.5px; letter-spacing: .1em; }
  .cell { padding: 6px 8px; }
  /* The level NAME is flavour and the number is identity, so the name goes
     rather than being cut to "5. Two Suppl...". Nothing here shrinks. */
  .cell-level .lv-name { display: none; }
  #mute-label { display: none; }
  #status-text { display: none; }
  .cell-status { padding-right: 12px; }
}

/* ---------- level select ---------- */
.menu-wide { width: min(640px, 100%); }
/* twenty-five tiles will not fit a phone; let the picker scroll */
#levels { align-items: flex-start; overflow-y: auto; overscroll-behavior: contain; }
#levels .menu-inner { padding-bottom: 16px; }
.levels-title {
  margin: 2px 0 10px; font-family: var(--mono); font-size: clamp(26px, 7vw, 40px);
  font-weight: 700; letter-spacing: .18em; text-indent: .18em; color: var(--ink);
}
.level-grid {
  display: grid; gap: 8px; width: 100%;
  max-height: 62vh; overflow-y: auto; overscroll-behavior: contain;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
}
/* One plate per room in the wing, with a status lamp. Cleared rooms show a
   green lamp; the one you are in shows cyan; a room you have not reached yet is
   an unpainted blank -- no padlock icon from some other icon set. */
.lvl {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  min-height: 54px; padding: 8px 10px 8px 22px; cursor: pointer;
  border: 1px solid #12171b; border-top-color: #4d5b65; border-radius: 2px;
  background: linear-gradient(180deg, #333e46 0%, #232c33 100%);
  color: #e2e7ea; font-family: var(--mono); text-align: left;
  transition: filter .15s ease, transform .06s linear;
}
.lvl::before {                    /* the status lamp, dark unless earned */
  content: ""; position: absolute; left: 9px; top: 12px;
  width: 7px; height: 7px; border-radius: 50%;
  background: #1a2023; box-shadow: inset 0 1px 1px rgba(0,0,0,.8);
}
.lvl:hover:not([disabled]) { filter: brightness(1.16); }
.lvl:active:not([disabled]) { transform: translateY(2px); }
.lvl:focus-visible { outline: 2px solid var(--them); outline-offset: 2px; }
.lvl-n { font-size: 15px; font-weight: 700; letter-spacing: .04em; }
/* stretch, or a two-word name spills out of the plate: three of the
   twenty-five are long enough to do it ("Bridge and Beam") */
.lvl-name { align-self: stretch; font-size: 9.5px; line-height: 1.35; letter-spacing: .08em; color: #a0abb3; }
.lvl[data-cleared]::before { background: var(--good); box-shadow: 0 0 5px var(--good); }
.lvl[data-current]::before { background: var(--them); box-shadow: 0 0 5px var(--them); }
.lvl[data-current] { border-top-color: var(--them); }
.lvl[data-locked] {
  cursor: default;
  background: linear-gradient(180deg, #232a2f 0%, #1a2024 100%);
  border-top-color: #2b3439; color: #8a949d;
}
.lvl[data-locked] .lvl-name { color: #88929b; }

/* teaching line and notices share one centred column, so they cannot collide */
/* The objective sits under the top plates, wherever they happen to end -- on a
   narrow phone the left group wraps to two rows and a fixed offset collides. */
.hud-center {
  /* Under the code strip, which is under the rail. Three things want this
     strip and none of them may be printed over another: the code you were
     sent to find, the objective, then the notices. */
  position: absolute; left: 50%; transform: translateX(-50%);
  top: calc(var(--hud-h, 38px) + var(--stack-r) + 8px);
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  width: min(46ch, calc(100% - 28px));
  pointer-events: none;
}
/* notices sit off the centre line: the door lives there and it is the objective */
.toasts {
  position: absolute; left: 12px;
  /* Notices always clear the code strip -- they share its left lane at every
     width -- and clear the objective too wherever the objective is wide. */
  top: calc(var(--hud-h, 38px) + var(--readout-h, 0px) + var(--stack-t) + 14px);
  display: flex; flex-direction: column; gap: 6px; align-items: flex-start;
  width: min(300px, calc(100% - 24px));
  pointer-events: none;
}
.toast { text-align: left; }
/* A notice slips out of the annunciator like a printed slip, left-aligned,
   with a coloured tab down its edge saying which system it came from. */
.toast {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px; border-radius: 1px;
  background: #232c33;
  border: 1px solid #12171b;
  font-family: var(--mono); font-size: 11px; letter-spacing: .08em; color: #b3bcc4;
  animation: toast-in .3s ease both; text-align: left;
}
.toast::before {
  content: ""; flex: none; width: 6px; height: 6px;
  background: #6b7780; box-shadow: inset 0 0 0 1px rgba(0,0,0,.5);
}
.toast[data-kind="them"] { color: var(--them); }
.toast[data-kind="them"]::before { background: var(--them); }
.toast[data-kind="good"] { color: var(--good); }
.toast[data-kind="good"]::before { background: var(--good); }
.toast.out { animation: toast-out .3s ease both; }
@keyframes toast-in { from { opacity: 0; transform: translateX(-10px); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(-8px); } }

/* remote control panel */
/* ---------- the remote-link console ----------
   Facility equipment, not software: a painted panel with a bolted rail, and
   keys that physically travel when you press them. Flat colour and hairlines;
   no radii to speak of, no blur, no glow at rest. The single cyan lamp on each
   key is the transmit indicator -- it lights only while a command is going out. */
.console {
  /* The rack is bounded on every side by the things it must not touch: the
     signage rail above, the intercom key beside it, the frame edge. A level
     with eight channels then scrolls inside its own track instead of growing
     over the voice key and off the bottom of a phone. */
  position: absolute; z-index: 3;
  right: var(--gap);
  bottom: calc(var(--gap) + var(--safe-b));
  left: calc(var(--mic-w) + var(--gap) * 2);   /* never under the intercom */
  max-height: calc(100% - var(--hud-h, 38px) - var(--gap) * 2);
  display: flex; flex-direction: column; min-height: 0;
  background: linear-gradient(180deg, #3a4750 0%, #2b353c 100%);
  border: 1px solid #12171b;
  border-top-color: #566772;
  box-shadow: 0 6px 18px -10px #000;
  padding: 0 0 7px;
}
.console-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 5px 9px 5px;
  background: #222a30;
  border-bottom: 1px solid #12171b;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .2em;
}
.console-title { color: #96a1a8; }
.console-dest { color: var(--them); letter-spacing: .14em; }

.console-keys {
  display: flex; gap: 6px; padding: 7px 7px 0;
  overflow-x: auto; overflow-y: hidden;
  min-height: 0; flex: 1 1 auto;
  scrollbar-width: thin; scrollbar-color: #5c6870 transparent;
}
.console-keys::-webkit-scrollbar { height: 5px; width: 5px; }
.console-keys::-webkit-scrollbar-thumb { background: #5c6870; }
/* A rack whose channels run past its edge has to look like it does, or the
   player never learns there is a channel 06. This is the bar of a drawer that
   is not fully closed, not decoration. */
.console[data-more]::after {
  content: ""; position: absolute; pointer-events: none;
  right: 1px; top: 24px; bottom: 8px; width: 22px;
  background: linear-gradient(90deg, rgba(43,53,60,0) 0%, #2b353c 85%);
  border-right: 2px solid var(--them);
}

.key {
  position: relative; cursor: pointer; flex: 0 0 auto;
  width: 68px; min-height: 66px; padding: 5px 4px 6px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  background: linear-gradient(180deg, #4f565d 0%, #434a51 45%, #363c42 100%);
  border: 1px solid #1a1f23;
  border-radius: 2px;
  box-shadow: 0 3px 0 #14181b, inset 0 1px 0 rgba(255,255,255,.22);
  font-family: var(--mono); color: #e7edf1;
  overflow: hidden;
  transition: transform .06s linear, box-shadow .06s linear;
}
/* the key travels: it goes down and its shadow disappears under it */
.key:active, .key[data-press] {
  transform: translateY(3px);
  box-shadow: 0 0 0 #14181b, inset 0 2px 4px rgba(0,0,0,.5);
}
.key:focus-visible { outline: 2px solid var(--them); outline-offset: 2px; }
.key[disabled] { opacity: .38; pointer-events: none; }

.key-ch {                             /* the stamped legend plate */
  align-self: stretch; margin: -5px -4px 1px; padding: 3px 0 2px;
  background: linear-gradient(180deg, #b9c0c6 0%, #9ca4ab 100%);
  border-bottom: 1px solid #262c31;
  font-size: 15px; font-weight: 700; line-height: 1.1; letter-spacing: .04em;
  color: #14181b; text-shadow: 0 1px 0 rgba(255,255,255,.3);
}
.key-name { font-size: 8.5px; letter-spacing: .1em; color: #e7edf1; }
/* a symbol key wears its symbol on the plate; the channel drops to the name */
.key[data-symbol] .key-ch { font-size: 21px; }
.key-sub  { font-size: 7.5px; letter-spacing: .08em; color: #c8d0d6; }
.key-lamp {                           /* transmit indicator, dark at rest */
  width: 7px; height: 7px; border-radius: 50%;
  background: #1e2427; border: 1px solid #10141600;
  box-shadow: inset 0 1px 1px rgba(0,0,0,.7);
  margin-top: 1px;
}
.key[data-press] .key-lamp,
.key[data-live] .key-lamp {
  background: var(--them);
  box-shadow: 0 0 5px var(--them);
}
/* refused at the far end: the transmit lamp goes red instead of cyan */
.key[data-refused] .key-lamp {
  background: var(--bad);
  box-shadow: 0 0 5px var(--bad);
  animation: refuse .18s steps(2) 3;
}
@keyframes refuse { 50% { opacity: .2; } }
.console[data-done] { opacity: .4; pointer-events: none; filter: grayscale(1); transition: opacity .5s; }
/* the far end is not answering: the panel says so and stops taking presses */
.console[data-down] { pointer-events: none; }
.console[data-down] .console-dest { color: var(--bad); animation: blink 1.1s infinite; }
.console[data-down] .console-keys { opacity: .35; filter: grayscale(1); }

@media (max-width: 480px) {
  /* Portrait wraps rather than scrolls, so every channel stays on screen, and
     the keys share the row out between them -- five across, or two rows of
     four on a level with eight. A rack that ends with one key stranded on its
     own row looks like an accident; an even row looks like it was built.
     These belong here, beside the other key rules: the first attempt put them
     in the 480px block two hundred lines up, where this one silently won. */
  .console-keys { flex-wrap: wrap; row-gap: 5px; padding-bottom: 5px; }
  .key {
    flex: 1 1 44px; width: auto; min-width: 0; max-width: 76px;
    min-height: 58px;
  }
  .key-ch { font-size: 13px; }
}

/* ---------- voice ---------- */
/* The intercom key: the same piece of hardware as the console, one key wide. */
.mic {
  position: absolute; left: 12px; bottom: calc(12px + var(--safe-b));
  z-index: 4; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  width: 68px; height: 68px; padding: 0;
  border-radius: 2px;
  background: linear-gradient(180deg, #4f565d 0%, #434a51 45%, #363c42 100%);
  border: 1px solid #1a1f23;
  box-shadow: 0 3px 0 #14181b, inset 0 1px 0 rgba(255,255,255,.22);
  color: #cbd3d9;
  font-family: var(--mono);
  transition: transform .1s ease, border-color .2s ease, color .2s ease, background .2s ease;
}
.mic:active { transform: scale(.93); }
.mic:focus-visible { outline: 2px solid var(--me); outline-offset: 3px; }
.mic-glyph {
  width: 20px; height: 20px; fill: none;
  stroke: currentColor; stroke-width: 1.9; stroke-linecap: round;
}
.mic-glyph path:first-child { fill: currentColor; stroke: none; }
.mic-label { font-size: 11px; letter-spacing: .06em; }
.mic-slash { opacity: 0; }

/* A recessed level meter across the foot of the key, the way a panel carries
   one -- the old ring was drawn for a round button and after the key became a
   rectangle it was a stray arc in the corner. */
.mic-meter {
  position: absolute; left: 8px; right: 8px; bottom: 5px; height: 3px;
  background: #14181b; box-shadow: inset 0 1px 1px rgba(0,0,0,.8);
  overflow: hidden;
}
.mic-meter-lv {
  display: block; height: 100%; width: 100%;
  background: var(--me);
  transform: scaleX(0); transform-origin: left center;
  transition: transform .1s linear;
}

/* the key is unlit until the line is actually open */
.mic[data-state="waiting"] .mic-meter,
.mic[data-state="connecting"] .mic-meter { opacity: .4; }
.mic[data-state="connecting"] .mic-label { animation: blink 1.2s infinite; }
/* live: the key is latched down and the lens is lit */
.mic[data-state="live"] {
  color: #2a1a04;
  background: linear-gradient(180deg, #e8a94a 0%, #d99a34 48%, #c2851f 100%);
  border-color: #5a3c11;
  transform: translateY(3px);
  box-shadow: 0 0 0 #14181b, inset 0 2px 5px rgba(0,0,0,.35);
}
.mic[data-state="live"] .mic-meter { background: rgba(30,18,4,.6); }
.mic[data-state="live"] .mic-meter-lv { background: #2a1a04; }
.mic[data-state="muted"] { color: #cbd3d9; }
.mic[data-state="muted"] .mic-meter-lv { transform: scaleX(0) !important; }
.mic[data-state="muted"] .mic-slash,
.mic[data-state="denied"] .mic-slash,
.mic[data-state="failed"] .mic-slash { opacity: 1; }
.mic[data-state="denied"], .mic[data-state="failed"] { color: var(--bad); }
.mic[hidden] { display: none !important; }

/* answers "are you muted?" at the moment it is asked, not only when it changed */
.peer-mute {
  position: absolute; left: 12px; bottom: calc(88px + var(--safe-b)); z-index: 4;
  display: flex; align-items: center; gap: 7px;
  padding: 5px 9px; border-radius: 1px; white-space: nowrap;
  background: #232c33;
  border: 1px solid #12171b;
  color: var(--them);
  font-family: var(--mono); font-size: 10px; letter-spacing: .08em;
  pointer-events: none;
}
.peer-mute::before {
  content: ""; flex: none; width: 6px; height: 6px;
  background: var(--them); box-shadow: inset 0 0 0 1px rgba(0,0,0,.5);
}

#peer-audio { display: none; }

@media (max-height: 460px) and (orientation: landscape) {
  .mic { width: var(--mic-w); height: var(--mic-h); left: 10px; bottom: calc(10px + var(--safe-b)); }
  .mic-label { display: none; }
  .peer-mute { left: 10px; bottom: calc(var(--mic-h) + 18px + var(--safe-b)); }
}

/* touch joystick */
.stick-zone { position: absolute; left: 0; bottom: 0; width: 55%; height: 62%; z-index: 2; }
.stick {
  position: absolute; width: 118px; height: 118px; margin: -59px 0 0 -59px;
  border-radius: 50%; border: 1.5px solid color-mix(in srgb, var(--me) 32%, transparent);
  background: radial-gradient(circle, color-mix(in srgb, var(--me) 10%, transparent), transparent 70%);
  pointer-events: none;
}
.stick-knob {
  position: absolute; left: 50%; top: 50%; width: 52px; height: 52px; margin: -26px 0 0 -26px;
  border-radius: 50%; background: color-mix(in srgb, var(--me) 55%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--me) 26%, transparent);
}

/* the line that teaches the game -- under the top HUD, where nothing else sits */
.teach {
  width: 100%;
  font-family: var(--mono); text-align: center;
  opacity: 0; transition: opacity .5s ease;
}
/* The standing order for this bay, printed on the annunciator above the door.
   The label is quiet; the order itself carries the weight. */
.teach-title {
  font-size: 9px; font-weight: 600;
  letter-spacing: .38em; text-indent: .38em;
  color: #7d8892;
  transition: color .3s ease;
}
.teach[data-done] .teach-title { color: var(--good); }
.teach[data-done] .teach-title::after { content: " \2713"; }
.teach-sub {
  margin-top: 4px; padding-top: 5px;
  border-top: 1px solid rgba(150,165,178,.22);
  font-size: clamp(12px, 1.6vw, 15px); font-weight: 600;
  letter-spacing: .1em; text-indent: .1em;
  color: #dfe6ec; line-height: 1.4;
}
.teach.bump .teach-sub { animation: obj-bump .45s cubic-bezier(.2,.9,.3,1) both; }
@keyframes obj-bump {
  from { opacity: 0; transform: translateY(5px); }
  to { opacity: 1; transform: none; }
}
.teach[data-show] { opacity: 1; }

/* An instruction plate screwed to the frame, the way a machine carries its
   operating notice. It goes as soon as the player has moved once. */
.controls-plate {
  /* Above the equipment, never behind it: this is the one caption that has to
     be read before the player has touched anything. */
  position: absolute; left: 50%; bottom: calc(var(--tray-h, 14px) + 16px + var(--safe-b));
  transform: translateX(-50%); max-width: calc(100% - 24px);
  padding: 6px 14px; border-radius: 2px; white-space: nowrap;
  background: linear-gradient(180deg, #333e46 0%, #232c33 100%);
  border: 1px solid #12171b; border-top-color: #4d5b65;
  font-family: var(--mono); font-size: 10px; letter-spacing: .16em; color: #a8b2ba;
  pointer-events: none; z-index: 5;
  transition: opacity .6s ease;
}
.controls-plate[data-going] { opacity: 0; }
@media (max-width: 480px) { .controls-plate { font-size: 9px; padding: 5px 10px; } }

/* ---------- win ---------- */
.win-title {
  margin: 4px 0; padding: 6px 18px;
  font-family: var(--mono); font-size: clamp(40px, 13vw, 68px); font-weight: 700;
  letter-spacing: .16em; text-indent: .16em; color: var(--good);
  border-top: 3px solid var(--good); border-bottom: 3px solid var(--good);
}
.win-time {
  font-family: var(--mono); font-size: 18px; letter-spacing: .22em;
  color: #a8b2ba; margin-bottom: 12px; font-variant-numeric: tabular-nums;
}

/* ---------- veil ---------- */
.veil {
  position: absolute; inset: 0; z-index: 50;
  display: flex; align-items: center; justify-content: center;
  background: rgba(9,12,14,.9); backdrop-filter: blur(6px);
}
.veil-box { display: flex; flex-direction: column; align-items: center; gap: 16px; font-family: var(--mono); font-size: 13px; letter-spacing: .12em; color: var(--ink-dim); text-align: center; padding: 20px; }
.spinner {
  width: 30px; height: 30px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--p2);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- landscape phones: keep controls off the room ---------- */
/* the control panel's caption wraps on a very narrow phone, so the partner
   badge shortens to stay clear of it */
@media (max-width: 420px) {
  .pm-long { display: none; }
}

/* landscape phone: the room needs every pixel of height it can get */
@media (max-height: 460px) and (orientation: landscape) {
  .key { width: 62px; min-height: 42px; padding: 4px; }
  .key-ch { font-size: 13px; }
  .key-sub { display: none; }
  .mic { width: var(--mic-w); height: var(--mic-h); }
  .mic-label { display: none; }
  .stick-zone { width: 45%; height: 80%; }
  .teach { display: none; }
}
@media (pointer: fine) {
  .stick-zone { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ---------- the control rail ----------
   Anywhere with room for it, all of the player's equipment racks up one edge
   of the frame: transmitter keys above, intercom below. That leaves the room
   itself a single uninterrupted rectangle instead of an L-shaped gap between
   two floating widgets, and it is worth roughly a third more room on screen. */
@media (min-width: 700px) and (min-height: 420px),
       (max-height: 460px) and (orientation: landscape) {
  :root { --rail: 1; --mic-w: 92px; --mic-h: 58px; --key-w: 78px; --key-h: 64px; }
  /* Racked up the edge, the track runs from under the signage down to the top
     of the intercom -- so the rack is exactly as tall as the space it has, and
     a level with eight channels scrolls rather than covering the voice key. */
  /* Capped by the track, sized by its contents. Stretching to fill the track
     made the rack a mostly-empty cabinet on a tall screen -- four channels at
     the top of a 1000px box. A max-height still resolves the column wrap,
     because when the rack does run out of room the cap is what binds. */
  .console {
    top: calc(var(--hud-h, 38px) + var(--gap));
    bottom: auto;
    left: auto; right: var(--gap);
    width: max-content;
    max-height: calc(100% - var(--hud-h, 38px) - var(--mic-h) - var(--pm-h)
                     - var(--gap) * 3 - var(--safe-b));
  }
  /* A rack that has run out of height gains a second column rather than a
     scrollbar. Eight channels on a landscape phone is the worst case the game
     has, and scrolling a rack mid-puzzle to reach channel 06 is not a thing
     equipment does. The track is a definite height, so the column wraps. */
  .console-keys {
    flex-direction: column; flex-wrap: wrap;
    align-content: flex-start; column-gap: 6px;
    overflow: hidden; padding: 7px 7px 0;
  }
  .console[data-more]::after {
    right: 8px; left: 8px; top: auto; bottom: 1px; width: auto; height: 18px;
    background: linear-gradient(180deg, rgba(43,53,60,0) 0%, #2b353c 85%);
    border-right: 0; border-bottom: 2px solid var(--them);
  }
  /* Every key the same height, because a rack breaks its columns on key
     count and keys of two heights break it raggedly -- four, then three, then
     one stranded in a third column. Racked equipment is identical modules. */
  .key { width: var(--key-w); height: var(--key-h); min-height: 0; }
  /* How many columns the rack needs depends on how tall its track is, which
     only the running page knows -- measureBands() in main.js sets --cols.
     Padding is in the width because box-sizing is border-box throughout. */
  .console-keys {
    width: calc(var(--key-w) * var(--cols, 1) + 6px * (var(--cols, 1) - 1) + 14px);
  }
  .mic { left: auto; right: var(--gap); bottom: calc(var(--gap) + var(--safe-b)); width: var(--mic-w); height: var(--mic-h); }
  .peer-mute { left: auto; right: var(--gap); bottom: calc(var(--mic-h) + var(--gap) + var(--safe-b)); }
}

/* A landscape phone matches the rail query above as well, so its own smaller
   hardware has to be declared after it or the desktop sizes win. Eight
   channels on a 390px-tall screen is the tightest thing the game asks for:
   the rack takes three columns of narrow keys, and every one is reachable
   without scrolling. */
@media (max-height: 460px) and (orientation: landscape) {
  /* 46 rather than 52: four keys of 46 plus their gaps fit the track on a
     390px-tall phone and four of 52 do not, which is the difference between
     a rack of two even columns and one of three that ends 3-3-2. Still above
     a 44px thumb. */
  :root { --mic-w: 62px; --mic-h: 44px; --key-w: 62px; --key-h: 46px; --gap: 8px; }
  .key { padding: 4px; }
}

/* A landscape phone gives the menu 390px of height for a logo, three keys, a
   code field and a note. It scrolls if it has to -- but fitting is better than
   scrolling, so the signage shrinks to signage size and the long explanation,
   which is the one thing here nobody needs twice, goes. */
@media (max-height: 460px) and (orientation: landscape) {
  .screen { padding-top: calc(10px + var(--safe-t)); padding-bottom: calc(10px + var(--safe-b)); }
  .menu-inner { gap: 7px; }
  .logo { font-size: clamp(30px, 7vh, 44px); }
  .tagline { margin: 2px 0 4px; font-size: 9px; letter-spacing: .22em; }
  .menu-note, .lobby-hint { display: none; }
  .menu-buttons { gap: 7px; margin-top: 2px; }
  /* No smaller than a thumb. A landscape phone cannot fit a logo, five keys
     and a code field in 390px at this size, so the panel scrolls -- which is
     an ordinary thing for a menu to do, and the reason .screen scrolls now. */
  .btn { min-height: 44px; }
  #code-input { height: 44px; font-size: 22px; }
  .join-form { gap: 7px; }
  .slots { margin: 8px 0 4px; }
  .level-grid { max-height: 42vh; }
  .levels-title { font-size: 20px; margin: 2px 0; }
  .win-title { font-size: clamp(26px, 8vh, 40px); }
  .win-time { font-size: 15px; margin-bottom: 6px; }
}

/* Narrow enough that the centred objective reaches into the code strip's lane:
   below about 1000px the 46ch objective and the 300px code strip cannot both
   have the same row. They stack instead. */
@media (max-width: 1000px) {
  :root { --stack-r: var(--readout-h, 0px); --stack-t: var(--teach-h, 0px); }
}
