/* Copyright (C) 2026 Douglas Riches — SPDX-License-Identifier: GPL-2.0-or-later */
/* web/styles.css */
/*
 * The chassis, ported from the Night Bench simulator onto a real framebuffer.
 *
 * Everything below is written against tokens rather than the 102 loose hex literals the source
 * carried, because the palette is the one part of this that is still undecided: the structure —
 * bezel, rockers, rail, keyboard geometry — is the same whichever direction wins, so a second
 * palette should cost a :root block and nothing else.
 *
 * Two rules that look like taste and are not. No Commodore logo art appears anywhere in this
 * file (spec §12: the name is used descriptively, the trade dress is not used at all). And
 * image-rendering is scoped to #c128-screen rather than `canvas`, so a later overlay canvas —
 * a CRT pass, a debug layer — is not silently nearest-neighboured too.
 */

:root {
  color-scheme: dark;

  /* Ground and panels. */
  --surface: #111211;
  --surface-raised: #1a1b18;
  --surface-glow: #332a22;
  --surface-sunken: #131512;

  /* Text on the dark surfaces. */
  --ink: #e8e5dc;
  --ink-dim: #8f9388;

  /* The moulded case: light faces, dark legends. Text on the chassis is a different pair from
     text on the bench, and mixing them is how a beige panel ends up with 3:1 body copy. */
  --chassis: #cbc5b4;
  --chassis-hi: #e4dfd1;
  --chassis-mid: #aaa596;
  --chassis-lo: #a8a394;
  --chassis-edge: #777268;
  --chassis-edge-hi: #f2eee2;
  --chassis-deep: #706c63;
  --ink-case: #242520;
  --ink-case-dim: #55544e;

  /* Hairlines. The soft one is for rules drawn on the chassis, where a solid line reads as a
     seam; the hard one is the instrument grid's ruling, which is a 1px gap showing through. */
  --hairline: #4b4c47;
  --hairline-soft: rgba(73, 71, 64, .28);

  /* Keycaps: two faces, dark and light, plus the shadow that makes them look pressable. */
  --key: #262724;
  --key-top: #40413d;
  --key-bottom: #1b1c1a;
  --key-edge: #10110f;
  --key-shadow: #111210;
  --legend: #ece8db;
  --legend-dim: #9e9f97;

  /* The tube. Only the dead screen's colour — everything lit is the machine's own pixels. */
  /* A powered-off CRT, not a blue field. The earlier values read as a screen showing something
     wrong rather than a machine that has not been switched on, which matters because that is the
     first thing anybody sees: the page opens with no firmware and nothing running. */
  --phosphor: #1b2220;
  --phosphor-mid: #141917;
  --phosphor-deep: #0a0d0c;

  /* State. One accent, and the two verdicts the firmware checklist needs. */
  --accent: #f1a947;
  --good: #68cf79;
  --bad: #ec554d;
  --focus: #fff1aa;

  --screen-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  --label-font: "Arial Narrow", "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; }

/* The UA rule behind the hidden attribute is a bare [hidden] selector, so any class here that
   sets display outranks it — which is how the dev-server button, hidden in the markup and left
   hidden by the probe that fails on a deployed page, still showed up on the panel. Restated at
   a specificity nothing in this file beats. */
[hidden] { display: none !important; }

/* Every min-width: 0 and minmax(0, …) in this file is load-bearing, not defensive noise: the
   keyboard is a fixed 46px-per-unit slab, and a grid or flex child defaults to min-content
   width. Without these the deck pushes the page wide and the whole document gains a horizontal
   scrollbar instead of the deck scrolling inside itself. */
html {
  min-width: 0;
  background: var(--surface);
}

body {
  margin: 0;
  min-width: 0;
  min-height: 100vh;
  overflow-x: hidden;
  color: var(--ink);
  font-family: var(--label-font);
  background:
    linear-gradient(100deg, transparent 0 47%, rgba(255,255,255,.012) 48% 49%, transparent 50%) 0 0 / 73px 100%,
    repeating-linear-gradient(7deg, rgba(255,255,255,.014) 0 1px, transparent 1px 7px),
    radial-gradient(ellipse at 50% -30%, var(--surface-glow) 0, var(--surface-raised) 42%, var(--surface) 78%);
}

button, input, select { font: inherit; }
button { -webkit-tap-highlight-color: transparent; }

button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible,
[tabindex]:focus-visible,
.file-picker:has(input:focus-visible) {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- the case ---------------------------------------------------------------------------- */

.bench {
  min-height: 100vh;
  padding: clamp(10px, 2vw, 28px);
}

.machine {
  width: min(1380px, 100%);
  margin-inline: auto;
  color: var(--ink-case);
  border: 1px solid #ede9dc;
  border-bottom-color: #504d46;
  border-radius: 7px 7px 12px 12px;
  padding: clamp(10px, 1.4vw, 20px);
  background:
    repeating-linear-gradient(92deg, rgba(255,255,255,.035) 0 1px, rgba(0,0,0,.02) 1px 4px),
    linear-gradient(175deg, var(--chassis-hi), var(--chassis) 45%, var(--chassis-lo) 100%);
  box-shadow:
    0 3px 0 #f7f3e7 inset,
    0 -5px 0 #777268 inset,
    0 25px 55px rgba(0,0,0,.55),
    0 4px 12px rgba(0,0,0,.62);
}

.machine-brand {
  min-height: 42px;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 16px;
  padding: 0 8px 10px;
  border-bottom: 1px solid var(--hairline-soft);
}

/* Deliberately not the logotype: normal case, normal tracking, one weight. The name identifies
   the hardware; it is not a mark. */
.machine-name {
  margin: 0;
  color: #373833;
  font-size: clamp(16px, 1.7vw, 22px);
  font-weight: 700;
  letter-spacing: .04em;
}

.machine-note {
  margin: 0;
  color: #66645d;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .1em;
  text-align: right;
}
.machine-note code { font: 700 10px/1 var(--screen-font); }

.top-deck {
  display: grid;
  grid-template-columns: minmax(0, 3.25fr) minmax(235px, .82fr);
  gap: clamp(10px, 1.25vw, 18px);
  padding-top: clamp(10px, 1.3vw, 18px);
}

.display-module {
  min-width: 0;
  padding: clamp(8px, 1vw, 14px);
  border: 1px solid var(--chassis-edge);
  border-top-color: var(--chassis-edge-hi);
  border-radius: 5px;
  background: linear-gradient(160deg, #bdb8aa, #989489);
  box-shadow: 0 2px 5px rgba(0,0,0,.35) inset, 0 1px 0 rgba(255,255,255,.75);
}

.display-labels {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin: 0 4px 6px;
  color: var(--ink-case-dim);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .16em;
}

/* --- the tube ---------------------------------------------------------------------------- */

.crt-bezel {
  /* The cap belongs here, not on the stage. Capping the stage let the bezel keep the module's
     full width while the picture shrank inside it, which on a short viewport put a slab of dead
     bezel around a small screen. The frame follows the picture at every size instead. */
  /* Shrink-wraps the picture. Now that the stage takes a whole multiple of the machine's own
     pixel size rather than the full width, a bezel that kept filling the module leaves a slab of
     dead frame around a smaller screen — the same gap that made the chassis look wrong before,
     arriving by a different route. */
  /* Capped at the picture's fitted size plus its own chrome, so the frame hugs the screen rather
     than leaving a slab of dead bezel around a smaller one. A px cap rather than fit-content:
     asking the frame how wide its child is, while the child is a percentage of the frame, is
     circular — it resolved to zero and the canvas disappeared. */
  /* One cap, and it is the fitter's. A second `70vh` arm here competed with it: when the viewport
     was short that arm won, the stage took whatever width it produced, and the whole-multiple
     result was discarded. The height constraint lives in fitFrame() now, where it can round. */
  max-width: min(100%, calc(var(--fitted-width, 200vw) + var(--bezel-chrome, 42px)));
  margin-inline: auto;
  padding: clamp(8px, 1.4vw, 19px);
  border: 2px solid #2c2e2c;
  border-bottom-width: 5px;
  border-radius: 19px 19px 26px 26px / 15px 15px 29px 29px;
  background: linear-gradient(145deg, #161817, #363936 42%, #121412);
  box-shadow:
    0 0 0 2px #686b64,
    0 4px 12px rgba(0,0,0,.75) inset,
    0 2px 0 rgba(255,255,255,.25);
}

/* The machine's reported aspect drives the box, and it changes live when the display option
   changes — a fixed 4/3 here would fight it. The fallback only ever applies before the first
   GEOMETRY message. */
.screen-stage {
  position: relative;
  aspect-ratio: var(--frame-aspect, 1.37);   /* the VDC's shape: what the page now boots into */
  /* The largest whole multiple of the machine's own pixel size that fits, written by the fit
     step. Nearest-neighbour upscaling only looks right at a whole multiple: at 1.7x some source
     rows land on two device rows and some on one, which reads as uneven stroke weight inside
     8-pixel glyphs. Falls back to filling the width before the machine has reported anything. */
  width: 100%;
  overflow: hidden;
  /* A gentle, fixed radius rather than a percentage. At 8%/7% the corners ate into the picture,
     and what they ate is the machine's own drawn border — part of the output, not a margin the
     chassis is entitled to crop. A CRT's corners are softer than its bezel's, and that is all
     this needs to say. */
  border-radius: 14px;
  isolation: isolate;
  background: radial-gradient(ellipse at center, var(--phosphor) 0, var(--phosphor-mid) 68%, var(--phosphor-deep) 100%);
  box-shadow:
    0 0 36px rgba(42,101,205,.28),
    0 0 24px rgba(0,0,0,.95) inset,
    0 0 3px #090e14 inset;
}

.screen-stage:focus-visible,
.screen-stage:focus-within {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

/* The drop target is the stage, and the overlays above it are pointer-transparent so crossing
   onto one does not read as leaving the stage. */
/* An idle machine says so. The stage is dark and empty before power-on, and without a word on
   it the page's first impression is a large black rectangle of unclear health. Hidden the moment
   the machine is powered, since after that the screen speaks for itself. */
.screen-stage::after {
  content: "NO SIGNAL — LOAD FIRMWARE AND POWER ON";
  position: absolute;
  inset: auto 0 8% 0;
  text-align: center;
  color: #4c5a55;
  font: 600 10px/1 var(--label-font);
  letter-spacing: .18em;
  pointer-events: none;
}
.screen-stage.is-live::after { content: none; }

.screen-stage.dragging {
  box-shadow: 0 0 0 3px var(--accent), 0 0 26px rgba(241,169,71,.35);
}

/* Zero safe area. VICE draws the machine's own border inside the frame — that border is part of
   the picture, and any inset here would crop it. The physical surround is the bezel's padding. */
#c128-screen {
  position: absolute;
  z-index: 2;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* The machine's pixels, not the browser's idea of a photograph. Scoped to this canvas so a
     later overlay canvas keeps its smoothing. */
  image-rendering: pixelated;
}

.glass-highlight,
.scanlines,
.screen-bloom {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
}

.glass-highlight {
  z-index: 4;
  background: linear-gradient(118deg, rgba(255,255,255,.09), transparent 16% 82%, rgba(255,255,255,.025));
}

/* Both properties come from the screen-fit observer: the pitch from the canvas's *rendered*
   height, so the stripes land on whole device rows, and the opacity from the display mode.
   Fixed-pitch scanlines beat against a nearest-neighbour framebuffer and drift as the window
   resizes — which is why the default here is 0, and an undecided overlay draws nothing. */
.scanlines {
  z-index: 5;
  opacity: var(--scanline-opacity, 0);
  background: repeating-linear-gradient(
    to bottom,
    rgba(255,255,255,.08) 0,
    rgba(255,255,255,.08) 1px,
    transparent 1px,
    transparent var(--scanline-pitch, 3px)
  );
}

/* z-index 3 is the free slot between the framebuffer and the glass. The bloom lives on its own
   overlay rather than on an ancestor of the canvas: a filter on the stage promotes a live
   framebuffer to its own layer and re-rasterises it every frame for as long as it is set. */
.screen-bloom {
  z-index: 3;
  opacity: 0;
  background: radial-gradient(ellipse at center, rgba(214,230,255,.85), rgba(120,170,255,.25) 60%, transparent 78%);
}
.screen-bloom.is-blooming { animation: phosphor-bloom 520ms ease-out both; }

@keyframes phosphor-bloom {
  0% { opacity: .85; }
  35% { opacity: .35; }
  100% { opacity: 0; }
}

/* --- machine actions --------------------------------------------------------------------- */

.display-actions {
  display: grid;
  grid-template-columns: repeat(3, minmax(68px, 1fr)) minmax(140px, 1.5fr);
  gap: 7px;
  margin-top: 12px;
}

.rocker,
.panel-button {
  min-height: 46px;
  border: 1px solid var(--key-edge);
  border-radius: 3px;
  color: var(--legend);
  background: linear-gradient(#383a36, #20211f);
  box-shadow: 0 3px 0 var(--key-edge), 0 1px 0 rgba(255,255,255,.13) inset;
  cursor: pointer;
  transition: transform 70ms ease, box-shadow 70ms ease, filter 120ms ease;
}
.rocker:not(:disabled):hover,
.panel-button:not(:disabled):hover { filter: brightness(1.13); }
.rocker:not(:disabled):active,
.panel-button:not(:disabled):active,
.rocker[aria-pressed="true"] {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--key-edge), 0 2px 6px rgba(0,0,0,.35) inset;
}
.rocker:disabled { cursor: default; filter: brightness(.72) saturate(.6); }
.rocker span,
.rocker small { display: block; }
.rocker span { font-size: 10px; font-weight: 850; letter-spacing: .08em; }
.rocker small { margin-top: 2px; color: #a7a99f; font-size: 7px; letter-spacing: .12em; }

.display-select {
  position: relative;
  display: grid;
  align-content: center;
  gap: 3px;
  min-height: 46px;
  padding: 4px 8px;
  border: 1px solid var(--chassis-edge);
  border-radius: 3px;
  background: linear-gradient(#dad5c5, var(--chassis-mid));
  box-shadow: 0 3px 0 var(--chassis-deep), 0 1px 0 #fff inset;
}
.display-select > span { display: block; color: #292a26; font-size: 9px; font-weight: 850; letter-spacing: .06em; }
.display-select > small { display: block; color: var(--ink-case-dim); font-size: 7px; letter-spacing: .12em; }
/* appearance: none is what lets the control take the case's colours, and it also removes the
   native caret — which has to be drawn back on, or the select reads as a text field and nobody
   opens it. Painted into the select's own background rather than onto the label, so it stays
   put whatever the label's stacked text does to the box. */
.display-select select {
  appearance: none;
  width: 100%;
  padding: 3px 22px 3px 5px;
  border: 1px solid var(--chassis-deep);
  border-radius: 2px;
  color: #1d1e1b;
  background-color: #eae5d6;
  background-image:
    linear-gradient(45deg, transparent 50%, #4a4a44 50%),
    linear-gradient(135deg, #4a4a44 50%, transparent 50%);
  background-position: calc(100% - 13px) calc(50% - 1px), calc(100% - 8px) calc(50% - 1px);
  background-size: 5px 5px;
  background-repeat: no-repeat;
  font: 700 10px/1.3 var(--screen-font);
  cursor: pointer;
}

.display-hint {
  margin: 9px 3px 0;
  color: var(--ink-case-dim);
  font-size: 10px;
  line-height: 1.45;
  letter-spacing: .02em;
}

/* --- instrument rail --------------------------------------------------------------------- */

.instrument-rail {
  min-width: 0;
  padding: 11px;
  border: 1px solid #777269;
  border-top-color: #f0ecdf;
  border-radius: 4px;
  background: linear-gradient(150deg, #b7b2a4, #969287);
  box-shadow: 0 2px 6px rgba(0,0,0,.25) inset, 0 1px 0 rgba(255,255,255,.7);
}

.rail-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 3px 9px;
  border-bottom: 1px solid #747168;
}
.rail-title {
  font-size: 9px;
  font-weight: 900;
  letter-spacing: .16em;
}

.rail-status {
  margin: 9px 3px;
  color: var(--ink-case);
  font-size: 11px;
  line-height: 1.45;
}

/* gap: 1px over a ruled background is the ruleline — the cells are opaque and the grid's own
   colour shows through the gaps, so there are no per-cell borders to double up at the seams. */
.instrument-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  margin: 10px 0 12px;
  border: 1px solid var(--hairline);
  background: var(--hairline);
}
.instrument-grid div { min-width: 0; padding: 6px; background: #1f211f; }
/* No dashes: a cell with no reading is not a cell. The rail says what the machine has said and
   nothing else, and an em dash is a claim that there is a value to show. */
.instrument-grid div:has(dd:empty) { display: none; }
.instrument-grid dt { color: var(--ink-dim); font-size: 7px; font-weight: 800; letter-spacing: .14em; }
.instrument-grid dd {
  margin: 2px 0 0;
  overflow: hidden;
  color: #e5e7dc;
  font: 650 11px/1.2 var(--screen-font);
  font-variant-numeric: tabular-nums;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- firmware bay ------------------------------------------------------------------------ */

.firmware-bay,
.software-bay {
  padding: 9px;
  border: 1px solid #363733;
  border-radius: 2px;
  color: #dddcd4;
  background: linear-gradient(#30322f, #20221f);
  box-shadow: 0 1px 0 #5a5c56 inset, 0 4px 8px rgba(0,0,0,.32);
}
.bay-face {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 6px;
}
.bay-name { font-size: 9px; font-weight: 900; letter-spacing: .16em; }
.bay-number { color: var(--ink-dim); font-size: 7px; letter-spacing: .13em; }

.bay-controls {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 5px;
  margin-top: 9px;
}
.panel-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  margin: 0;
  padding: 4px 6px;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: .07em;
  text-align: center;
}
.file-picker { position: relative; }

.firmware-list {
  max-height: 168px;
  margin: 7px 0 0;
  padding: 5px;
  overflow: auto;
  border: 1px solid #3d3f38;
  background: var(--surface-sunken);
  list-style: none;
  font: 9px/1.5 var(--screen-font);
}
.firmware-list:empty { display: none; }
.firmware-list li {
  position: relative;
  padding: 1px 0 1px 13px;
  color: var(--ink-dim);
}
.firmware-list li::before { position: absolute; left: 0; }
.firmware-list li.present { color: var(--ink); }
.firmware-list li.present::before { content: "✓"; color: var(--good); }
.firmware-list li.missing::before { content: "·"; color: var(--bad); }
.firmware-list li.optional::before { content: "·"; }

/* The software bay reuses the firmware bay's shell above; below is only what is its own. */
.software-bay { margin-top: 9px; }
.program-list {
  margin: 7px 0 0;
  padding: 0;
  list-style: none;
}
.program-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  align-items: center;
  padding: 6px 0;
  border-top: 1px solid #3d3f38;
}
.program-row:first-child { border-top: 0; padding-top: 2px; }
.program-text { display: grid; gap: 2px; min-width: 0; }
.program-title { color: var(--ink); font-size: 10px; font-weight: 850; letter-spacing: .08em; }
.program-summary,
.program-controls { color: var(--ink-dim); font-size: 8px; line-height: 1.45; }
.program-list .panel-button { width: auto; min-height: 34px; padding: 4px 10px; }
.program-list .panel-button:disabled { cursor: default; filter: brightness(.72) saturate(.6); }
.bay-hint {
  margin: 6px 0 0;
  color: var(--ink-dim);
  font-size: 9px;
  line-height: 1.45;
  letter-spacing: .02em;
}
/* With no rows there is nothing to offer: a bay that advertised programs and listed none would be
   what a visitor with scripting blocked saw. Hidden like the empty firmware list and the empty
   keyboard deck are, and self-correcting the moment the host renders a row. */
.software-bay:has(.program-list:empty) { display: none; }

/* --- cheat strip and keyboard deck -------------------------------------------------------- */

/* Static: these are the ROM's own KEY defaults, not a readout, so it carries no live region —
   an aria-live on a strip whose text is rewritten wholesale is a chatter generator waiting for
   the first render that changes a character. */
.cheat-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 30px;
  margin: 12px 2px 5px;
  padding: 4px 9px;
  overflow: hidden;
  border: 1px dashed #898478;
  color: #4c4b45;
  background: rgba(240,236,223,.32);
  font-size: 8px;
  font-weight: 750;
  letter-spacing: .08em;
  white-space: nowrap;
}
.cheat-strip > span { font-weight: 900; }
kbd { padding: 1px 3px; border: 1px solid #88847b; border-radius: 2px; font: 700 7px/1 var(--screen-font); background: #d7d2c3; }

/* All three of these matter. overflow-x: auto and width: max-content are what let a 46px-unit
   keyboard scroll inside its own deck instead of widening the document; overscroll-behavior-inline
   is what stops a horizontal trackpad swipe over the keys from triggering browser
   back-navigation and losing the session. */
.keyboard-deck {
  min-width: 0;
  max-width: 100%;
  padding: clamp(8px, 1vw, 14px);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-inline: contain;
  border: 1px solid var(--chassis-edge);
  border-top-color: #f3efe3;
  border-radius: 4px 4px 8px 8px;
  background: linear-gradient(#bab5a7, #9e9a8f);
  box-shadow: 0 2px 5px rgba(0,0,0,.25) inset, 0 1px 0 #eeeade;
}
/* The deck is generated. Until it is, an empty slab of chassis is furniture that does nothing. */
.keyboard-deck:has(> .virtual-keyboard:empty) { display: none; }

/* --key-unit and --key-gap, --key-width/--key-height per cap and --row-offset per row are a
   contract with the keyboard generator: it writes them inline, this file consumes them. Change
   a name here and the deck silently loses its geometry. */
.virtual-keyboard {
  --key-unit: 46px;
  --key-gap: 4px;
  display: grid;
  width: max-content;
  min-width: max-content;
  gap: 13px;
  padding: 3px 2px 7px;
}

.keyboard-top-clusters,
.keyboard-lower-deck {
  display: flex;
  width: max-content;
  align-items: flex-start;
}

.keyboard-top-clusters {
  width: 100%;
  gap: 18px;
  align-items: stretch;
}

.keyboard-lower-deck { gap: 28px; }

.keyboard-block {
  min-width: max-content;
  margin: 0;
  border: 0;
  padding: 0;
}

.keyboard-functions { margin-left: auto; }

.keyboard-top-clusters .keyboard-block {
  display: flex;
  align-items: center;
  padding: 4px;
  border: 1px solid rgba(91,88,80,.28);
  border-radius: 4px;
  background: rgba(225,220,205,.16);
  box-shadow: 0 1px 0 rgba(255,255,255,.32) inset;
}

.keyboard-row {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--key-gap);
  margin-left: calc(var(--row-offset, 0) * var(--key-unit));
}

.keyboard-main {
  display: grid;
  gap: var(--key-gap);
}

.keyboard-keypad {
  display: grid;
  grid-template-columns: repeat(4, var(--key-unit));
  grid-auto-rows: var(--key-unit);
  grid-auto-flow: row;
  gap: var(--key-gap);
  padding-left: 18px;
  border-left: 2px groove rgba(112,108,99,.5);
}

/* The gap has to be inside the width or a 1.8u RETURN stops lining up with the row above it:
   n units of cap plus the n-1 gaps they span. On the keypad --key-width means grid columns
   instead of units, which is why that block overrides the width below. */
.virtual-key {
  position: relative;
  width: calc(
    (var(--key-width, 1) * var(--key-unit)) +
    ((var(--key-width, 1) - 1) * var(--key-gap))
  );
  min-width: 44px;
  min-height: 44px;
  height: var(--key-unit);
  flex: 0 0 auto;
  padding: 4px 2px;
  border: 1px solid var(--key-edge);
  border-radius: 3px 3px 4px 4px;
  color: var(--legend);
  background: linear-gradient(150deg, var(--key-top) 0, var(--key) 40%, var(--key-bottom) 100%);
  box-shadow: 0 3px 0 var(--key-shadow), 0 1px 0 rgba(255,255,255,.12) inset;
  cursor: pointer;
  font: 700 9px/1.02 var(--screen-font);
  text-align: center;
  text-transform: uppercase;
  /* A cap is a key, not a link: no double-tap zoom delay, and no text selection when a finger
     slides off one during a fast run of typing. */
  touch-action: manipulation;
  user-select: none;
  transition: transform 65ms ease, box-shadow 65ms ease, filter 100ms ease;
}

.keyboard-keypad .virtual-key {
  width: auto;
  height: auto;
  grid-column: span var(--key-width, 1);
  grid-row: span var(--key-height, 1);
}

.virtual-key:hover { filter: brightness(1.18); }
.virtual-key:active,
.virtual-key.is-active,
.virtual-key[aria-pressed="true"] {
  transform: translateY(2px);
  box-shadow: 0 1px 0 var(--key-shadow), 0 2px 5px #000 inset;
}
.virtual-key .key-primary-legend { display: block; }
.virtual-key small {
  display: block;
  margin-top: 2px;
  color: var(--legend-dim);
  font: 650 6px/1 var(--label-font);
  letter-spacing: .025em;
}
.virtual-key .key-secondary-legend { font-size: 5.5px; }
.virtual-key.special {
  color: #292a26;
  border-color: var(--chassis-edge);
  background: linear-gradient(150deg, #d2cdbf, var(--chassis-mid));
  box-shadow: 0 3px 0 var(--chassis-deep), 0 1px 0 #fff inset;
}
.virtual-key.special small { color: #55564f; }
.virtual-key.danger { color: #ffe0d6; background: linear-gradient(150deg, #6e3b34, #352522); }
.virtual-key.space { letter-spacing: .16em; }

/* --- machine log --------------------------------------------------------------------------- */

.log-bay {
  width: min(1380px, 100%);
  margin: 14px auto 0;
  padding: 12px 15px;
  border: 1px solid #32342f;
  border-radius: 4px;
  color: #bfc2b8;
  background: rgba(16,18,16,.78);
}
.log-bay h2 { margin: 0; color: #e7e8e2; font-size: 11px; letter-spacing: .13em; text-transform: uppercase; }
.log-bay p { margin: 4px 0 8px; color: #858a80; font-size: 10px; }

.machine-log {
  max-height: 168px;
  margin: 0;
  padding: 7px;
  overflow: auto;
  border: 1px solid #3d3f38;
  background: var(--surface-sunken);
  color: #a8b1a3;
  font: 11px/1.45 var(--screen-font);
  white-space: pre-wrap;
}
.machine-log.has-error { border-color: var(--bad); }

/* --- colophon ------------------------------------------------------------------------------ */

.colophon {
  width: min(1380px, 100%);
  margin: 14px auto;
  padding: 0 clamp(10px, 2vw, 28px) clamp(10px, 2vw, 28px);
  color: #7d8078;
  font-size: 11px;
  line-height: 1.6;
}
.colophon p { margin: 0 0 6px; max-width: 78ch; }
.colophon a { color: #b9bcb2; }
.colophon code { font: 11px/1 var(--screen-font); }

noscript {
  display: block;
  width: min(1380px, 100%);
  margin: 0 auto;
  padding: 0 clamp(10px, 2vw, 28px) clamp(10px, 2vw, 28px);
  color: var(--accent);
  font-size: 12px;
}

/* --- narrow ------------------------------------------------------------------------------- */

@media (max-width: 820px) {
  .bench { padding: 8px; }
  .machine { padding: 9px; }
  .machine-note { text-align: left; }
  .top-deck { grid-template-columns: minmax(0, 1fr); }
  .instrument-rail { display: grid; grid-template-columns: minmax(0, 1fr) minmax(160px, .8fr); gap: 9px; }
  .rail-header,
  .rail-status { grid-column: 1 / -1; }
  .rail-status { margin: 0 3px; }
  .instrument-grid { margin: 0; }
  .firmware-bay,
  .software-bay { min-width: 0; }
  /* A third child of a two-column grid: full width under the other two, and no margin of its own
     on top of the grid's gap — the same reset .instrument-grid gets above. */
  .software-bay { grid-column: 1 / -1; margin-top: 0; }
  .display-actions { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .display-select { grid-column: span 3; }
  .cheat-strip { overflow-x: auto; }
  .keyboard-deck { scrollbar-width: thin; }
}

@media (max-width: 480px) {
  .machine-brand { min-height: 34px; }
  .machine-name { font-size: 15px; }
  .machine-note { font-size: 8px; }
  .display-module { padding: 6px; }
  .crt-bezel { padding: 7px; border-radius: 12px 12px 17px 17px; }
  .display-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .display-select { grid-column: 1 / -1; }
  .instrument-rail { grid-template-columns: 1fr; }
  .instrument-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .instrument-grid div { padding: 5px 4px; }
  .instrument-grid dd { font-size: 9px; }
  .virtual-key { font-size: 8px; }
  .log-bay { padding: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  /* The source also pinned its blinking cursor here. That accommodation cannot be honoured any
     more: VICE's cursor blinks inside the framebuffer, where CSS cannot reach it, and the only
     fix is at the emulator level. Recorded rather than quietly dropped. */
}

/* The firmware bay once it has collapsed: a statement of fact and a way to undo it, nothing
   more. Sized down deliberately — it should read as a label on the machine, not a control. */
.bay-summary {
  margin: .5rem 0 0;
  color: var(--legend-dim, #8b8d84);
  font: 600 9px/1.5 var(--label-font);
  letter-spacing: .1em;
  text-transform: uppercase;
}
.panel-button.subtle {
  margin-top: .4rem;
  padding: .3rem .5rem;
  font-size: 8px;
  opacity: .75;
}
.panel-button.subtle:hover { opacity: 1; }
