/* Two themes, one stylesheet. destruction is primary black, reconstruction
   is primary white, the whole palette is tokenised so the only difference
   between the two documents is `class="light"` on <body>. Anything that was
   a bare #fff or #000 in here is now --strong / --inverse, which swap
   together; a hardcoded colour would survive the flip and look wrong on one
   side. */
:root {
  --sidebar: 348px;
  --bg: #000000;
  --panel: #000000;
  --border: #2a2a2a;
  --border-soft: #1c1c1c;
  --text: #e1e1e1;
  --muted: #767676;
  --hover: #191919;

  --strong: #ffffff;        /* max-contrast text and accent fills */
  --inverse: #000000;       /* text sitting on top of a --strong fill */
  --strong-dim: #d8d8d8;    /* --strong, hovered */
  --faint: #3a3a3a;         /* chevrons, separators */
  --faint2: #4a4a4a;        /* group headings, focus rings */
  --track: #1c1c1c;         /* unfilled part of a progress bar */
  --fill: #6a6a6a;          /* filled part of a progress bar */
  --rail: #141414;          /* timeline groove */
  --rail-2: #2f2f2f;        /* kept region of the timeline */
  --knob: #555555;          /* switch knob, off */
  --stage: #000000;         /* letterbox behind the canvas */
  --danger: #d08a8a;
  --warn: #e8c15a;          /* the "this one is expensive" mark */

  --scrim: rgba(0, 0, 0, 0.92);
  --scrim-note: rgba(0, 0, 0, 0.72);
  --scrim-gate: rgba(0, 0, 0, 0.94);
  --scrim-modal: rgba(0, 0, 0, 0.7);
  --scrim-tag: rgba(0, 0, 0, 0.75);
}

body.light {
  --bg: #ffffff;
  --panel: #ffffff;
  --border: #d0d0d0;
  --border-soft: #e6e6e6;
  --text: #1a1a1a;
  --muted: #767676;
  --hover: #f1f1f1;

  --strong: #000000;
  --inverse: #ffffff;
  --strong-dim: #333333;
  --faint: #c2c2c2;
  --faint2: #a6a6a6;
  --track: #e3e3e3;
  --fill: #8c8c8c;
  --rail: #ececec;
  --rail-2: #cdcdcd;
  --knob: #b3b3b3;
  --stage: #f4f4f4;
  --danger: #a83232;
  --warn: #8a6400;          /* darker, the light page needs contrast on white */

  --scrim: rgba(255, 255, 255, 0.92);
  --scrim-note: rgba(255, 255, 255, 0.8);
  --scrim-gate: rgba(255, 255, 255, 0.94);
  --scrim-modal: rgba(255, 255, 255, 0.72);
  --scrim-tag: rgba(255, 255, 255, 0.8);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* the arrow carries the meaning visually; screen readers get words */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

html, body { height: 100%; }

body {
  background: var(--bg);
  color: var(--text);
  font: 13.5px/1.5 ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
  -webkit-tap-highlight-color: transparent;
}

header {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; justify-content: space-between;
  height: 48px;
  padding: 0 16px;
  background: var(--scrim);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border-soft);
}
/* ---- brand as the tool switch ----
   The whole brand is the control: logo, wordmark and arrow all navigate to
   the other tool. Plain <a>, not JS routing, a hard navigation drops the
   outgoing tool's WebGL/WebGPU context and decoded video instead of leaving
   them resident while the other one runs. */
.brand {
  display: flex; align-items: center; gap: 10px; min-width: 0;
  color: inherit; text-decoration: none;
}
a.brand { cursor: pointer; }
a.brand:hover h1 { color: var(--strong); }
.brand-arrow {
  flex: 0 0 auto;
  font-size: 15px; line-height: 1;
  color: var(--muted);
  transition: transform 0.16s ease, color 0.16s;
}
/* nudge in the direction of travel, so the arrow reads as "go there"
   rather than as decoration */
a.brand:hover .brand-arrow { color: var(--strong); transform: translateX(3px); }
a.brand.back:hover .brand-arrow { transform: translateX(-3px); }
.logo {
  width: 20px; height: 20px; flex: 0 0 auto;
  border-radius: 4px; object-fit: cover;
}
h1 {
  font-size: 14px; font-weight: 500; letter-spacing: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.header-actions { display: flex; gap: 8px; flex: 0 0 auto; }

/* the wordmark is the thing being switched, so on a phone give the room to
   it rather than to the buttons, it still ellipsises, but with more of the
   name left. The arrow never shrinks (flex: 0 0 auto); losing it would take
   the switch affordance with it. */
@media (max-width: 719px) {
  header { padding: 0 12px; }
  header .btn { padding: 5px 10px; font-size: 12.5px; }
  .brand { gap: 8px; }
}

/* Below ~500px the full wordmark can't fit, and ellipsising it eats
   "destruction"/"reconstruction", the one word that differs between the
   two tools, and the whole point of the switch. Drop the shared prefix
   instead so the changing word survives intact. */
@media (max-width: 500px) {
  .wm-prefix { display: none; }
}

.btn {
  display: inline-block;
  padding: 7px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer; user-select: none;
  text-transform: lowercase;
}
.btn:hover { background: var(--hover); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--strong); border-color: var(--strong); color: var(--inverse); }
.btn.primary:hover { background: var(--strong-dim); }
.btn.small { padding: 5px 10px; font-size: 12.5px; }
.btn[disabled] { opacity: 0.4; pointer-events: none; }

/* export progress: a loading bar that wipes left→right across the button */
#exportBtn.exporting {
  --p: 0%;
  background: linear-gradient(90deg, var(--fill) 0 var(--p), var(--track) var(--p) 100%);
  border-color: var(--fill);
  color: var(--strong);
  opacity: 1;
  min-width: 62px;
  text-align: center;
  transition: none;
}
#exportBtn.exporting:hover { background: linear-gradient(90deg, var(--fill) 0 var(--p), var(--track) var(--p) 100%); }

main { max-width: 680px; margin: 0 auto; padding: 14px 14px 48px; }

/* The desktop layout needs stage + master panel in one grid cell, but a
   sticky element is clipped to its PARENT's box, with a real wrapper the
   phone preview stopped sticking the moment the wrapper ended, i.e. right
   where the effects list starts. display:contents removes the wrapper box
   entirely, so .stage sticks against <main> for the whole page as it did
   before. The wrapper becomes a real box again only on desktop. */
.preview-col { display: contents; }

/* the stage (preview + video controls) stays pinned under the header so
   the live view is visible while scrolling the effect controls */
.stage {
  position: sticky; top: 48px; z-index: 5;
  background: var(--bg);
  padding-bottom: 8px;
}

.canvas-wrap {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--stage);
}
.canvas-wrap.clickable { cursor: pointer; }
#glCanvas {
  display: block; width: 100%; height: auto;
  max-height: 46vh; object-fit: contain;
  margin: 0 auto;
}
/* the splat viewer is a free camera, so it has no source aspect to honour,
   it gets a fixed frame instead of sizing itself to the media */
#splatCanvas {
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 9;
  max-height: 46vh;
  margin: 0 auto;
  cursor: grab;
  touch-action: none;
}
#splatCanvas:active { cursor: grabbing; }

/* WebGPU gate: covers the viewer entirely, because there is nothing
   partially usable to fall back to */
.gpu-gate {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim-gate);
  padding: 20px; text-align: center;
  z-index: 3;
}
.gpu-gate[hidden] { display: none; }
/* when gated there is nothing to look at, so the panel sizes to the warning
   rather than being clipped by the viewer's 16:9 box, on a phone the copy
   is taller than the box it would otherwise be trapped in */
.canvas-wrap.gated #splatCanvas,
.canvas-wrap.gated .canvas-note { display: none; }
.canvas-wrap.gated .gpu-gate { position: static; }
.gate-inner { max-width: 460px; }
.gate-icon { font-size: 26px; margin-bottom: 8px; }
.gpu-gate h2 {
  font-size: 14px; font-weight: 500; color: var(--strong);
  text-transform: lowercase; margin-bottom: 8px;
}
.gpu-gate p { font-size: 12px; line-height: 1.55; color: var(--text); }
.gpu-gate .gate-sub { margin-top: 8px; color: var(--muted); font-size: 11.5px; }

.canvas-note {
  position: absolute; left: 8px; bottom: 8px;
  font-size: 11px; color: var(--muted);
  background: var(--scrim-note);
  padding: 2px 7px; border-radius: 6px;
  pointer-events: none;
}

.video-bar[hidden] { display: none; }
.video-bar {
  margin-top: 10px;
  padding: 4px 12px 9px;
  border: 1px solid var(--border);
  border-radius: 10px;
}
.vb-row { display: flex; align-items: center; gap: 10px; }
.vb-spacer { flex: 1; }
.time-label { font-size: 12px; color: var(--muted); white-space: nowrap; }
.trim-label {
  font-size: 11px; color: var(--muted); white-space: nowrap;
  border: 1px solid var(--border-soft); border-radius: 5px;
  padding: 1px 6px;
}
.trim-label[hidden] { display: none; }

/* ---- timeline with trim handles ----
   the track is inset so the handles have room to sit at 0% / 100%
   without being clipped */
.timeline {
  position: relative;
  padding: 14px 9px 12px;
  touch-action: none;
  cursor: pointer;
}
.tl-track {
  position: relative; height: 6px;
  border-radius: 999px;
  background: var(--rail);
  border: 1px solid var(--border-soft);
}
/* the kept region, everything outside it reads as trimmed away */
.tl-range {
  position: absolute; top: -1px; bottom: -1px;
  background: var(--rail-2);
  border-radius: 2px;
}
.tl-progress {
  position: absolute; top: -1px; bottom: -1px;
  background: var(--strong);
  border-radius: 2px;
}
.tl-handle {
  position: absolute; top: 50%; left: 0;
  width: 9px; height: 22px;
  transform: translate(-50%, -50%);
  border-radius: 3px;
  background: var(--inverse);
  border: 1px solid var(--strong);
  cursor: ew-resize;
  touch-action: none;
}
/* fat invisible hit area so the handles are grabbable on touch */
.tl-handle::after {
  content: ""; position: absolute; inset: -10px -9px;
}
.tl-handle:hover, .tl-handle.dragging { background: var(--strong); }
.tl-head {
  position: absolute; top: 50%; left: 0;
  width: 11px; height: 11px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--strong);
  pointer-events: none;
  box-shadow: 0 0 0 2px var(--inverse);
}

/* play/pause icon button */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 30px; padding: 0; flex: 0 0 auto;
}
.icon-btn svg { width: 15px; height: 15px; display: block; fill: currentColor; }

/* rows shown only when a video is loaded (trim controls) */
.video-only { display: none; }

/* high-resolution warning popup */
.modal-overlay {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  background: var(--scrim-modal);
  backdrop-filter: blur(3px);
}
.modal-overlay[hidden] { display: none; }
.modal {
  width: 100%; max-width: 340px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 22px 20px 18px;
  text-align: center;
}
.modal-icon { font-size: 30px; line-height: 1; margin-bottom: 10px; }
.modal-text {
  font-size: 13px; line-height: 1.5; color: var(--text);
  margin-bottom: 16px;
}
.modal .btn { width: 100%; padding: 9px; }

/* a second choice below the primary one, same full-width shape */
.modal .btn + .btn { margin-top: 8px; }

/* persistent opt-out, centred to sit with the rest of the dialog */
.modal-check {
  display: inline-flex; align-items: center; gap: 8px;
  margin-bottom: 14px;
  font-size: 12.5px; color: var(--muted);
  cursor: pointer; user-select: none;
}
.modal-check:hover { color: var(--text); }
.modal-check input {
  width: 15px; height: 15px; flex: 0 0 auto;
  accent-color: var(--strong);
  cursor: pointer;
}
.video-bar select {
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 4px 6px; font: inherit; font-size: 12px;
}

.export-status {
  margin-top: 8px;
  font-size: 12.5px; color: var(--muted);
  padding: 0 2px;
}

.master-panel { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
.controls { margin-top: 14px; display: flex; flex-direction: column; gap: 8px; }

/* sidebar toolbar: how many effects are live, and a way out of all of them */
.fx-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 2px 2px 6px;
}
.fx-bar .fx-title {
  font-size: 11px; letter-spacing: 1px; text-transform: lowercase;
  color: var(--muted);
}
.fx-bar .fx-count {
  font-size: 11px; color: var(--muted);
  border: 1px solid var(--border-soft); border-radius: 5px;
  padding: 0 6px;
  font-variant-numeric: tabular-nums;
}
.fx-bar .fx-count.live { color: var(--strong); border-color: var(--border); }
.fx-bar .fx-spacer { flex: 1; }
.fx-bar .btn { padding: 3px 9px; font-size: 11.5px; border-radius: 7px; }

/* group headings break the 11 effects into scannable runs */
.fx-group {
  font-size: 10.5px; letter-spacing: 1.5px; text-transform: lowercase;
  color: var(--faint2);
  padding: 10px 2px 2px;
}
.fx-group:first-child { padding-top: 2px; }

.card-reset {
  margin-top: 10px;
  padding: 3px 9px; font-size: 11.5px; border-radius: 7px;
  color: var(--muted);
}

.card {
  background: var(--panel);
  border: 1px solid var(--border-soft);
  border-radius: 10px;
  overflow: hidden;
}
.card-head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px;
  cursor: pointer; user-select: none;
}
.card-head:hover { background: var(--hover); }
.card-head .step {
  font-size: 10.5px; color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0px 5px; min-width: 18px; text-align: center;
}
.card-head .title {
  flex: 1; font-size: 13px; text-transform: lowercase; color: var(--muted);
}
.card.enabled .card-head .title { color: var(--strong); }

/* the one deliberate splash of colour on the page: the effects that will
   cost you frames. Just the mark, in the page's own font, no badge around
   it, so it reads as punctuation on the name rather than as a control. */
.card-head .heavy {
  flex: 0 0 auto;
  color: var(--warn);
  font-size: 14px; font-weight: 700; line-height: 1;
  cursor: help;
}
/* pipeline position + reorder arrows, only present while the effect is on */
.card-head .ord {
  font-size: 11px; color: var(--strong);
  min-width: 12px; text-align: right;
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}
.card-head .ord[hidden] { display: none; }
.ord-box { display: flex; flex-direction: column; gap: 2px; flex: 0 0 auto; }
.ord-box[hidden] { display: none; }
.ord-btn {
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 12px; padding: 0;
  background: var(--bg); color: var(--muted);
  border: 1px solid var(--border-soft); border-radius: 3px;
  font: inherit; font-size: 7px; line-height: 1;
  cursor: pointer;
}
.ord-btn:hover:not(:disabled) { background: var(--hover); color: var(--strong); }
.ord-btn:disabled { opacity: 0.22; cursor: default; }

.card-head .chev { color: var(--faint); transition: transform 0.15s; font-size: 14px; }
.card.open .chev { transform: rotate(90deg); }
.card-body { display: none; padding: 2px 12px 12px; border-top: 1px solid var(--border-soft); }
.card.open .card-body { display: block; }

.subgroup {
  margin-top: 8px; padding: 4px 10px;
  border: 1px solid var(--border-soft);
  border-radius: 8px;
}
.subgroup.open { padding-bottom: 8px; }
.subgroup > .sub-title {
  display: flex; align-items: center; gap: 6px;
  font-size: 10.5px; letter-spacing: 1px; text-transform: lowercase;
  color: var(--muted);
  padding: 4px 0;
  cursor: pointer; user-select: none;
}
.subgroup > .sub-title:hover { color: var(--strong); }
.sub-chev {
  display: inline-block; color: var(--faint); font-size: 12px;
  transition: transform 0.15s;
}
.subgroup.open .sub-chev { transform: rotate(90deg); }
.sub-body { display: none; }
.subgroup.open .sub-body { display: block; }

.row { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.row label.name {
  flex: 0 0 148px;
  font-size: 12.5px; color: var(--text); text-transform: lowercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.row .val {
  flex: 0 0 46px; text-align: right;
  font-size: 11.5px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.row input[type="range"] { flex: 1; min-width: 0; accent-color: var(--strong); height: 24px; }
.row select {
  flex: 1;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 8px; font: inherit; font-size: 12.5px;
  text-transform: lowercase;
}

.row input[type="text"] {
  flex: 1; min-width: 0;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 8px; font: inherit; font-size: 12px;
}
.row input[type="text"]:focus { outline: none; border-color: var(--faint2); }

/* a view switch, not a parameter: it changes what you're looking at rather
   than what the effect does, so it gets a rule above it to sit apart from
   the controls it previews */
.row.mask-view {
  margin-top: 4px; padding-top: 10px;
  border-top: 1px solid var(--border-soft);
}
.row.mask-view label.name { color: var(--warn); }

/* explanatory copy under a control; deliberately quieter than a label */
.hint {
  font-size: 11.5px; line-height: 1.5; color: var(--muted);
  padding: 6px 2px 2px;
}
.hint code {
  font: inherit; color: var(--text);
  border: 1px solid var(--border-soft); border-radius: 4px; padding: 0 3px;
}

/* the one place on this page that needs to be read, not skimmed */
.callout {
  font-size: 11.5px; line-height: 1.55; color: var(--text);
  border: 1px solid var(--border); border-left-width: 2px;
  border-radius: 8px;
  padding: 8px 10px;
  margin: 6px 0 4px;
}
.callout em { color: var(--strong); font-style: normal; }

.filerow { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.filerow[hidden] { display: none; }
.fname {
  font-size: 11.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0;
}
.fname.ok { color: var(--strong); }
.fname.bad { color: var(--danger); }

.progress {
  height: 6px; margin: 8px 0 2px;
  background: var(--track); border-radius: 999px; overflow: hidden;
}
.progress[hidden] { display: none; }
.progress-fill { height: 100%; width: 0%; background: var(--fill); }

/* toggle switch, monochrome */
.switch { position: relative; width: 36px; height: 20px; flex: 0 0 auto; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .track {
  position: absolute; inset: 0;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 999px; transition: background 0.12s;
}
.switch .track::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--knob); transition: all 0.12s;
}
.switch input:checked + .track { background: var(--strong); border-color: var(--strong); }
.switch input:checked + .track::after { left: 18px; background: var(--inverse); }

footer { margin-top: 28px; text-align: center; }
footer p { font-size: 11.5px; color: var(--muted); }
footer p:first-child { color: var(--strong); font-size: 13px; margin-bottom: 4px; }

.socials { margin-bottom: 2px; }
.socials a {
  color: var(--text); text-decoration: none;
  border-bottom: 1px solid var(--border);
  pointer-events: auto;          /* the footer itself is click-through */
}
.socials a:hover { color: var(--strong); border-bottom-color: var(--strong); }
.socials .dot { color: var(--faint); margin: 0 3px; }

/* the about link rides along with the build tag, which is otherwise
   click-through on desktop, so it has to opt itself back in */
#versionTag a {
  color: var(--muted); text-decoration: none;
  border-bottom: 1px solid var(--border);
  pointer-events: auto;
  margin-left: 4px;
}
#versionTag a:hover { color: var(--strong); border-bottom-color: var(--strong); }

@media (min-width: 720px) {
  .row label.name { flex-basis: 180px; }
}

/* ---- about: the one prose page ----
   It reuses the same shell (header, footer, tokens) but none of the app
   layout, so it opts out of the desktop grid below rather than fighting it. */
main.doc {
  display: block;
  max-width: 720px; margin: 0 auto; padding: 18px 14px 64px;
}
/* the app pins the footer and the build tag to the corners of the desktop
   grid. There is no grid here, so both go back to sitting at the end of
   the page; the class beats the element/id rules in the media query. */
.doc-page footer {
  position: static; width: auto; margin-top: 28px;
  padding: 18px 0 12px; text-align: center;
  background: none; pointer-events: auto;
}
.doc-page #versionTag {
  position: static; width: auto; text-align: center;
  background: none; padding: 0; pointer-events: auto;
}
.doc h2 {
  font-size: 17px; text-transform: lowercase;
  color: var(--strong); font-weight: 700;
  padding: 30px 0 10px;
  border-top: 1px solid var(--border-soft);
  margin-top: 30px;
}
.doc h2:first-of-type { margin-top: 12px; border-top: 0; padding-top: 8px; }
.doc h3 {
  font-size: 13px; color: var(--strong); font-weight: 700;
  text-transform: lowercase;
  margin: 22px 0 6px;
}
.doc p, .doc li {
  font-size: 13px; line-height: 1.7; color: var(--text);
  margin-bottom: 10px;
}
.doc ul { padding-left: 18px; }
.doc li { margin-bottom: 6px; }
.doc a { color: var(--strong); text-decoration: none; border-bottom: 1px solid var(--border); }
.doc a:hover { border-bottom-color: var(--strong); }
.doc strong { color: var(--strong); font-weight: 500; }
.doc code {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 12px; color: var(--strong);
  background: var(--hover); border-radius: 4px; padding: 1px 4px;
}
.doc .lede { color: var(--strong); font-size: 14px; line-height: 1.65; }
.doc .stamp { color: var(--muted); font-size: 11.5px; margin-bottom: 24px; }
.doc .soon { color: var(--muted); font-style: italic; }

/* jump list, so the four policies are reachable without scrolling past
   the ones you didn't come for */
.doc-nav {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin: 16px 0 4px;
}
.doc-nav a {
  font-size: 11.5px; color: var(--muted);
  border: 1px solid var(--border); border-radius: 7px;
  padding: 4px 10px; text-decoration: none;
}
.doc-nav a:hover { color: var(--strong); background: var(--hover); }

/* ---- desktop: effects sidebar left, big preview right ----
   Below this width the layout is untouched, single column, .stage
   pinned under the header, exactly as on phones. */
@media (min-width: 1000px) {
  main {
    max-width: 1560px;
    padding: 16px 20px 32px;
    display: grid;
    grid-template-columns: var(--sidebar) minmax(0, 1fr);
    grid-template-rows: auto auto;
    column-gap: 20px;
    align-items: start;
  }

  /* the trailing padding lets the last card scroll clear of the pinned
     footer instead of ending underneath it */
  .controls { grid-column: 1; grid-row: 1; margin-top: 0; padding-bottom: 84px; }

  /* the whole preview column sticks; .stage stops being its own sticky
     context so the master panel rides along under the player */
  .preview-col {
    display: block;
    grid-column: 2; grid-row: 1 / span 2;
    position: sticky; top: 64px;
    max-height: calc(100vh - 78px);
    overflow-y: auto;
    scrollbar-width: thin;
  }
  .stage { position: static; padding-bottom: 0; }

  #glCanvas, #splatCanvas { max-height: calc(100vh - 290px); }

  /* pinned to the bottom-left corner so the credit line stays put while
     the sidebar scrolls. pointer-events:none so it can't swallow clicks
     on the card underneath it. */
  footer {
    position: fixed;
    left: 20px; bottom: 0;
    width: var(--sidebar);
    margin: 0;
    padding: 18px 0 12px;
    text-align: left;
    background: linear-gradient(to top, var(--bg) 62%, transparent);
    pointer-events: none;
    z-index: 6;
  }
  footer p { line-height: 1.4; }

  /* the build tag sits in the opposite corner from the credits. Small dark
     backdrop so it stays legible wherever the preview column ends. */
  #versionTag {
    position: fixed;
    right: 18px; bottom: 8px; left: auto;
    width: auto; text-align: right;
    background: var(--scrim-tag);
    padding: 1px 6px; border-radius: 5px;
    pointer-events: none;
    z-index: 7;
  }

  /* the sidebar is narrow, give labels less of it and let controls breathe */
  .controls .row label.name { flex-basis: 132px; }
  .controls .row .val { flex-basis: 40px; }
}

@media (min-width: 1320px) {
  :root { --sidebar: 392px; }
  .controls .row label.name { flex-basis: 160px; }
}
