/* Atari TOS / GEM.
 *
 * The rules that make it read as GEM rather than as "flat design with square
 * corners": black and white do all the work and the greys only sit things
 * behind others, every edge is one hard black pixel, nothing is rounded,
 * nothing has a gradient, and the only depth cue is a solid black drop shadow
 * offset down-right. Selection is an invert, not a tint.
 *
 * The desktop behind the dialogs is the 50% dither - a 2px checkerboard, which
 * is how a 1-bit display made grey and why GEM looks the way it does.
 */

:root {
  color-scheme: light only;
  /* Everything inside <body> is drawn at this scale - see the note there. */
  --scale: 0.8;
  --ink: #000;
  --paper: #fff;
  --face: #cfcfcf;   /* dialog fill */
  --face-dim: #b3b3b3;  /* the unchosen tabs, one step back from the face */
  --shadow: 3px 3px 0 var(--ink);
  /* The step chart's label column width - read by both .chart-corner (so the
     grid actually has that column that wide) and the LAST STEP playhead's
     inline `left` (so it lands on the same column boundaries without
     measuring the table, which would come back as 0 before the chart is
     inserted into the page). One number, not two kept in sync by hand. */
  --corner: 7.4rem;
  /* One PATTERN button, and with it the grid: sixteen buttons and the step
     grid are the same object seen two ways, so they are sized from the same
     number and end on the same line. The grid gets a height rather than a row
     height, because INST. has thirteen lanes and EXT sixteen - fixing the row
     height would leave INST. three rows short of the column it sits beside. */
  --pattern-step: 22px;
  --ruler: 10px;            /* the numberless band the playhead handle sits in */
  /* The pinned status bar's height, read by both the bar itself and the room
     body leaves below its content so the bar never covers the last line. */
  --statusbar: 24px;
  /* Every box in the readouts row under the chart, so they stay one height
     whether the row is on one line or folded into two. */
  --readout-h: 22px;
  /* The row labels on the dump/restore grid, and the gutter opposite them.
     Kept narrow because every pixel it takes is a pixel off sixteen columns of
     cells, which divide whatever is left. */
  --matrix-label: 1.5rem;
  --grid-height: calc(var(--pattern-step) * 16);
  --font: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace;
  --dither: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2' shape-rendering='crispEdges'%3E%3Crect width='1' height='1'/%3E%3Crect x='1' y='1' width='1' height='1'/%3E%3C/svg%3E");
}

* { box-sizing: border-box; }

/* The desktop, drawn here rather than on <body> so it sits outside the scale
   below: the checkerboard is meant to be 2 hard pixels, and scaling it to 1.6
   resamples the one thing in the design that must not be resampled. */
html {
  background-color: var(--paper);
  background-image: var(--dither);
  background-size: 2px 2px;
}

/* Any rule setting `display` on a tag or class outranks the user agent's
   [hidden] rule, so a hidden element renders anyway. That has now bitten the
   tab panels (`section`) and the alert box (`.alert`); one global rule is the
   fix, rather than remembering a guard per component. */
[hidden] { display: none !important; }

body {
  /* The window furniture was drawn a fifth too large: at 100% it read as a
     zoomed-in screenshot, and the size it is meant to be is the size it had at
     80% browser zoom. That, baked in - one number rather than two hundred
     hand-scaled px values, and the 1px rules that make this look like GEM are
     still declared as 1px. Undoing it is deleting this line.

     zoom, not transform: scale(), because zoom scales layout. A transform would
     leave the box its old size and hand grid.js pointer coordinates in one
     space and rectangles in another. */
  zoom: var(--scale);
  margin: 0;
  /* The viewport, in this element's own pre-scale units: 100vh would come out a
     fifth short and leave the page ending above the bottom of the window. */
  min-height: calc(100vh / var(--scale));
  /* The status bar is fixed, so it is out of the flow and would sit on top of
     whatever the page ends with. This is the room it needs back. */
  padding-bottom: var(--statusbar);
  display: flex;
  flex-direction: column;
  color: var(--ink);
  font: 13px/1.45 var(--font);
  letter-spacing: 0.02em;
  -webkit-font-smoothing: none;
  font-smooth: never;
}

/* ---------- window title ---------- */

/* The title bar spans the dialog, so it needs the padding pulled back off it. */
.titlebar {
  margin: -14px -16px 12px;
  padding: 3px 10px;
  background: var(--paper);
  border-bottom: 1px solid var(--ink);
  text-align: center;
}
.titlebar span {
  letter-spacing: 0.14em;
  font-weight: 700;
  white-space: nowrap;
}

/* ---------- the dialog header ---------- */

/* Panel names on the left, MIDI accessories on the right, sharing one baseline
   rule that runs the width of the dialog. */
.shell-head {
  display: flex;
  align-items: flex-end;
  gap: 14px;
  margin: -2px -2px 14px;
  border-bottom: 1px solid var(--ink);
}

/* The chosen tab is left the colour of the dialog face, so it joins the panel
   below it through the gap in the header rule - which is what a folder tab is.
   The unchosen ones sit one step back in a dimmer grey: enough to read as
   behind rather than beside, without the hard invert drawing the eye to
   whichever tab you are not looking at. */
.tabs { display: flex; gap: 4px; }
.tabs button[role="tab"] {
  background: var(--face-dim);
  border: 1px solid var(--ink);
  border-bottom: none;
  color: var(--ink);
  font: inherit;
  letter-spacing: 0.08em;
  padding: 3px 16px;
  cursor: pointer;
  /* An unchosen tab rests on the header rule, so the line reads as its closed
     bottom edge. */
  position: relative;
  top: 0;
}
.tabs button[role="tab"]:hover:not([aria-selected="true"]) {
  background: var(--paper);
  color: var(--ink);
}
/* The chosen tab drops one pixel to cover the rule, which opens it into the
   panel below. That break in the line - not the grey - is what says which tab
   is chosen; one step of lightness alone is too quiet to carry it. */
.tabs button[role="tab"][aria-selected="true"] {
  background: var(--face);
  color: var(--ink);
  top: 1px;
}

.shell-status {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding-bottom: 5px;
}

.pill { white-space: nowrap; }
.pill-ok::before { content: "\2666  "; }     /* filled diamond: ports ready */
.pill-warn::before { content: "\25C7  "; }   /* hollow diamond: half ready */
.pill-idle::before { content: "\25C7  "; }

/* ---------- dialogs ---------- */

main {
  flex: 1;
  padding: 18px;
  max-width: 1200px;
  width: 100%;
  margin-inline: auto;
}

.dialog {
  background: var(--face);
  border: 1px solid var(--ink);
  box-shadow: var(--shadow);
  padding: 14px 16px 16px;
}


/* All four panels occupy one grid cell, so the dialog is sized by the largest
   of them and stops resizing as you switch tabs - which a centred dialog makes
   very visible, since a change in either dimension moves the whole box.
   Sizing it from the panels themselves rather than from a hardcoded width and
   height means it stays right when a panel gains a control.

   This is the one deliberate exception to the global `[hidden]` rule above: the
   inactive panels have to keep taking up space to do the sizing. `visibility:
   hidden` still takes them out of the accessibility tree and off the tab order,
   so `hidden` remains true in every sense but the one being borrowed. */
.panels { display: grid; }
.panels > section { grid-area: 1 / 1; }
.panels > section[hidden] {
  display: block !important;
  visibility: hidden;
  pointer-events: none;
}

.row {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  align-items: flex-start;
  margin-bottom: 14px;
}
.row:last-child { margin-bottom: 0; }
.row > * { flex: 1 1 20rem; min-width: 15rem; }

/* The chart is the reason this panel exists, so it gets the whole width and
   the top: sixteen steps across eleven voices is the widest thing in the app,
   and every pixel taken off it for a file list is a pixel off the grid. The
   file list and the setup record are both things you consult, not things you
   work in, so they go underneath and share the line.

   A grid rather than the flex row the other panels use, because this one has
   three boxes in two different arrangements and flex can only reorder within a
   line. Stacked, the setup record goes to the bottom - under the pattern
   editor rather than between the file list and it, because on one column the
   editor is what you came for and a wall of setup text above it is a wall to
   scroll past. Named areas say both in two lines. */
.browse-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas:
    "detail detail"
    "files  config";
  align-content: start;
}
.browse-row > .col-files { grid-area: files; }
.browse-row > .col-config { grid-area: config; }
.browse-row > .col-detail { grid-area: detail; }
/* The row's own boxes size themselves; the flex sizing on .row > * is inert
   under grid, but min-width would still floor the columns. */
.browse-row > * { min-width: 0; }

/* GEM group boxes: a hairline rectangle with the label sitting on the edge. */
fieldset.group {
  border: 1px solid var(--ink);
  padding: 10px 12px 12px;
  margin: 0 0 14px;
  min-width: 0;
}
section > fieldset.group:last-child { margin-bottom: 0; }
/* Centred on the box's top rule rather than sitting at its left corner. An
   auto margin either side is how a legend is moved along its own edge - it is
   laid out on the border, not in the flow, so text-align on the fieldset does
   not reach it. */
fieldset.group legend {
  padding: 0 6px;
  margin-inline: auto;
  background: var(--face);
}
fieldset.group p { margin: 0 0 8px; }
fieldset.group p:last-child { margin-bottom: 0; }

.hint { margin: 0 0 14px; }
.muted { opacity: 0.72; }

/* ---------- radio lists (the port pickers) ---------- */

.radios {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 15rem;
  overflow: auto;
}
.radios li {
  display: flex;
  align-items: baseline;
  gap: 7px;
  padding: 1px 2px;
  cursor: pointer;
}
/* The GEM radio: a hollow diamond that gains a solid centre when chosen. */
.radios li::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  border: 1px solid var(--ink);
  transform: rotate(45deg);
  position: relative;
  top: -1px;
}
.radios li[aria-selected="true"]::before { background: var(--ink); }
.radios li:hover { text-decoration: underline; }
.radios li > span:first-of-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.radios li .sub { margin-left: auto; padding-left: 10px; opacity: 0.7; white-space: nowrap; }
.radios li.empty { cursor: default; opacity: 0.7; }
.radios li.empty::before { visibility: hidden; }
.radios li.empty:hover { text-decoration: none; }

/* ---------- list boxes (files, contents) ---------- */

.listbox {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--paper);
  border: 1px solid var(--ink);
  min-height: 8rem;
  max-height: 20rem;
  overflow: auto;
}
.listbox li {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 2px 5px;
  cursor: pointer;
  white-space: nowrap;
  /* Double-clicking a row opens a rename, and a double click on text otherwise
     selects the word under it - which is not what the gesture is for here. */
  user-select: none;
}
/* The rename box, sized and coloured to sit exactly where the name was so the
   row does not jump when it opens. */
.listbox input.rename {
  display: block;
  width: 100%;
  margin: -2px 0;
  padding: 1px 3px;
  border: 1px solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
}
/* One row per entry, always. A wrapped filename turns a list box into a
   paragraph and the inverted selection stops reading as a row. Whatever gets
   truncated, the full text is on the row's title. */
#files li > span {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Selection inverts. On a 1-bit display it was the only thing it could do, and
   it is still the least ambiguous way to say "this row". */
.listbox li[aria-selected="true"] { background: var(--ink); color: var(--paper); }
.listbox li:hover:not([aria-selected="true"]) { text-decoration: underline; }
.listbox li .sub { opacity: 0.7; text-align: right; }
.listbox li[aria-selected="true"] .sub { opacity: 0.85; }
.listbox li.empty { cursor: default; opacity: 0.7; }
.listbox li.empty:hover { text-decoration: none; }

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

label.field { display: block; margin-bottom: 9px; }

input[type="text"], input:not([type]), select {
  display: block;
  width: 100%;
  margin-top: 3px;
  background: var(--paper);
  border: 1px solid var(--ink);
  border-radius: 0;
  color: var(--ink);
  font: inherit;
  padding: 3px 5px;
}
select { appearance: none; padding-right: 22px;
  /* The GEM popup arrow, drawn rather than fetched. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='6'%3E%3Cpath d='M0 0h9L4.5 6z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 6px center;
}
input:focus, select:focus, .btn:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 1px;
}

label.check { display: flex; align-items: center; gap: 7px; margin-bottom: 9px; cursor: pointer; }
input[type="checkbox"] {
  appearance: none;
  width: 11px;
  height: 11px;
  margin: 0;
  border: 1px solid var(--ink);
  background: var(--paper);
  display: grid;
  place-content: center;
  cursor: pointer;
}
input[type="checkbox"]:checked::before {
  content: "\2715";           /* the GEM tick is an x */
  font-size: 9px;
  line-height: 1;
}

/* ---------- buttons ---------- */

.btn {
  background: var(--face);
  border: 1px solid var(--ink);
  border-radius: 0;
  color: var(--ink);
  font: inherit;
  padding: 3px 14px;
  cursor: pointer;
}
.btn:hover:not(:disabled) { background: var(--paper); }
.btn:active:not(:disabled) { background: var(--ink); color: var(--paper); }
/* The default button carries a second rule around it, which is how GEM said
   "this is what Return does". */
.btn.default { box-shadow: 0 0 0 1px var(--face), 0 0 0 2px var(--ink); margin: 2px; }
/* Flash: solid ink and the dialog's own drop shadow, because it is the one
   button on the page that writes something a unit cannot be talked out of.
   Inverted like a chosen tab, raised like a window - it should not look like
   the buttons that only read a file. */
.btn.primary {
  background: var(--ink);
  color: var(--paper);
  box-shadow: var(--shadow);
}
/* Hover and press swap the fill back rather than lightening it: the only two
   colours here are ink and paper, and the shadow collapsing is what "pressed"
   looks like when nothing can move. */
.btn.primary:hover:not(:disabled) { background: var(--paper); color: var(--ink); }
.btn.primary:active:not(:disabled) { box-shadow: none; transform: translate(1px, 1px); }
.btn.primary:disabled { box-shadow: none; }
.btn.small { padding: 1px 9px; }
/* A glyph-only button (undo/redo): square rather than padded to a word's
   width, and big enough - GEM or not - to stay a comfortable click target. */
.btn.icon {
  width: 22px;
  height: 22px;
  padding: 0;
  display: grid;
  place-content: center;
  font-size: 13px;
  line-height: 1;
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

.buttons { display: flex; gap: 10px; margin-top: 10px; }

/* The dump selector: the pattern chart's language borrowed for a different
   question. Banks across, slots down, a filled cell meaning "this one is
   coming" - the same invert the chart uses for a step that plays, so the two
   grids on this page read the same way. */
.matrix { overflow-x: auto; margin-bottom: 10px; }
/* Collapsed, so the cells touch and the grid is one ruled surface rather than
   128 loose chips - the same way the step chart on Browse is drawn. A block of
   selected cells does read as a solid mass, which is the point: it is a filled
   region, and the row and column labels are what say where it starts. */
table.matrix-grid { border-collapse: collapse; }
.matrix-grid th, .matrix-grid td { padding: 0; }
/* Centred in its box, which only shows below the breakpoint - above it the
   table is the full width and there is no slack to centre in. */
table.matrix-grid { margin-inline: auto; }
/* An empty column mirroring the row labels on the far side, so the block of
   cells is centred in the box rather than pushed off it: the labels take a
   column's worth on the left, and without this the cells end flush right with
   nothing balancing them. */
.matrix-gutter { padding: 0; }
/* line-height 0 on the cells that hold a button, because a table cell keeps a
   line box under an inline-level child and that was holding them a pixel or
   two apart in both directions. Not on the headers: they hold text, and a
   zero line box clips it. */
.matrix-grid td { line-height: 0; }
.matrix-head {
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.08em;
  font-weight: 400;
  text-align: center;
  padding: 1px 4px;
  cursor: pointer;
  user-select: none;   /* a header is clicked repeatedly; do not select the letter */
}
.matrix-head:hover { background: var(--face-dim); }
.matrix-corner { cursor: default; }
.matrix-row-head { text-align: right; padding-right: 6px; }
.matrix-cell {
  appearance: none;
  display: block;
  padding: 0;
  border: 1px solid var(--ink);
  background: var(--paper);
  cursor: pointer;
}
/* Filled, with a hairline of paper inside its own edge. The cells touch, so a
   selected block would otherwise be one undivided mass and there would be no
   telling twelve chosen patterns from thirteen. The inset ring keeps every
   cell its own square while the region still reads as one. */
.matrix-cell.on {
  background: var(--ink);
  box-shadow: inset 0 0 0 1px var(--paper);
}
/* A slot the chosen backup has nothing in. Hollow and dashed rather than
   hidden, so the grid keeps its shape and A5 is in the same place whichever
   file is loaded - the same reason Browse disables an empty PATTERN button
   instead of dropping it. */
.matrix-cell:disabled {
  border-style: dashed;
  opacity: 0.4;
  cursor: not-allowed;
}
.matrix-cell:hover { box-shadow: inset 0 0 0 2px var(--face-dim); }
.matrix-cell:focus-visible { outline: 1px solid currentColor; outline-offset: -4px; }
/* The tracks are the last row of the same table, so they inherit the grid's
   metrics and sit under the pattern columns they line up with. A rule above
   them says they are a different thing being counted. */
.matrix-tracks .matrix-cell { border-top-width: 2px; }

/* As wide as the box it sits in, with a fixed layout so the sixteen slot
   columns divide what is left after the row labels and the gutter opposite
   them. Height follows width through aspect-ratio rather than being set: only
   the browser knows what the column came out as.

   The floor is what stops the cells shrinking when the box does. Below it the
   columns stop dividing and the table overflows instead, which .matrix scrolls
   - a grid you scroll is usable, a grid of five-pixel cells is not. */
/* No minimum width, on the table or on the cells, and both were tried.
 *
 * A min-width on the cell does nothing under table-layout: fixed - the column
 * keeps its share and the cell draws past it, over its neighbour. Moving the
 * floor onto the table fixed that but bought a horizontal scrollbar instead:
 * sixteen columns of any worthwhile size do not fit the box when Dump and
 * Restore share the width, so the table overflowed and the grid was cut off
 * mid-row.
 *
 * So the columns simply divide what there is. The cells get small on a small
 * window, which is the honest trade: the whole grid stays on screen, which is
 * the thing it is for. */
table.matrix-grid {
  width: 100%;
  table-layout: fixed;
}
.matrix-corner, .matrix-row-head, .matrix-gutter { width: var(--matrix-label); }
.matrix-cell { width: 100%; height: auto; aspect-ratio: 1; }

/* The image list and Flash on one line. The list takes every spare pixel, so
   the button ends up against the right-hand edge without being pushed there -
   and the select's own `display: block; width: 100%` is overridden here rather
   than qualified there, since that rule is right for every other field. */
.image-pick { display: flex; align-items: center; gap: 10px; }
.image-pick select { flex: 1; width: auto; min-width: 0; margin-top: 0; }
.image-pick .btn { flex: none; }

a { color: var(--ink); }
u { text-decoration-thickness: 1px; text-underline-offset: 2px; }

/* The Detail pane's own header: the filename (and item, once one is picked)
   on the left, undo/redo on the right - one line shared by the chart and the
   plain-text view below it, so the buttons neither vanish for a track/config
   item nor get rebuilt away every time the chart redraws. */
.detail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.detail-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.detail-actions { display: flex; gap: 4px; flex: none; }

/* ---------- Detail: bank / pattern / view controls ----------
 *
 * BANK picks which 16-pattern block the PATTERN column and INST./EXT below
 * are looking at; INST./EXT picks which 16-column view of that pattern the
 * chart draws. Both now live here rather than inside the chart itself - see
 * grid.js's own comment on why chart-tabs moved out - so a bank or view
 * change looks exactly like a pattern change: pick a control, the chart
 * underneath is rebuilt.
 */
/* The rule the top tabs rest on, and that the chosen one breaks to open into
   the sheet below - the same cue the window tabs use. */
.control-bar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 -8px 8px;
  padding: 6px 8px 0;
  border-bottom: 1px solid var(--ink);
}
/* Banks are tabs, the same idiom as INST./EXT one row over: the chosen one
   takes the colour of the surface it opens onto and the others sit a step
   back. They select what you are looking at, exactly as those do. */
.bank-btn { min-width: 26px; text-align: center; }

/* The 1-16 pattern picker runs down the chart's left edge, level with the
   instrument names inside it - the margin-top is the chart's own padding plus
   its ruler row, so button 1 sits beside the first lane rather than the
   chart's outer border. */
/* The pattern column beside the chart. Deliberately NOT called .chart-row:
   that class is already on every <tr> in the grid, and giving those rows
   display:flex collapses the table's columns to slivers. */
.detail-body { display: flex; align-items: flex-start; gap: 8px; }
/* margin-top clears the chart's own padding and the ruler band above the first
   lane, so button 1 starts level with the first row. */
/* Tabs down the side, opening rightwards onto the same sheet the top ones open
   downwards onto: the chosen one takes the paper's colour and steps over the
   rule, the rest sit back in the dimmer grey. margin-top clears the ruler band
   so PATTERN 1 starts level with the first lane. */
.pattern-col {
  display: flex;
  flex-direction: column;
  flex: none;
  margin-top: var(--ruler);
  border-right: 1px solid var(--ink);
}
.pattern-btn {
  width: 30px;
  height: var(--pattern-step);
  padding: 0;
  text-align: center;
  background: var(--face-dim);
  border: 1px solid var(--ink);
  border-right: none;
  border-bottom: none;
  position: relative;
  left: 0;
}
.pattern-btn:last-child { border-bottom: 1px solid var(--ink); }
.pattern-btn:hover:not(:disabled):not([aria-selected="true"]) { background: var(--paper); }
.pattern-btn[aria-selected="true"] {
  background: var(--paper);
  left: 1px;
}

/* The setup record's own window. Shorter than the chart's paper, because a
   config record is a dozen lines and a 12rem floor would leave it mostly
   blank; it still grows to whatever it holds. */
.col-config #config-view { min-height: 0; }

/* ---------- paper areas ---------- */

pre.paper {
  margin: 0;
  background: var(--paper);
  border: 1px solid var(--ink);
  padding: 8px 10px;
  font: inherit;
  overflow: auto;
  white-space: pre;
  min-height: 12rem;
}
/* The grid must never wrap - a wrapped step row is unreadable - but a log line
   can be a sentence, and scrolling sideways to read an error is worse than a
   soft wrap. */
pre.log { min-height: 6rem; max-height: 14rem; white-space: pre-wrap; }
pre.log .bad { font-weight: 700; }
pre.log .bad::before { content: "! "; }
#legend { margin: 8px 0 0; white-space: pre-line; }

/* ---------- the step chart ----------
 *
 * The printed chart on the machine, in the two colours this page has. Fixed
 * rows so a voice sits in the same place in every pattern, a heavier rule every
 * fourth step so 16 cells stay countable, and no colour doing any work: loud is
 * solid, soft is the dither, and the two are told apart at a glance without
 * relying on hue.
 */

/* The paper moved out to the panel, which now holds the tabs on two edges as
   well as the grid. The chart itself is just what is drawn on it. */
.chart { overflow-x: auto; }

.chart-panel {
  background: var(--paper);
  border: 1px solid var(--ink);
  padding: 8px;
}

/* INST. / EXT, the same folder-tab idiom as the window tabs one level up: the
   chosen one takes the colour of the surface it opens onto - the chart's own
   paper - and the other sits a step back. app.js puts these in the Detail
   column's control bar rather than inside the chart now, so the strip's own
   underline and bottom margin - meant for sitting flush above that paper -
   are switched off there and the control bar supplies its own spacing. */
.chart-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--ink);
}
.control-bar .chart-tabs { margin-bottom: 0; border-bottom: none; }
.control-bar .bank-buttons { flex: none; }

.chart-tab {
  background: var(--face-dim);
  border: 1px solid var(--ink);
  border-bottom: none;
  color: var(--ink);
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.1em;
  padding: 1px 12px;
  cursor: pointer;
  position: relative;
  top: 0;
}
.chart-tab:hover[aria-selected="false"] { background: var(--face); }
.chart-tab[aria-selected="true"] {
  background: var(--paper);
  top: 1px;
}

table.chart-grid {
  border-collapse: collapse;
  table-layout: fixed;
  width: 100%;
  /* The lanes divide this between them, so the grid ends level with PATTERN 16
     whether it is showing thirteen of them or sixteen. The ruler band is added
     on top rather than taken out of the middle: the pattern column starts level
     with the first lane, not with the band above it. */
  height: calc(var(--grid-height) + var(--ruler));
}

.chart-corner, .chart-num, .chart-label {
  font: inherit;
  font-weight: 400;
  text-align: left;
  padding: 1px 4px;
  white-space: nowrap;
}
/* Sized to the longest label the chart prints, so the grid gets the rest. */
.chart-corner { width: var(--corner); letter-spacing: 0.12em; }
/* A thin ruler band rather than a row of numbers: it carries the beat rules,
   gives the playhead handle somewhere to sit above the grid, and keeps the
   column that the handle's hit-testing reads. */
.chart-num {
  width: auto;
  height: var(--ruler);
  padding: 0;
}
.chart-label {
  border: 1px solid var(--ink);
  border-right: none;
  font-size: 11px;
  letter-spacing: 0.04em;
}

.chart-row { cursor: pointer; }
.chart-row[aria-selected="true"] .chart-label { background: var(--ink); color: var(--paper); }
.chart-row:hover .chart-label { text-decoration: underline; }
/* TRIG and ACCENT are not voices, so they do not look like ones. The ext lanes
   have a whole view to themselves now and need no marking inside it. */
.chart-utility .chart-label { font-style: italic; }
/* The rule between the two utility lanes and the kit below them, drawn on
   the first kit row so it lands whichever utility lanes are present. */
.chart-utility + .chart-row:not(.chart-utility) td.cell { border-top-width: 2px; }

td.cell {
  border: 1px solid var(--ink);
  padding: 0;
  position: relative;
}
/* The beat rule. The machine prints it every four steps and it is the only
   reason a 16-step row is readable without counting. */
td.cell.beat, th.chart-num.beat { border-left-width: 2px; }
/* Where a short ext loop starts over. Dashed because it is not a boundary in
   the pattern - the steps to its right are the same steps repeating. */
td.cell.wrap { border-left: 2px dashed var(--ink); }
td.cell.loud { background: var(--ink); }
td.cell.soft { background-image: var(--dither); background-size: 2px 2px; }
/* Flam: a filled corner, which is the one mark that survives on top of both a
   solid cell and a dithered one. */
td.cell.flam::after {
  content: "";
  position: absolute;
  inset: 0 auto auto 0;
  border-top: 6px solid var(--paper);
  border-right: 6px solid transparent;
}
/* Past the last step: struck through, so a 12-step pattern reads as 12 steps
   rather than as four silent ones. */
td.cell.past {
  background-image: linear-gradient(
    to bottom right,
    transparent calc(50% - 0.5px), var(--ink) calc(50% - 0.5px),
    var(--ink) calc(50% + 0.5px), transparent calc(50% + 0.5px));
  opacity: 0.4;
}

/* Editable cells. The pointer is the affordance rather than any decoration -
   a chart where every cell announces itself stops reading as a chart - and the
   hover outline is inset so it does not shift the grid by a pixel. */
td.cell.editable { cursor: pointer; }
td.cell.editable:hover { box-shadow: inset 0 0 0 2px var(--ink); }
td.cell.editable.loud:hover { box-shadow: inset 0 0 0 2px var(--paper); }
/* Mid-drag the pointer is laying down steps, so the browser must not decide it
   is selecting text across the grid instead. */
.chart.painting { user-select: none; }

/* The LAST STEP playhead: where the pattern loops. `left` is set inline in
   grid.js from the same --corner-based column math table-layout: fixed uses
   for the grid itself, so the rule and the handle land on the actual column
   boundary without measuring the table.

   Solid and full height, on purpose against the two dashed/thin rules it
   shares the grid with: the ext wrap (td.cell.wrap) is dashed because it is
   not a boundary - the steps past it are the same steps repeating - and the
   beat rule is a plain cell border that never leaves the grid. The playhead is
   the one rule that means "the pattern stops here", so it is the heaviest
   mark on the chart and the only one that reaches past the grid's own edge. */
.chart-grid-wrap { position: relative; }
.playhead {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 0;
  margin-left: -1px;   /* centres the 2px rule on the boundary, not beside it */
  border-left: 2px solid var(--ink);
  pointer-events: none;
}
/* The grip: a filled diamond straddling the top of the grid, the same shape
   the GEM radio buttons use for "this one is live" - here it says "this is
   what moves". */
/* A marker pointing back down the loop rather than a symmetrical diamond: it
   sits at the end and points at the steps that actually play. Translated a
   whole width left so its flat edge meets the rule, and padded so the hit area
   is bigger than the glyph - a 12px arrowhead is a hard thing to grab. */
.playhead-handle {
  position: absolute;
  top: -3px;
  transform: translateX(-100%);
  padding: 0 3px;
  font-size: 13px;
  line-height: 1;
  color: var(--ink);
  cursor: ew-resize;
  z-index: 1;
  user-select: none;
}
/* Grows rather than glows: a size change is the only hover cue available that
   stays inside two hard-edged colours. */
.playhead-handle:hover, .playhead-handle:active { font-size: 16px; top: -5px; }


/* The gap is read back by .readout-break's negative margin, which cancels one
   of them - change it here and change it there. */
.chart-readouts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
.readout {
  border: 1px solid var(--ink);
  padding: 1px 7px;
  display: flex;
  gap: 7px;
  align-items: baseline;
  font-size: 11px;
  /* One height for every box, rather than each line finding its own. LAST STEP
     is the tallest naturally - its value is a size up from the labels - so on
     one line the others stretched to it and matched, and the moment the row
     folded in two the line without it came out shorter and the square had two
     different rectangle heights. This is that natural height, written down. */
  min-height: var(--readout-h);
}
.readout-label { letter-spacing: 0.08em; opacity: 0.75; }
.readout-value { font-size: 13px; }
/* A readout holding a control rather than a number centres its row instead.
   A dial and a strip of divisions are boxes with no text on the baseline the
   label sits on, so baseline alignment leaves the label riding high above
   them. The text readouts keep it - LAST STEP's small label and large number
   do share a baseline, and that is what lines those two up. */
.readout-control { align-items: center; }
/* SCALE centred in the slack, and LAST STEP hard right on the corner under the
   end of the grid - the readout for the playhead, which lives at the
   right-hand end of the same rectangle.

   Two auto margins, both on the left, and that is the whole trick: flexbox
   splits the spare pixels equally between every auto margin on the line, so
   one before SCALE and one before LAST STEP puts the same gap either side of
   SCALE - centring it - while carrying LAST STEP to the far end.

   The obvious alternative, an auto margin either side of SCALE and none on
   LAST STEP, centres just as exactly but breaks the moment the row wraps: with
   no auto margin of its own, LAST STEP lands hard LEFT on the new line, which
   is the one place it must never be. Three auto margins do not work either -
   the slack still splits evenly, but two of the three land in the same gap and
   SCALE sits off to one side. */
.readout-mid, .readout-last { margin-left: auto; }
/* Off everywhere but the band below - see the media query at the foot. */
.readout-break { display: none; }
/* SCALE brings its own bordered strip, so the readout box around it would be a
   second rule enclosing the first. The label keeps its left padding pulled off
   so the row still starts on the same line as the boxes either side. */
/* SCALE brings its own bordered strip, so the readout box around it would be a
   second rule enclosing the first. The border stays as transparent rather than
   being removed so the label still starts on the same line as the boxes either
   side; the vertical half of it goes, along with the vertical padding, so the
   strip can be the full height of its box and its rectangle lines up with the
   SHUFFLE and FLAM rectangles beside it rather than sitting 3px short inside
   an invisible one. `stretch` is what makes it take that height - the row
   stretches the boxes to a common height already, and this passes it on. */
.readout-bare {
  border-color: transparent;
  border-block-width: 0;
  padding-block: 0;
  padding-left: 0;
  align-items: stretch;
}
/* Stretch is for the strip, not for the word beside it: a stretched label is a
   full-height box with its text sitting at the top of it, which read as the
   label riding an inch high. */
.readout-bare .readout-label { align-self: center; }

/* SHUFFLE and FLAM as the eight-position dials they are on the panel. Round
   here against a rulebook that rounds nothing, because these are the one thing
   on this page that is not GEM furniture: they are the machine's own indicator
   lamps, and a lamp is a circle. Filled is the position the dial is on. */
/* The strip is the control, not the eight circles on it: the padding is the
   slack that makes a press between two circles, or above and below the row,
   land on the dial rather than on nothing. grid.js hit-tests the pointer's x
   against the circle centres, so the gaps belong to whichever is nearer.

   The row is centred against its label by .readout-control, not from here: a
   row of circles has no text on the baseline the label sits on. */
.dial {
  display: flex;
  gap: 4px;
  align-items: center;
  padding: 4px 2px;
  margin: -4px 0;   /* the slack must not make the readout box taller */
}
.chart.editable .dial { cursor: pointer; }
/* Mid-drag the pointer is sweeping the dial, so the browser must not decide it
   is selecting the labels either side of it. */
.chart.turning { user-select: none; }
.lamp {
  appearance: none;   /* a <button> otherwise arrives with the UA's own face on it */
  width: 8px;
  height: 8px;
  margin: 0;
  padding: 0;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background: var(--paper);
  /* The circle is a mark on the strip, not a target of its own - the strip
     under it takes the press, and hands it to the nearest circle. */
  pointer-events: none;
}
.lamp.on { background: var(--ink); }
/* pointer-events does not reach the keyboard, so the circle is still the radio
   focus lands on and still shows where the keyboard is. */
button.lamp:focus-visible { outline: 1px solid var(--ink); outline-offset: 2px; }
/* Only ever seen on a record the machine never wrote - see relightDial(). */
.dial-odd:not(:empty) { margin-left: 4px; font-size: 11px; }

/* SCALE, in the readouts row between FLAM and LAST STEP. Four named divisions
   are a list to pick from rather than a knob to sweep, so unlike the dials
   beside it this is four buttons in a strip, and the chosen one is an invert -
   which is how GEM said "this one", and what the pattern column and the
   window tabs already do. The strip draws its own border and the rules between
   the divisions, which is why its readout has none - see .readout-bare. */
.seg {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--ink);
  background: var(--paper);
}
.seg-btn {
  appearance: none;
  background: var(--paper);
  border: none;
  border-left: 1px solid var(--ink);
  color: var(--ink);
  font: inherit;
  font-size: 11px;
  line-height: 1.35;
  padding: 0 5px;
  cursor: pointer;
}
span.seg-btn { cursor: default; }   /* a read-only chart draws spans */
.seg-btn:first-child { border-left: none; }
.seg-btn:hover:not(.on) { background: var(--face); }
.seg-btn.on { background: var(--ink); color: var(--paper); }
/* currentColor, not --ink: the chosen division is inverted, and a black ring
   inside a black button is no ring at all. */
.seg-btn:focus-visible { outline: 1px solid currentColor; outline-offset: -3px; }
/* Only a record the machine never wrote gets here - see relightScale(). */
.seg-odd:not(:empty) {
  font-size: 10px;
  padding: 0 5px;
  border-left: 1px solid var(--ink);
  align-self: center;
}

/* A GEM progress bar: solid ink filling a white trough, no animation. */
progress {
  appearance: none;
  width: 100%;
  height: 12px;
  margin-bottom: 10px;
  border: 1px solid var(--ink);
  background: var(--paper);
}
progress::-webkit-progress-bar { background: var(--paper); }
progress::-webkit-progress-value { background: var(--ink); }

.dropzone {
  border: 1px dashed var(--ink);
  padding: 9px 12px;
  margin-bottom: 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.dropzone.over { background: var(--paper); border-style: solid; }

kbd {
  border: 1px solid var(--ink);
  padding: 0 4px;
  background: var(--paper);
}
code { font: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* ---------- alert boxes ---------- */

.alert {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  background: var(--face);
  border: 1px solid var(--ink);
  box-shadow: var(--shadow);
  padding: 12px 14px;
  margin: 18px 18px 0;
}
main + .alert, .alert { max-width: 1200px; }
#unsupported { margin-inline: auto; }
/* The GEM stop/alert icon: a heavy exclamation in a hard box. */
.alert-icon {
  flex: none;
  width: 26px;
  height: 26px;
  border: 1px solid var(--ink);
  display: grid;
  place-content: center;
  font-weight: 700;
  font-size: 16px;
}
.alert h2 { margin: 0 0 6px; font: inherit; font-weight: 700; }
.alert p { margin: 0; }

dialog {
  background: var(--face);
  color: var(--ink);
  border: 1px solid var(--ink);
  box-shadow: var(--shadow);
  border-radius: 0;
  padding: 14px;
  max-width: 34rem;
  font: inherit;
}
dialog .alert { margin: 0; box-shadow: none; border: none; background: none; padding: 0; }
dialog::backdrop {
  background-color: var(--paper);
  background-image: var(--dither);
  background-size: 2px 2px;
  opacity: 0.5;
}
.dialog-buttons { display: flex; justify-content: center; gap: 14px; margin-top: 14px; }

/* ---------- status line ---------- */

/* Pinned to the window, not parked at the end of the page. A status bar that
   scrolls away is a status bar you have to go looking for, and what it carries
   - whether the ports are open, what the last transfer did - is exactly what
   you want in view while working somewhere else on the page.

   Fixed inside a zoomed body: `zoom` scales the fixed box along with
   everything else, so the height reserved for it below has to be in the same
   pre-scale units the bar itself is measured in, which is what --statusbar is
   for. inset rather than left/right/bottom separately - it spans the window. */
.statusbar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 10;
  display: flex;
  align-items: center;
  /* One line, always. The height below is reserved as a fixed number, so a bar
     that wrapped would grow past what the page has left for it and sit on the
     content - which is what a narrow window did. */
  flex-wrap: nowrap;
  gap: 12px;
  background: var(--paper);
  border-top: 1px solid var(--ink);
  padding: 3px 8px;
  height: var(--statusbar);
}
/* The connection state and the link are fixed facts and keep their width; the
   transient message is the one thing here of unknown length, so it is the one
   that gives way. */
.statusbar > #connection, .status-right { flex: none; }
#status {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.status-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }

/* The dialog floats in the middle of the desktop once there is room for it,
   which is where GEM put one. `main` is already the flex child that takes the
   height left between the desktop bar and the status line, so centring inside
   it is centring in the window.

   Below the breakpoint it goes back to filling the width, because a centred box
   on a narrow window is just a narrower box. */
@media (min-width: 1100px) and (min-height: 720px) {
  main {
    display: grid;
    place-content: center;
  }
  #shell {
    width: fit-content;
    max-width: 100%;
  }
}

/* The last thing that can go before the row has to break. At 900 the readouts
   have 46px of slack left and are losing 30 for every 30 the window loses; the
   word SCALE and the gap after it are 37 of them, and the four divisions beside
   it read as a scale without being told - 1/16 and 1/8t are not ambiguous.
   The strip keeps its aria-label, so nothing is lost to a screen reader.

   Hidden from here down rather than only in a band: it is worth more width the
   narrower things get, and a label that vanished at 900 and came back at 850
   would draw more attention than the label does. */
@media (max-width: 900px) {
  .readout-bare .readout-label { display: none; }
  /* With the label gone the box is nothing but the strip, so the padding that
     held the strip off the box's right edge is holding it off nothing - seven
     invisible pixels between SCALE and whatever follows it. */
  .readout-bare { padding-right: 0; }
}

/* Where the readouts stop fitting on one line, they fold into a square rather
   than wrapping wherever they run out:

       SHUFFLE                LAST STEP
       FLAM                       SCALE

   Each pair keeps the edge it had - the dials down the left, the two things
   that report the pattern's shape down the right - so nothing moves sideways
   on the way from one line to two, it only doubles up.

   This happens twice, because the panel gets a second wind in between. From
   761 to 850 Detail is at its narrowest, with the file list taking its 15rem
   out of the width. Below 760 the panel goes to one column and Detail takes
   the whole window, so the row fits again - until 612, where it does not, and
   the same square comes back.

   The order values are what fold it: the break is a zero-height item claiming
   a whole line, which is how flexbox is asked for a break, and the two on
   either side of it are ordered around it. Its negative margin takes back the
   row gap the empty line would otherwise add. The auto margins already on
   LAST STEP and SCALE put each of them at the right-hand end of its own line
   without a rule of their own. */
@media (min-width: 761px) and (max-width: 850px), (max-width: 612px) {
  .readout-shuffle { order: 0; }
  .readout-last { order: 1; }
  .readout-break {
    order: 2;
    display: block;
    flex-basis: 100%;
    height: 0;
    margin-bottom: -4px;   /* one .chart-readouts gap, cancelled */
  }
  .readout-flam { order: 3; }
  .readout-mid { order: 4; }
}

@media (max-width: 760px) {
  .row { flex-direction: column; }
  /* flex: none, because the basis on .row > * is 20rem and a basis is measured
     along the main axis - which this rule has just turned vertical. Left as it
     is, every box in a stacked row is 20rem TALL before it holds anything, so
     a port list with one port in it stood 256px high around 17px of content.
     Stacked, the boxes should be exactly as tall as what is in them. */
  .row > * { width: 100%; flex: none; }
  /* Each panel gets its own height. The panels normally share one grid cell so
     the dialog is sized by the largest and stops resizing as you switch tabs -
     worth it on a wide window, where a centred box jumping about is very
     visible. Stacked, it costs Device and Transfer a screenful of nothing
     underneath them to match Browse, and the box is not centred anyway. So the
     hidden ones go back to being hidden rather than merely invisible. */
  .panels { display: block; }
  .panels > section[hidden] { display: none !important; }

  /* One column, and the setup record moves to the bottom - under the pattern
     editor rather than above it. See .browse-row. */
  .browse-row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "files"
      "detail"
      "config";
  }
  /* The tabs and the MIDI accessories stop fitting on one line well before the
     panels do, so the header stacks first. */
  .shell-head { flex-wrap: wrap; }
  /* The auto margin stays, so Enable MIDI keeps the right edge whether the
     accessories are still sharing the tabs' line or have wrapped below them.
     Left-aligning it under the tabs put the one button in the header nowhere
     in particular; against the right edge it is where it was, one line down. */
  .shell-status { padding-bottom: 8px; }
  .tabs { flex-wrap: wrap; }
}

/* Dump and Restore hold out longer than the rest of the panel. The row above
   stacks everything at 760, but these two are the same grid asking opposite
   questions and are read against each other, so they stay beside each other
   for another two hundred pixels. Below 560 they stack in the order they are
   written - taking a backup off the unit before putting one back - and each
   takes the full width, which by then is the only way either grid fits. */
@media (min-width: 561px) and (max-width: 760px) {
  .transfer-row { flex-direction: row; }
  .transfer-row > * { width: auto; flex: 1 1 0; min-width: 0; }
}

/* No raised floor through 651-760. Sixteen columns of anything bigger do not
   fit the box the two grids share there - the table overflowed and .matrix
   clipped it at the twelfth column, which is a worse answer than smaller
   cells. The narrower row labels above are what buys the cells their width
   back, and they divide whatever is left. */

