/* ============================================================================
   JamWare Audio — one stylesheet for the whole site.

   The palette below is NOT invented for the web. It is copied from the apps'
   own :root (MutationStation/interface.html, mirrored in Spectrl and
   Chordinator) so the site is another surface in the same instrument family
   rather than a brochure about one. Ink, plate and stroke come from
   brand/brand.json; each product owns exactly ONE accent, set per-card via
   --accent. Change an accent in brand.json, change it here — nowhere else.
   ========================================================================= */

:root {
  /* --- the faceplate, from the apps ------------------------------------- */
  --ui-bg:          hsl(48, 3%, 89%);
  /* --face-hi/--face-lo were near-neutral greys (10%/6% sat) top-to-bottom --
     2026-08-17: nudged toward the same warm-top/grey-bottom split the app
     panel gradients already use (see the hsl(45,24%..) / hsl(40,11%..) pairs
     below), just at lower saturation since this pair paints the whole page
     baseplate rather than one card and needs to stay in the background.

     2026-08-19, CREAM PASS — TRIED AND REVERTED THE SAME DAY. Saturation was
     roughly doubled across this whole group (8→26, 16→40, 10→24, 18→34) to
     chase the warm beige chassis of the hardware the site references. On the
     live page the user's verdict was immediate: "the warmer orange/reddish
     ground — lets revert that, i want the cream / grey instead". These are
     the original values, restored.

     Why it failed, so nobody re-derives it: saturation on a full-page
     baseplate does not behave like saturation on a card. Four tokens at
     ~2x sat each are individually subtle and cumulatively a colour cast, and
     because the hue band here is 38-45 that cast lands as orange rather than
     as cream. Cream on real hardware is a LIGHT warm grey — it reads warm
     because of what surrounds it, not because the plate itself is saturated.
     The dark panel heads, screws and lit accents added the same day are what
     actually supply that surround, and they work against exactly this cooler
     ground.

     So: if the plate ever reads too grey again, the fix is contrast and
     surround (darker heads, more accent), NOT saturation in this block. Do
     not re-run the cream pass.

     2026-08-19, DE-ORANGE PASS — the round after the revert above. Restoring
     the pre-cream values was NOT enough: "It is still too orange/red, the
     faceplate. [...] No orange/red tone at all." The revert had put the
     saturations back but left the HUE where it had always been, in the 26-45
     band — and that band is the problem, not the amount. Hue 26-45 is the
     orange/red side of yellow, so every warm neutral on the page, however
     lightly saturated, was tinted toward terracotta rather than toward
     paper. That is why halving saturation alone reads as "slightly less
     orange" instead of "not orange".

     The fix is therefore a hue move, applied across the whole page and not
     just this block: every low-saturation warm neutral in the sheet (any
     hsl() with hue 20-48 and saturation <= 26%) was moved to hue 48 and had
     its saturation roughly halved. Hue 48 is on the yellow side, where a
     small amount of saturation reads as ivory/cream; the halving then keeps
     it a whisper rather than a tint. The plate tokens in this block were
     hand-set a little lower still (3-5%) because they paint the largest
     areas, where cast accumulates fastest.

     Deliberately NOT touched by that sweep, so they are not "missed" and
     re-swept later: the product accents (`--accent` and the hsl(8,80%,55%)
     fallback), the amber LCD faces on the demo transport (hsl(38,45%,68%),
     hsl(38,30%,84%) and their glow) — those are lit displays and are
     supposed to be orange — and anything at saturation > 26%. The warm
     shadow ink rgba(28,22,14,...) WAS neutralised to rgba(22,22,20,...) —
     nine call sites — because a red-skewed shadow under every button and
     panel edge is the same cast arriving by a different route.

     Rule of thumb after two failed rounds: on this page, warmth is a HUE
     decision before it is a saturation one. Keep warm neutrals at hue ~48;
     if something must be warmer, raise saturation there, do not rotate the
     hue back down toward 40. */
  --face-hi:        hsl(48, 5%, 96%);
  --face-lo:        hsl(48, 3%, 76%);
  --control-bg:     hsl(48, 5%, 95%);
  --control-border: hsl(48, 3%, 56%);
  --hairline:       hsl(48, 3%, 73%);
  /* Shared LCD-picker bezel gradient, hoisted here so both the closed
     `.hero-lcd-select` control and the `.hero-lcd-popup` (appended to
     <body>, a sibling rather than a descendant of `.hero-lcd`) can read it.
     `--bezel` was previously declared only inside `.hero-lcd-select`; the
     popup's `background: ... var(--bezel) border-box` referenced an unset
     custom property with no fallback, which makes the whole `background`
     shorthand invalid at computed-value time -- the popup rendered as a
     plain unstyled white list instead of the LCD bezel. */
  --bezel: linear-gradient(180deg,
     hsl(48,4%,20%) 0%, hsl(48,4%,7%) 40%, hsl(48,4%,5%) 70%, hsl(48,4%,13%) 100%);
  --cell: 6.5px;
  /* Corner-screw dots on .panel -- deliberately its own token rather than
     reusing --control-border, so a future palette pass can retune screw
     contrast without touching the border colour everything else keys off. */
  --screw:          hsl(48, 3%, 49%);

  /* --- the dark graphite plate, from Chordinator's .seq-col --------------
     Same three-stop gradient and border colour as
     Chordinator/src/renderer/skins/duotone.css .seq-col, copied verbatim
     rather than approximated so the top row of a website panel is the same
     plate as the sequencer panel it's referencing, not a lookalike. Briefly
     used on .panel-head (2026-08-16) before the user called it "not very
     nice" the same day and asked for silver instead -- kept here, unused,
     because .status/.features text and any future graphite panel may still
     want it, and because reverting the palette shouldn't mean re-deriving
     these numbers from Chordinator's source a second time. */
  --darkplate-hi:     hsl(50, 4%, 16%);
  --darkplate:        hsl(50, 4%, 11%);
  --darkplate-lo:     hsl(48, 3%, 8%);
  --darkplate-border: hsl(50, 4%, 7%);

  /* --- brushed silver, .panel-head's actual plate as of 2026-08-16 -------
     The dark-graphite band above read heavy sitting on top of every cream
     panel on the page -- fine for the one sequencer panel it was copied
     from, wrong repeated eight times down a page that is mostly light
     aluminium already. This is a brighter, cooler-neutral aluminium strip
     instead: close enough to --face-hi/--control-bg to belong to the same
     material family as the rest of the plate, distinct enough (cooler hue,
     slightly darker) to still read as its own strip rather than a seamless
     continuation of the panel body under it. Keeps the same scanline/sheen/
     bloom LAYER STRUCTURE as the graphite recipe below it (brushed metal
     texture is the point the user asked to keep), just re-lit for a light
     base instead of a dark one. */
  --silverplate-hi:     hsl(48, 4%, 94%);
  --silverplate:        hsl(48, 3%, 85%);
  --silverplate-lo:     hsl(48, 3%, 75%);
  --silverplate-border: hsl(48, 3%, 58%);

  --text-primary:   hsl(48, 6%, 14%);
  --text-secondary: hsl(48, 4%, 31%);
  --text-heading:   hsl(48, 8%, 18%);

  /* --- the dark inset screen, from Spectrl's analyser panel -------------- */
  --screen-bg:      hsl(48, 4%, 11%);
  --screen-edge:    hsl(48, 5%, 6%);
  --screen-text:    hsl(48, 7%, 86%);
  --screen-dim:     hsl(48, 4%, 58%);

  /* --- brand.json ------------------------------------------------------- */
  --ink:            #2A2621;
  --accent:         #D13A2C; /* default; every card overrides this */

  --radius:         4px;
  --measure:        66ch;

  /* --- plate type, from the apps ----------------------------------------
     Every app's faceplate typography — the engraved wordmark in the top row,
     the panel headers, the control labels — runs on this exact stack (see
     MutationStation/standalone/dist/interface.html, mirrored in the others).
     The site had never declared it, so all of it fell through to -apple-system
     and the milled headings came out in SF next to screenshots of the same
     words in Futura. Nobody spots the substitution as such; it just makes the
     site look like a brochure ABOUT the instruments rather than another panel
     of the same instrument, which is the one thing the whole palette is for.

     Century Gothic ships with Office and Futura with macOS, so on this machine
     and most Mac buyers' the first or second entry hits. -apple-system stays in
     the stack as the fallback rather than a webfont: a 200KB download to render
     four headings, blocking or flashing on first paint, is the wrong trade for
     a page whose job is to load fast and look solid.

     ⚠ Body prose deliberately stays on -apple-system. Futura at 15px over 66ch
     is a poster face doing a paragraph's job — the apps do not set body copy in
     it either, because they have none. This token is for text that is cut into
     the plate, not text printed on the page.                                  */
  --font-plate: "Century Gothic", Futura, -apple-system, "Segoe UI", Arial, sans-serif;

  /* --- display / graffiti wordmark --------------------------------------
     The brand wordmark ("JAMWARE AUDIO") is meant to render in a spiky
     graffiti-tag face (the user's "BELTERBOX" reference). There is no such
     face on the system, and a convincing one cannot be hand-lettered from a
     picture — a real font file is required. This token points the wordmark and
     the hero nameplate at that face, and FALLS BACK to --font-plate so the site
     renders in the milled Futura/Century-Gothic stack, exactly as before, for
     as long as no file is present. The @font-face just below wires the file up:
     drop a graffiti font at site/assets/fonts/jamware-display.{woff2,otf,ttf}
     (any one of them) and it activates here on every page with no other change.
     Kept a variable rather than hard-coded so the fallback and the future face
     are declared in one place. See assets/fonts/README.md. */
  --font-display: "JamWare Display", var(--font-plate);
}

/* The graffiti wordmark face. Self-hosted (never a webfont CDN — the site's
   whole point is to load fast and offline-clean; see --font-plate's note). The
   src list names every format a hand-off might arrive in; the browser takes the
   first that loads and SKIPS the 404s, so only one of these files need exist.
   With none present, every rule using --font-display resolves to --font-plate
   and the wordmark is unchanged. `swap` so first paint is never blocked on it. */
@font-face {
  font-family: "JamWare Display";
  src: url("assets/fonts/jamware-display.woff2") format("woff2"),
       url("assets/fonts/jamware-display.woff")  format("woff"),
       url("assets/fonts/jamware-display.otf")   format("opentype"),
       url("assets/fonts/jamware-display.ttf")   format("truetype");
  font-weight: 400 900;
  font-display: swap;
}

/* Dot-matrix LCD font used by the apps (DotGothic16). Referencing the
    standalone app asset so the dropdown matches the in-app look. On deploy
    consider copying the WOFF2 into site/assets/fonts for GitHub Pages. */
@font-face {
   font-family: "DotGothic16";
   src: url("assets/fonts/DotGothic16-ascii.woff2") format("woff2");
   font-display: swap;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

/* Hide native scrollbars — Juno fader replaces them visually and functionally */
html, body {
   scrollbar-width: none; /* Firefox */
   -ms-overflow-style: none; /* IE 10+ */
}
html::-webkit-scrollbar, body::-webkit-scrollbar {
   width: 0; height: 0; display: none;
}

body {
  margin: 0;
  background: var(--ui-bg);
  color: var(--text-primary);
  font: 15px/1.62 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}
/* The brushed-metal faceplate: a 1px horizontal grain over a vertical
   light-fall, so the whole page reads as ONE milled panel you are scrolling
   across, not a stack of separate cards. Was `background-attachment: fixed`
   directly on body; Chrome/Safari repaint a fixed background per GPU
   compositing tile as the page scrolls, and at this page's length that
   showed up as faint rectangular seams ("squares") tiled down the page. A
   `position: fixed` pseudo-element behind everything paints the same
   viewport-pinned gradient without going through that per-tile repaint path,
   so it stays one continuous surface. */
/* 2026-08-17: the 1px repeating-linear-gradient grain (still present as of
   the comment above this rule) turned out to be a second source of the same
   rectangular-seam bug that position:fixed was meant to have already fixed.
   A live repeating-gradient is recomputed per GPU raster tile; at this page's
   height Chrome still splits a `fixed` layer into multiple tiles well before
   it reaches the bottom, and a 1px-period pattern is exactly fine-grained
   enough for adjacent tiles to phase-mismatch by a pixel at the seam --
   visible as a hard rectangular edge in the lower half of the page. Swapping
   the live gradient for a tiny (1x2px) rasterized tile repeated via
   background-repeat sidesteps this: the browser rasterizes that bitmap once
   and repeats the exact same pixels everywhere, so there is no live
   computation left to fall out of phase at a tile boundary. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='2'><rect width='1' height='1' fill='white' fill-opacity='0.04'/><rect y='1' width='1' height='1' fill='black' fill-opacity='0.015'/></svg>") repeat,
    linear-gradient(180deg,
      var(--face-hi) 0%, var(--ui-bg) 42%, var(--face-lo) 100%);
}

/* Engraved-legend type: the small uppercase labels silkscreened onto hardware.
   Used for every eyebrow, panel header and legend on the site.

   font-family switched to a monospace stack 2026-08-16, at the user's request
   for a stencil/rack-label feel on these specifically. NOT a new webfont —
   the site already turned down two licensed display faces this month
   (Antihero, Permanent Marker) over exactly the load-weight/flash-of-wrong-
   font cost this rule's neighbour (--font-plate, above) argues against, and a
   monospace system stack (ui-monospace hits the OS's own SF Mono/Menlo/
   Consolas with zero download) gets most of the same "stencilled, not
   printed" read without repeating that churn. Scoped to .legend only — body
   copy and headings keep --font-plate / --font-display untouched. */
.legend {
  font-family: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-secondary);
  text-shadow: 0 1px 0 rgba(255,255,255,0.6);
}
/* .legend and .panel-head are already the site's "readout" typography —
   the mono/LCD accent face the 2026-08-16 request asked for doesn't need a
   second font stack bolted on top of them, it needs THIS pairing (existing
   monospace + a hair more tracking + a faint warm glow standing in for
   phosphor) applied where a value/label reads like a rack display: numbers,
   short category words, spec strings. `.lcd` is that treatment as a
   reusable class, for the odd inline bit (a tagline's category word, a
   "130 BPM"-style detail) that isn't already inside .legend/.panel-head. */
.lcd {
  font-family: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  color: color-mix(in srgb, var(--accent) 70%, var(--text-heading));
  text-shadow: 0 0 6px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* App-style LCD dropdown for the header. This copies the app's passive LCD
   select styling (bezel, dot-matrix font, ghost cells) scoped to
   `.hero-lcd-select` so it does not affect other selects on the site. The
   font file is referenced above; copy it into `site/assets/fonts` when ready
   for production. */
.hero-lcd { display: inline-flex; align-items: center; gap: 6px; margin-left: 14px; }
.hero-lcd-select {
   /* Exact MutationStation passive-LCD recipe, scoped to the header select */
   font-family: "DotGothic16", "SF Mono", Menlo, Consolas, monospace;
   letter-spacing: 0; font-size: 13px; font-weight: 400;
   --lcd-bezel: 3px; --lcd-radius: 5px;
   padding: 3px var(--cell);
   color: hsl(100,5%,28%);
   border: var(--lcd-bezel, 3px) solid transparent;
   border-radius: var(--lcd-radius, 5px);
   --lcd-screen:
      repeating-linear-gradient(90deg,
         rgba(28,36,22,0.055) 0px, rgba(28,36,22,0.055) calc(var(--cell) - 1px),
         transparent calc(var(--cell) - 1px), transparent var(--cell)) left center / 100% 62% no-repeat padding-box,
      linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 45%) padding-box,
      linear-gradient(180deg, hsl(100,6.5%,74%), hsl(100,6.5%,74%)) padding-box,
      var(--bezel) border-box;
   /* Exact chevron from MutationStation's `select` rule -- a single small
      hand-drawn SVG triangle, NOT a CSS gradient pair. A gradient pair sized
      `auto` (the previous version here) has no intrinsic size, so its
      "auto" background-size defaults to the full padding-box -- it drew a
      giant diagonal split across the whole control instead of an 8x5px
      arrow. Copied verbatim rather than re-derived so this can't recur. */
   padding-right: 18px;
   background:
      url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="8" height="5"><path d="M0 0h8L4 5z" fill="%23555046"/></svg>') right 6px center / 8px 5px no-repeat padding-box,
      var(--lcd-screen);
   box-shadow: inset 0 3px 5px rgba(0,0,0,0.42), inset 0 0 0 1px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.30), 0 1px 0 rgba(255,255,255,0.5);
   /* `appearance: base-select` only exists in Chromium 131+. On every other
      engine (Safari, Firefox, older Chrome) an unsupported value is dropped
      by the parser -- NOT treated as invalid-and-ignored-per-property, but
      the whole declaration is skipped, leaving `appearance` at its default
      `auto`. That kept the native OS chevron/popup active *alongside* our
      hand-drawn SVG chevron above (the "double symbol" bug) and, worse,
      meant clicking the control opened the real native list instead of the
      .hero-lcd-popup below (the "boring macOS dropdown" bug) -- the JS click
      handler in site-header.js is bound to a <button> child of the <select>,
      which non-supporting browsers never lay out/hit-test, so the click fell
      straight through to the native control. Base is `none` (single custom
      chevron, no native popup machinery anywhere); base-select is opted back
      in only where the engine actually understands it, and even there the
      native popup stays fully overridden by the JS popup via the always-on
      overlay button (see .hero-lcd-overlay + site-header.js). */
   appearance: none;
   min-width: 140px; max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
   pointer-events: none;
}
@supports (appearance: base-select) {
   .hero-lcd-select { appearance: base-select; }
}
.hero-lcd-select::picker-icon { display: none; }
/* Invisible full-cover control, positioned over the <select> in JS, that
   owns every click/keyboard interaction so opening always goes through the
   .hero-lcd-popup below -- see the "appearance: none" comment above for why
   the <select> itself can no longer be trusted to do this consistently
   across engines. The <select> keeps pointer-events:none (mouse only; it is
   still the thing that carries the actual form value and is still reachable
   for assistive tech that inspects the DOM), while this sits on top for
   both mouse and keyboard. */
/* z-index 21, not the old 5: the overlay is appended to <body>, a sibling of
   .site-header (z-index 20, its own stacking context) rather than a child of
   it, even though it sits visually right on top of the header's own LCD
   select. At z-index 5 the header's stacking context painted above it, so
   every click landed on the header (which does nothing at that point) instead
   of the overlay -- the dropdown looked dead. 21 clears the header by one. */
.hero-lcd-overlay { position: absolute; background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer; z-index: 21; }
.hero-lcd-overlay:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent); outline-offset: 2px; }
.hero-lcd-select:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent); outline-offset: 2px; }
.hero-lcd-select option { background: transparent; color: inherit; }

/* The open picker (where supported by Chromium-based browsers) mirrors the
   LCD bezel; fallback is the native menu. */
/* Open picker for the header LCD (Chromium customizable picker) */
.hero-lcd-select::picker(select) {
   appearance: base-select;
   font-family: "DotGothic16", "SF Mono", Menlo, Consolas, monospace;
   font-size: inherit; color: hsl(100,5%,28%);
   border: var(--lcd-bezel, 3px) solid transparent; border-radius: var(--lcd-radius, 5px);
   background:
      repeating-linear-gradient(90deg,
         rgba(28,36,22,0.055) 0px, rgba(28,36,22,0.055) calc(var(--cell, 6.5px) - 1px),
         transparent calc(var(--cell, 6.5px) - 1px), transparent var(--cell, 6.5px)) padding-box,
      linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 45%) left top / 100% 25px no-repeat padding-box,
      linear-gradient(180deg, hsl(100,6.5%,74%), hsl(100,6.5%,74%)) padding-box,
      var(--bezel) border-box;
   box-shadow: inset 0 3px 5px rgba(0,0,0,0.42), inset 0 0 0 1px rgba(0,0,0,0.12), 0 4px 10px rgba(0,0,0,0.35);
   padding: 2px; margin-top: 3px;
}
/* Same reddish hover as the .hero-lcd-popup JS fallback above, applied to the
   native customizable-select option rows so both pickers give matching
   feedback. */
.hero-lcd-select option:hover { background: color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 22%, hsl(100,6.5%,74%)); color: hsl(0,0%,12%); }

/* Custom JS-driven popup to reproduce the app's open list in all browsers */
.hero-lcd-popup {
   position: fixed; z-index: 9999; display: none; min-width: 160px; max-width: 420px;
   border-radius: var(--lcd-radius,5px); border: var(--lcd-bezel,3px) solid transparent;
   /* Match MutationStation's open picker stack exactly so every row gets the
      same ghost-cell texture and the sheen is clamped to the closed-box height. */
   background:
     repeating-linear-gradient(90deg,
       rgba(28,36,22,0.055) 0px, rgba(28,36,22,0.055) calc(var(--cell,6.5px) - 1px),
       transparent calc(var(--cell,6.5px) - 1px), transparent var(--cell,6.5px)) padding-box,
   linear-gradient(180deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 45%) left top / 100% 25px no-repeat padding-box,
     linear-gradient(180deg, hsl(100,6.5%,74%), hsl(100,6.5%,74%)) padding-box,
     var(--bezel) border-box;
   box-shadow: inset 0 3px 5px rgba(0,0,0,0.42), inset 0 0 0 1px rgba(0,0,0,0.12), 0 8px 18px rgba(0,0,0,0.35);
   padding: 4px 2px; max-height: 440px; overflow: auto;
}
.hero-lcd-popup.open { display: block; }
/* padding-left reserves a fixed gutter (var(--cell) base indent + 14px) on
   EVERY row, selected or not, for the checkmark ::before below. It used to
   match the plain var(--cell) padding every other side uses, which put the
   selected row's "✓" at left:6px directly on top of the label's first glyph
   (e.g. "Chordinator" read as "✓hordinator") since no space was reserved for
   it. Reserving the gutter on all rows keeps every row's text left-edge
   aligned instead of only the selected row shifting right. */
.hero-lcd-popup .opt { padding: 4px var(--cell,6.5px) 4px calc(var(--cell,6.5px) + 14px); border-radius: 2px; cursor: pointer; color: hsl(100,5%,28%); font-family: "DotGothic16", "SF Mono", Menlo, Consolas, monospace; font-size: 13px; line-height: 1.6; position: relative; }
/* Hover is the same reddish accent feedback as the header buttons' pressed
   LED (.site-nav a:active::before), not the sage tone that was here before --
   the selected row keeps its own quieter background so the two states stay
   visually distinct. */
.hero-lcd-popup .opt:hover { background: color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 22%, hsl(100,6.5%,74%)); color: hsl(0,0%,12%); }
.hero-lcd-popup .opt[aria-selected="true"] { background: hsl(100,10%,64%); }
.hero-lcd-popup .opt[aria-selected="true"]:hover { background: color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 22%, hsl(100,10%,64%)); }
/* Selected row shows a tick at the left; uses a pseudo-element so no markup change */
.hero-lcd-popup .opt[aria-selected="true"]::before { content: "✓"; position: absolute; left: 6px; top: 50%; transform: translateY(-50%); color: hsl(100,5%,28%); }

/* Scrollbar styling to match the app: narrow track, white thumb with subtle inset */
.hero-lcd-popup::-webkit-scrollbar { width: 12px; }
.hero-lcd-popup::-webkit-scrollbar-track { background: transparent; }
.hero-lcd-popup::-webkit-scrollbar-thumb { background: #ffffff; border-left: 1px solid rgba(0,0,0,0.08); border-radius: 6px; }

/* The JS injects a non-interactive <button><selectedcontent> into each
    select so the closed label can ellipsise reliably across browsers. These
    rules style that injected element; they are present in the app's CSS. */
select > button { all: unset; box-sizing: border-box; display: block; width: 100%; min-width: 0; overflow: hidden; }
select selectedcontent { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

a { color: var(--ink); text-decoration-color: var(--accent); text-underline-offset: 3px; }
a:hover { color: var(--accent); }

/* 1000px until 2026-08-13, which left a third of a laptop screen as bare
   faceplate on either side while .two-col panels were squeezing 66ch of prose
   into ~470px columns — the ragged, half-empty boxes on the Chordinator page
   were that squeeze, not a bug in the panels. 1240 is picked off --measure
   rather than off the viewport: it puts each .two-col column at ~576px, which
   is where a 66ch line actually stops wrapping early. Wider than this and the
   single-column sections (hero copy, .prose.story) start reading as a stripe
   of text floating in a very large panel, which is the problem in the other
   direction. */
/* ⚠ The side padding here is the ONLY thing keeping text off the viewport edge,
   and for a long time it was not doing it. Every content section on every page
   is `class="hero wrap"` / `"section wrap"` / `"product-hero wrap"`, and each of
   those three rules set a `padding` SHORTHAND for its vertical rhythm —
   `padding: 40px 0` and friends. Same specificity as this rule, declared later
   in the file, so the shorthand's `0` won and the horizontal padding was gone
   sitewide. It was invisible above ~1300px, where `max-width` centres the
   column and leaves margin either side by accident, and only showed up when the
   window was halved: the copy ran flush into the left edge with nothing between
   it and the frame.

   Those three rules now use `padding-block`, which cannot reach this axis. Keep
   it that way — if you add a section rule here, give it `padding-block`, never
   the shorthand. */
.wrap { max-width: 1240px; margin: 0 auto; padding: 0 32px; }

/* ============================================================ PROMO BANNER */
/* 2026-08-19: time-boxed "free for two weeks" notice, added at the user's
   instruction after the pricing-strategy discussion landed on disclosed
   urgency over a silent donation model (site/HANDOFF.md has the reasoning).
   Sits in normal flow ABOVE .site-header, which is itself position:sticky —
   so it scrolls away first and the header takes over the top of the
   viewport, rather than the banner permanently eating header height.

   Styled as the dark inset screen language (--screen-bg/--screen-edge) the
   product panels use for readouts, with the accent as the lit digits, so it
   reads as another LCD on the same instrument rather than a bolted-on
   marketing strip. The end date is hardcoded text, not computed — there is
   no build step on this static site to derive "2 weeks from deploy", and a
   wrong countdown is worse than a plain date. If the window changes, edit
   the date in every page's .promo-banner text (index.html + the four
   apps/*.html) together with this comment. */
.promo-banner {
  background: linear-gradient(180deg, var(--screen-bg), var(--screen-edge));
  border-bottom: 1px solid var(--screen-edge);
  text-align: center;
  padding: 8px 32px;
}
.promo-banner p {
  margin: 0;
  font-family: var(--font-display);
  font-size: 13px; font-weight: 700; letter-spacing: 0.04em;
  color: var(--screen-text);
}
.promo-banner strong {
  color: var(--accent, hsl(8,80%,55%));
  text-shadow: 0 0 6px color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 55%, transparent);
}

/* ============================================================ SITE HEADER */

.site-header {
  border-bottom: 1px solid var(--hairline);
  box-shadow: 0 1px 0 rgba(255,255,255,0.55);
  position: sticky; top: 0; z-index: 20;
  backdrop-filter: blur(8px);
  background: linear-gradient(180deg,
    hsl(48 5% 95% / 0.92), hsl(48 4% 88% / 0.88));
}
/* 2026-08-19: 58px -> 50px, user asked for a slightly thinner top row (the nav
   buttons were tightened in the same pass, see .site-nav a below). 50 is the
   floor, not a round number: .brand svg/img is 34px and the user has twice
   asked for the wordmark and mark BIGGER, so shrinking the lockup to buy header
   height is the wrong trade. 50px leaves 8px of clearance above and below a
   34px mark, which is the least that still reads as a band the logo sits IN
   rather than a band the logo is jammed into. Going lower means shrinking the
   lockup first, and that needs a fresh ask.

   Nothing hardcodes this number. site-header.js measures the header's own
   height to place the page-fader (see the .page-fader comment), so the fader
   follows automatically -- do not add a matching constant anywhere. */
.site-header .wrap {
  display: flex; align-items: center; gap: 20px;
  height: 50px;
}
/* flex-shrink: 0 -- 2026-08-20: the 900px halved-window fix below turned this
   row into flex-wrap, but a flex item shrinks before it wraps unless told
   not to, and nothing here said not to. .brand was the one item with no
   fixed width (the mark is a fixed 34px, but the wordmark's box is just
   "however wide the text needs"), so the flex algorithm shrank *it* instead
   of wrapping the row -- squeezing the wordmark's box narrower than its own
   text, which forced "JAMWARE AUDIO" to wrap onto two lines and blow up the
   row height into the nav row below it. flex-shrink: 0 keeps .brand at its
   natural width so the row wraps as a whole instead. */
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; flex-shrink: 0; }
.brand svg, .brand img { height: 34px; width: auto; display: block; }
.wordmark {
  /* Cut into the header band 2026-08-16, the same treatment as .product h3 a
     rather than the app-title-only .engraved recipe (that one is tuned for
     34-60px nameplates; the header sits at 16px, so it borrows .product h3
     a's lighter, smaller-scale fill/lip pairing instead of the heavier one).
     Fill sits close to the header's own plate colour and reads as a groove,
     not printed ink — "more white, like the background" per the request.

     2026-08-17: bumped 16px -> 20px, user asked for the wordmark bigger. The
     logo mark beside it (.brand svg/img) grew from 22px to 27px in step so
     the lockup stays proportioned rather than the text alone dwarfing it.
     Same day, second ask: bumped again 20px -> 24px / 27px -> 34px logo, still
     in step. .site-header .wrap height (58px) has enough headroom already so
     this didn't need a matching bump. */
  font-family: var(--font-display);
  font-size: 24px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap; /* belt-and-suspenders with .brand's flex-shrink: 0
    above -- the two together mean this text can never wrap, regardless of
    what a future edit does to either rule in isolation. */
  color: color-mix(in srgb, var(--control-bg) 92%, black);
  text-shadow:
    0  1px 0 hsla(0,0%,100%,0.95),
    0 -1px 0 hsla(28,12%,14%,0.4);
}
.site-nav { margin-left: auto; display: flex; gap: 16px; align-items: center; }
/* Raised hardware button: taller wall (padding + thicker bottom edge) and a
   two-tone border (light top-left / dark bottom-right lip) so the face reads
   as sitting proud of the plate rather than flush with it, plus a real
   bottom "skirt" shadow standing in for the button's own drop shadow. A
   small rectangular LED sits left of the label and only lights on
   press/active, per the reference hardware-button photo. Lift reduced and
   flex-shrink disabled 2026-08-17 -- these sit in a fixed-height flex row and
   must never compress before wrapping.

   2026-08-19: vertical padding 7px -> 4px, in step with the header band going
   58px -> 50px. Only the vertical axis moved. The horizontal 10px is holding
   the four links to one line on a 360px phone (see the 720px block near the
   bottom of this file, which already tightens it further) and the LED, gap and
   font-size are all load-bearing for that fit -- squeezing this axis to make
   the button "thinner" would trade a 3px height saving for a second nav row on
   every phone. The button still reads as raised: the wall is made by the
   two-tone border and the skirt shadow, not by the padding. */
.site-nav a { text-decoration: none; display: inline-flex; align-items: center; gap: 7px;
   padding: 4px 10px; border-radius: 5px; font-weight: 700; font-size: 13px; color: var(--text-primary);
   flex: none; white-space: nowrap;
   border: 1px solid hsl(48,5%,40%);
   border-bottom-color: hsl(48,6%,28%);
   background: linear-gradient(180deg, hsl(48,12%,98%) 0%, hsl(48,8%,90%) 45%, hsl(48,6%,75%) 100%);
   box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -1px 1px rgba(0,0,0,0.16),
      0 1px 0 rgba(255,255,255,0.5), 0 1px 0 hsl(48,5%,60%), 0 2px 3px rgba(0,0,0,0.18);
   text-shadow: 0 1px 0 rgba(255,255,255,0.5);
   transform: translateY(0);
}
.site-nav a::before {
   content: ""; width: 8px; height: 5px; border-radius: 1px; flex: none;
   background: hsl(48,4%,58%);
   box-shadow: inset 0 1px 1px rgba(0,0,0,0.35), inset 0 -1px 0 rgba(255,255,255,0.15);
}
.site-nav a:hover { filter: brightness(1.05); }
.site-nav a:active {
   transform: translateY(1px);
   box-shadow: inset 0 2px 4px rgba(0,0,0,0.4), 0 1px 0 rgba(0,0,0,0.1);
}
.site-nav a:active::before {
   background: var(--accent, hsl(8,80%,55%));
   box-shadow: 0 0 5px color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 70%, transparent), inset 0 1px 1px rgba(0,0,0,0.25);
}
/* Current-page LED: same lit treatment as :active, held steady rather than
   only flashing on press, driven by site-header.js adding .current to the
   nav link matching location.pathname. */
.site-nav a.current::before {
   background: var(--accent, hsl(8,80%,55%));
   box-shadow: 0 0 5px color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 70%, transparent), inset 0 1px 1px rgba(0,0,0,0.25);
}

/* Product page back-link: same raised button look as header, slightly smaller */
.product-hero .back, .section .back {
   text-decoration: none; display: inline-flex; align-items: center; gap: 7px;
   padding: 6px 9px; border-radius: 5px; font-weight: 700; font-size: 12px; color: var(--text-primary);
   border: 1px solid hsl(48,5%,40%);
   border-bottom-color: hsl(48,6%,28%);
   background: linear-gradient(180deg, hsl(48,12%,99%) 0%, hsl(48,8%,92%) 45%, hsl(48,6%,77%) 100%);
   box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -1px 1px rgba(0,0,0,0.14),
      0 1px 0 rgba(255,255,255,0.5), 0 1px 0 hsl(48,5%,62%), 0 2px 3px rgba(0,0,0,0.14);
   text-shadow: 0 1px 0 rgba(255,255,255,0.45);
   transform: translateY(0);
}
.product-hero .back::before, .section .back::before {
   content: ""; width: 7px; height: 5px; border-radius: 1px; flex: none;
   background: hsl(48,4%,58%);
   box-shadow: inset 0 1px 1px rgba(0,0,0,0.35), inset 0 -1px 0 rgba(255,255,255,0.15);
}
.product-hero .back:hover, .section .back:hover { filter: brightness(1.04); }
.product-hero .back:active, .section .back:active {
   transform: translateY(1px);
   box-shadow: inset 0 2px 4px rgba(0,0,0,0.32), 0 1px 0 rgba(0,0,0,0.1);
}
.product-hero .back:active::before, .section .back:active::before {
   background: var(--accent, hsl(8,80%,55%));
   box-shadow: 0 0 5px color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 70%, transparent), inset 0 1px 1px rgba(0,0,0,0.25);
}

/* Ensure back-links never get a text-underline on hover */
.product-hero .back, .section .back { text-decoration: none !important; text-decoration-color: transparent !important; }
.product-hero .back:hover, .section .back:hover { text-decoration: none !important; text-decoration-color: transparent !important; }

/* Page fader: fixed Juno-style fader on the right edge of the site */
.page-fader {
   /* top was 20px, which sat the track under the sticky 58px-tall
      .site-header (plus its 1px hairline) -- the cap could then land in the
      header band itself. 74px clears the header with an ~15px gap, same as
      the 20px gap already used at the bottom. */
   position: fixed; right: 10px; top: 74px; bottom: 20px; width: 34px; z-index: 60; pointer-events: auto; display: block;
}
/* Gumroad's checkout overlay is a shadow-DOM host appended to <body> with
   inline style.zIndex = "999999" (its own script, not ours -- see
   site-header.js's getOverlayScroller() comment), which would otherwise
   bury the fader at z-index 60 the moment the popup opens. site-header.js
   toggles this class exactly while the overlay is open, so the fader stays
   on top and keeps working as the popup's scroll control instead of
   vanishing under it. */
.page-fader.on-overlay { z-index: 1000000; }
.page-fader .fader-slot {
   position: absolute; right: 6px; top: 6px; bottom: 6px; width: 8px; border-radius: 4px;
   background: var(--screen-bg); box-shadow: inset 0 2px 4px rgba(0,0,0,0.9), 0 1px 0 rgba(255,255,255,0.14);
   overflow: hidden;
}
.page-fader .fader-fill {
   position: absolute; left: 0; right: 0; bottom: 0; height: 0;
   background: linear-gradient(180deg, var(--value-fill, hsl(12,88%,51%)) 0%, hsl(18,88%,40%) 100%);
   box-shadow: 0 0 6px var(--value-glow, rgba(255,100,60,0.25));
}
.page-fader .fader-cap {
   /* Centered on the 8px-wide slot (slot: right:6px width:8px -> center at
      container-right - 10). A 28px-wide cap centered there needs its own
      right offset at -4px, not -8px -- -8px put its center 4px past the
      slot's, which is what "not centered on the bar line" meant. */
   position: absolute; right: -4px; left: auto; width: 28px; height: 16px; bottom: 0; border-radius: 2.5px;
   background: linear-gradient(180deg, hsl(220,7%,32%) 0%, hsl(220,11%,9%) 100%);
   border: 1px solid hsl(220,14%,5%);
   box-shadow: 0 2px 4px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.16);
   cursor: ns-resize;
}
.page-fader .fader-cap::after {
   content: ""; position: absolute; left: 3px; right: 3px; top: 50%; height: 2px; margin-top: -1px; background: hsl(0,0%,90%); box-shadow: 0 0 2px rgba(255,255,255,0.4);
}

/* Was fully hidden below 900px, on the theory it would interfere with touch
   scrolling -- but the drag target is only the 28x16px cap, so in practice
   it never did, and the user asked for it back on mobile rather than
   dropped. The actual mobile bug was the fixed `top: 74px` above: that
   clears the DESKTOP header's single row, but the mobile header wraps to
   two (see `.site-header .wrap` in the 720px breakpoint below) and is
   taller, so the fader's top edge landed inside the header/body copy.
   site-header.js now sets `top` from the header's own measured height
   instead of this fixed value, so it always starts just under it; this
   query only narrows the track and gives it a bit more right-edge margin
   so it clears text at phone widths, plus reserves matching space in
   `.wrap` so body copy doesn't run under it in the first place. */
@media (max-width: 900px) {
  .page-fader { width: 26px; right: 6px; }
  .page-fader .fader-slot { right: 4px; width: 6px; }
  .page-fader .fader-cap { width: 22px; }
  .wrap { padding-right: 46px; }
  /* The fader sits below the header (site-header.js positions its top at
     the header's own measured bottom), not beside it -- but `.wrap` above
     is the generic selector every wrap uses, so the header's row was
     silently losing the same 26px extra margin the fader actually needs
     for body copy. That's 26px the brand+select-fits-on-one-row math never
     accounted for, and was the real reason the header kept measuring as
     fitting in isolated tests but still wrapped to a 3rd row on the live
     page. Put the header back to its ordinary symmetric padding (matches
     the base .wrap rule's 32px right side; the 720px breakpoint below
     narrows both sides to 20px together for phones). */
  .site-header .wrap { padding-right: 32px; }

  /* 2026-08-19: the phone-width two-row treatment below (720px) left this
     900-720px band on flex-nowrap with everything at full desktop size --
     brand (~200px) + hero-lcd select (~154px) + four nav buttons (~408px)
     sums to over 750px before any gap, so a "halved" laptop window (a
     common width squarely in this band, e.g. a 1512px display split in
     two) had no room and the row crammed/overflowed instead of wrapping.
     Turning on the same wrap here, at full desktop type size since there's
     enough width for it to read fine on two rows without shrinking fonts
     (that shrink is phone-specific and stays in the 720px block below). */
  .site-header .wrap {
    height: auto; min-height: 50px;
    flex-wrap: wrap; gap: 8px 16px; padding-top: 10px; padding-bottom: 10px;
  }
  .hero-lcd { margin-left: 0; }
  .site-nav { margin-left: auto; width: 100%; justify-content: flex-end; }
}

/* ================================================================== HERO */

.hero { padding-block: 40px 24px; }
.hero .eyebrow { margin: 0 0 18px; }
.hero h1 {
  font-size: clamp(30px, 5vw, 46px);
  line-height: 1.12; margin: 0 0 20px;
  letter-spacing: -0.015em;
  color: var(--text-heading);
  max-width: 20ch;
  text-shadow: 0 1px 0 rgba(255,255,255,0.5);
}
.hero p { font-size: 17px; color: var(--text-secondary); margin: 0; max-width: var(--measure); }

/* ---- The engraved nameplate ----------------------------------------------

   The h1 is the brand name cut into the faceplate, not printed on it. Three
   layers do that, and all three are needed — drop any one and it flattens back
   into grey text:

     1. The glyph FILL is a top-dark to bottom-light gradient, clipped to the
        text. That is the inner wall of a cut: the far wall is in shadow, the
        near wall catches the room. A flat dark fill reads as ink.
     2. A 1px WHITE drop-shadow BELOW. That is the lower lip of the groove,
        the only genuinely bright edge on a real engraving, and it is what
        sells the depth.
     3. A 1px DARK drop-shadow ABOVE, the upper lip's own shadow.

   filter: drop-shadow() rather than text-shadow, because text-shadow paints
   behind the glyph and background-clip:text has already made the glyph a
   window — the shadow would show straight through it and muddy the fill.
   drop-shadow works on the composited alpha instead, so it hugs the outline.

   -webkit-text-fill-color: transparent is not optional and not legacy cruft:
   without it the plain `color` paints over the clipped background in every
   WebKit and Blink browser, which is all of them here. Keep `color` set as
   well, as the fallback for anything that ignores background-clip.

   .engraved carries the cut; whoever uses it sets its own size and casing. The
   depth is tuned for the homepage nameplate at ~60px, and it survives being
   reused at the product pages' 34px because every offset is 1-3px rather than
   a proportion of the type size — an engraving does not get deeper because the
   letters got bigger, which is exactly why fixed pixels are right here.       */
.engraved {
   font-family: var(--font-plate);
  /* ⚠ THESE STOPS USED TO BE SHARED WITH gumroad/cover.html. THEY ARE NOT ANY
     MORE, BY THE USER'S RULING OF 2026-08-14: the product name on the cover art
     was to be "brighter, more like the background white", this nameplate was
     not. cover.html's h1 now runs in a near-plate range with a heavier upper
     lip; this one stays mid-grey. That file's header carries the matching note.

     Everything else about the two is still shared and still has to be — the
     recipe, the stop ORDER, the lips' geometry. They are duplicated rather than
     imported because cover.html is a standalone render target for headless
     Chrome and deliberately depends on no stylesheet. If you retune the recipe
     here, retune it there and re-run gumroad/make-gumroad-art.sh; if you only
     retune the lightness, that is now a per-surface decision.

     The range went very light on 2026-08-14 and came back to this mid-grey the
     same day: light enough that the cut is what you read, dark enough that the
     name survives against the body copy sitting directly under it. That revert
     is about THIS surface only — do not apply it to the covers.

     Keep the stops in this order if you retune. Dark at the top is the far wall
     of the groove; inverting it turns the letter into a ridge standing proud,
     which is the one thing this site never does.                              */
   /* Match the header wordmark's lighter, near-background engraved fill */
   color: color-mix(in srgb, var(--control-bg) 92%, black);
   text-shadow:
      0 1px 0 hsla(0,0%,100%,0.95),
      0 -1px 0 hsla(28,12%,14%,0.40);
}

/* ---- No .raised rule here, and that is on purpose -------------------------
   A raised/embossed counterpart to .engraved was built here and removed the
   same day: the user's ruling is that this site uses engraving only, not the
   inverse. Depth on these pages is always cut IN, never standing proud. If the
   header wordmark ever looks like it is competing with the engraved h1, the fix
   is scale and weight, not a second relief treatment.                        */

/* Uppercase with open tracking because the homepage h1 is a wordmark now, and
   because caps have no descenders to break the single milled baseline. */
.hero h1.engraved {
  /* the front-page "JAMWARE AUDIO" mark — graffiti face when a file is present,
     milled plate stack otherwise. This is the mark's best home: large, so a
     spiky face reads cleanly here in a way it cannot at 16px in the header. */
  font-family: var(--font-display);
  font-size: clamp(30px, 6vw, 60px);
  font-weight: 800;
  line-height: 1.06;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  /* 15ch until the plate font landed. `ch` is the width of a zero, and Futura's
     is much wider than SF's, so the same 15ch stopped fitting "COMPANION APPS"
     and broke the wordmark across three lines with APPS orphaned on its own. */
  max-width: 18ch;
  margin: 0 0 18px;
}

/* The claim that used to be the h1. Sized between the nameplate and body copy
   so the eye lands name, claim, paragraph in that order. */
.hero .hero-claim {
  font-size: clamp(18px, 2.4vw, 23px);
  line-height: 1.3;
  font-weight: 600;
  color: var(--text-heading);
  letter-spacing: -0.01em;
  margin: 0 0 18px;
  max-width: 24ch;
  text-shadow: 0 1px 0 rgba(255,255,255,0.6);
}

.hero .logo-lockup { max-width: 300px; margin: 0 0 28px; }
.hero .logo-lockup img { width: 100%; display: block; }

/* ================================================================ PANELS

   The panel + engraved header band, lifted from the apps' .panel > h2 so a
   section on the site is built out of the same furniture as a section in the
   software. Every product card and content block is one of these.          */

.panel {
  /* Rack-module framing, added 2026-08-16 at the user's request. First pass
     stacked four radial-gradient() corner dots whose colour stops were
     color-mix() results -- e.g. `color-mix(in srgb, var(--control-border) 40%,
     white) 0%` -- inside a 3px-radius circle. That combination (color-mix()
     output used as a stop inside a very small radial-gradient, several such
     gradients stacked in one `background`) rendered as a solid dark fill
     covering the whole panel in Chrome, while every CSSOM/DOM inspection
     still reported the correct cream values -- a paint-time bug, not a
     cascade or caching problem. Fixed same day by dropping color-mix() from
     inside the gradient stops entirely: each corner dot now uses --screw, a
     plain hsl() token with no nesting, and a hard-edged two-stop circle
     instead of a soft three-stop one. If corner dots go dark/solid again,
     that nesting is the first thing to suspect -- do not reach for color-mix()
     inside a radial-gradient() stop on this rule.

     Two layers on top of the plate fill:
     1. Four small solid dots at the corners, inboard of the border the way a
        real rack screw sits inboard of the U-rail edge, not flush on it.
        `at left 8px top 8px` etc. is the same <position> syntax
        background-position uses, so each dot is pinned to its own corner
        regardless of the panel's size.
     2. An inset ring 4px in from the real border (as a box-shadow, not a
        background layer), which is what actually reads as a bezel lip -- a
        single stroke on a warm cream plate just looks like a heavier
        outline, not a panel let into a frame. */
  position: relative;
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  background:
    /* ---- Bottom pair only. The top pair moved to .panel::before ---------
       2026-08-19: the user reported "only 2 of them per panel, on the bottom
       half, but it should be 4 on each". All four WERE here — the two top
       ones were simply painted over by .panel-head, which is an opaque
       brushed-silver band starting at y=0 and running ~26px down, i.e.
       straight across the `top 8px` dots. Adding more gradients here would
       have changed nothing; the top pair has to be drawn by something that
       paints ABOVE the band, which is .panel::before below. That is also
       what a real rack module does — the screws go through the visible
       nameplate strip, not under it.

       Recipe changed at the same time. The old one was three hard-edged
       flat circles at slightly offset centres, faking a DOMED rivet sitting
       proud of the plate; at 5px it read as a sticker. The apps already had
       a better one (Chordinator src/renderer/styles.css .screw): a recessed
       DIMPLE lit from top-middle, so the far (lower) inner wall catches the
       light and the near (upper) rim shadows into the hole. Ported here as
       two circles rather than the app's gradient+inset-box-shadow, because a
       background layer cannot carry an inset shadow: a lower rim-catch disc
       behind, and the dimple itself in front, its stops soft on the inside
       and hard at the outer edge.

       ⚠ The stops are plain hsl()/rgba() on purpose. color-mix() as a stop
       inside a small radial-gradient on THIS rule paints the whole panel a
       solid dark colour in Chrome while CSSOM still reports cream — see the
       long note above. Do not reintroduce it here. */
    radial-gradient(circle 2.6px at left 8px bottom 8px,
      rgba(255,255,255,0.60) 0%, hsl(48,4%,58%) 46%, hsl(48,4%,34%) 97%, transparent 100%),
    radial-gradient(circle 3.1px at left 8px bottom 8.9px, rgba(255,255,255,0.5) 99%, transparent 100%),
    radial-gradient(circle 2.6px at right 8px bottom 8px,
      rgba(255,255,255,0.60) 0%, hsl(48,4%,58%) 46%, hsl(48,4%,34%) 97%, transparent 100%),
    radial-gradient(circle 3.1px at right 8px bottom 8.9px, rgba(255,255,255,0.5) 99%, transparent 100%),
    /* Brushed-metal noise + a faint radial vignette, both restricted to the
       panel plate rather than the page — "read as physical rather than flat
       CSS gradients" per the 2026-08-16 request. The noise is the same 1px
       repeating-linear-gradient scanline trick .panel-head already uses
       (kept consistent rather than inventing a second texture language), at
       a much lower opacity since it is now covering a whole card instead of
       an 11px label band. The vignette darkens the four corners only —
       radial-gradient with a transparent centre — so the plate reads as
       lit from above-centre instead of flat. */
    repeating-linear-gradient(0deg, rgba(255,255,255,0.025) 0px, rgba(255,255,255,0.025) 1px, rgba(0,0,0,0.025) 1px, rgba(0,0,0,0.025) 2px),
    radial-gradient(140% 100% at 50% 8%, rgba(0,0,0,0) 55%, rgba(0,0,0,0.05) 100%),
    linear-gradient(180deg,
      var(--face-hi) 0%, var(--control-bg) 20%,
      var(--control-bg) 78%,
      color-mix(in srgb, var(--control-bg) 94%, black) 100%);
  background-repeat: no-repeat;
  /* Used to also carry `inset 0 0 0 4px var(--control-bg)` here, meant to
     read as a recessed bezel lip 4px in from the real border. In practice,
     against most panel/page background combinations it read as a stray pale
     ring traced around every panel instead -- removed 2026-08-17. */
  box-shadow:
    0 1px 0 rgba(255,255,255,0.6), 0 2px 5px rgba(0,0,0,0.14);
}
/* The TOP pair of screws, drawn as an overlay so they land ON the header
   band instead of under it. See the long note in .panel's background above
   for why they cannot live in that background layer.

   Deliberately a single pseudo-element carrying both screws as background
   layers, not two elements: .panel::after is already the accent spine and
   .panel-head::before is already the pilot lamp, so ::before here is the
   last free slot on the pair and has to do both corners. z-index 2 clears
   the band (auto) and the spine; pointer-events:none keeps it out of the
   way of the panel's own links.

   Same two-circle dimple recipe as the bottom pair, unchanged — a screw
   through a silver nameplate strip and a screw through the cream plate are
   the same hole, and giving the band its own lighter/darker variant made
   the four read as two different fasteners. If .panel-head ever goes dark
   (the graphite/gunmetal proposal), this is the rule that needs a darker
   variant, not the one above. */
.panel::before {
  content: "";
  position: absolute; inset: 0; z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  background:
    radial-gradient(circle 2.6px at left 8px top 8px,
      rgba(255,255,255,0.60) 0%, hsl(48,4%,58%) 46%, hsl(48,4%,34%) 97%, transparent 100%),
    radial-gradient(circle 3.1px at left 8px top 8.9px, rgba(255,255,255,0.5) 99%, transparent 100%),
    radial-gradient(circle 2.6px at right 8px top 8px,
      rgba(255,255,255,0.60) 0%, hsl(48,4%,58%) 46%, hsl(48,4%,34%) 97%, transparent 100%),
    radial-gradient(circle 3.1px at right 8px top 8.9px, rgba(255,255,255,0.5) 99%, transparent 100%);
  background-repeat: no-repeat;
}

.panel > .panel-head {
  /* Monospace, matching .legend's switch above — same stencil-label reasoning. */
  font-family: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.09em;
  margin: 0; padding: 6px 14px 7px;
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
  display: flex; align-items: center; gap: 8px;

  /* ---- Brushed silver band, not the cream plate's own engraved cut -------
     2026-08-16: this started as a straight copy of Chordinator's .seq-col
     (dark graphite -- see --darkplate-* above), matched to the app pixel for
     pixel at the user's insistence. Same day, on the live site, the user
     called that "still not very nice" repeated across eight panels down a
     mostly-light page and asked for silver instead, explicitly keeping "the
     subtle grain/noise overlay" -- i.e. keep the layer structure (scanline +
     diagonal sheen + overhead bloom over a 3-stop vertical gradient), re-tune
     only the lightness/hue so the band reads as a cooler, slightly darker
     strip of aluminium sitting on top of the panel's own warmer cream face,
     the way a milled model-strip insert sits proud of a chassis it's bolted
     to, rather than the near-black plate a single graphite sequencer panel
     can get away with but a whole page of them can't. */
  background:
    repeating-linear-gradient(0deg, rgba(255,255,255,0.05) 0px, rgba(255,255,255,0.05) 1px, rgba(0,0,0,0.05) 1px, rgba(0,0,0,0.05) 2px),
    linear-gradient(112deg, rgba(255,255,255,0) 24%, rgba(255,255,255,0.5) 34%, rgba(255,255,255,0.1) 43%, rgba(255,255,255,0) 52%, rgba(255,255,255,0.35) 68%, rgba(255,255,255,0) 78%),
    radial-gradient(120% 60% at 50% -8%, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 55%),
    linear-gradient(180deg, var(--silverplate-hi) 0%, var(--silverplate) 30%, var(--silverplate-lo) 100%);
  border-bottom: 1px solid var(--silverplate-border);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    inset 0 -1px 0 rgba(0,0,0,0.18);
  /* Switched from printed to CUT 2026-08-16, at the user's request: "the text
     in the top row of each panel should be engraved" -- not silkscreened ink
     sitting proud of the plate, but the same milled-groove pairing every
     other headline on the site now uses (.product-hero h1.engraved,
     .product h3 a): a fill close to the plate's own colour, with a dark
     upper lip standing in for the far wall of the cut and a light lower lip
     for the near one. Fill is a mid-point of the silver plate darkened
     toward black rather than a flat grey, so the letters still read as this
     PLATE'S material, not a universal ink colour dropped onto whichever
     plate happens to be under it. */
  color: color-mix(in srgb, var(--silverplate) 55%, black);
  text-shadow:
    0  1px 0 hsla(0,0%,100%,0.7),
    0 -1px 0 hsla(28,12%,14%,0.45);
}
/* The accent pilot lamp on the header band — the one place each product's
   colour announces itself before you have read anything. */
.panel > .panel-head::before {
  /* Rectangular, not round: a pilot lamp on real rack gear is a slotted LED
     bezel, not a dot, and round read as a bullet/status-dot rather than a
     lamp. 3:7 aspect (narrow, tall) keeps it readable at 7px without becoming
     a bar. Changed 2026-08-16 at the user's request, extended to .status and
     .features below for one LED vocabulary across the site. */
  content: ""; width: 5px; height: 8px; border-radius: 1.5px;
  background: var(--accent);
  box-shadow: 0 0 0 1px rgba(0,0,0,0.28) inset, 0 0 6px color-mix(in srgb, var(--accent) 60%, transparent);
  flex: none;
}
.panel-body { padding: 18px 20px 20px; }

/* The homepage hero shot. Capped in height so a tall dialog screenshot cannot
   push the product list below the fold; the image is anchored to its top so
   the part that sells it stays visible. */
/* Wider than the 1000px text column, and taller than the panels below it, for
   one reason: the apps are laid out for a 1600pt window, so every pixel the
   frame loses comes off legibility until the screenshot is decorative. This is
   the only element on the site allowed out of the measure. Together with the
   BLEED crop in make-hero.py it puts the front window back at a size where the
   piano roll reads as a piano roll. */
.wrap.hero-shot { max-width: 1420px; padding-bottom: 10px; }
/* max-width before width:auto, in that order of importance: width:auto lets the
   height cap drive the size, which on a phone resolved to a ~1000px image in a
   371px viewport and gave the whole page a horizontal scrollbar. */
.hero-shot .screen img { max-height: 620px; max-width: 100%; width: auto; margin: 0 auto; }
/* text-align (not the img's own margin:0 auto) is what centers .hero-stack-frame,
   because the frame has to shrink-wrap the image's browser-computed rendered
   size for the percentage overlay below to line up -- margin:auto on a
   display:inline-block element does not center it, only text-align on the
   ancestor does. */
.hero-shot .screen { text-align: center; }
.caption { display: block; margin: 10px 2px 0; }

/* ---- MutationStation re-roll loop, composited into the cascade -----------
   The MutationStation window in hero-stack.png is not fully static: the piano
   roll inside it is a real re-roll loop (Auto-Lock Structural on, red notes
   holding pitch while orange notes mutate), positioned with .hero-inset to sit
   exactly where MutationStation's piano roll renders in the front layer of the
   cascade. This used to be a separate section below the cascade; folding it in
   reads as "one of the four windows is actually alive" rather than a second,
   disconnected clip, and is the site's only motion.

   .hero-stack-frame is display:inline-block; line-height:0 so it shrink-wraps
   exactly to the <img>'s rendered box (the height-capped, width:auto size from
   the rule above) -- .hero-inset's percentages are relative to THIS box, not
   the .screen container, which is why the wrapper has to match the image
   pixel-for-pixel rather than being e.g. width:100%.

   The percentages themselves are not eyeballed. mutationstation.png (the still
   in the cascade) and mutationstation-reroll.mp4 (this loop) were captured
   from the same live app window in one CDP session on 2026-08-16 so they would
   actually line up -- a screenshot and video shot on different days at
   different window sizes do not overlay cleanly, which is what broke the first
   attempt at this (site/HANDOFF.md, MutationStation/HANDOFF.md have the
   postmortem and the revert commit). The window's own
   .pianoroll-wrap.getBoundingClientRect() at capture time was
   {x:8, y:174, w:850, h:576} against a 1728x1009 window. Run through
   make-hero.py's placement math for the frontmost (mutationstation, last in
   ORDER) layer -- scale to WIN_W=1500, offset by PAD + STEP_X*3 / PAD +
   STEP_Y*3, against the uncropped (BLEED=0) 1970x1262 canvas -- that rect
   lands at left 21.88%, top 38.91%, width 37.46%, height 39.62%. Recompute
   this whole block from scratch (do not nudge these numbers by eye) if either
   asset is ever re-shot without the other. */
.hero-stack-frame { position: relative; display: inline-block; line-height: 0; }
.hero-inset {
  position: absolute;
  left: 21.88%;
  top: 38.91%;
  width: 37.46%;
  height: 39.62%;
  object-fit: cover;
  pointer-events: none;
}
/* iOS/Safari draws its own paused-state play button over a <video> whenever
   autoplay is blocked (Low Power Mode, Data Saver) -- independent of the
   `controls` attribute, so removing that attribute doesn't stop it, and the
   JS play() retry next to the element can't force playback without a user
   gesture. Hidden outright: the element is decorative (pointer-events: none
   above) so a paused frame with no play button reads as a still image, which
   is the correct fallback when autoplay genuinely can't run. */
.hero-inset::-webkit-media-controls-start-playback-button { display: none !important; }
/* The pseudo-element hide above only covers Safari's specific icon and only
   once autoplay is confirmed blocked -- every engine still draws some native
   loading/play affordance for the brief gap between the page appearing and
   the video's first decoded frame, which read as a quick flash on every
   visit. video.hero-inset starts invisible and only fades in once the
   'playing' event actually fires (see the inline script in index.html);
   .hero-inset-poster is a plain <img> of the video's own first frame sitting
   in the identical spot underneath, so there is never a gap with nothing (or
   worse, a native play icon) showing through. */
video.hero-inset { opacity: 0; transition: opacity 0.25s ease-out; }
video.hero-inset.is-playing { opacity: 1; }

/* ========================================================= PRODUCT CARDS */

.section { padding-block: 40px; }
.section > .legend { display: block; margin-bottom: 14px; }

.products { display: grid; gap: 20px; }

/* Beat-grouped tick divider between product panels -- replaces a plain
   border/gap with the site's own rectangular-LED vocabulary (see
   .panel-head::before above) rather than another hairline. Ticks cluster in
   fours. Added 2026-08-16, replacing the four-color .vu-strip the user cut the
   same day -- same impulse (a small rhythmic flourish between sections)
   without repeating a strip of all four accents at once.

   2026-08-19, TWO CHANGES, both at the user's request:

   1. ONE COLOUR, NOT FOUR. Each divider used to inherit the accent of the
      panel that FOLLOWED it (an inline --accent on the .tick-row), so the rule
      previewed which app came next. That is gone: every divider is now the
      site red, --tick-lit below, and the inline overrides were stripped from
      index.html. The preview idea was cute and it cost more than it paid --
      three dividers in three different oranges, sitting between panels that
      each carry their own accent already, read as colour noise rather than as
      a repeated element. One red across all three makes them the same object
      appearing three times, which is what a divider should be. If the preview
      behaviour is ever wanted back, it is one inline style per row.

   2. A DIFFERENT SEQUENCE ON EACH. All three used to run the same walking
      pattern -- lit tick = beat index, so 1/6/11/16 -- which made the three
      rows literally identical and gave away that they were decoration rather
      than a readout. Each row now carries a distinct 16-step pattern, written
      out in a comment above it in index.html: four-on-the-floor with a pickup,
      a 3-2 son clave, then a broken 16th figure. They get denser and more
      syncopated down the page. The patterns live in the MARKUP, not here,
      because they are content -- which steps are lit is a rhythm, and putting
      it in :nth-child() rules would hide it from anyone reading the page.

   --tick-lit is deliberately NOT var(--accent). The .tick-row sits between two
   .product panels, each of which sets its own --accent on itself; a bare
   var(--accent) here would resolve to :root's default, which happens to be the
   same red today and would silently follow along if that default is ever
   retuned for another reason. Naming it separately is what keeps the divider
   red pinned to the divider. */
.tick-row { display: flex; justify-content: center; margin: 6px 0; --tick-lit: #D13A2C; }
.tick-row .beat { display: flex; gap: 5px; margin-right: 18px; }
.tick-row .beat:last-child { margin-right: 0; }
.tick-row .tick {
  width: 4px; height: 10px; border-radius: 1px; flex: none;
  background: color-mix(in srgb, var(--control-border) 70%, transparent);
}
.tick-row .tick.lit {
  background: var(--tick-lit);
  box-shadow: 0 0 5px color-mix(in srgb, var(--tick-lit) 55%, transparent);
}

.product { display: grid; }
.product .panel-body {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: 22px;
  align-items: start;
}
.product .icon { width: 96px; height: 96px; display: block; border-radius: 18px; }

/* Remove any decorative lines/frames around app icons — prefer the raw artwork */
.product .icon,
.product-hero .icon,
.suite-icons img {
   border: none !important;
   box-shadow: none !important;
   background: transparent !important;
   outline: none !important;
}
.product h3 {
  font-family: var(--font-plate);
  margin: 0 0 2px; font-size: 21px; letter-spacing: -0.01em;
  /* All-caps, 2026-08-16 at the user's request: the apps' own top-right
     wordmark is set in caps (see .product-hero h1.engraved and .panel-head,
     both already uppercase), so the homepage card name was the one place a
     product's name still ran mixed-case against every other appearance of
     that name on the site. */
  text-transform: uppercase;
}
/* ---- The product name, cut into the card ---------------------------------
   Every product page's h1 has been .engraved since the site was built; the
   homepage card carrying the same name was flat ink, which is the inconsistency
   the user pointed at. Engraved here too — but with the apps' 1px recipe, not
   the .engraved class, for the reason set out over .panel-head: the clipped
   gradient and 2-3px lips in .engraved are tuned for a 34-60px nameplate and at
   21px the lip starts reading as a second letter rather than a groove edge.

   ⚠ The fill is deliberately LIGHT — near the plate rather than near the ink,
   which is the user's own instruction ("rather more white or like the
   background looking"), and it is also the apps' actual number: about twelve
   points of lightness below the surface the word sits on. Our card body is
   control-bg at 94%, so the fill lands ~82%. That is much fainter than a
   heading normally dares to be, and it works for the same reason it works on
   the hardware: what you read is not the contrast of the letter against the
   plate, it is the white lower lip and the dark upper lip drawing the edges of
   the cut. Take either shadow away and this collapses into illegible grey.

   Set on the <a>, not the <h3>: the anchor carries `color: var(--ink)` from the
   base rule, which would otherwise paint straight over anything set on the
   heading. Hover tints the fill toward the product's accent and keeps the cut,
   rather than swapping to flat accent-coloured text — the name should not pop
   out of the plate on the way to being clicked. */
.product h3 a {
   text-decoration: none;
   /* Match the header wordmark's lighter, near-background engraved fill */
   color: color-mix(in srgb, var(--control-bg) 92%, black);
   text-shadow:
      0 1px 0 hsla(0,0%,100%,0.95),
      0 -1px 0 hsla(28,12%,14%,0.40);
}
.product h3 a:hover {
  color: color-mix(in srgb, var(--accent) 48%, var(--control-bg));
}
.product .tagline { margin: 0 0 12px; color: var(--text-secondary); font-size: 14px; }
.product .blurb { margin: 0 0 14px; max-width: var(--measure); }

/* The spec chips that used to sit under each blurb are gone, and .specs went
   with them on 2026-08-13. They were three short chips per card, and three
   chips read as *the* feature list: MutationStation was reduced to "33 pitch
   strategies, 8-lane Live modulation" when it does a great deal more, and every
   card spent one of its three on "macOS · Apple Silicon", which is a system
   requirement rather than a reason to buy anything. The same strip had already
   been cut from the Gumroad cover art for the same reason. Do not reintroduce
   it: a finite badge row on a card always undersells an app with more in it
   than fits, and the blurb underneath is doing that job properly. */

/* ================================================================ BUTTONS

   The commit button, built like the apps' primary action: a moulded cap in
   the product's own accent, with a hard printed top edge rather than a blur. */

/* `--btn-accent` is a copy of the product's accent, taken one level above the
   button. It exists for exactly one reason, spelled out in the GUMROAD OVERLAY
   GUARD below: Gumroad's injected stylesheet redefines `--accent` *on the
   button element itself*, so a buy button cannot read the page's accent from
   `var(--accent)` any more. It can still read it from an ancestor. */
.btn-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  --btn-accent: var(--accent); }

.btn {
  display: inline-flex; align-items: center; gap: 8px;
  font: inherit; font-size: 13px; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase;
  padding: 9px 16px;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, black);
  color: #fff; text-decoration: none;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 88%, white) 0%,
    var(--accent) 46%,
    color-mix(in srgb, var(--accent) 82%, black) 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.34), 0 1px 3px rgba(0,0,0,0.26);
  text-shadow: 0 1px 1px rgba(0,0,0,0.32);
  cursor: pointer;
}
/* Hover reads as a switch about to be pressed, not a link fading brighter:
   a slight lift (a touch more contact shadow) rather than the flat
   brightness(1.06) wash this used to be — that wash is exactly the "web
   hover fade" the 2026-08-16 request asked to move away from. :active still
   owns the actual "pressed" state below. */
.btn:hover {
  color: #fff;
   box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), inset 0 -1px 1px rgba(0,0,0,0.22), 0 1px 2px rgba(0,0,0,0.12);
}
.btn:active {
   box-shadow: inset 0 2px 5px rgba(0,0,0,0.36), inset 0 0 0 1px rgba(0,0,0,0.12);
   filter: brightness(0.96);
   transform: translateY(1px);
}

/* Make `.btn` share the same raised/beveled treatment as header nav buttons */
.btn {
   box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), inset 0 -1px 1px rgba(0,0,0,0.22), 0 1px 2px rgba(0,0,0,0.12);
   text-shadow: 0 1px 0 rgba(255,255,255,0.5);
   border: 1px solid color-mix(in srgb, var(--accent) 55%, black);
}

/* 2026-08-17: unify every `.btn`/`.btn-ghost` (buy + details, on every panel
   and app page) with the header's `.site-nav a` hardware-button family --
   same two-tone bevel border (darker along the bottom edge, like a cap seen
   from slightly above) and the same small LED indicator chip ahead of the
   label. `.btn`'s own per-product accent gradient is kept -- that IS the
   button's "colour" (Gumroad red, an app's accent, etc.) -- this only
   touches the border/shadow/LED, which were flat and inconsistent with the
   header before. */
.btn::before, .btn-ghost::before {
   content: ""; width: 8px; height: 5px; border-radius: 1px; flex: none;
   background: hsl(48,4%,58%);
   box-shadow: inset 0 1px 1px rgba(0,0,0,0.35), inset 0 -1px 0 rgba(255,255,255,0.15);
}
/* The ghost (neutral cream) button sits on the same pale surface as the
   header, so its LED lights the same way -- accent-coloured, like
   `.site-nav a.current::before`. A `.btn` is already sitting ON its own
   accent-coloured gradient, where a same-hue LED would vanish, so it lights
   plain white/bright instead -- the same "something just switched on"
   read, tuned for a coloured face rather than a cream one. */
.btn-ghost:active::before {
   background: var(--accent, hsl(8,80%,55%));
   box-shadow: 0 0 5px color-mix(in srgb, var(--accent, hsl(8,80%,55%)) 70%, transparent), inset 0 1px 1px rgba(0,0,0,0.25);
}
.btn:active::before {
   background: #fff;
   box-shadow: 0 0 5px rgba(255,255,255,0.7), inset 0 1px 1px rgba(0,0,0,0.25);
}
.btn {
   border-bottom-color: color-mix(in srgb, var(--accent) 30%, black);
}

/* --------------------------------------------------------------------------
   GUMROAD OVERLAY GUARD
   Every buy button carries `class="btn gumroad-button"`. The second class is
   the hook gumroad.js binds its modal checkout to (see the comment at the foot
   of index.html for why the overlay is worth having at all).

   The problem it creates: gumroad.js does not only bind behaviour, it injects
   a stylesheet that paints `.gumroad-button` as a Gumroad-branded button —
   pink, its own font, its own padding, its own border-radius. Left alone that
   would replace our button on every page with a foreign one, which is a
   strange thing to hand a customer at the exact moment they decide to pay.

   This block is NOT guesswork. `gumroad.js` is a 539-byte loader that appends
   `assets.gumroad.com/vite/assets/entrypoints/overlay-<hash>.css`, and that
   file was fetched and read — it needs no product to be published, so the one
   part of the overlay that could be checked in advance was. Three things in it
   matter, and the third is a trap:

   1. It styles `a.gumroad-button` — one type + one class. Our selector adds a
      second class and outranks it outright. `!important` is therefore belt and
      braces rather than the mechanism, kept because the file is versioned by
      content hash and can be reissued at any time without warning.
   2. It ships `:hover` and `:active` rules, which are one pseudo-class *more*
      specific than our base rule and so would win against it. That is why the
      two rules below exist: without them the button slides up-left by a quarter
      rem on hover, grows a hard offset shadow, and turns Gumroad pink.
   3. ⚠ It sets custom properties on `.gumroad-button` itself, among them
      `--accent: 255 144 232`. Anything here reading `var(--accent)` would
      resolve it against *that* — a bare `255 144 232` is not a colour, so the
      gradient and border would not merely go pink, they would fail to parse and
      drop out entirely, leaving a transparent button. Hence `--btn-accent`,
      copied off `.btn-row` above, which Gumroad's rules never touch. **Do not
      "simplify" these back to `var(--accent)`.**

   Every property Gumroad's rules set is answered below. If it ever reissues
   that stylesheet with a property not named here, that property gets through —
   the fix is to add it, not to drop the overlay.
   -------------------------------------------------------------------------- */
/* 2026-08-17: gumroad.js doesn't only inject the overlay stylesheet above --
   gumroad-bundle.js (which it loads) also mutates each `.gumroad-button`
   anchor, appending a `.logo-full` child (a CSS-masked "gumroad" wordmark,
   ~7em wide) once it has run. Until that happens the button is just our own
   plain "GET IT ON" text at its base width; the wordmark landing a beat later
   pops the button wider mid-page-load, which read as cheap. Buttons start
   `visibility: hidden` (NOT display:none/opacity, which wouldn't reserve the
   box) and the inline script at the foot of index.html adds `.gr-ready` the
   moment it sees `.logo-full` land (with a fallback timeout for a blocked or
   slow gumroad.js) -- so the button always appears already at final size. */
a.btn.gumroad-button { visibility: hidden; }
a.btn.gumroad-button.gr-ready { visibility: visible; }

a.btn.gumroad-button {
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  gap: 8px !important;
  font: inherit !important;
  font-size: 13px !important;
  font-weight: 700 !important;
  line-height: normal !important;
  letter-spacing: 0.05em !important;
  text-transform: uppercase !important;
  text-decoration: none !important;
  padding: 9px 16px !important;
  height: auto !important;
  width: auto !important;
  min-width: 0 !important;
  border-radius: var(--radius) !important;
  border: 1px solid color-mix(in srgb, var(--btn-accent) 55%, black) !important;
  border-bottom-color: color-mix(in srgb, var(--btn-accent) 30%, black) !important;
  color: #fff !important;
  background-color: transparent !important;
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--btn-accent) 88%, white) 0%,
    var(--btn-accent) 46%,
    color-mix(in srgb, var(--btn-accent) 82%, black) 100%) !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.34), 0 1px 3px rgba(0,0,0,0.26) !important;
  text-shadow: 0 1px 1px rgba(0,0,0,0.32) !important;
  transform: none !important;
  transition: none !important;
}
/* Point 2 above. These mirror `.btn:hover` / `.btn:active` exactly, so a buy
   button and an ordinary button still feel identical under the cursor. */
a.btn.gumroad-button:hover {
  color: #fff !important;
  filter: brightness(1.06) !important;
  transform: none !important;
  background-color: transparent !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.34), 0 1px 3px rgba(0,0,0,0.26) !important;
}
a.btn.gumroad-button:active {
  transform: none !important;
  filter: brightness(0.96) !important;
  box-shadow: inset 0 2px 4px rgba(0,0,0,0.34) !important;
}

.btn-ghost {
  /* Same warm-cream moulded cap as `.site-nav a` (the header's own buttons),
     not the flatter --face-hi/--control-bg wash this used to be -- 2026-08-17,
     part of unifying every "Details"-style button on panels/apps with the
     header's button family. */
  background: linear-gradient(180deg, hsl(48,12%,98%) 0%, hsl(48,8%,90%) 45%, hsl(48,6%,75%) 100%);
  color: var(--text-primary);
  border: 1px solid hsl(48,5%,40%);
  border-bottom-color: hsl(48,6%,28%);
  text-shadow: 0 1px 0 rgba(255,255,255,0.5);
  box-shadow:
     inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -1px 1px rgba(0,0,0,0.16),
     0 1px 0 rgba(255,255,255,0.5), 0 1px 0 hsl(48,5%,60%), 0 2px 3px rgba(0,0,0,0.18);
}
.btn-ghost:hover { color: var(--text-primary); filter: brightness(1.05); }
.btn-ghost:active {
  transform: translateY(1px);
  box-shadow: inset 0 2px 4px rgba(0,0,0,0.4), 0 1px 0 rgba(0,0,0,0.1);
}

.price { font-size: 13px; font-weight: 600; color: var(--text-secondary); }

/* A product that is not for sale yet. It gets the panel furniture like
   everything else — the point is that it reads as a considered part of the
   range rather than a broken buy button. */
.notice { max-width: var(--measure); margin-top: 6px; }
.status {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-secondary);
  border: 1px dashed var(--control-border); border-radius: var(--radius);
  padding: 8px 14px;
  background: color-mix(in srgb, var(--face-hi) 55%, transparent);
  text-shadow: 0 1px 0 rgba(255,255,255,.55);
}
.status::before {
  /* Rectangular pilot lamp, matching .panel-head::before -- see that rule for
     why round became rectangular. */
  content: ""; width: 5px; height: 8px; border-radius: 1.5px;
  background: var(--accent); opacity: .55;
}

/* ================================================================= SUITE

   The four icons in a row, at the size they read at in a Dock — which is the
   argument for the bundle made visually rather than in prose. */

.suite .panel-body { display: grid; grid-template-columns: auto 1fr; gap: 26px; align-items: center; }
.bundles { display: grid; gap: 20px; }
/* Column flow rather than a fixed 2-wide grid: the Creative Bundle carries two
   icons and the Toolbox three, and a 2-column grid put the third on its own
   second row looking like an afterthought. */
.suite-icons { display: grid; grid-auto-flow: column; gap: 10px; }
.suite-icons img { width: 74px; height: 74px; border-radius: 15px; display: block; }

@media (max-width: 720px) {
  .suite .panel-body { grid-template-columns: 1fr; gap: 18px; }
  
  .suite-icons img { width: 60px; height: 60px; border-radius: 12px; }
}

/* ================================================== SCREENSHOTS, CUT OUT

   These used to sit in a dark inset recess, borrowed from Spectrl's analyser
   window and MutationStation's piano roll — both of which are genuinely a dark
   screen sunk into a cream plate. That reads correctly for a piece of UI inside
   an app and wrong for a picture of a whole app: it wrapped a black rectangle
   around a cream window, so the pages read as a stack of dark cards laid on the
   plate rather than as windows lying on it. The user's call, and it is the right
   one — the screenshots now sit directly on the faceplate with nothing behind
   them.

   .screen is kept as a class name rather than renamed across 23 call sites in
   index.html and apps/, because it still marks the same thing (this is imagery,
   frame it), and .screen.empty below still is a screen.

   THE SHADOW IS `filter`, NOT `box-shadow`, and that is the whole trick. macOS
   rounds a window's corners but a screenshot flattens whatever was behind them
   into the file, so these captures arrived with black or grey corner nubs;
   make-cutouts.py masks them to transparency (read its docstring before
   recapturing anything). A box-shadow would then trace the image's bounding
   BOX, printing a hard square shadow behind those now-transparent corners and
   undoing the cutout. drop-shadow() traces the alpha channel, so the shadow
   follows the real rounded silhouette. Two of them, tight-and-dark over
   wide-and-soft, is the usual contact-plus-ambient pair; a single shadow either
   floats or smudges.

   No border-radius here on purpose: the curve lives in the PNG's alpha now. A
   CSS radius on top would cut a second, differently-sized arc across the first. */

.screen { display: block; }
.screen img {
  width: 100%;
  display: block;
  filter:
    drop-shadow(0 1px 2px rgba(46, 46, 42, 0.30))
    drop-shadow(0 14px 28px rgba(46, 46, 42, 0.22));
}

/* The hero composite is the one image that must NOT get the filter: make-hero.py
   composites its own shadow under each window as it stacks them (SHADOW_BLUR,
   SHADOW_OFFSET, SHADOW_ALPHA in that file), because the shadows there have to
   fall BETWEEN the stacked windows, which no single CSS shadow on the finished
   PNG can do. Adding this one on top gives every window in the stack two. */
.hero-shot .screen img { filter: none; }

/* mutationstation.png is the one capture in assets/shots without an alpha
   channel (sips -g hasAlpha: no, vs. yes on every other app's window shot) --
   it was captured as a flat rectangle instead of a transparent macOS window,
   so it shows square corners where every other product page's hero shot has
   the window's real rounded corners baked into the PNG itself (this file has
   no border-radius of its own; the roundedness everywhere else is the image
   content, not CSS). Recapturing it is a MutationStation-repo/build-lane job,
   not a site-repo one, so this is a CSS patch, not the real fix: clip this one
   image to an approximate macOS window radius. border-radius is a property of
   the RENDERED box, not the source file's native pixel grid -- it does not
   care that this PNG is 3456px wide. Both this image and chordinator.png are
   capped by `.hero-shot .screen img { max-height: 620px }` and land at almost
   the same *displayed* width (~1063px, since both captures are close to the
   same aspect ratio), so the number that matters is chordinator's real alpha
   corner (~15px, measured pixel-by-pixel down its left edge) scaled by that
   1063/2000 display shrink -- ~8px. (First pass scaled 15px UP by this image's
   *native* 3456px width instead of down by the *displayed* size, landing on
   26px -- visibly too round next to the other three genuinely-alpha hero
   shots. Do not repeat that mistake: always scale from displayed px, never
   native.) Remove this rule (and the class in apps/mutationstation.html) once
   mutationstation.png is recaptured with alpha like the others. */
.win-round { border-radius: 8px; }

/* ---- Zoomed feature crops -------------------------------------------------

   The per-feature shots on the product pages are crops cut out of the
   full-window captures already in assets/shots, not fresh captures. Two
   reasons, and the second is the one that matters: the originals are
   2000-3456px wide, so a single panel comes out at native resolution with room
   to spare; and relaunching the apps to shoot a panel walks straight into the
   verification traps in the workspace CLAUDE.md (an Electron single-instance
   lock hands the relaunch back to the old process, and an occluded window
   renders differently).

   Their aspect ratios are deliberately not normalised — a 1920x157 keybed
   strip sits next to a 322x645 knob column, because cropping a panel to a
   common ratio would mean cutting off half of it. So HEIGHT is what is capped
   here, not width, and each crop is centred in its recess. Capping width
   instead is the obvious move and it is wrong: it blows the tall panels up to
   the full measure, where a 322px-wide column becomes a soft 1000px smear. */

.zoom { margin: 22px 0 0; }
/* These crops are interiors — slices from the middle of a window — so unlike the
   full-window shots they have square corners and no transparency, and they are
   left that way (make-cutouts.py explains why rounding them would invent a curve
   the app does not have). The drop-shadow inherited from .screen img is what
   separates them from the plate now that the recess is gone. */
.zoom .screen img { width: auto; max-width: 100%; max-height: 620px; margin: 0 auto; display: block; }
/* Deliberately NOT .legend. The engraved-label style above is for two or three
   words silkscreened onto hardware; a caption here is a sentence explaining what
   the crop is showing, and a sentence set in 10px uppercase with 0.14em tracking
   is unreadable — it was tried and it shouted. Sentence case, secondary colour,
   sized just under body copy so it reads as an aside. */
.zoom figcaption {
  display: block;
  margin: 10px 2px 0;
  max-width: 66ch;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-secondary);
}

/* ---- Portrait crops: .zoom-pair and .media-split ---------------------------

   Four of the crops on these pages are portrait — Chordinator's library rail,
   MidiMirror's Remote Script / Modes panel, MutationStation's open algorithm
   list and its Acid Character column. Inside a plain figure.zoom each was
   centred at its natural width, which on a 1240px .wrap means a ~270px picture
   with two ~450px stripes of empty faceplate either side. That was the worst
   dead space left on the site, and the user called it out on three separate
   pages in a row, which is what these two layouts answer.

   Two of them, because there are two different situations:

   .zoom-pair stands two figures side by side, each left-aligned in its own
   track with its caption under its own image. Used where there genuinely is a
   second thing worth showing next to the tall one — Chordinator's history strip
   beside the library, MutationStation's shaping column beside Acid Character,
   the Pitch Mutation panel beside the list of what its Algorithm menu holds.

   .media-split runs the prose down the left and stands the figure in a track on
   the right, for the tall crop that has no partner: the text explaining it is
   then the thing filling the other half, which is better than inventing a
   screenshot to fill it. The prose drops to one column inside the split — it is
   already narrow there, and two 30em tracks squeezed into half a page read as a
   newspaper clipping.

   Deliberately NOT reusing the .two-col grid for either. two-col is 1fr 1fr and
   these both need the picture track sized to the picture: a portrait crop given
   half of 1176px either scales up past its pixel width and goes soft, or sits
   centred in its own half and reproduces the same dead space one level down.

   They collapse at 900px rather than the site's usual 720 breakpoint, because a
   430px track is already too narrow for a screenshot with legible labels in it;
   these want to stack well before the rest of the page does. */
.zoom-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 26px 34px;
  align-items: start;
  margin: 22px 0 0;
}
.media-split {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 34px;
  align-items: start;
  margin: 22px 0 0;
}
.zoom-pair > .zoom, .media-split > .zoom { margin: 0; }
.media-split > .zoom { max-width: 400px; }
.media-split .prose.story { columns: 1; }

/* `margin: 0 auto` on the image is precisely what centres it in the full-width
   case, so it is the thing being undone here — the whole point of both layouts
   is that the picture starts at its track's left edge. The height cap comes
   down too: 620px was set for a figure with the full page to itself. */
.zoom-pair .screen img,
.media-split .screen img { margin: 0; max-height: 540px; }
/* 66ch does not fit in half a page, and a caption wrapping at some width other
   than its own track's looks like a mistake rather than a measure. */
.zoom-pair figcaption, .media-split figcaption { max-width: none; }

@media (max-width: 900px) {
  .zoom-pair, .media-split { grid-template-columns: 1fr; }
  .media-split > .zoom { max-width: 100%; }
}

/* ---- No .render rule here, and that is on purpose --------------------------

   There used to be one, for Blender renders of the apps as physical objects.
   Three separate attempts at that idea were made and all three were rejected by
   the user: a CSS/PIL chassis, extruded screenshots ("it just looks like ai
   slop"), and finally a fully modelled metal nameplate for the wordmark, which
   was geometrically correct and rejected on taste anyway.

   The conclusion, which is a design decision and not a gap: this site is FLAT.
   Depth is drawn the way the apps draw it, with the engraved/embossed text
   treatments above and the .screen.empty recess, never with a photograph of an
   object that does not exist. Do not reintroduce a render class or the assets
   that went with it. */

/* Placeholder for a product that has no screenshot yet. Deliberately looks
   like an unlit screen, not a broken image — which is why this one keeps the
   dark inset the photographs gave up: there is no window to cut out here, and a
   bare line of text on the plate would read as missing copy rather than as a
   panel waiting to be filled. This is now the last user of --screen-bg and its
   siblings; keep those variables when the arranger shot lands and this rule
   goes, since they are the apps' own screen colours. */
.screen.empty {
  display: grid; place-items: center;
  min-height: 220px; text-align: center; padding: 30px;
  background: var(--screen-bg);
  border: 1px solid var(--screen-edge);
  border-radius: var(--radius);
  box-shadow:
    inset 0 2px 6px rgba(0,0,0,0.6),
    inset 0 0 0 1px rgba(255,255,255,0.04),
    0 1px 0 rgba(255,255,255,0.5);
  color: var(--screen-text);
}
.screen.empty .legend { color: var(--screen-dim); text-shadow: none; }

/* ========================================================== PRODUCT PAGES */

.product-hero { padding-block: 48px 28px; }
/* The screenshot section right after the hero was stacking its own top
   padding on top of the hero's bottom padding (28px + 40px desktop, 22px +
   28px mobile) with nothing visible in between -- the buy button that lives
   at the bottom of the hero is `visibility: hidden` until Gumroad's script
   tags it `.gr-ready` (see the GUMROAD OVERLAY GUARD comment below), so that
   gap read as one big blank strip of dead space before the first screenshot.
   Zeroing the section's top padding here leaves the hero's own bottom
   padding as the only space between them -- flagged by the user 2026-08-17
   on the Chordinator app page, mobile Safari. */
.product-hero + .section { padding-top: 0; }
.product-hero .row { display: flex; align-items: center; gap: 22px; margin-bottom: 22px; }
.product-hero .icon { width: 88px; height: 88px; border-radius: 17px; flex: none; }
/* line-height and --engrave-line must stay equal: the cut gradient tiles once
   per --engrave-line, so if they drift apart the sweep stops lining up with the
   glyphs and the name goes flat. See the .engraved block. */
.product-hero h1 { margin: 0 0 4px; font-size: 34px; line-height: 1.15; --engrave-line: 1.15em; letter-spacing: -0.015em; color: var(--text-heading); }
/* Product-page h1s carried the base .engraved recipe (background-clip: text
   gradient, --font-plate) -- the homepage nameplate's treatment. At the
   nameplate's ~60px that gradient reads as a milled cut; at a bold 34px in
   Century Gothic/Futura's round strokes it reads as a puffy 3D emblem
   instead, nothing like the actual engraving in the corner of every app's own
   window (small, mono, uppercase, a hard 1px light-lip/dark-lip pair -- see
   .panel-head::before's comment, which already ported that exact recipe once
   for the site's section-header band). User flagged the mismatch 2026-08-16
   and asked for these titles to read as the SAME engraving as the app
   window's, not a family resemblance -- so this overrides .engraved's
   gradient-clip approach with .panel-head's hard-shadow lip technique,
   scaled up, rather than retuning the gradient version. Do not merge this
   back into the base .engraved rule; the homepage nameplate keeps its own
   look on purpose (root CLAUDE.md: comments carry the why, and the why here
   is these are two different surfaces that happened to share a class name). */
.product-hero h1.engraved {
  /* Font stays var(--font-plate), inherited from the base .engraved rule below
     -- NOT switched to monospace. First pass here swapped in the app corner
     wordmark's literal font (ui-monospace) to chase its exact recipe, which
     made this page's h1 the one title on the site set in a different
     typeface than the homepage card name and the app's own top row (both
     --font-plate). The user called that out as looking "different and bad"
     2026-08-16 -- copy the app wordmark's SHADOW recipe, not its font. Do not
     reintroduce a font-family override on this rule. */
  text-transform: uppercase;
  background: none;
  -webkit-background-clip: initial;
  background-clip: initial;
  -webkit-text-fill-color: currentColor;
  /* Fill lightened 2026-08-16 to match .product h3 a's card-name recipe --
     the user pointed at the homepage card title (light, near the plate) and
     this page's h1 (darker, --ui-bg at 66% black) side by side and asked for
     the SAME fill, not just the same lip-shadow technique. Same ~87%-toward-
     white mix as the card name; the two now differ only in what surface
     they're read against (--ui-bg here vs --control-bg on the card), not in
     how dark the cut runs. */
  color: color-mix(in srgb, var(--ui-bg) 87%, black);
  text-shadow:
    0  1px 0 hsla(0,0%,100%,0.88),
    0 -1px 0 hsla(28,12%,14%,0.42);
}
.product-hero .tagline { margin: 0; color: var(--text-secondary); font-size: 16px; }
.product-hero .lede { font-size: 17px; max-width: var(--measure); margin: 0 0 24px; }

.features { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
.features li { padding-left: 18px; position: relative; max-width: var(--measure); }
/* the step-dot from the company mark, doing the work of a bullet */
.features li::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
}
.features strong { color: var(--text-heading); }

.back { display: inline-block; margin-bottom: 22px; font-size: 13px; text-decoration: none; }
.back:hover { text-decoration: underline; }

.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }

/* ---- The closing panel group ----------------------------------------------

   Every app page ends with the same four-ish reference panels — Why standalone,
   Requirements, Setting it up, and whichever integration note the app has. Until
   2026-08-13 these were a .two-col with one panel on the left and the other
   three stacked inside a wrapper <div> on the right, which is exactly what it
   looked like: a tall ladder of boxes running off the bottom of the page while
   the left half sat empty below the first one.

   Multi-column rather than grid, and that is the whole reason this is not
   `grid-template-columns: repeat(3, 1fr)`. The panels have wildly different
   heights (Requirements is four paragraphs, Why standalone is one), so a grid
   gives you a short row, a tall row, and a final row holding one panel beside
   two-thirds of nothing — the same dead space in a new arrangement.

   2026-08-19: `columns: 3 320px` itself turned out to be the wrong tool for
   "balance total height across the tracks." CSS column-balancing only ever
   estimates one candidate column height and then fills columns in SOURCE
   ORDER, breaking to the next column the moment a block would overflow that
   estimate -- it never moves a block once placed. That works while the blocks
   are all a similar size, but MutationStation grew a fifth panel
   ("Particular integration with Live", ~500px, break-inside:avoid so it can't
   be split) and the estimate that lets it fit without overflowing anywhere
   left the *next* column mostly empty -- the exact dead-space bug this
   section was written to avoid, just relocated. Chordinator has the same
   imbalance, smaller and less visible only because its tall panel is first.

   Flex-wrap + a JS masonry pass (panel-grid.js) replaces the column-balance
   guess with an actual greedy shortest-column placement, which cannot strand
   a whole column behind one oversized block -- worst case it's off by one
   panel's height, not a whole column. Flex-wrap alone (i.e. with JS disabled)
   is the fallback: panels wrap into even rows instead of true masonry, uneven
   row bottoms but never a stranded empty column. Track width and dropping to
   two tracks, then one, as the viewport narrows falls out of flex-wrap's own
   basis/grow math, so there is still no separate mobile rule for it. */
.panel-grid { display: flex; flex-wrap: wrap; gap: 24px; margin-top: 24px; align-items: flex-start; }
.panel-grid > .panel { flex: 1 1 320px; min-width: 0; margin: 0; }
/* panel-grid.js reparents the flat .panel children above into these once it
   has measured them -- one per masonry column, laid out as its own vertical
   flex stack so the 24px rhythm matches the un-enhanced fallback exactly. */
.panel-grid-col { display: flex; flex-direction: column; gap: 24px; flex: 1 1 320px; min-width: 0; }

/* The long "What it does" / "What else is in it" list that opens the closing
   section is deliberately NOT one of the .panel-grid children. It runs to eight
   or nine items, which is taller than a third of the group on its own, so the
   balancer had nowhere to put it but a column of its own — leaving the third
   track empty and reproducing the dead space this was meant to remove. It gets
   the full width above the grid instead, with the list flowed into its own
   columns. Items do not break, because a bullet split across a column boundary
   loses its dot. */
.features.cols { display: block; columns: 3 290px; column-gap: 34px; }
.features.cols li { break-inside: avoid; max-width: none; margin: 0 0 14px; }

.prose { max-width: var(--measure); }
.prose p { margin: 0 0 14px; }

/* A full-width panel telling one story (Controlled randomness, The drop).

   These used to be a single --measure column centred in the panel, on the
   argument that the leftover width then read as margin. It did not; it read as
   a stripe of text with a lot of faceplate either side, and it got worse when
   .wrap went to 1240px. Left-aligned multi-column instead: the text starts at
   the panel's own left edge like every other block on the page, the line length
   still lands near --measure because the track width is what sets it, and the
   full width of the panel is actually used. Deliberately no `break-inside` on
   the paragraphs — the short stories (one or two paragraphs) need to split
   mid-paragraph to fill both tracks, which is the case that was worst before. */
.prose.story { max-width: none; margin-left: 0; margin-right: 0; columns: 2 30em; column-gap: 46px; }
.prose h3 { font-size: 15px; margin: 22px 0 8px; color: var(--text-heading); }
.prose code {
  font: 12.5px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  background: color-mix(in srgb, var(--face-hi) 80%, transparent);
  border: 1px solid var(--hairline); border-radius: 3px; padding: 1px 5px;
}

/* ================================================================ FOOTER */

.site-footer {
  margin-top: 56px;
  border-top: 1px solid var(--hairline);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.55);
  padding: 28px 0 44px;
  color: var(--text-secondary); font-size: 13px;
}
.site-footer .wrap { display: flex; flex-wrap: wrap; gap: 16px 28px; align-items: center; }
.site-footer .spacer { margin-left: auto; }

/* =============================================================== MOBILE */

/* ---- Phones ---------------------------------------------------------------

   This used to be four declarations, on the assumption that a one-column layout
   degrades gracefully on its own. Mostly it does; these are the places where it
   does not, each one checked at 390px:

     - the header is a sticky 58px bar carrying a wordmark and five nav items,
       which collide long before 390px. The nav wraps under the brand and the
       bar stops being a fixed height rather than squeezing the type further;
     - background-attachment: fixed forces iOS Safari to repaint the whole
       gradient on every scroll frame, which is the single most expensive thing
       on this page on a phone and buys nothing once the viewport is narrower
       than the panel it is meant to unify. Scroll it with the page instead;
     - the bundle icon rows and the buy buttons are laid out for a mouse. Both
       get full-width targets rather than 13px type in a 9px-tall pill.       */
@media (max-width: 720px) {
  .product .panel-body { grid-template-columns: 64px 1fr; gap: 16px; }
  .product .icon { width: 64px; height: 64px; border-radius: 13px; }
  .two-col { grid-template-columns: 1fr; }
  .hero { padding-block: 48px 36px; }

  /* padding-left only -- a `padding: 0 20px` shorthand here was clobbering
     the 46px padding-right the 900px breakpoint above reserves for the fixed
     page-fader, since this block (max-width:720px) also matches every phone
     width and comes later in the cascade. That let body copy run under the
     fader on real phones even though the reservation looked correct on paper. */
  .wrap { padding-left: 20px; }
  .site-header .wrap { padding-right: 20px; }
  .section { padding-block: 28px; }

  .site-header .wrap {
    height: auto; min-height: 50px;
    flex-wrap: wrap; gap: 8px 16px; padding-top: 10px; padding-bottom: 10px;
  }
  /* Was 3 rows on a phone: brand, then site-nav wrapping ITSELF into two
     lines (select+Bundles / Licence+About+Support) because the select's
     140px min-width left no room for the four links on one line below the
     brand. Moving .hero-lcd out of .site-nav in the markup (it's a sibling
     of .site-nav now, not a child) lets it sit on the brand's row instead --
     .brand's own width plus the select's ~140-150px comfortably fits a
     phone-width row -- leaving .site-nav's four short links, which do fit
     on one line, as the second and last row. */
  .hero-lcd { margin-left: 0; gap: 4px; }
  /* The four links' own width (padding + LED + gap, all sized for desktop)
     ran ~380px against ~353px of usable row width at 393px viewports, so
     `flex-wrap: wrap` here was still firing -- nav split into its own two
     lines, undoing the two-row goal above. Tightened padding/gap/font enough
     to clear a 360px-wide phone on one line; flex-wrap stays as a safety net
     for anything narrower rather than the primary layout mechanism. */
  /* Measured directly (a same-origin iframe pinned to 375px, not the
     browser-tool's resize_window -- that reports a stubborn 478px in this
     environment regardless of the width requested, which is why an earlier
     "fixed" pass didn't survive a real 390px phone). At font-size:12px /
     padding:6px 8px / gap:8px, the four links summed to ~341px against
     ~331px of row width -- Support alone cleared onto its own third row.
     Cut padding and gap further and dropped to 11px; re-measured in the
     same harness at 371px effective width with all four links landing on
     one row (top offset identical across all four). */
  .site-nav { margin-left: auto; width: 100%; justify-content: flex-end; gap: 4px; font-size: 11px; flex-wrap: wrap; }
  .site-nav a { padding: 5px 6px; gap: 4px; }
  .wordmark { font-size: 14px; }
  /* The 2026-08-17 desktop bump (24px wordmark / 34px logo) read oversized
     once the header wraps to two rows on a phone-width viewport -- it and
     the LCD select were eating close to a third of the screen height before
     any page content showed. Scaled both back down for this breakpoint only;
     desktop keeps the bigger mark. */
  .brand svg, .brand img { height: 19px; }
  .brand { gap: 6px; }
  /* Brand + select still measured as fitting one row against a wider test
     viewport than a real phone actually renders at -- against a genuine
     ~390px-wide device the two wrapped onto separate lines (3 rows total,
     not the intended 2). The select already ellipsis-truncates long app
     names (text-overflow: ellipsis, set unconditionally above), so shrinking
     its min-width here costs nothing but a few characters of the closed
     label -- it buys the margin that made brand+select unreliable.

     A second, separate bug (the generic `.wrap` padding-right:46px reserved
     for the page-fader was also landing on `.site-header .wrap`, which
     doesn't need it -- see the 900px/720px breakpoints above) was actually
     the main cause of that failure, and is fixed at the source now. These
     sizes are shrunk a further notch on top of that fix, not instead of it,
     as real margin against font-metric differences between browsers rather
     than a break-even fit. */
  .hero-lcd-select { font-size: 10px; min-width: 88px; max-width: 130px; padding-right: 14px; }

  .btn { padding: 8px 12px; font-size: 13px; }
  .btn-row .btn { flex: 1 1 100%; justify-content: center; }
  .btn-row .price, .btn-row .status { flex: 1 1 100%; }

  /* The product-page title row overflowed by 3px on a 371px viewport, and only
     on MutationStation: "MutationStation" is one unbreakable 15-character word,
     so at a fixed 34px it sets a min-content width the flex row cannot shrink
     below, and an 88px icon plus a 22px gap ahead of it spent the rest of the
     measure. Both ends give a little here. The h1 is a clamp rather than a flat
     smaller size so the other four pages, whose names fit, keep their weight —
     and it is not `overflow-wrap: break-word`, which would fix the number by
     hyphenating the product's name in half. If a longer name ever ships, lower
     the vw term, do not let it break. */
  .product-hero { padding-block: 32px 22px; }
  .product-hero .row { gap: 16px; }
  .product-hero .icon { width: 64px; height: 64px; border-radius: 13px; }
  /* Re-tuned after the .wrap padding-right fix above: reserving the fader's
     46px (vs. the flat 20px this was originally measured against) leaves
     only ~225px for the h1 itself once the 64px icon and 16px gap are taken
     out of a 305px row -- the old 7.2vw put "MutationStation" at ~274px
     scrollWidth, well past that, and the fader fix re-exposed the overlap
     rather than causing a new one. Lowered until it clears in the same
     375px-iframe harness. */
  .product-hero h1 { font-size: clamp(20px, 6vw, 34px); }
}

/* The faceplate is a physical object with one lighting direction; there is no
   dark-mode version of a milled aluminium panel. The design commits to the
   light look deliberately, so colours are painted explicitly above and no
   prefers-color-scheme block overrides them. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* ==================================================================
   WARMTH PASS — 2026-08-16
   ==================================================================

   The faceplate above is deliberately monochrome: one milled panel, one
   lighting direction, the per-product accent rationed down to a 7px pilot lamp
   and the buy button. Correct as an *object*, but as a *page* it read as one
   endless grey sheet — the user's word was "sterile". This block adds colour
   and rhythm back WITHOUT touching any of the tuned recipes above.

   It is intentionally one appended, self-contained block for two reasons:

     1. Revert is `delete from this banner to EOF` — nothing above changes, so
        the milled look is recoverable byte-for-byte (git tag
        `pre-restyle-2026-08-16` is the belt to this braces).
     2. Everything here is ADDITIVE — new pseudo-elements, new :hover states,
        new properties the base rules never set (transition, a left spine, a
        band, a glow). It never re-declares a *value* the tuned rules or the
        responsive @media blocks above already own (padding, the engraving
        shadows, the palette tokens), so cascade order can't clobber them.

   The governing rule of the pass: BANDS ARE TONAL, ACCENTS ARE PER-CARD.
   Section rhythm is done with faint lightness shifts only (no colour cast over
   the whole page); the per-product colour is what carries the palette, and it
   only ever appears on that product's own furniture.
   ------------------------------------------------------------------ */

/* --- 1. Let each product's accent breathe --------------------------------

   (a) An accent spine down the left edge of every panel. Done as ::after, not
       a fat `border-left`, so the 1px control-border stays symmetric and the
       spine can carry its own rounded corners matching --radius. Now the four
       cards read as four colours at a glance instead of four identical greys,
       and the single content panels (licence green, why-standalone blue, about
       amber) pick up their accent too — the page has a palette.
   (b) The icon tile gets a faint ring + drop-glow in the same accent, so the
       artwork sits in its colour instead of floating on grey-on-grey. Pure
       box-shadow — the PNG itself is untouched. */
.panel { position: relative; transition: transform 140ms ease, box-shadow 140ms ease; }
.panel::after {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--accent);
  border-radius: var(--radius) 0 0 var(--radius);
  /* a hair of its own glow so the spine reads as lit, like the pilot lamp */
  box-shadow: 0 0 6px -1px color-mix(in srgb, var(--accent) 55%, transparent);
}
.product .icon, .suite-icons img {
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent),
    0 6px 16px -8px color-mix(in srgb, var(--accent) 60%, transparent);
}

/* --- 2. Alternate section backgrounds (tonal bands) — REMOVED 2026-08-19 ---

   There used to be a faint full-bleed darken behind #bundles and
   #why-standalone, so products / bundles / licence / why / about read as
   deliberate bands rather than one sheet:

       #bundles, #why-standalone {
         box-shadow: 0 0 0 100vmax rgba(31, 26, 21, 0.030);
         clip-path: inset(0 -100vmax);
       }

   THIS IS THE "RECTANGULAR ARTIFACTS IN THE BACKGROUND" BUG, and it was
   misdiagnosed twice before being found. Read this before adding any
   full-bleed section wash back.

   The user reported, repeatedly, four faint rectangles in the lower half of
   the page — two on the left edge, two on the right. Both earlier attempts
   assumed a GPU compositing-tile seam and rewrote `body::before` accordingly
   (first `background-attachment: fixed` -> a fixed pseudo-element, then a live
   1px repeating-linear-gradient -> a rasterised 1x2 SVG tile; both of those
   comments are still up at the body rule, and both changes were harmless but
   beside the point). Neither touched the actual cause, so the artifacts
   survived both.

   The cause is this rule, and the mechanism is not a rendering bug at all —
   the wash renders exactly as designed. It is full-bleed and continuous across
   the whole viewport width. The problem is what is drawn ON TOP of it: each of
   these two sections holds a .panel that covers the middle of the band with an
   opaque cream plate. So the only places the 3%-black wash is ever visible are
   the empty left and right margins beside that panel — two disconnected
   slivers per section, with hard vertical edges where the panel starts and
   hard horizontal edges where the section box ends. Two sections carrying the
   rule x two visible margins each = the four rectangles, in the lower half of
   the page, exactly as reported.

   Note the trap: this is invisible at narrow viewports, because the panel
   spans the full width and leaves no margin for the wash to show in. It only
   appears once the viewport is wide enough for the section to hit its
   max-width and gutters to open up — which is why it reads as intermittent and
   is easy to "confirm fixed" on the wrong window size. Reproduce it at ~1900px
   of layout width, not at 1000px.

   If a tonal band is wanted again, it has to be a treatment that reads as a
   band across the FULL width, not one that is only visible where nothing
   covers it: put the wash on a wrapper that sits behind the panel as well
   (so the eye sees one continuous stripe), or drop the panel's opaque plate
   over that section. Do not restore the rule above as-is. */

/* Step-grid texture (repeating-linear-gradient ticks behind the alternating
   bands) was tried 2026-08-16 and dropped the same day at the user's request
   ("weird lines going through the white" -- doesn't look good). Do not
   reintroduce it; the tonal wash above is the whole treatment for these
   bands now. */

/* --- 5. Micro-life: hover lift + accent glow, header underline -----------

   Small motions that stop the page reading as a static spec sheet.
   - Buyable cards (products, bundle suites) lift a touch and gain an
     accent-tinted shadow on hover. The single content panels do NOT lift —
     you don't hover a licence block on your way to anything.
   - The buy button already brightens on hover (rule above); we ADD an accent
     halo, additively, so the moulded cap glows in its own colour.
   - A short accent tick under each section legend — the root accent (brand
     red) since a section header carries no per-card colour — so the eyebrow
     labels feel struck rather than typed. */
.product:hover, .suite:hover {
  transform: translateY(-2px);
  box-shadow:
    0 1px 0 rgba(255,255,255,0.6),
    0 10px 24px -10px color-mix(in srgb, var(--accent) 45%, transparent),
    0 4px 10px rgba(0,0,0,0.12);
}
.btn { transition: filter 120ms ease, box-shadow 140ms ease; }
.btn:hover {
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent),
    0 4px 14px -4px color-mix(in srgb, var(--accent) 55%, transparent);
}
.section > .legend { position: relative; padding-bottom: 8px; }
.section > .legend::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 34px; height: 2px;
  background: var(--accent);
  box-shadow: 0 0 5px -1px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* Honour reduced-motion for everything this pass added. */
@media (prefers-reduced-motion: reduce) {
  .panel, .btn { transition: none; }
  .product:hover, .suite:hover { transform: none; }
}


/* ==================================================================
   PHOSCYON PASS — 2026-08-19
   ==================================================================

   Six changes signed off from the mockup at
   claude.ai/code/artifact/6433d25c-6d78-471b-a35c-ee83527cfe2b, after a
   round asking what would make the page read less sterile. Same appended-
   block discipline as the WARMTH PASS above and for the same reasons:
   revert is "delete from this banner to EOF", and everything here either
   adds a rule or re-declares one whole property on an existing selector at
   equal specificity, so nothing above needs editing to be overridden.

   The exception, deliberately, is the :root cream pass — that one had to be
   edited in place, because tokens are read by ~40 rules and shadowing them
   from down here would leave two competing palettes in one file.

   ⚠ ONE HISTORY NOTE, so this does not read as an unlearned lesson: the
   graphite panel head below was already tried on 2026-08-16 and rejected
   the same day ("still not very nice" across eight panels). It is back at
   the user's explicit request after seeing it in the mockup, and the thing
   that changed in between is context, not taste — the band now carries two
   screws through it and a lit pilot lamp, the plate under it is warmer, and
   the page has an accent spine and a dark video screen elsewhere, so a dark
   strip reads as one member of a family rather than the only dark thing on
   a pale page. If it is rejected a third time, the silver recipe is intact
   above; deleting this one rule restores it.
   ------------------------------------------------------------------ */

/* --- 1. The panel head goes graphite ------------------------------------
   Straight to --darkplate-*, i.e. Chordinator's actual .seq-col plate, not
   one of the intermediate greys — the user picked the reference sample off
   the mockup over pewter/graphite/gunmetal. Layer structure is untouched
   (scanline + diagonal sheen + overhead bloom over a 3-stop vertical
   gradient); only the base gradient and the lighting weights change, since
   sheen tuned for a light plate blows out on a dark one. */
/* 2026-08-19 -- "more of the SHINE of Chordinator's sequencer panel".
   The colour was already right (the user said so); what was missing was
   specular behaviour, and the fix is not "raise every white to taste". Four
   distinct things separate a shiny anodised strip from a dark gradient, and
   the old recipe only had weak versions of two of them:

   1. A HARD top edge. Real anodised metal catches a bright, nearly 1px line
      where the top face turns over -- not a soft ramp. The inset highlight
      goes 0.10 -> 0.22 and a second, dimmer 2px line sits under it so the
      turn-over has depth instead of being a single stroke.
   2. A TIGHT overhead bloom. The old radial was 120% x 60% at 0.13, so broad
      and so faint it just lifted the whole band a little. Narrowed to
      90% x 46% and taken to 0.20: a smaller, brighter pool reads as a light
      SOURCE reflected in the surface; a wide faint one reads as grey paint.
   3. A DOUBLE diagonal sweep. The single 112deg band is the giveaway --
      brushed metal under a room light shows a bright leading edge and a
      weaker trailing one. The first sweep goes 0.10 -> 0.19 with a hard
      shoulder (34% to 38% instead of 34% to 43%), and the second 0.07 ->
      0.13, which is what actually reads as "the same sheen as the app".
   4. A FLOOR bounce. New, and the cheapest of the four: a wide, very faint
      (0.05) radial at the bottom edge standing in for light bouncing off the
      cream plate below into the underside of the strip. Without it the band
      goes dead flat toward the bottom and the whole thing reads as a
      rectangle rather than a surface.

   The scanline stays where it was. Raising it alongside the highlights was
   tried and it is what tips the band from "shiny" into "textured plastic" --
   the scanline is machining marks, and machining marks do not get brighter
   when the light does. The base 3-stop gradient and every colour token are
   untouched, so the colour the user approved is exactly as it was; only the
   lighting on top of it changed. */
.panel > .panel-head {
  background:
    repeating-linear-gradient(0deg, rgba(255,255,255,0.035) 0px, rgba(255,255,255,0.035) 1px, rgba(0,0,0,0.05) 1px, rgba(0,0,0,0.05) 2px),
    linear-gradient(112deg, rgba(255,255,255,0) 24%, rgba(255,255,255,0.19) 34%, rgba(255,255,255,0.05) 38%, rgba(255,255,255,0) 52%, rgba(255,255,255,0.13) 68%, rgba(255,255,255,0) 78%),
    radial-gradient(90% 46% at 50% -6%, rgba(255,255,255,0.20) 0%, rgba(255,255,255,0) 62%),
    radial-gradient(140% 40% at 50% 108%, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0) 70%),
    linear-gradient(180deg, var(--darkplate-hi) 0%, var(--darkplate) 30%, var(--darkplate-lo) 100%);
  border-bottom: 1px solid var(--darkplate-border);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.22),
    inset 0 2px 0 rgba(255,255,255,0.07),
    inset 0 -1px 0 rgba(0,0,0,0.45);
  /* The engraving INVERTS on a dark plate. Cut into pale metal, the far
     (upper) wall of the groove is in shadow and the near (lower) one catches
     light — which is the recipe above. Cut into near-black, the letters are
     the only thing the overhead light can reach, so the fill goes lighter
     than the plate and the lips swap: dark above, faint light below. Getting
     this backwards is what makes engraved text on a dark panel look like
     ordinary grey type with a drop shadow. */
  color: hsl(48, 7%, 74%);
  text-shadow:
    0 -1px 0 rgba(0,0,0,0.80),
    0  1px 0 hsla(44,20%,100%,0.10);
}

/* --- 2. Screws, dark variant for the top pair ---------------------------
   Same two-circle dimple as the cream pair (see .panel::before above), lit
   for near-black metal: the highlight drops from 0.60 to 0.30 and the body
   from 58% to 34% lightness, because a dimple keeps its cream lighting on a
   graphite band and stops being a hole — it becomes a bright dot stuck on
   the plate. The bottom pair is untouched; it is still on cream. */
.panel::before {
  background:
    radial-gradient(circle 2.6px at left 8px top 8px,
      rgba(255,255,255,0.30) 0%, hsl(48,3%,34%) 46%, hsl(48,4%,13%) 97%, transparent 100%),
    radial-gradient(circle 3.1px at left 8px top 8.9px, rgba(255,255,255,0.20) 99%, transparent 100%),
    radial-gradient(circle 2.6px at right 8px top 8px,
      rgba(255,255,255,0.30) 0%, hsl(48,3%,34%) 46%, hsl(48,4%,13%) 97%, transparent 100%),
    radial-gradient(circle 3.1px at right 8px top 8.9px, rgba(255,255,255,0.20) 99%, transparent 100%);
}

/* --- 3. Things that are actually lit ------------------------------------
   The pilot lamp already existed but glowed 6px onto a silver band that was
   nearly as bright as the lamp, so there was nothing for the bloom to land
   on. On graphite it can finally read: a two-stage halo (tight bright core,
   wide soft falloff) plus a hairline of the accent bled onto the plate
   around it, which is what a real LED does to the metal it is set into. */
.panel > .panel-head::before {
  box-shadow:
    0 0 0 1px rgba(0,0,0,0.45) inset,
    0 0 5px 0 color-mix(in srgb, var(--accent) 85%, transparent),
    0 0 13px 1px color-mix(in srgb, var(--accent) 42%, transparent);
}
/* The section eyebrow tick gets the same treatment — a struck mark with a
   little light on it rather than a flat 2px rule. */
.section > .legend::after {
  box-shadow:
    0 0 5px -1px color-mix(in srgb, var(--accent) 70%, transparent),
    0 0 12px 0 color-mix(in srgb, var(--accent) 30%, transparent);
}

/* --- 4. Warm shadow ink -------------------------------------------------
   The single cheapest change in this pass. Photographed metal casts a
   shadow that picks up the warmth of the surface it lands on; a neutral
   rgba(0,0,0,…) shadow under a cream plate is the clearest tell that the
   panel was drawn in CSS rather than lit. hsl-equivalent rgb(22,22,20) is
   this page's black (it was rgb(28,22,14) until the de-orange pass -- see
   the :root comment; a red-skewed shadow under every edge is a colour cast
   arriving by a second route). Geometry is identical to the rules above — only the
   ink changes — so nothing reflows. */
.panel {
  box-shadow:
    0 1px 0 rgba(255,255,255,0.62),
    0 2px 5px rgba(22,22,20,0.16),
    0 10px 28px -18px rgba(22,22,20,0.45);
}
.product:hover, .suite:hover {
  box-shadow:
    0 1px 0 rgba(255,255,255,0.62),
    0 10px 24px -10px color-mix(in srgb, var(--accent) 45%, transparent),
    0 4px 10px rgba(22,22,20,0.16);
}

/* --- 5. Control hierarchy -----------------------------------------------
   In the apps you never hunt for the important button: "Send to Live" is
   saturated and raised, undo/redo sit recessed and grey. On the site the buy
   button and the Details button weighed about the same, so this pushes them
   apart from both ends rather than just enlarging one.

   ⚠ The buy button cannot be enlarged here. It carries `.gumroad-button`,
   whose overlay guard above pins padding/font-size with !important for
   reasons documented at length at that rule — changing size would mean
   changing both copies and re-checking them against a stylesheet Gumroad can
   reissue at any time. So the primary gains WEIGHT (a stronger halo and a
   deeper contact shadow) and the secondary LOSES it, which reads the same
   and touches nothing the guard owns. */
.btn:hover {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.85),
    0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent),
    0 3px 6px rgba(22,22,20,0.22),
    0 10px 26px -8px color-mix(in srgb, var(--accent) 70%, transparent);
}
/* Cut into the plate instead of sitting proud of it. The bevel inverts:
   the old cap had a bright top lip and a dark bottom one; a recess has the
   shadow at the top (light blocked by the near rim) and the catch at the
   bottom. Ink drops to --text-secondary so it reads as the quieter of the
   two before you have parsed either label. */
.btn-ghost {
  background: linear-gradient(180deg, hsl(48,8%,84%) 0%, hsl(48,11%,90%) 60%, hsl(48,13%,93%) 100%);
  color: var(--text-secondary);
  border-color: hsl(48,6%,52%);
  border-bottom-color: hsl(48,7%,62%);
  box-shadow:
    inset 0 2px 3px rgba(22,22,20,0.22),
    inset 0 -1px 0 rgba(255,255,255,0.70),
    0 1px 0 rgba(255,255,255,0.55);
}
.btn-ghost:hover {
  color: var(--text-primary);
  box-shadow:
    inset 0 2px 3px rgba(22,22,20,0.18),
    inset 0 -1px 0 rgba(255,255,255,0.80),
    0 1px 0 rgba(255,255,255,0.60);
}
.btn-ghost:active {
  box-shadow: inset 0 3px 6px rgba(22,22,20,0.34);
  transform: translateY(1px);
}

/* --- 6. The demo reel ---------------------------------------------------

   THREE DEPTH LEVELS, which is the whole point of the section: the page is
   the chassis, .panel is a plate bolted to it, and .video-stage is a screen
   let INTO that plate. The site had the first two and stopped; every dark
   area on it was a band, never a recess. A screen is what an instrument
   panel does with the one region that is not metal, and it is the strongest
   available cue that the plate around it is solid.

   Shipped with no footage on purpose. The demos are being recorded; the
   furniture goes in now so dropping a file in later is a src attribute and
   nothing else. The transport is therefore rendered inert (aria-disabled +
   .is-idle) rather than omitted — a section that looks operable and does
   nothing is worse than one that says what it is waiting for. See the HTML
   comment on the section for exactly what to change when the files land. */
.video-panel .panel-body { padding: 16px 16px 18px; }

.video-stage {
  position: relative;
  /* Capped and centred rather than filling the panel. At the 936px content
     width a full-bleed 16:9 screen is ~526px tall — taller than any other
     element on the page and, while there is no footage in it, a very large
     empty black rectangle. 720px keeps it clearly the biggest thing in its
     own section without owning the whole scroll. */
  max-width: 720px; margin: 0 auto;
  aspect-ratio: 16 / 9;
  display: grid; place-items: center;
  overflow: hidden;
  border-radius: 4px;
  background: linear-gradient(180deg, var(--screen-edge), var(--screen-bg) 42%);
  /* The recess, in one shadow: a deep inner drop from the near rim, a
     hairline of true black at the cut edge, and a single bright line on the
     plate just below the opening — the lip catching the overhead light. */
  box-shadow:
    inset 0 3px 8px rgba(0,0,0,0.78),
    inset 0 0 0 1px rgba(0,0,0,0.62),
    0 1px 0 rgba(255,255,255,0.55);
}
/* Scanline + vignette so an empty screen reads as powered rather than as a
   hole in the page. Same scanline vocabulary as every other surface here. */
.video-stage::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background:
    repeating-linear-gradient(0deg, rgba(255,255,255,0.020) 0px, rgba(255,255,255,0.020) 1px, rgba(0,0,0,0) 1px, rgba(0,0,0,0) 3px),
    radial-gradient(120% 90% at 50% 45%, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0) 60%, rgba(0,0,0,0.45) 100%);
}
.video-stage-inner {
  position: relative; z-index: 2;
  display: grid; gap: 14px; justify-items: center; text-align: center;
  padding: 12px;
}

/* The big centred play control: a milled ring, dark like the screen it sits
   in, going accent-coloured only under the cursor. */
.video-play {
  width: 74px; height: 74px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 24px; line-height: 1; padding-left: 5px;
  color: hsl(38, 30%, 84%);
  border: 1px solid hsl(48, 4%, 34%);
  background: radial-gradient(120% 120% at 50% 0%, hsl(48,4%,30%) 0%, hsl(48,4%,17%) 60%, hsl(48,4%,12%) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.16),
    inset 0 -2px 5px rgba(0,0,0,0.55),
    0 5px 16px rgba(0,0,0,0.5);
  cursor: pointer;
  transition: transform 130ms ease, box-shadow 140ms ease, color 130ms ease, border-color 130ms ease;
}
.video-play:hover {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 60%, black);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.20),
    inset 0 -2px 5px rgba(0,0,0,0.55),
    0 6px 20px rgba(0,0,0,0.55),
    0 0 24px -2px color-mix(in srgb, var(--accent) 55%, transparent);
}
.video-play:active { transform: translateY(1px) scale(0.985); }

.video-caption {
  font-family: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
  font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
  color: hsl(48, 5%, 52%);
}

/* Transport strip. ◀◀ / ▶▶ rather than undo/redo: undo and redo are editor
   verbs and mean nothing on a page, where the only thing there is to step
   through is the reel itself. Same moulded cap as .btn-ghost's family. */
.video-bar { display: flex; align-items: center; gap: 12px; margin: 14px auto 0; max-width: 720px; flex-wrap: wrap; }
.video-transport { display: inline-flex; align-items: center; gap: 6px; }
.video-transport button {
  width: 40px; height: 32px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-size: 12px;
  border-radius: var(--radius);
  border: 1px solid hsl(48,5%,40%);
  border-bottom-color: hsl(48,6%,28%);
  color: var(--text-primary);
  background: linear-gradient(180deg, hsl(45,30%,98%) 0%, hsl(48,11%,90%) 45%, hsl(48,8%,75%) 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -1px 1px rgba(22,22,20,0.16), 0 1px 2px rgba(22,22,20,0.14);
  cursor: pointer;
}
.video-transport button:active { transform: translateY(1px); box-shadow: inset 0 2px 4px rgba(22,22,20,0.34); }
/* The play cap carries the LED, exactly as Chordinator's #seq-play::after
   does — unlit at rest, accent with a halo while running. */
.video-transport .video-play-cap { position: relative; width: 46px; }
.video-transport .video-play-cap::after {
  content: ""; position: absolute; top: 4px; right: 4px;
  width: 4px; height: 4px; border-radius: 50%;
  background: hsl(48, 4%, 58%);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.3);
}

/* The LCD readout, same bezel family as the header's app picker. */
.video-read {
  flex: 1 1 220px; min-width: 0;
  display: flex; align-items: center; gap: 10px;
  padding: 7px 12px; border-radius: 4px;
  font-size: 11px;
  background: linear-gradient(180deg, var(--screen-edge), var(--screen-bg) 40%);
  box-shadow: inset 0 2px 6px rgba(0,0,0,0.75), inset 0 0 0 1px rgba(0,0,0,0.6), 0 1px 0 rgba(255,255,255,0.55);
  font-family: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
}
.video-read .video-read-label { color: hsl(48,5%,48%); letter-spacing: 0.12em; text-transform: uppercase; font-size: 10px; flex: none; }
.video-read .video-read-value {
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: hsl(38, 45%, 68%);
  text-shadow: 0 0 7px hsl(30 70% 45% / 0.5);
  letter-spacing: 0.06em;
}

/* Idle state — no footage yet. Everything stays visible at reduced contrast
   so the section reads as "not loaded" rather than "broken", and the
   controls are genuinely inert, not just styled that way (see the HTML). */
.video-panel.is-idle .video-transport button,
.video-panel.is-idle .video-play { cursor: default; opacity: 0.55; }
.video-panel.is-idle .video-transport button:active,
.video-panel.is-idle .video-play:active { transform: none; }
.video-panel.is-idle .video-play:hover {
  color: hsl(38, 30%, 84%);
  border-color: hsl(48, 4%, 34%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.16),
    inset 0 -2px 5px rgba(0,0,0,0.55),
    0 5px 16px rgba(0,0,0,0.5);
}

@media (prefers-reduced-motion: reduce) {
  .video-play { transition: none; }
  .video-play:active, .video-transport button:active, .btn-ghost:active { transform: none; }
}

/* ---------------------------------------------------------------------------
   §7  SCREENSHOT MICRO-INTERACTIONS — 2026-08-19, ADDED AND REVERTED THE SAME
       DAY. NO RULES BELOW; THIS BANNER IS THE WHOLE SECTION.

   What was here: `.screen:not(.empty) img` lifted 3px on hover with its
   `drop-shadow()` deepening and spreading to match, and settled flat with a
   tight shadow on `:active`. It was deliberately the smallest possible
   interaction — no scale, no tilt, no overlay — on the reasoning that a
   screenshot is a picture of a real window and anything that transforms it
   reads as a web flourish stuck onto a photograph.

   Why it went: the user's verdict on the live page was that the motion is
   "messy and buggy", and asked for every image on the site to be static under
   the cursor. Two things the original reasoning got wrong, worth keeping so
   this is not re-derived:

   - Restraint did not help. The problem was never the SIZE of the movement,
     it was that these images sit inside panels that already have their own
     hover treatment, so a cursor crossing a card set two different things
     moving at two different speeds. That reads as a glitch no matter how
     small the second one is.
   - `drop-shadow()` on a large PNG with alpha is re-rasterised per frame.
     On the long product pages, where several shots are near the viewport at
     once, the transition can visibly stutter — which is exactly what "buggy"
     describes, and it is a cost the lift never earned back.

   So: screenshots are static. If a future pass wants feedback on them, put it
   on the CONTAINER (a cursor change, or the panel it already lives in), not on
   the image, and do not reach for animated `drop-shadow()` on these PNGs.

   The base `.screen img` drop-shadow (the resting shadow, in the .screen rules
   further up this file) is untouched — that is the images' normal appearance,
   not an interaction.
   --------------------------------------------------------------------------- */
