/* ------------------------------------------------------------------
   QR Keeper — house style, matched to hackenshaw.com / qr.hackenshaw.com:
   dark CRT terminal, chartreuse ink, rounded panel, "$ prompt" idioms.

   The canvas paints its "light" cells fully transparent (see js/config.js
   color.background), so .frame is left transparent too — the floor tone
   is just the panel showing through, not a separately painted color.
   js/config.js color.pageBackground must be kept equal to --panel-bg below,
   since the verifier and the embed snippet's iframe background can't read
   the CSS cascade and need the literal value.
   ------------------------------------------------------------------ */
:root {
  --hue-rgb: 127, 255, 0;
  --ink: rgb(var(--hue-rgb));
  --ink-dim: rgba(var(--hue-rgb), 0.55);
  --ink-faint: rgba(var(--hue-rgb), 0.18);
  --ink-ghost: rgba(var(--hue-rgb), 0.08);
  --page-bg: #272727;
  --panel-bg: #181818;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --pixel: "Press Start 2P", var(--mono);
}
* { box-sizing: border-box; }
/* Fixed page, scrolling only where explicitly opted into (.sections below) —
   the panel fills the window instead of the window scrolling past it, same
   shape as qr.hackenshaw.com / nextslide. */
html, body { height: 100%; overflow: hidden; }
body {
  margin: 0;
  height: 100vh;
  height: 100dvh; /* survives a phone's collapsing address bar */
  background: var(--page-bg);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.4;
  display: flex;
  flex-direction: column;
  padding: 1.6vh 3vw;
  gap: 0.8rem;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; }

/* ---------- CRT overlay: scanlines + flicker ---------- */
.crt-overlay::before,
.crt-overlay::after {
  content: " ";
  display: block;
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}
.crt-overlay::before {
  background:
    linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
    linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
  background-size: 100% 3px, 3px 100%;
}
.crt-overlay::after {
  background: rgba(18, 16, 16, 0.1);
  opacity: 0;
  animation: flicker 0.15s infinite;
}
@keyframes flicker {
  0% { opacity: 0.28; } 15% { opacity: 0.91; } 25% { opacity: 0.84; }
  40% { opacity: 0.27; } 50% { opacity: 0.96; } 60% { opacity: 0.20; }
  75% { opacity: 0.37; } 90% { opacity: 0.70; } 100% { opacity: 0.24; }
}
@keyframes textShadow {
  0% { text-shadow: 0.44px 0 1px rgba(0, 30, 255, 0.5), -0.44px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
  25% { text-shadow: 1.61px 0 1px rgba(0, 30, 255, 0.5), -1.61px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
  50% { text-shadow: 0.08px 0 1px rgba(0, 30, 255, 0.5), -0.08px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
  75% { text-shadow: 1.89px 0 1px rgba(0, 30, 255, 0.5), -1.89px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
  100% { text-shadow: 2.62px 0 1px rgba(0, 30, 255, 0.5), -2.62px 0 1px rgba(255, 0, 80, 0.3), 0 0 3px; }
}
@keyframes blink { to { opacity: 0.2; } }
@keyframes cursorBlink { 50% { opacity: 0; } }

/* ---------- site header ----------
   Trimmed small: on a page pinned to the viewport, a full-size header eats
   space the game panel needs. Same trade nextslide/qr.hackenshaw.com make. */
header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  text-align: center;
  flex-shrink: 0;
}
.avatar {
  width: clamp(34px, 5vmin, 50px);
  aspect-ratio: 1;
  border-radius: 50%;
  object-fit: cover;
  -webkit-user-drag: none;
  user-select: none;
  border: 2px solid var(--ink);
  box-shadow: 0 0 18px rgba(var(--hue-rgb), 0.25);
}
a.home {
  display: inline-flex;
  border-radius: 50%;
  transition: transform 0.18s ease, filter 0.18s ease;
}
a.home:hover, a.home:focus-visible {
  transform: scale(1.06);
  filter: drop-shadow(0 0 14px rgba(var(--hue-rgb), 0.6));
  outline: none;
}
header h1 {
  margin: 0;
  font-size: clamp(1rem, 3.2vmin, 1.4rem);
  letter-spacing: 0.08em;
  font-weight: 700;
  text-transform: uppercase;
  animation: textShadow 1.6s infinite;
}
header .tagline {
  color: var(--ink-dim);
  font-size: clamp(0.66rem, 1.6vmin, 0.8rem);
  letter-spacing: 0.05em;
}

/* ---------- terminal panel ---------- */
main { flex: 1; min-height: 0; display: flex; justify-content: center; }
.panel {
  background: var(--panel-bg);
  border-radius: 25px;
  padding: clamp(0.8rem, 2.4vmin, 1.4rem);
  width: 100%;
  max-width: 900px;
  max-height: 100%;
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  /* The panel is the scroller: the page stays pinned to the window, and
     expanding a section scrolls here rather than shrinking the map. */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.panel::-webkit-scrollbar { width: 0; height: 0; display: none; }
/* ---------- embed: the playable code and nothing else ----------
   Every scrap of chrome goes, and the panel keeps no padding or background of
   its own, so the canvas fills the iframe exactly at its native pixel size —
   main.js sizes the iframe to that, with no allowance for anything else.
   body still paints panel-bg: the canvas's light cells are transparent and
   this is the surface they show through, so it must match the floor tone. */
body.embed { background: var(--panel-bg); padding: 0; gap: 0; }
body.embed > header,
body.embed > footer,
body.embed .topline,
body.embed .side,
body.embed .stage::before,
body.embed .orbState,
body.embed .controls-line,
body.embed .sections,
body.embed .touchpad { display: none; }
body.embed .panel {
  background: transparent;
  box-shadow: none;
  border-radius: 0;
  max-width: none;
  padding: 0;
  gap: 0;
  overflow: hidden;
}

.cursor {
  display: inline-block;
  width: 0.55em;
  height: 1em;
  margin-left: 0.15em;
  background: var(--ink);
  vertical-align: text-bottom;
  animation: cursorBlink 1.1s steps(1) infinite;
}

/* ---------- brand + HUD + autopilot: one row across the top of the panel ---------- */
.topline {
  width: 100%;
  flex: 0 0 auto;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--ink-faint);
}
.brand {
  font-family: var(--pixel);
  font-size: clamp(1rem, 3.4vmin, 1.5rem);
  letter-spacing: 0.06em;
  white-space: nowrap;
  text-shadow: 0 0 12px rgba(var(--hue-rgb), 0.6);
}
.brand span { color: var(--ink-dim); margin-left: 0.4em; }

.hud { display: flex; align-items: center; justify-content: center; gap: 22px; flex-wrap: wrap; }
.stat { display: flex; flex-direction: column; gap: 3px; min-width: 52px; }
.stat[hidden] { display: none; }
.stat label { font-size: 9px; letter-spacing: 0.18em; color: var(--ink-dim); }
.stat b { font-family: var(--pixel); font-size: 13px; font-weight: normal; }
#scan.ok { color: var(--ink); }
#scan.bad { color: #ff5a3c; animation: blink 0.5s steps(2) infinite; }

.integrity { min-width: 120px; }
#integrity { display: flex; gap: 7px; height: 13px; align-items: center; }
#integrity .block { display: flex; gap: 1px; }
#integrity i { display: block; width: 4px; height: 11px; background: var(--ink); box-shadow: 0 0 6px rgba(var(--hue-rgb), 0.5); transition: background 0.2s, box-shadow 0.2s; }
#integrity i.used { background: var(--ink-faint); box-shadow: none; }
#integrity.low i:not(.used) { animation: blink 0.35s steps(2) infinite; }

.mode {
  flex: 0 0 auto;
  font-size: 10px;
  letter-spacing: 0.14em;
  padding: 6px 10px;
  border: 1px solid var(--ink-faint);
  border-radius: 8px;
  white-space: nowrap;
}
.mode.auto { border-color: var(--ink-dim); animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse { 50% { border-color: var(--ink-faint); color: var(--ink-dim); } }

/* ---------- stage: the map, resized to fill whatever room is left ----------
   Height-driven: .stage claims all the vertical space the rest of the panel
   doesn't need (flex: 1), and .frame fills it — height:100% + aspect-ratio
   derives the width, clamped by max-width so it never outgrows the panel.
   814px caps it at native canvas resolution so it's never upscaled/blurry. */
/* Sized against the panel's *visible* height, not its content: flex-grow can't
   do this once the panel is a scroller (it grows to content, not to the
   viewport). The reserve is the fixed rows that share the first screenful —
   topline, orb state, key legend, the two collapsed summaries, and the gaps.
   Fixed size + no shrink means opening a section scrolls the panel instead of
   resizing the game. */
.stage {
  width: 100%;
  flex: 0 0 auto;
  height: calc(100% - 120px);
  min-height: 200px;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: clamp(0.8rem, 2vw, 1.6rem);
}
/* The hints live beside the code, not under it: the panel has width to spare
   and height is what the map is short of. */
/* Stretched to the code's full height so "centre" means the code's centre.
   Both children are taken out of flow: otherwise the legend sitting at the
   bottom would push the orb state off-centre. */
.side {
  flex: 0 1 240px;
  min-width: 0;
  align-self: stretch;
  position: relative;
}
.side .orbState {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
}
.side .controls-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
}
/* An empty mirror of .side on the other flank, so the code stays centred on
   the panel instead of the code+hints pair centring as a unit. Same basis and
   shrink factor, so the two stay equal at every width. */
.stage::before {
  content: "";
  flex: 0 1 240px;
  min-width: 0;
}
.frame {
  position: relative;
  height: 100%;
  max-height: 814px;
  max-width: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  /* Don't let the flanking columns shrink the code: when the row is over
     budget they give up their width first, since theirs is slack and the
     code's is the point of the page. */
  flex-shrink: 0;
}
/* Embed is the opposite case: the iframe's width is the known/fixed quantity
   (embedSize in main.js sizes it to the code's native pixel dimensions), so
   the frame fills that width and derives height from it, not the reverse —
   and the stage just wraps it, with none of the reserve above (there are no
   rows to reserve for; embed is one HUD line and the canvas). */
body.embed .stage { height: auto; min-height: 0; }
body.embed .frame { width: 100%; height: auto; max-height: none; max-width: none; }

/* Shown whole, quiet zone included — the margin around the code is the QR
   spec's, not padding, so the frame does not crop it. */
canvas#game {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: auto;
  will-change: transform;
}

.overlay[hidden] { display: none; }
.overlay {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--pixel); font-size: 11px; letter-spacing: 0.1em;
  background: rgba(0, 0, 0, 0.7);
  text-align: center;
  padding: 0 12px;
}
.orbState { flex: 0 0 auto; font-size: 10px; letter-spacing: 0.12em; line-height: 1.7; color: var(--ink-dim); }

/* ---------- keyboard legend + mute ---------- */
.controls-line {
  width: 100%;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  font-size: 10px;
  letter-spacing: 0.1em;
  line-height: 1.9;
  color: var(--ink-dim);
  border-top: 1px solid var(--ink-faint);
  padding-top: 0.8rem;
}
.controls-line kbd { color: var(--ink); font-family: inherit; }

button, .generator button {
  background: transparent; color: var(--ink); border: 1px solid var(--ink-dim); border-radius: 8px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; padding: 5px 10px; cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease;
}
button:hover, .generator button:hover { background: var(--ink-ghost); border-color: var(--ink); }

/* ---------- collapsible sections: instructions + generator ----------
   Free to grow: the panel above is the scroller, so an expanded section
   just extends past the fold and you scroll down to it. */
.sections {
  width: 100%;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
details.section {
  width: 100%;
  flex: 0 0 auto;
  border-top: 1px solid var(--ink-faint);
  padding-top: 0.9rem;
}
details.section summary {
  cursor: pointer;
  font-size: 10px;
  letter-spacing: 0.16em;
  color: var(--ink-dim);
  user-select: none;
  list-style: none;
}
details.section summary::-webkit-details-marker { display: none; }
details.section summary::before { content: "▸ "; }
details.section[open] summary::before { content: "▾ "; }
details.section summary:hover { color: var(--ink); }
details.section .body { display: grid; gap: 12px; padding-top: 14px; width: 100%; }

.rules { list-style: none; display: flex; flex-direction: column; gap: 0.7rem; }
.rules li { display: flex; flex-direction: column; gap: 2px; border-left: 2px solid var(--ink-faint); padding-left: 10px; }
.rules .title { font-size: 11px; letter-spacing: 0.06em; color: var(--ink); }
.rules .title::before { content: "→ "; color: var(--ink-dim); }
.rules .desc { font-size: 11px; line-height: 1.6; color: var(--ink-dim); }
.instructions-note { font-size: 10px; line-height: 1.7; color: var(--ink-dim); }
.instructions-note code { color: var(--ink); }

#genForm { display: flex; gap: 8px; }
#genForm input { flex: 1; background: var(--ink-ghost); border: 1px solid var(--ink-faint); border-radius: 8px; color: var(--ink); font-family: var(--mono); font-size: 13px; padding: 8px 10px; outline: none; transition: border-color 0.18s ease; }
#genForm input:focus { border-color: var(--ink-dim); }
.meter { display: flex; align-items: center; gap: 12px; font-size: 10px; letter-spacing: 0.08em; color: var(--ink-dim); }
.meterBar { flex: 1; height: 6px; background: var(--ink-ghost); border-radius: 3px; position: relative; max-width: 260px; }
.meterBar i { position: absolute; inset: 0 auto 0 0; background: var(--ink); border-radius: 3px; transition: width 0.15s; }
.meter.over .meterBar i { background: #ff5a3c; }
.meter.over { color: #ff5a3c; }
.tip { font-size: 10px; color: var(--ink-dim); }
.error { color: #ff5a3c; font-size: 11px; border: 1px solid rgba(255, 90, 60, 0.4); border-radius: 8px; padding: 8px 10px; }
.outputs { display: grid; gap: 8px; }
.outputs label { display: grid; grid-template-columns: 90px 1fr auto; gap: 8px; align-items: center; font-size: 10px; letter-spacing: 0.12em; color: var(--ink-dim); }
.outputs input, .outputs textarea { width: 100%; background: var(--ink-ghost); border: 1px solid var(--ink-faint); border-radius: 8px; color: var(--ink); font-family: var(--mono); font-size: 11px; padding: 6px 8px; resize: none; }
.outputs textarea { height: 44px; }
.hostNote { font-size: 10px; line-height: 1.5; color: var(--ink-dim); border-left: 2px solid var(--ink-faint); padding: 2px 0 2px 8px; }
.hostNote[hidden] { display: none; }
.hostNote code { color: var(--ink); }
.toneInfo { font-size: 9px; letter-spacing: 0.08em; color: var(--ink-dim); opacity: 0.7; }

/* ---------- touch controls ---------- */
.touchpad { display: none; flex: 0 0 auto; justify-content: space-between; align-items: center; width: 100%; gap: 20px; user-select: none; touch-action: none; }
body.touch .touchpad { display: flex; }
.dpad { display: grid; grid-template-columns: repeat(3, 48px); grid-template-rows: repeat(3, 48px); gap: 4px; }
.dpad button, .actions button { background: var(--ink-ghost); border: 1px solid var(--ink-faint); color: var(--ink); font-family: var(--pixel); font-size: 12px; border-radius: 8px; touch-action: none; }
.dpad button.down, .actions button.down { background: var(--ink-faint); }
.dpad [data-act="up"] { grid-column: 2; grid-row: 1; }
.dpad [data-act="left"] { grid-column: 1; grid-row: 2; }
.dpad [data-act="right"] { grid-column: 3; grid-row: 2; }
.dpad [data-act="down"] { grid-column: 2; grid-row: 3; }
.actions { display: flex; gap: 10px; }
.actions button { width: 64px; height: 64px; border-radius: 50%; font-size: 9px; }

/* ---------- footer ---------- */
footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.2rem;
  flex-shrink: 0;
}
footer a { display: flex; padding: 0.3rem; border-radius: 8px; transition: transform 0.18s ease, filter 0.18s ease; }
footer a:hover, footer a:focus-visible { transform: translateY(-3px); filter: drop-shadow(0 0 8px rgba(var(--hue-rgb), 0.7)); outline: none; }
footer svg { width: 20px; height: 20px; fill: var(--ink); }

@media (prefers-reduced-motion: reduce) {
  .crt-overlay::after, header h1, .cursor,
  .mode.auto, #scan.bad, #integrity.low i:not(.used) { animation: none; }
}

@media (max-width: 720px) {
  body { padding: 1.2vh 3vw; gap: 0.5rem; }
  .panel { border-radius: 18px; }
  .hud { gap: 10px; }
  .outputs label { grid-template-columns: 1fr auto; }
  .outputs label > span { grid-column: 1 / -1; }
  /* No width to spare for a side column — hints go back under the code, and
     the map gives up the height they need. The centring spacer must go: in a
     column the flex-basis would eat 240px of height. */
  .stage { flex-direction: column; height: calc(100% - 190px); gap: 0.6rem; }
  .stage::before { display: none; }
  /* Back to normal flow: stacked under the code there is no column height to
     centre against or pin to. */
  .side {
    flex: 0 0 auto;
    width: 100%;
    align-self: auto;
    position: static;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }
  .side .orbState, .side .controls-line { position: static; transform: none; }
  .orbState { text-align: center; }
  .controls-line { flex-direction: row; justify-content: space-between; align-items: center; flex-wrap: wrap; line-height: 1.4; }
}

/* Short viewports (landscape phones, small laptops in a browser with lots of
   chrome): the header/footer are the first things to give, since the map and
   the topline HUD are what the page is actually for. */
@media (max-height: 620px) {
  header .tagline { display: none; }
  .avatar { width: clamp(28px, 4vmin, 40px); }
  header h1 { font-size: clamp(0.85rem, 2.6vmin, 1.1rem); }
  footer { display: none; }
}
