/* Checked in under web/fonts/ rather than linked to a font host: the page is
   served straight out of this directory, so it works offline and from a file://
   copy, and a visitor's browser never announces itself to a third party. Both
   are variable fonts - one file covers every weight, which is why there is no
   separate bold. See web/fonts/LICENSE.txt. */
@font-face {
  font-family: 'Roboto Condensed';
  src: url('fonts/roboto-condensed-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Roboto Mono';
  src: url('fonts/roboto-mono-latin.woff2') format('woff2');
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: dark;
  --bg: #14161a;
  /* The one surface: the panel's ground and the scope cells. Barely a fill -
     enough to settle them over whatever the backdrop is doing, not enough to
     hide it. Neutral, because the traces and the LEDs supply all the colour.

     Read from here by web/const.js as well, so it is a literal rather than a
     var() of something else: it has to survive getComputedStyle into a canvas
     fillStyle. */
  --surface: rgba(0, 0, 0, .15);
  --line: #5e5e5e;
  --fg: #dfe3ea;
  --muted: #8b929d;
  --accent: #6fc3ff;

  /* One hover colour for every button on the page. Buttons are otherwise
     colourless unless they carry a meaning of their own, so hover is the only
     thing that has to read as "this one, now" - and it says that best by being
     the same everywhere rather than by being each button's own colour. */
  --hover: #c0a3ff;
  --danger: #e8705f;

  /* One colour per source, used by both the buttons that pick it and the
     readout that reports it - so the two cannot appear to disagree about which
     one the page is on.

     Red for the simulator because picking it is disconnecting from a module,
     which is the one thing here worth hesitating over. Deliberately not the
     warn amber, which on this page already means something has gone wrong
     rather than something is selected. */
  --src-sim: var(--danger);
  --src-usb: #6fc3ff;
  --src-probe: #c0a3ff;

  /* The quantizer's keyboard. Off is one translucent grey whatever the key is;
     on is opaque, and only then does it matter which kind of key it was. */
  --key-off: rgba(140, 146, 157, .30);
  --key-white: #cdd2da;
  --key-black: #1b1e23;
  --warn: #e3b341;

  /* The ground a scope cell is drawn on. Darker than --surface, which the panel
     sits on: eight traces have to read against this, and the backdrop coming
     through at 15% was competing with them. Read by web/const.js into a canvas
     fillStyle, so a literal rather than a var() of something else. */
  --scope-fill: rgba(0, 0, 0, .42);

  /* Condensed because a panel is lettered condensed: it takes tracking without
     the line getting long, which is what makes small uppercase labels work.
     The fallbacks are named so a first paint before the file lands is close in
     width rather than reflowing the whole page when it arrives. */
  --font: 'Roboto Condensed', 'Arial Narrow', ui-sans-serif, Helvetica, Arial, sans-serif;
  --mono: 'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* One tracking value for every label, button and heading on the page. Wide
     letter-spacing on small uppercase text is the whole of this look; getting
     it slightly different in five places is what makes a page feel assembled
     rather than designed. */
  --track: .14em;

  /* One radius for everything with an edge: buttons, fields, the panel's
     ground, a scope cell, the menu. Two pixels is barely a corner - enough to
     take the hardness off a rectangle, not enough to read as a rounded style.
     web/const.js carries the same number for the cells the canvas draws. */
  --radius: 2px;
}

/* Small, uppercase, widely tracked, and never quite white: the way a panel is
   silkscreened. Applied by name so the same rule covers a readout's label, a
   section heading and a button face. */
.label,
h2,
button {
  font-size: .68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track);
}

* { box-sizing: border-box; }

/* The colour behind everything, and what shows if the photograph does not
   load. On html rather than body so that body's own background can stay
   transparent - see body::before. */
html { background: var(--bg); }

body {
  margin: 0;
  padding: 2.25rem 2.5rem;
  color: var(--fg);
  font: 400 14px/1.65 var(--font);
}

/* The backdrop: a photograph, dimmed, on its own fixed layer.
 *
 * Two layers over it, read top down:
 *
 *   1. a grey wash, heaviest at the top and gone by the bottom, so the readouts
 *      and the panel sit against something even and the skyline opens up below;
 *   2. a flat dim over the whole thing, because a photograph at full strength
 *      competes with eight scope traces and wins.
 *
 * A fixed pseudo-element rather than `background-attachment: fixed` on the
 * body, which is the obvious way to do it and repaints the whole image on every
 * scroll - on the same main thread the probe's poll loop is already competing
 * for. This gets its own compositor layer and scrolling costs nothing. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(34, 38, 45, .93) 0%, rgba(34, 38, 45, .38) 55%, rgba(34, 38, 45, 0) 100%),
    linear-gradient(rgba(16, 18, 22, .58), rgba(16, 18, 22, .58)),
    url('bg-tokyo.jpg') center / cover no-repeat;
}

/* How wide the page is allowed to get, on the title row and the content alike -
   one number, because a header wider than the content it titles puts the source
   buttons somewhere the right edge of the page is not.

   1200px, down from 1600. The extra width went on the scopes, and past a point
   a wider trace tells you nothing a taller one would not: what it cost was the
   panel, the table and the scopes no longer fitting on one screen together,
   which is the whole argument for having them side by side. */
:root { --page: 1200px; }

header, main { max-width: var(--page); }

main {
  display: grid;
  grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
  gap: 2.5rem;
  align-items: start;
}
@media (max-width: 860px) { main { grid-template-columns: 1fr; } }

/* The scope should stop growing once it is comfortably readable; past that the
   width is better spent putting the controls alongside it than on an ever
   wider set of eight traces. */
#side-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* Tight enough that the three boxes read as one column of related things.
     They were 2.25rem apart, which is a section break - and the page only has
     room for so many of those before the scopes leave the screen. */
  gap: 1.5rem;
  align-items: start;
}

/* ---- panel ------------------------------------------------------------- */

/* The panel gets a ground to sit on, unlike everything else on the page.

   The artwork is a dark panel with its cutouts punched through, so what shows
   through those holes is whatever is behind - and a skyline behind the encoder
   cutouts is not a panel. This is what they show instead. */
/* Ground for the panel and nothing else. It had a border and padding as well,
   which framed a thing that already has an outline of its own - and the bracket
   below it now does the marking-off that frame was doing. */
#panel-wrap { background: var(--surface); border-radius: var(--radius); }

/* The ratio of the real panel, from panel.json - 81mm x 128.5mm.
 *
 * Stated here so the space is reserved before anything runs. The SVG has no
 * size until spec.js has fetched panel.json and panel.js has set a viewBox off
 * it, so on load the column was collapsing to nothing and everything below
 * jumped down a screen when the fetch landed. */
#panel {
  width: 100%; height: auto; display: block; touch-action: none;
  aspect-ratio: 81 / 128.5;
}

/* What the pointer is over, in full sentences and big enough to read at a
   glance. This was a fixed 78% line of monospace under a one-line hint naming
   the component; there is only one text now and it is the one worth reading.

   A minimum rather than a fixed height, and reserved, because these run to two
   or three lines and the panel above must not move when one replaces another. */
.help {
  margin: .9rem 0 0;
  min-height: 8.5em;
  max-width: 58ch;
  color: var(--muted);
}

/* A control button carries two things that are not variations of each other: a
   tap chooses what the encoders edit, a hold repaints the whole panel. Each
   gets its own heading, because said as one paragraph the button sounded like
   it did one vague thing. */
.help h3 {
  margin: 0 0 .2rem;
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--fg);
}
.help h3 ~ h3 { margin-top: .8rem; }
.help p {
  margin: 0;
  font-size: .92rem;
  line-height: 1.65;
}
kbd { background: rgba(255, 255, 255, .09); padding: .05em .35em; font-size: .9em; }

/* Interactive SVG parts */
.hit { cursor: pointer; }
.enc-ring { cursor: ns-resize; }
.slider-hit { cursor: ns-resize; }

/* ---- side panels -------------------------------------------------------- */

#side { min-width: 0; }

/* Layout only. These were cards - background, border, rounded corners, padding -
   which put three nested rectangles between the page and a number: the box, the
   readout inside it, and the value inside that. The grid's own gap does the
   separating now, and what is left of .panel-box is a grid child. */
.panel-box { min-width: 0; }


.readouts { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: 0; align-items: stretch; }
/* Neither outline nor fill. What makes these read as cells is the label above
   the number and the space between them - the box was doing nothing the layout
   was not already doing. */
.readout {
  flex: 1 1 120px;
  padding: .7rem .9rem .7rem 0;
}

/* The scale gets the height the row has rather than a fixed one, and sits at
   the end of it - it is the widest thing there and the only one that is a
   picture rather than a number.

   Bounded in both directions, because a stretched flex item on a row that has
   wrapped is sized by its own line rather than by the row: at the width where
   the readouts break onto two lines the scale had a line to itself and grew to
   fill it, which doubled the height of the box and drew a keyboard several
   times longer than a keyboard. */
.readout-keys {
  display: flex; align-items: stretch;
  flex: 1 1 150px;
  max-width: 280px;
}
/* The label sits well above its number rather than against it: the gap is what
   makes the pair read as one thing with a caption, and it is the cheapest
   space on the page to give away. */
.readout .label { display: block; margin-bottom: .35rem; color: var(--muted); }
.readout .value {
  display: block;
  font: 500 1.2rem/1.15 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}

/* Both scopes, stacked: eight outputs on 4x2, then the four inputs on one row
 * beneath them at the same width.
 *
 * They used to sit side by side, the outputs 4x2 and the inputs 2x2, in columns
 * whose 2:1 ratio was what kept a cell the same rectangle in either grid. One
 * row of four is simpler and reads better - the inputs line up under the
 * outputs they feed, and an input cell is now wider than it is tall, which
 * suits a single trace.
 */
/* Two columns: a label beside each scope, and the input controls under the
   input canvas rather than under the whole thing. The bracket spans exactly
   its own canvas, so the input one no longer changes height when the controls
   are hidden. */
.scopes {
  max-width: 1660px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: .5rem .45rem;
  align-items: stretch;
}
/* The stack holds both; the controls are positioned inside it. */
.in-stack { grid-column: 2; }

/* A bracket, not a rule: two segments with a square tick turned in at each far
   end and the label sitting in the break between them. Vertical beside the
   scopes, reading bottom-to-top; lying down under the panel.
 *
 * Borders, not a background plus a positioned pseudo-element. Two boxes whose
 * edges have to meet is exactly what a border corner is, and it is mitred for
 * free - where the old arrangement put the tick inside the segment with left/
 * right offsets and landed a pixel out at both ends, so the line ran past the
 * corner instead of turning it.
 *
 * One length for the tick whichever way round the bracket is, so the two can
 * never disagree about how far they turn in. */
.bracket {
  --tick: 6px;
  display: flex; flex-direction: column; align-items: center;
  gap: .35rem;
}
.bracket i {
  flex: 1;
  width: var(--tick);
  min-height: 6px;
  border-left: 1px solid var(--line);
}
/* The radius goes on the corner the two borders actually make, which is a
   different one on each segment and swaps again when the bracket lies down. */
.bracket i:first-of-type {
  border-top: 1px solid var(--line);
  border-top-left-radius: var(--radius);
}
.bracket i:last-of-type {
  border-bottom: 1px solid var(--line);
  border-bottom-left-radius: var(--radius);
}
.bracket span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: .6rem;
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--fg);
  white-space: nowrap;
}

/* The same bracket lying down, for the panel. Every border moves round one
   quarter turn: the line becomes the bottom edge and the ticks stand up from
   its ends. */
.bracket.horizontal { flex-direction: row; margin-top: .55rem; }
.bracket.horizontal i {
  width: auto; min-width: 6px;
  height: var(--tick); min-height: 0;
  border: 0; border-bottom: 1px solid var(--line);
}
.bracket.horizontal i:first-of-type {
  border-left: 1px solid var(--line);
  border-radius: 0 0 0 var(--radius);
}
.bracket.horizontal i:last-of-type {
  border-right: 1px solid var(--line);
  border-radius: 0 0 var(--radius) 0;
}
.bracket.horizontal span { writing-mode: horizontal-tb; transform: none; }

/* Both declare their ratio so the page reserves the space before the first
   resize callback runs - without it everything below them jumps once on load.
   The inputs are half the height of the outputs at the same width, which is
   what makes one cell of 4x1 the same rectangle as one cell of 4x2. */
#scope { width: 100%; display: block; aspect-ratio: 1000 / 440; }
#inscope { width: 100%; display: block; aspect-ratio: 1000 / 220; }

/* The canvas and the controls on top of it, in one box so the overlay can be
   positioned against the cells rather than against the page. */
.in-stack { position: relative; grid-column: 2; min-width: 0; }

/* Four columns of controls, one per input cell, laid over the canvas.
 *
 * The columns are plain fractions of the same box the canvas divides into
 * quarters, so a control sits in the cell whose trace is behind it without
 * either side being told where the other put things. They were a row of their
 * own underneath for a while, which cost the scope that height and put a
 * control a whole cell away from the signal it belonged to. */
#in-controls {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* Nothing here is a target except the controls themselves - the rest of this
     layer is over the input scope, which takes a drag to set a level. */
  pointer-events: none;
}
.in-cell { display: flex; min-width: 0; }

/* Bottom left of the cell, and centred against each other rather than against
   the cell: the pulse button and the bpm field want to sit on one line, not in
   the middle of a scope trace. The margins keep them off the cell's edges.
   Every control in the row is the same height, or they read as one having
   slipped off a common baseline. */
.in-ctl {
  display: flex; flex-flow: row wrap; align-items: center;
  align-self: flex-end;
  margin: 0 .5rem .4rem;
  gap: .25rem .35rem;
}
.in-ctl > * { pointer-events: auto; }
.in-ctl button,
.in-ctl input[type=number] { height: 1.4rem; }

/* The gate buttons sit inside a scope cell with a few millimetres to spare, so
   they keep the uppercase face but drop most of the tracking and all of the
   padding - the page's button proportions do not fit in an overlay. A backing
   fill because a trace may run behind them. */
.in-ctl button {
  padding: 0 .5rem;
  font-size: .58rem;
  letter-spacing: .06em;
  opacity: .85;
  background: rgba(20, 22, 27, .7);
}
.in-ctl button:not(:disabled):hover { opacity: 1; }
.in-ctl .clock { display: flex; align-items: center; gap: .25rem; font-size: .7rem; color: var(--muted); }
/* Beats the display above, which would otherwise outrank the browser's own rule
   for [hidden] and show a clock generator on every input.

   visibility rather than display: a jack that is not a clock keeps the space
   its generator would take, so the pulse buttons stay in one line across the
   four cells whichever of them happen to be clocks. */
.in-ctl .clock[hidden] { display: flex; visibility: hidden; }

/* The shared field face, shrunk to sit on a scope trace: the same background,
   border and lack of spinner, at the size a control 1.4rem tall can carry. */
.in-ctl input[type=number] {
  width: 3.4em;
  padding: 0 .25rem;
  font: .7rem/1 var(--mono);
  letter-spacing: normal;
}

/* Vertical fader, filled from the centre so 0V reads as "no offset". A native
   range input fills from its minimum, which put a solid bar at -10V. */
/* No rail. It ran the full height of the cell and straight through the IN0-IN3
   label in the corner; the knob and the fill from centre say where the level is
   without one. */

.row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; font-size: .8rem; color: var(--muted); }
.row input[type=range] { flex: 1; }

/* A field and the word that names it, kept together when the row wraps. The
   label is the quiet half - it says what the control is, and the control says
   what it is set to. */
.field { display: flex; align-items: center; gap: .4rem; color: var(--muted); }
.field input[type=number] { width: 5em; text-align: right; font-family: var(--mono); }
.row output { font-family: var(--mono); width: 5.5em; text-align: right; color: var(--fg); }

/* An aside, not fine print: quieter than the body by colour, the same size
   within a hair. */
.note { margin: .9rem 0 0; font-size: .86rem; line-height: 1.7; color: var(--muted); }
code { background: #2b2f36; padding: 0 .25em; font-size: .9em; }

/* Fixed layout and tabular figures: the columns used to resize on every value
   change, which made the whole table jitter. */
table {
  width: 100%; border-collapse: collapse; table-layout: fixed;
  font-size: .78rem; font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
/* Rows twice as tall as they were. A dense grid of numbers is the one place on
   this page where space genuinely helps you read: the eye needs a line to
   travel along, and 0.2rem of padding was not giving it one. */
/* One line per cell, always.
 *
 * A row is read across, and a cell that wraps makes its row taller than its
 * neighbours - so the eye loses the line it was travelling along, which is the
 * one thing the padding above exists to give it. "trig CH3" in the quantizer
 * column was enough to do it.
 *
 * Clipped rather than wrapped or shrunk. An ellipsis would cost a character of
 * the width it is trying to save, and the columns below are sized so that the
 * longest thing each one can actually hold fits - this is the guarantee that a
 * narrow window degrades by hiding rather than by reflowing. */
th, td {
  padding: .45rem .4rem; text-align: right; border-bottom: 1px solid var(--line);
  white-space: nowrap; overflow: hidden;
}
th:first-child, td:first-child { text-align: left; color: var(--muted); }
th {
  color: var(--muted);
  font: 600 .6rem/1 var(--font);
  text-transform: uppercase;
  letter-spacing: var(--track);
  padding-bottom: .6rem;
}
tr.muted-row td { opacity: .4; }

/* Twelve control changes as twelve narrow meters. The only thing that moves is
   the mark's `bottom`, so the draw path writes one property per meter and the
   browser does not relayout anything. */
/* min-height for the same reason #params has one: the meters are built from
   JavaScript, so without it the note under them starts higher and drops. */
/* Close under the readouts above: this is what the port in that row is
   sending, not a section of its own. */
#midi-cc { display: flex; gap: .5rem; align-items: flex-end; margin-top: .1rem; min-height: 8.5rem; }
.meter { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: .4rem; }

.meter-track {
  position: relative;
  width: 3px;
  height: 64px;
  background: rgba(255, 255, 255, .07);
}

/* The mark, and the whole of the readout: a line at the height of the value.
   Wider than its track on both sides so it reads as an index against a scale
   rather than as a segment of a bar. */
.meter-track i {
  position: absolute;
  left: -3px; right: -3px;
  height: 2px;
  margin-bottom: -1px;
  background: var(--fg);
}
.meter.railed .meter-track i { background: var(--warn); }
/* Nothing said yet - the module is silent on this CC. Parked mid-scale, where
   0V would be, and faded rather than hidden so the row keeps its shape. */
.meter.silent .meter-track i { opacity: .18; }

/* Which channel the meter above it is. These name twelve columns three pixels
   wide, so they are the only thing telling one from another - which is why
   they are neither small nor grey. They were both, at .58rem in the muted
   grey, and the row read as a texture rather than as twelve labelled things. */
.meter-name {
  font-size: .68rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--fg);
}

/* Which control change it is, under the name of what it carries. It was in the
   hover title, which is the one place a number nobody can guess should not be:
   CC 16 is not derivable from "ch 0". Quieter than the name above it, because
   the name is what you read and this is what you look up. */
.meter-cc {
  margin-top: -.25rem;
  font-size: .58rem;
  font-family: var(--mono);
  color: var(--muted);
}

/* On the header cell as well as the body: table-layout is fixed, which takes
   every column width from the first row, so a width set only on the td below
   is ignored and the four columns come out equal. */
#midi-cc th:nth-child(4), #midi-cc td:nth-child(4) { width: 45%; }

/* ---- fields -------------------------------------------------------------- */

/* A picker and a typed value are controls in a row of controls, so they wear
   the button's face rather than the operating system's. appearance:none drops
   the native chrome.
 *
 * Shaped like a button, but not lettered like one. A button's face is a label
 * we wrote and can uppercase; these show data - a version tag, a port name, a
 * tempo - and `v0.10.0` rendered as `V0.10.0` is the control editing its own
 * contents. The weight and border carry the family resemblance instead.
 *
 * By element rather than by id, which is how this started: the rule was written
 * for the simulator's port picker and named after it, so the diagnostics page -
 * which loads this same stylesheet - showed raw operating-system widgets beside
 * buttons styled like these. Two pages, one face. */
select,
input[type=number] {
  appearance: none;
  background: rgba(20, 22, 27, .7);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .55rem 1.1rem;
  font: 500 .78rem/1 var(--font);
  letter-spacing: .04em;
}

/* The arrow, drawn as a background so there is no extra element to place. */
select {
  background:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%) calc(100% - 1.05rem) 55% / 5px 5px no-repeat,
    linear-gradient(-45deg, transparent 50%, var(--muted) 50%) calc(100% - .7rem) 55% / 5px 5px no-repeat,
    rgba(20, 22, 27, .7);
  padding-right: 2.2rem;
  cursor: pointer;
}

select:hover { border-color: var(--accent); color: var(--accent); }
select:focus-visible,
input:focus-visible { outline: none; border-color: var(--accent); }

/* The open list is drawn by the OS, which does not inherit any of the above -
   naming the colours here is what keeps it from being a white popup. */
option { background: #1b1e23; color: var(--fg); text-transform: none; letter-spacing: normal; }

/* No spinner anywhere. Two arrows a few pixels tall, drawn in whatever grey the
   browser fancies, inside controls that are mostly too short to hit them in.
   The field takes typing and the wheel, which is how a tempo gets set anyway.
   Two rules because the two engines suppress it differently. */
input[type=number] { -moz-appearance: textfield; }
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button { appearance: none; margin: 0; }

/* The simulator's port picker fills the readout it sits in; the face above is
   the part every page shares. */
#midi-port { margin-top: .35rem; width: 100%; }

/* Outlined and quiet by default, so the one button that does something
   irreversible or connects hardware can be the only coloured thing in its row.
   Colour on hover rather than a lighter grey: it says which control you are
   about to use without another rectangle. */
button {
  background: transparent;
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .55rem 1.1rem;
  cursor: pointer;
  font-family: var(--font);
  transition: color .12s, border-color .12s, background-color .12s;
}
/* :not(:disabled) on every hover rule. A disabled button still receives hover,
   and an id selector outranks `button:disabled:hover` however that rule is
   written - so Reset FRAM went on lighting up amber while it was out of
   service, offering to do something it would refuse. */
button:not(:disabled):hover { color: var(--hover); border-color: var(--hover); }

/* The two that change the module rather than the page. Quiet like everything
   else until approached, then their own colour instead of the shared one: amber
   for the reset that playing undoes, red for the wipe that nothing does. A
   button that is permanently red reads as a warning about the page; one that
   turns red under the pointer reads as a warning about the click. */
#reset:not(:disabled):hover { color: var(--warn); border-color: var(--warn); }
#reset-fram:not(:disabled):hover { color: var(--danger); border-color: var(--danger); }

button:disabled { opacity: .35; cursor: default; }

.muted { color: var(--muted); font-size: .8rem; }

/* The transient "module reset" / "FRAM cleared" message, beside the buttons it
   belongs to and in the same voice as their faces. */
#status {
  font-size: .68rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track);
  color: var(--accent);
}

/* Status words. `warn` was already in use from midi.js and had no rule, so it
   read exactly like a normal value - which is the one thing a warning must not
   do. */
/* The rate row inside the module box: four narrow columns rather than the wide
   top-of-page readouts, and smaller, because these are diagnostics rather than
   the numbers you play by. */
.rates { margin: .6rem 0 .2rem; }
.rates .value { font-size: .95rem; }
/* Narrow enough that four or five fit on one line. The default 120px basis put
   the fourth rate on a row of its own. */
.rates .readout { flex: 1 1 0; min-width: 0; padding: .45rem .5rem; }

/* The module box holds both halves of a mutually exclusive pair, and shows the
   half that applies. A page watching hardware has nothing to say about a MIDI
   port it is not driving; a page running the simulation has nothing to report
   about a probe rate. Done in CSS off the class mode.js already sets, so
   neither half needs a line of JavaScript to appear or disappear. */
body.live .when-sim,
body:not(.live) .when-live { display: none; }

/* And inside the MIDI half, the part that only means anything once a port is
   selected - see setSending() in midi.js. */
#midi:not(.sending) .when-midi { display: none; }

/* Buttons on one line, wrapping, with the status message trailing them rather
   than on a line of its own. */
.value.ok { color: #7fd88f; }
.value.warn { color: var(--warn); }

/* The input controls drive the simulation's jacks. A physical module's are
   whatever is patched into it, so they do nothing there - but they stay where
   they are and go grey, because a row of controls that disappears takes the
   height of the page with it and everything below jumps. */
/* Dimmed as a group, so the words between the controls - "bpm" - go with them.
   What makes them inert is the disabled attribute inputs.js sets; pointer-events
   cannot do it here, because the controls re-enable it for themselves to be
   clickable over the canvas and that override wins. */
body.live #in-controls { opacity: .3; }

/* ---- the quantizer's scale ---------------------------------------------- */

/* One octave, drawn as a keyboard rather than twelve labels, because that is
   how a scale is read. The mask is twelve bits and nothing else on the page
   showed it - a channel set to quantize looked identical whether it was
   snapping to a major scale or to every semitone there is.

   SVG, filling whatever cell it is given in both directions. It was stacked
   divs at a fixed key size, which meant one shape that only looked right at one
   width - and a translucent sharp let the white key beneath it show through,
   which at these opacities made an unlit black key look lit. The white keys are
   notched around their neighbours now, so no two shapes ever overlap. */
/* A definite height, which is the whole of why this used to overflow.
   An <svg> with a viewBox has an intrinsic ratio, and `height: 100%` against a
   parent whose own height is indefinite resolves to that ratio instead - so at
   a wide container it computed a keyboard several times taller than the box and
   simply drew outside it. Stretching the cell gives the height a definite value
   for the percentage to be a percentage of.

   overflow guards the case anyway: nothing about a debug readout is worth
   drawing over its neighbours. */
.keys {
  display: block;
  width: 100%; min-width: 7ch;
  height: 100%; max-height: 3rem;
  overflow: hidden;
}
.keys svg { display: block; width: 100%; height: 100%; }

/* Off is one colour for both kinds of key, so "in the scale" is the only thing
   the eye has to separate; on is what kind of key it was. Three colours at once
   - lit, unlit, and black-against-white - was one distinction too many, and an
   unlit black key read as ambiguous next to a lit one.

   The stroke does not scale with the shape, so a keyboard stretched wide keeps
   hairline separations rather than growing fat ones. */
.keys path, .keys rect {
  fill: var(--key-off);
  stroke: var(--line);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.keys .nat.on { fill: var(--key-white); }
.keys .acc.on { fill: var(--key-black); }

/* The channel table's columns, as shares of whatever width it gets.
 *
 * Three widths rather than ten, because there are only three kinds of column
 * here. Percentages rather than em, so the shares hold at any page width and
 * the table cannot end up wider than the box it is in.
 *
 *   ch        one digit, and nothing else will ever be in it
 *   mix, qnt  a mode and the thing it points at - "IN3 x", "trig CH3" - which
 *             is the longest content in the table by some margin
 *   the rest  a signed number of two or three significant figures, all of
 *             them the same shape, so they get equal shares of what is left
 *
 * The two wide ones are what set the floor: at the page's max width the qnt
 * column holds "trig CH3" with a character to spare, and it is the first thing
 * to clip as the window narrows - which is the intended order, since a mode
 * with no room to name its source is still legible as a mode. */
/* min-height reserves the space before the rows exist: the table is built from
   JavaScript after the wasm has loaded, and without it everything below jumps
   down when it arrives. */
#params { table-layout: fixed; width: 100%; min-height: 15.5rem; }
/* Sized by its heading rather than its content: the cells hold one digit, but
   "CH" in the header's uppercase face is wider than that, and a clipped column
   name is worse than a slightly loose column. */
#params .c-ch { width: 5%; }
#params .c-mix { width: 12.5%; }
#params .c-qnt { width: 13.5%; }

/* The output is not the same shape as the numbers to its left, though it looks
   like one: it carries a sign and a unit, so "-10.01 V" is half again as long
   as anything in the parameter columns.
 *
 * Worth its own width rather than an equal share, because of which end clips.
 * These cells are right-aligned, so content that does not fit loses its *first*
 * characters - and the first character of an output is its sign. Every other
 * column degrades into something shorter but still true; this one degrades into
 * a negative voltage that reads as a positive one. */
#params .c-out { width: 11%; }

/* ---- picking a source ---------------------------------------------------- */

.source-row {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: .6rem 1.2rem;
  margin-bottom: .2rem;
}
.source-row > .readout { flex: 0 1 auto; }

/* The buttons themselves live on the title row - see the header rules further
   down. They are the one control that changes what every other number on the
   page means, and putting them beside the box that reports on that made them
   read as part of the report. */
.sources { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
.src { padding: .55rem 1rem; }

/* margin-left:auto rather than a spacer element: it is the same growth without
   an empty div to explain. Falls back to sitting under the sources when the row
   wraps, which is what it should do. */
.actions {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: .4rem .6rem;
  margin-left: auto;
}

/* Quiet like every other button until it is the one the page is on. A row of
   three permanently coloured buttons said "three things are happening"; only
   one ever is.

   These hover to their own colour rather than to the shared one, because for
   these buttons hovering is a preview: it shows the colour the page is about to
   take, and the readout beside them will say the same. Everywhere else the
   hover is the shared purple, since a plain button has no colour of its own to
   promise. */
.src { background: transparent; }
.src.active { border-color: currentColor; }

.src-sim.active,
.src-sim:not(:disabled):hover { color: var(--src-sim); border-color: var(--src-sim); }
.src-usb.active,
.src-usb:not(:disabled):hover { color: var(--src-usb); border-color: var(--src-usb); }
.src-probe.active,
.src-probe:not(:disabled):hover { color: var(--src-probe); border-color: var(--src-probe); }

/* Beats the page's own rule for a disabled button, which dims it to 35% - right
   for a button that cannot be used, wrong for one that is already doing its
   job. */
.src:disabled { opacity: 1; cursor: default; }

/* The source the page is on: filled in its own colour, faintly, so the three
   read as one switch with one position thrown rather than as three buttons of
   which two happen to be clickable. */
.src-sim.active { background: rgba(232, 112, 95, .18); }
.src-usb.active { background: rgba(111, 195, 255, .18); }
.src-probe.active { background: rgba(192, 163, 255, .18); }

/* A source this browser cannot reach at all, as opposed to one it is already
   on. The only other reason one of these is disabled. */
.src:disabled:not(.active) { opacity: .3; }

/* The readout says the same thing in the same colour. */
#source-name.on-sim { color: var(--src-sim); }
#source-name.on-usb { color: var(--src-usb); }
#source-name.on-probe { color: var(--src-probe); }

/* ---- the page before it has anything to say ------------------------------ */

/* Two states where a number on this page is not a number yet: the moment before
   the wasm has loaded, and the moment while a source is being switched. Both
   used to show whatever was last true, or nothing at all, and the page settled
   in a series of jumps as each part arrived.
 *
 * The reservations matter more than the dimming. A table built from JavaScript
 * has no height until it exists, so everything under it moves when it does. */
#keys { min-height: 26px; }

/* Dimmed rather than hidden, so the shape of the page is visible while it is
   still filling in - a layout that arrives complete but grey reads as loading,
   where one that arrives piece by piece reads as broken. */
body.loading .value,
body.loading #params,
body.loading #midi-cc,
body.loading .keys,
body.switching .value,
body.switching #params,
body.switching .keys {
  opacity: .3;
  transition: opacity .12s linear;
}

/* Not the source readout: while a switch is in progress that line is the one
   thing on the page still saying something true. */
body.switching #source-name { opacity: 1; }

/* ---- the page's own name ------------------------------------------------- */

/* Matching the update page, which is the only other page here and had a title
   while this one did not.

   The title row also carries the source switch, at the far end. align-items is
   baseline rather than centre so the buttons sit on the title's line instead of
   on the middle of its box, and the row wraps to two lines on a narrow window
   rather than squeezing the buttons - they are the widest thing here and the
   first to run out of room. */
header {
  margin: 0 0 1.5rem;
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: .6rem 1.2rem;
}
h1 {
  margin: 0;
  font-size: 1.5rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
}
h1 .sub { color: var(--muted); font-weight: 400; }

/* Everything after the title goes to the right of it. */
header .sources { margin-left: auto; }

/* ---- naming another control inside a description ------------------------- */

/* A control of the module, named in a sentence about something else: a page,
   a parameter, an input mode. Set off so the sentence does not have to spell
   out "the page called", and quiet - this marks a word as a name, it does not
   ask to be clicked.
 *
 * By class rather than by where it sits. The simulator's hover text boxes these
 * automatically (see REFERENCES in panel.js); the manual and the other document
 * pages write <b class="ref">CPY</b> by hand, and both should look the same,
 * because they are naming the same button. */
.ref {
  display: inline-block;
  font-weight: 600;
  font-size: .82em;
  letter-spacing: .06em;
  padding: .1em .35em;
  border: 1px solid var(--line);
  color: var(--fg);
  white-space: nowrap;
}

/* ==========================================================================
   The document pages: the manual, the updater, the diagnostics instruments.

   One stylesheet for all four pages rather than two. It was two - the simulator
   had one and the documents shared another that restated its palette, its type
   and its buttons - and the copies drifted exactly where you would expect: the
   page menu sat a few pixels differently on the simulator because its header
   rule was written separately from theirs.

   The document pages carry `class="doc"` on the body. Everything above this
   line is shared; everything below either belongs to those pages or is a value
   they need different.
   ========================================================================== */

/* 15px, not 14. The simulator's text sits beside a panel and a scope and is
   mostly labels; on a document page it is the whole content.

   The width cap goes on --page, which header and main already read, so a
   document page is capped the same way the simulator is. */
body.doc {
  --page: 64rem;
  padding: 2.25rem 2.5rem 4rem;
  font-size: 15px;
  line-height: 1.6;
}
body.doc main { display: block; max-width: var(--page); }

/* The tagline under the name and the menu. flex-basis rather than a wrapper:
   it is the only thing in the header that wants its own row. */
.tagline { flex-basis: 100%; margin: .35rem 0 0; color: var(--muted); }

/* A section heading takes its face from the .label rule at the top of this
   file - a heading is a label for a section - and adds only what is its own. */
h2 { margin: 0 0 .8rem; color: var(--muted); }

/* No cards. A rule across the top and room underneath separates one step from
   the next, which is all the card was doing - and a numbered heading already
   says where one step ends. */
.step {
  border-top: 1px solid var(--line);
  padding: 1.35rem 0 0;
  margin-bottom: 1.75rem;
}
.step[hidden] { display: none; }
.step > :last-child { margin-bottom: 0; }
.step p { margin: .55rem 0; }

/* Two columns under the heading, and the rule above them running the full width
   of both.
 *
 * The rule used to be three times the length of the text under it, which read
 * as a section that had lost its right-hand half. Widening the page and filling
 * it is the fix rather than shortening the rule: a column of about sixty
 * characters is what these want to be, and two of them is a page. */
.cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .1rem 3rem;
  align-items: start;
}
.cols > * { min-width: 0; }
.cols > :first-child > :first-child,
.cols > :nth-child(2) > :first-child { margin-top: 0; }

/* One column again when there is not room for two. 46rem is where a column
   drops below about forty characters, which is where prose stops reading as
   prose. */
@media (max-width: 46rem) {
  .cols { grid-template-columns: minmax(0, 1fr); gap: .1rem; }
}


button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #06090c;
}
button.primary:not(:disabled):hover { background: #93d4ff; border-color: #93d4ff; color: #06090c; }

button:disabled { opacity: .35; cursor: default; }


/* A typed value wears the same face as the picker beside it. The diagnostics
   page asks for a tempo; nothing on the updater does, but the rule belongs with
   its neighbours rather than in one page's head. */
input[type=number] {
  appearance: none;
  -moz-appearance: textfield;
  background: rgba(20, 22, 27, .7);
  color: var(--fg);
  border: 1px solid var(--line);
  padding: .55rem 1.1rem;
  font: 500 .78rem/1 var(--font);
  letter-spacing: .04em;
}
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button { appearance: none; margin: 0; }

/* The filename sits beside the button, so it stays quiet and monospaced while
   the button itself matches every other button on the page. */
input[type=file] {
  font: 400 .8rem/1 var(--mono);
  color: var(--muted);
  max-width: 100%;
}
input[type=file]::file-selector-button {
  font: 600 .68rem/1 var(--font);
  text-transform: uppercase;
  letter-spacing: var(--track);
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--line);
  padding: .55rem 1.1rem;
  margin-right: .7rem;
  cursor: pointer;
  transition: color .12s, border-color .12s;
}
input[type=file]:hover::file-selector-button { color: var(--accent); border-color: var(--accent); }


.bad { color: var(--bad); }
.good { color: var(--good); }

.lede { color: var(--muted); font-size: .92rem; line-height: 1.65; }
.lede strong { color: var(--fg); font-weight: 600; }


.lede { color: var(--muted); font-size: .92rem; line-height: 1.65; }
.lede strong { color: var(--fg); font-weight: 600; }

.row + .row { margin-top: .5rem; }


progress {
  width: 100%;
  height: .5rem;
  accent-color: var(--accent);
}

/* What a run found, set off from the prose around it. */
.verdict {
  padding: .7rem .9rem;
  background: rgba(255, 255, 255, .06);
  border-radius: var(--radius);
  margin: .9rem 0;
  font-size: .92rem;
}
.verdict[hidden] { display: none; }

#log {
  border-radius: var(--radius);
  font: 12.5px/1.6 var(--mono);
  color: var(--muted);
  background: rgba(0, 0, 0, .35);
  padding: .8rem .9rem;
  max-height: 12rem;
  overflow-y: auto;
  white-space: pre-wrap;
}


dl { margin: .7rem 0; }
dt { font-weight: 700; margin-top: .85rem; }
dd { margin: .35rem 0 0; color: var(--muted); }

/* A figure fills its column and says what it is underneath. The surface behind
   it is the one the simulator gives the panel, so a screenshot of that page
   sits on the same ground here as it does there. */
figure { margin: .3rem 0 1rem; }
figure img {
  display: block; width: 100%; height: auto;
  background: rgba(0, 0, 0, .15);
}
figcaption {
  margin-top: .5rem;
  color: var(--muted); font-size: .82rem; line-height: 1.55;
}

/* Where an illustration is wanted and there is not one yet. Deliberately plain
   and deliberately labelled - an empty box that looked designed would get left
   in. */
.placeholder {
  display: flex; align-items: center; justify-content: center;
  min-height: 11rem; padding: 1rem;
  border: 1px dashed var(--line);
  color: var(--muted); font-size: .82rem; text-align: center;
}

code { background: rgba(255, 255, 255, .09); padding: 0 .25em; font-family: var(--mono); font-size: .9em; }


/* ---- the page menu ------------------------------------------------------- */
.pages {
  position: relative;
  display: inline-block;
}

/* Sized and coloured like the subtitle it replaces, plus a caret. list-style
   and the webkit marker both have to go or the browser draws its own triangle
   in front. */
/* The size and face the subtitle had when it was a <span> inside the h1: this
   replaced it, so it has to sit on the title's line and read as part of it. */
.pages > summary {
  list-style: none;
  cursor: pointer;
  color: var(--muted);
  font-size: 1.5rem; font-weight: 400;
  text-transform: uppercase; letter-spacing: .06em;
  white-space: nowrap;
}
.pages > summary::-webkit-details-marker { display: none; }
.pages > summary::after {
  content: '';
  display: inline-block;
  margin-left: .45em;
  vertical-align: .12em;
  border: .3em solid transparent;
  border-top-color: currentColor;
  border-bottom: 0;
}
.pages > summary:hover { color: var(--fg); }
.pages[open] > summary { color: var(--fg); }
.pages > summary:focus-visible { outline: 1px solid var(--accent); outline-offset: .2em; }

/* Over the page, not in the flow: opening the menu must not move the content
   under it. */
.pages nav {
  position: absolute;
  top: calc(100% + .5rem);
  left: 0;
  z-index: 20;
  min-width: max(100%, 15rem);
  display: flex;
  flex-direction: column;
  background: #1b1e23;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

/* The same face every other label on the page wears, so the menu reads as part
   of the instrument rather than as a browser widget. */
.pages nav a {
  padding: .6rem .9rem;
  color: var(--fg);
  text-decoration: none;
  font-size: .68rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track);
  white-space: nowrap;
}
.pages nav a + a { border-top: 1px solid rgba(255, 255, 255, .07); }
.pages nav a:hover { background: rgba(255, 255, 255, .06); color: var(--accent); }

/* The page you are on, listed rather than omitted: a menu whose contents change
   from page to page is one you have to read every time. */
.pages nav a[aria-current] { color: var(--muted); }
.pages nav a[aria-current]:hover { background: none; color: var(--muted); cursor: default; }

/* Off to the project itself, which is the one destination that leaves. */
.pages nav a.away { color: var(--muted); }
.pages nav a.away:hover { color: var(--accent); }
.pages nav a.away::after { content: ' ↗'; }
