/* The chassis. This file is the layout half of the dial board; the other half is
   CONFIG in src/config.js, which holds every constant the JavaScript reads.
   Geometry and type live here because they are layout, and mirroring them into
   both places would guarantee the two copies drift. */

:root {
  --bg: #000;

  /* Three levels of white and nothing else. The chrome is monochrome on purpose:
     the artwork is the only colour in the frame, apart from the Optics swatch,
     which is the artwork's own palette shown as a sample. */
  --ink-1: rgba(255, 255, 255, 0.92);   /* hero values, the stage bezel */
  --ink-2: rgba(255, 255, 255, 0.55);   /* ordinary values */
  --ink-3: rgba(255, 255, 255, 0.26);   /* labels, rules, meter troughs */

  --chassis-inset: clamp(10px, 1.8vmin, 26px);
  --rail-w: clamp(150px, 15vw, 208px);
  --gutter: clamp(10px, 1.6vmin, 22px);

  --bezel-out: 9px;      /* gap between the stage's rule and the outer hairline */
  --bezel-w: 2px;        /* the stage's own rule: heavier than anything else on screen */
  --bracket: 15px;       /* arm length of the corner brackets */

  --t-label: 9.5px;
  --t-value: 11px;
  --t-bar: 10.5px;
  --t-hero: 23px;
  --track: 0.16em;

  --stagger: 80ms;       /* per-panel delay as the rails come up */

  --mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", Menlo,
          Consolas, "Liberation Mono", monospace;
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background: var(--bg);
  overflow: hidden;
}

body {
  font-family: var(--mono);
  color: var(--ink-2);
  -webkit-font-smoothing: antialiased;
}

/* ---------- Chassis ---------- */

.chassis {
  position: relative;
  margin: var(--chassis-inset);
  height: calc(100% - var(--chassis-inset) * 2);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
}

/* The outer rule, split into a horizontal pair and a vertical pair so the boot
   can draw them in as two strokes rather than fading a box in. */
.chassis::before,
.chassis::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
}
.chassis::before {
  border-top: 1px solid var(--ink-2);
  border-bottom: 1px solid var(--ink-2);
  transform: scaleX(0);
}
.chassis::after {
  border-left: 1px solid var(--ink-2);
  border-right: 1px solid var(--ink-2);
  transform: scaleY(0);
  transition-delay: 140ms;
}
.chassis.boot-rule::before { transform: scaleX(1); }
.chassis.boot-rule::after  { transform: scaleY(1); }

/* ---------- Titlebar ---------- */

.titlebar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px var(--gutter);
  border-bottom: 1px solid var(--ink-3);
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.28em;
  color: var(--ink-3);
  opacity: 0;
  transition: opacity 320ms ease;
}
.boot-head .titlebar { opacity: 1; }

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--ink-1);
}
.brand-mark {
  width: 9px;
  height: 9px;
  border: 1px solid var(--ink-1);
  background: linear-gradient(135deg, var(--ink-1) 0 50%, transparent 50% 100%);
}
.brand-ver { letter-spacing: 0.12em; }

.link {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-left: auto;
}
.link-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--ink-3);
}
.boot-done .link-dot {
  background: var(--ink-1);
  animation: pulse 2s ease-in-out infinite;
}

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

/* ---------- Deck: rail | square | rail ---------- */

.deck {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr) var(--rail-w);
  min-height: 0;
}

.rail {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--gutter);
  padding: var(--gutter);
  min-width: 0;
}

/* ---------- The square ---------- */

/* container-type: size makes cqw/cqh resolve against this cell's *content* box,
   so the padding below is already excluded and the outer bezel hairline has
   somewhere to sit without overflowing. CSS sizes the square on its own; the
   ResizeObserver in main.js only reads the result, which is what keeps the first
   frame from flickering through a JS-measured size. */
.stage-cell {
  container-type: size;
  display: grid;
  place-items: center;
  padding: var(--bezel-out);
  min-width: 0;
  min-height: 0;
}

.stage {
  position: relative;
  width: min(100cqw, 100cqh);
  aspect-ratio: 1;
  border: var(--bezel-w) solid transparent;
  transition: border-color 420ms ease;
}
.boot-frame .stage { border-color: var(--ink-1); }

/* The second, lighter rule outside the first: two concentric strokes are what
   reads as a bezel rather than as a box. */
.bezel {
  position: absolute;
  inset: calc(var(--bezel-out) * -1);
  border: 1px solid var(--ink-3);
  pointer-events: none;
}

.bracket {
  position: absolute;
  width: var(--bracket);
  height: var(--bracket);
  transform: scale(0);
  transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
}
.boot-frame .bracket { transform: scale(1); }

.bracket-tl { top: -1px; left: -1px;  border-top: 2px solid var(--ink-1); border-left: 2px solid var(--ink-1);  transform-origin: top left; }
.bracket-tr { top: -1px; right: -1px; border-top: 2px solid var(--ink-1); border-right: 2px solid var(--ink-1); transform-origin: top right;    transition-delay: 60ms; }
.bracket-br { bottom: -1px; right: -1px; border-bottom: 2px solid var(--ink-1); border-right: 2px solid var(--ink-1); transform-origin: bottom right; transition-delay: 120ms; }
.bracket-bl { bottom: -1px; left: -1px;  border-bottom: 2px solid var(--ink-1); border-left: 2px solid var(--ink-1);  transform-origin: bottom left;  transition-delay: 180ms; }

.surface {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 500ms ease;
}
.boot-live .surface { opacity: 1; }
.surface canvas { display: block; }

/* ---------- Panels ---------- */

.panel {
  position: relative;
  min-width: 0;
  border: 1px solid var(--ink-3);
  padding: 15px 11px 11px;
  contain: layout style;
  opacity: 0;
  transform: translateY(5px);
  transition: opacity 380ms ease, transform 380ms cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i) * var(--stagger));
}
.boot-rails .panel { opacity: 1; transform: none; }

/* Notched into the top rule: the black background is what breaks the line. */
.panel-label {
  position: absolute;
  top: 0;
  left: 9px;
  transform: translateY(-50%);
  margin: 0;
  padding: 0 6px;
  background: var(--bg);
  font-size: var(--t-label);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-2);
}

.row {
  display: flex;
  align-items: center;
  gap: 7px;
  height: 17px;
}
.k {
  flex: 0 0 auto;
  width: 6.4ch;
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
}
/* tabular-nums plus margin-left:auto pins the right edge, so a digit changing
   width moves nothing but itself - and `contain` on the panel stops even that
   from reaching the page. */
.v {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: var(--t-value);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

/* .row.row-vec, not .row-vec: the narrow breakpoint re-declares .row's height,
   and at equal specificity the later rule would win and crush the three stacked
   components of the aim vector into one row's worth of space. */
.row.row-vec { height: auto; align-items: flex-start; }
.row-vec .k { padding-top: 2px; }
.vec {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
}
.vec .v { margin-left: 0; font-weight: 400; }

.metered .v { width: 5.2ch; text-align: right; }

.meter {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 72px;    /* a meter as wide as the panel stops reading as a gauge */
  display: flex;
  gap: 2px;
}
.meter i {
  flex: 1 1 0;
  height: 5px;
  background: var(--ink-3);
}
.meter i.on { background: var(--ink-1); }

.hero {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 6px;
  margin-bottom: 6px;
}
.hero-v {
  font-size: var(--t-hero);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--ink-1);
}
.hero-k {
  font-size: var(--t-label);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-3);
}

.swatch {
  height: 9px;
  margin-bottom: 9px;
  border: 1px solid var(--ink-3);
}

/* ---------- Statusbar ---------- */

.statusbar {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 8px var(--gutter);
  border-top: 1px solid var(--ink-3);
  font-size: var(--t-bar);
  opacity: 0;
  transition: opacity 340ms ease;
}
.boot-done .statusbar { opacity: 1; }

.statusbar .k {
  width: auto;
  margin-right: 5px;
  font-size: var(--t-label);
}
.statusbar .v { margin-left: 0; font-size: var(--t-bar); }

.run {
  display: flex;
  align-items: center;
  gap: 7px;
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--ink-1);
  font-size: var(--t-label);
}
/* A triangle when running, two bars when suspended, both drawn rather than typed
   so they sit on the baseline the same way at any size. */
.run-glyph {
  width: 0;
  height: 0;
  border-left: 6px solid var(--ink-1);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
}
.run-glyph.paused {
  width: 6px;
  height: 8px;
  border: 0;
  background: linear-gradient(90deg, var(--ink-1) 0 40%, transparent 40% 60%, var(--ink-1) 60% 100%);
}

.sb { display: flex; align-items: baseline; }
.sb-cam { margin-left: auto; gap: 3px; }
.sb-cam .v { margin-right: 9px; }
.sb-cam .v:last-child { margin-right: 0; }
/* Lit while the pointer is actually driving the camera. */
.sb-cam.active .v { color: var(--ink-1); }

.hint {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;   /* wrapping here grows the statusbar and shoves the deck up */
  overflow: hidden;
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-size: var(--t-label);
  color: var(--ink-3);
}
/* Breathes until the piece has been touched once, then stops for good: the one
   interaction is worth pointing at, and worth pointing at only until it lands. */
.hint.calling { animation: pulse 3.2s ease-in-out infinite; }
.hint.used { opacity: 0.35; }

/* ---------- Narrow ---------- */

/* On a screen this narrow the HUD is noise: the panels are too small to read
   and the square they flank has no room. Hiding the chrome and giving the
   artwork the whole frame is the only layout that works on a phone — the
   panels report on a machine that has nowhere to be seen. Fullscreen on a
   small screen is the view that matters; the readouts are for the desk. */
@media (max-width: 760px) {
  .titlebar, .rail, .statusbar { display: none; }

  .chassis {
    margin: 0;
    height: 100%;
    display: block;
  }

  .deck {
    display: block;
    height: 100%;
  }

  .stage-cell {
    padding: 0;
    height: 100%;
  }

  /* The bezel and brackets belong to the frame, not the artwork; with no
     frame around it the border reads as a stray line. Keep the stage's own
     border (it lights on boot) but drop the outer hairline. */
  .bezel { display: none; }
}

/* SKEW is the first thing to go: it is the slowest-moving number in the bar and
   the only one a viewer can lose without losing the sense of a running system. */
@media (max-width: 900px) {
  .statusbar { gap: 14px; }
  .sb-skew { display: none; }
}

/* The SKEW readout is the slowest-moving number in the bar and the only one
   a viewer can lose without losing the sense of a running system; it is the
   first to go on a narrow screen. Below 760px the HUD is gone entirely —
   see the breakpoint above. */

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .chassis::before, .chassis::after,
  .titlebar, .stage, .bracket, .surface, .panel, .statusbar {
    transition-duration: 200ms !important;
    transition-delay: 0s !important;
  }
  .link-dot, .hint.calling { animation: none !important; }
}
