/* Shared chrome for every page here.
 *
 * Deliberately spare: these are instrument panels for prototypes, not a
 * product. `color-scheme: light dark` lets the OS decide, and every color
 * below is either derived from currentColor or one of the semantic tokens in
 * `:root` — so there is one palette to maintain rather than two.
 */

/* ---- the bitmap faces a stamp can be drawn in --------------------------
 *
 * Shipped beside the page, in `builtin/fonts/` with their licenses, so every
 * browser has them and none goes to fetch them from anywhere else. Which faces
 * there are and what grid each is drawn on is `FACES` in dala/text.js; this is
 * only where the files are. A bold is declared where the family has one — the
 * browser would otherwise synthesize one, which at eight pixels runs the
 * strokes together. */
@font-face {
  font-family: "Pixel Operator 8";
  src: url("builtin/fonts/PixelOperator8.ttf");
}
@font-face {
  font-family: "Pixel Operator 8";
  font-weight: 700;
  src: url("builtin/fonts/PixelOperator8-Bold.ttf");
}
@font-face {
  font-family: "Press Start 2P";
  src: url("builtin/fonts/PressStart2P.ttf");
}
@font-face {
  font-family: Silkscreen;
  src: url("builtin/fonts/Silkscreen.ttf");
}
@font-face {
  font-family: Silkscreen;
  font-weight: 700;
  src: url("builtin/fonts/Silkscreen-Bold.ttf");
}
@font-face {
  font-family: Tiny5;
  src: url("builtin/fonts/Tiny5.ttf");
}

:root {
  color-scheme: light dark;
  --edge: color-mix(in srgb, currentColor 18%, transparent);
  /* A line drawn inside a box rather than around one — between two subjects in
     a pane, or around a readout that is already inside a bordered panel. At the
     same weight it reads as a second frame; this is the same line, further
     off. */
  --edge-faint: color-mix(in srgb, var(--edge) 70%, transparent);
  --dim: color-mix(in srgb, currentColor 45%, transparent);
  /* The page's own ink, at full strength: near white on a dark scheme and near
     black on a light one, which is what `CanvasText` is for.

     An absolute value and not a mix, because the things that use it sit inside
     boxes that have already dimmed `currentColor` — a readout, a toolbar — and
     nothing mixed from that can come back brighter than the box it is in. This
     is what "the title, and the control you have picked" is set in, and the
     only two things in a panel that are. */
  --bright: CanvasText;
  /* ---- type -------------------------------------------------------------

     Two faces, and which one a thing is set in says what kind of thing it is.
     `--ui` is the default and everything wears it: titles, labels, buttons,
     prose. `--mono` is for figures and for log lines — the two places where
     what matters is that one line sits under the last character for character,
     and the only two where a proportional face is wrong.

     Two sizes and no more. `--text` is the page, `--fine` is a panel: a
     control or a figure in a box a few words wide, where the page's own size
     would fit three columns instead of six. Anything that reaches for a third
     is a thing that has not decided which of the two it is. */
  --ui: system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --text: 13px;
  --fine: 11px;
  --label: auto;
  --beside: 1.1rem;

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

     Two corners and three grounds, because a box here is one of two sizes and
     is in one of three states, and there is nothing else for either to be. */
  --round: 6px;
  --round-tight: 4px;

  /* A box's own ground, the one it has just for being a box. */
  --fill: color-mix(in srgb, currentColor 5%, transparent);
  /* Under the pointer, or marked as different from its neighbors. */
  --fill-over: color-mix(in srgb, currentColor 10%, transparent);
  /* And the one that is chosen. */
  --fill-on: color-mix(in srgb, currentColor 20%, transparent);
  --ok: #2a7;
  --fail: #d44;
  --warn: #c8811f;
  /* Which way bytes are going, on the one graph that draws both. Color is the
     only thing telling them apart, so these are a hue apart rather than two
     shades of the panel's own ink. */
  --in: #4b8fd6;
  --out: #d1603f;
  /* The stage's backing-store size. `ui.js` holds the same pair as STAGE and
     sets the canvas from it — two languages describing one rectangle, so change
     one and change the other. Everything else here is derived. */
  --stage-w: 960;
  --stage-h: 600;
  /* The narrowest the world is drawn. Its width flexes between this and its
     column's full width, and its height follows from the shape above; the
     canvas is measured rather than told, so nothing but the layout decides.
     Full width is where the detail stops improving, and this is the narrowest
     it stays worth looking at — which is what leaves room for the right column
     beside it on a laptop. */
  --board-min: 40rem;
  /* The page is as wide as the window, so growing the window grows the page
     at every width. */
  --pane: 100%;
  /* ---- how wide a panel is ---------------------------------------------

     Three sizes, and every panel is built for one of them. They are what a
     panel *holds*, not where it sits: an icon and its word, a column of
     controls, or lines you read across.

       compact  a rail of icons — the tool picker
       small    a column of controls, or a readout — the tool panel, the
                tuning panel, a readout in the diagnostics panel
       large    lines read across, as wide as the column it is in — the main
                toolbar and the log

     A panel says which it is with a class of that name, and the slots panels
     sit in are sized from the same two values, so a panel and the room made
     for it cannot drift apart.

     `large` has no width here because it does not have one: it is whatever
     the column gives it. */

  /* A column of controls. Fixed, so switching tools cannot change how much of
     the world is under it on a narrow page, nor how wide the second column is
     on a wide one — it is both. */
  --small: 17rem;
  /* How far a panel sits from the world's edge — and, between two of them,
     from each other. Everything that floats reads this rather than carrying
     its own copy. */
  --inset: .6rem;
  /* An icon and its word, stacked. Named because the panel beside it has to
     clear it. */
  --compact: 3.825rem;
  /* What a box over the world is made of: enough of the page's own background
     that terrain under it does not compete with the text on it.

     Opacity and no blur. A blurred backdrop is a full-screen read of what is
     behind every box, every frame, on the same GPU drawing the world — and
     what it gives is a quieter ground, which this color gives at no cost. Held
     low enough that the world still reads through it: a box over the world
     should say what is under it is the thing being worked on. */
  --over: color-mix(in srgb, Canvas 80%, transparent);
  /* What separates one box in `main` from the next, down the page and across
     it, and therefore part of how wide two columns of them are. */
  --gutter: 1rem;
}

body {
  font: var(--text)/1.6 var(--ui);
  margin: 0;
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .75rem;
}

:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

/* ---- header ------------------------------------------------------------ */

/* The header and `main` share one width. */
header, main { width: var(--pane); }
/* Rows and columns, and nothing on top of anything. The world panel is the
   first column, down beside the toolbar and the world, in edit mode — play
   mode has no panel and no column for one, see the state rules below. The
   toolbar is the top row over the world, and the footer the bottom one across
   everything. The right column arrives when there is room for it — see the
   media query below. */
main {
  display: grid;
  grid-template-columns: var(--small) minmax(0, 1fr);
  grid-template-areas: "tree bar" "tree board" "foot foot";
  gap: var(--gutter);
}
/* The top row, as wide as the world under it. `large` is what says the width;
   the area is the world's column at every width — see the media query, where
   `bar` keeps the world's column and not the panel's. */
#toolbar { grid-area: bar; }
.board { grid-area: board; }
#footer { grid-area: foot; }

/* ---- the world panel ---------------------------------------------------- */

/* Everything the world holds, down the left of the page: its own column at
   every width, and as tall as the rows beside it. `contain: size` leaves what
   it lists out of the grid's sums, so a world gaining a tile never moves the
   world beside it; the panel scrolls instead. */
#world-tree {
  grid-area: tree;
  contain: size;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: .6rem;
  padding: .45rem;
}
#world-tree .branch { display: flex; flex-direction: column; gap: .2rem; }
/* A category's name, over what it holds, and pressed to open or close it. */
#world-tree .branch > summary {
  margin: 0;
  font-size: var(--fine);
  font-weight: 400;
  color: var(--dim);
  cursor: pointer;
}
/* The tile settings' `edit…`, at the head of its section rather than
   stretched across it. */
#sim-edit { align-self: start; }
/* A row under the one it belongs to: a template under its package, a tile
   under its island. */
#world-tree .option.under { padding-left: 1.1rem; }
/* What the world depends on: what it wants, and in a narrow column of its own
   where that came from or that it is missing. */
#tree-depends .option { display: flex; align-items: baseline; gap: .5rem; }
#tree-depends .who { flex: 1; }
#tree-depends .kind { color: var(--dim); }
/* The row a list with nothing in it still has. */
#world-tree .option.empty { color: var(--dim); }

/* ---- what the state shows -----------------------------------------------
 *
 * `main` carries the state and these rules are everything on screen because
 * of it. It is `main` and not a box inside it because the state has to
 * govern things that are not near each other — the picker is on the world and
 * the toolbar is a row of the page — and a rule anchored to one of them stops
 * matching the moment anything moves.
 *
 * Rules rather than a `hidden` per element: `hidden` is a property whose
 * effect depends on a `display` declaration elsewhere, so a box styled
 * `display: flex` on a class silently ignores it, and the failure is invisible
 * until somebody looks at the wrong mode.
 *
 * `disabled` is not here and cannot be: it is behavior, not appearance. The
 * box sets it, from the same state these read. */
main[data-mode="play"] .picker,
main[data-mode="play"] .tool-column,
main[data-mode="play"] #world-tree { display: none; }
/* And no track for it: play mode's grid is edit mode's less the panel's
   column, so the world takes that room rather than a gap standing where the
   panel was. The wide layout's is in the media query below. */
main[data-mode="play"] {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "bar" "board" "foot";
}
/* What the world panel takes off the left of the display in fullscreen: its
   width and the inset, and nothing where there is no panel. */
main { --tree-room: calc(var(--small) + var(--inset)); }
main[data-mode="play"] { --tree-room: 0rem; }
main[data-mode="play"] #world-controls { display: none; }

/* Local mode: no connection, so nothing about one — the title screen over the
   world says how each server is doing — and no world, so no mode to play or
   edit it in. The layout is play mode's: see `editing` in hud.js. */
main[data-phase="local"] :is(#disconnect, #where-server, #doing, #world-controls) { display: none; }
/* And none of the diagnostics about a connection or a world. */
main[data-phase="local"] :is(#debug-window, #debug-link, #debug-server, #debug-me,
                             #debug-world, #debug-players, #readout-window,
                             #readout-link, #readout-server, #readout-me,
                             #readout-world, #readout-players) { display: none; }

/* What a tile simulates is not shown when there is no tile, and what is shown
   instead is how to get one.

   This is the exception to *show what cannot be used yet, disabled, with a
   reason*: dark controls say "not yet", and these would be saying something —
   a gravity, a period and a spread, describing whatever was picked last or
   nothing at all. A pane about a thing, with no thing, is a pane about how to
   get one. */
main[data-picked="nothing"] #pane-select .picks { display: none; }
main[data-picked="something"] #how-to-pick { display: none; }

/* A hint is reference: dimmed throughout, label and all, so it reads as the
   thing to do rather than as something the world is telling you. */
.hint { color: var(--dim); }
.hint .label { color: inherit; }

/* A control that acts on the pane rather than on a field in it, so it has no
   label column to sit beside and takes the whole width instead. */
.controls button.wide { width: 100%; }

/* What is picked, as a card each — and everything the pane says about it
   inside that card.

   A card and not a row, because it is a thing rather than a field of one. What
   the rows in it edit is the card's subject, which is why they are in it: a
   press may land on an object as well as on the tile under it, and rows
   sitting outside the cards would belong to none of them.

   `aria-current` marks the one being edited, which is the only state a card has
   and the one a reader needs the moment there is more than one. */
.picks { display: grid; gap: .35rem; }
.pick {
  row-gap: .25rem;
  padding: .3rem .4rem;
  border: 1px solid var(--edge);
  border-radius: var(--round);
  background: var(--fill);
}
.pick[aria-current="true"] { border-color: currentColor; }

/* What the card is about. A row like the ones under it, so the two columns
   line up, and set at `--text` rather than `--fine` because it names the thing
   they all edit. The word is the category and the figure beside it says which
   one, which is the same shape every row in the card has. */
.pick-head {
  font-size: var(--text);
  padding-bottom: .25rem;
  border-bottom: 1px solid var(--edge);
}
/* The word that names a card also picks it, so it is a button — and it is the
   part of the card a keyboard can reach, since a press anywhere on the card
   does the same. Nothing of the button about it: the card's own edge is what
   says which one is current. */
.pick-head .kind {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 600;
  color: var(--bright);
  text-align: left;
  opacity: 1;
  cursor: pointer;
}

#stage, #log, form { width: 100%; }

header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem 1.5rem;
  flex-wrap: wrap;
  padding-bottom: .75rem;
  border-bottom: 1px solid var(--edge);
}

h1 { font-size: .95rem; font-weight: 600; margin: 0; letter-spacing: .01em; }
h1 a { color: inherit; text-decoration: none; opacity: .45; }
h1 a:hover { opacity: 1; }
h1 .slash { opacity: .3; margin: 0 .15em; }
h1 .sub { font-weight: 400; color: var(--dim); margin-left: .6rem; }

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

#stats { display: flex; gap: 1.1rem; flex-wrap: wrap; color: var(--dim); }
#stats span:empty { display: none; }

/* A control sized to the widest label it can ever hold — see `fixed` in ui.js.
   Every candidate is in the element at once, stacked in one grid cell, so the
   browser does the sizing and it stays right when a word changes. `visibility`
   rather than `hidden`, which would take the label out of the layout and the
   sizing with it. */
.swap { display: inline-grid; }
.swap > * { grid-area: 1 / 1; justify-self: center; }
/* Except where the control is left-aligned: centering `live` inside the width
   of `connecting` would indent it away from the dot beside it. */
#link .swap > * { justify-self: start; }
/* One shows and the rest hold the space. */
.swap > * { visibility: hidden; }
.swap > .on { visibility: visible; }

/* The connection indicator. These prototypes exist to prove datagrams arrive,
   so whether they are arriving right now is worth showing without being asked:
   a live link breathes, and a light that has stopped is the signal.

   It carries a dot before its label, so its stack is what has to be centered
   rather than the button. */
#link { display: inline-flex; align-items: center; gap: .45rem; color: inherit; }
#link::before {
  content: "";
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: var(--dim);
}
#link[data-state="connecting"]::before { animation: breathe .9s ease-in-out infinite; }
#link[data-state="live"] { color: var(--ok); }
#link[data-state="live"]::before { background: var(--ok); animation: breathe 1.6s ease-in-out infinite; }
/* A square: a link that has stopped is not a live one, and the shape says so
   before the color does. */
#link[data-state="stalled"] { color: var(--warn); }
#link[data-state="stalled"]::before { background: var(--warn); border-radius: 0; }
#link[data-state="lost"] { color: var(--fail); }
/* Not a connection state: the page threw. Red like a lost link because it is
   worse than one, and still, because nothing is going to change on its own. */
#link[data-state="broken"] { color: var(--fail); }
#link[data-state="broken"]::before { background: var(--fail); }
/* Hung up on purpose. Still, like `lost`, and dim, because it is not a failure
   and does not read as one. */
#link[data-state="off"] { color: var(--dim); }

#link[data-state="lost"]::before { background: var(--fail); }

@keyframes breathe { 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) {
  #link::before, #title .servers :is(button[data-state="joining"], .light) { animation: none; }
}

/* ---- the board --------------------------------------------------------- */

.howto { color: var(--dim); margin: 0; max-width: 78ch; }

#stage {
  display: block;
  width: 100%;
  aspect-ratio: var(--stage-w) / var(--stage-h);
  touch-action: none;
}

/* The board column: the world, and the diagnostics under it, which are here
   rather than in the right column because their lines are read across and a
   `small` column folds them. */
.board { position: relative; display: flex; flex-direction: column; gap: var(--gutter); }
/* The world's rectangle, and what sits on it. `.board` is no good for that —
   it holds the readouts under the world, so its bottom edge is not the
   world's.

   `user-select` is inherited, so one declaration here covers the whole
   subtree you point at. A drag over a canvas has nothing in it to select, but
   the browser starts a selection all the same and runs it out into whatever
   text the pointer reaches — so without this a pan leaves the page highlighted
   and the next press goes to dismissing that instead of to the world. */
.world { position: relative; user-select: none; }

/* Two canvases occupying one rectangle: terrain underneath, drawn with
   WebGPU, and everything small and moving on top, drawn with a 2D context.
   The overlay takes the pointer events, since it is the one on top.
   Nothing else in flow may go in here: the overlay is `inset: 0` against it,
   so a third child in flow would stretch the rectangle the overlay has to
   match — which is why the picker is `.world`'s child and not this one's. The
   title screen is here, and out of flow. */
/* The frame is the rectangle's, not one canvas's.
   
   A border on `#stage` alone would put that canvas's drawing surface a pixel
   in from the overlay's on every side, so everything drawn over the world
   would sit a screen pixel up and left of the terrain, and the two would
   differ in width. */
.stack {
  position: relative;
  border: 1px solid var(--edge);
  border-radius: var(--round);
  overflow: hidden;
}

.stack #overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* The title screen covers the world's rectangle, over both canvases. */
.stack #title {
  position: absolute;
  inset: 0;
  overflow: auto;
}
/* Laid out for fullscreen, where the rectangle is the display: the name and
   the list together in the middle, sized by the screen's shorter side. The
   flex box is a child so that `#title` keeps the user agent's `display` and
   its `hidden` still hides it. */
#title .title-box {
  box-sizing: border-box;
  min-height: 100%;
  padding: var(--gutter);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(2 * var(--gutter));
}
#title-name {
  margin: 0;
  font-size: clamp(2rem, 12vmin, 8rem);
  font-weight: 600;
  letter-spacing: .05em;
}
/* The servers this page knows, one to an item: the press that joins it and its
   address, and under the address what it said about itself. */
#title .servers {
  box-sizing: border-box;
  width: min(100%, 34rem);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: clamp(.9rem, 2.2vmin, 1.3rem);
}
#title .servers li + li { margin-top: .9em; }
/* The button in a column of its own, so everything else lines up under the
   address, and on the baseline of the first line of what is beside it. */
#title .servers li {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: .6em;
  align-items: baseline;
}
#title .servers :is(.said, .stats, .failure) { grid-column: 2; }
/* The address takes what the button leaves, and breaks where it has to. */
#title .servers .url { min-width: 0; overflow-wrap: anywhere; }
#title .servers :is(.said, .stats) {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: .6em;
  margin-top: .1em;
}
#title .servers .name { font-weight: 600; }
#title .servers :is(.world, .players) { color: var(--dim); }
/* The figures are smaller than what they describe, each after its word, and
   each holds the widest it can say, so nothing after it moves as it changes. */
#title .servers .stats { font-size: .8em; font-variant-numeric: tabular-nums; }
#title .servers .stat > :first-child { margin-right: .3em; color: var(--dim); }
#title .servers .stat .swap > * { justify-self: start; }
/* Whether it answers, as the connection light says it: a dot while it does, a
   ring when it does not, and breathing while the page waits to hear. */
#title .servers .light {
  flex: none;
  align-self: center;
  width: .55em;
  height: .55em;
  border-radius: 50%;
  background: var(--dim);
}
#title .servers [data-status="asking"] .light { animation: breathe .9s ease-in-out infinite; }
#title .servers [data-status="up"] .light { background: var(--ok); }
#title .servers [data-status="down"] .light { background: none; box-shadow: inset 0 0 0 .12em var(--fail); }
/* A join in flight: the button is how to give it up, and it breathes as the
   connection light does while a connection is being made. */
#title .servers button[data-state="joining"] { animation: breathe .9s ease-in-out infinite; }
/* Why the last join failed or the session ended, under its server. Contained,
   so a long reason wraps at the list's width instead of widening it. */
#title .servers .failure {
  margin-top: .1em;
  color: var(--fail);
  font-size: .8em;
  contain: inline-size;
  overflow-wrap: anywhere;
}

/* Filling the display.
 *
 * `main` is what goes fullscreen rather than the board, because the readouts,
 * the log and the tuning panel are in it, and nothing outside the fullscreen
 * element is painted. What changes is where they sit: under the board on a
 * page, over the world on a screen.
 *
 * `main`'s padding holds every box off the edge — one declaration for all of
 * them — which is why the stack is `fixed`: the world is the display, not a box
 * inside that padding. */
/* ---- where a panel sits --------------------------------------------------
 *
 * Every panel keeps the same place relative to the world, and the only thing
 * that changes is whether it sits **outside** that edge or **inset** from it:
 *
 *     #toolbar      above the world      · inset from the top in fullscreen
 *     .picker       inset, right edge    · the same, always
 *     .tool-column  outside on the right · inset from the right where there is
 *                                          no room outside, which includes
 *                                          fullscreen
 *     #world-tree   outside on the left  · inset from the left in fullscreen
 *     #debug        below the world      · inset from the foot in fullscreen
 *
 * Outside is the page's grid; inset is `--inset` from the world's edge.
 * Fullscreen is not a fourth arrangement — it is the case where there is never
 * room outside, because the world is the whole display. */

main:fullscreen {
  /* Its own arrangement, and it is not the page's grid: here every box floats
     over a world that is the whole display. Said outright rather than left to
     what `main` happens to be, so changing the page's layout cannot quietly
     change this one.

     Top down, in markup order, so the toolbar is the top row here as it is on
     a page. */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--inset);
  padding: var(--inset);
}

/* Neither column exists here — the world is the display and every box floats
   over it — so their children are handed straight to `main`, which is the
   arrangement every rule below is written against. */
main:fullscreen .board, main:fullscreen .world { display: contents; }

/* Over a world that is the whole display, at the same inset as everything
   else: `.tool-column`'s narrow-page position, measured against the display
   instead of the board. Fixed rather than absolute, since `main:fullscreen`'s
   own padding would otherwise inset it twice. */
main:fullscreen .tool-column {
  position: fixed;
  top: var(--inset);
  right: var(--inset);
  left: auto;
  /* The picker is over the world here too, so the room for it stays. Every
     value the wide layout's rule sets is restated, because that rule applies
     in fullscreen too and what this does not say, it does not undo. */
  margin-right: calc(var(--compact) + var(--inset));
}

/* And the world panel over its left edge, as `.tool-column` is over its right. What
   flows down the display starts to the panel's right by `--tree-room`, which
   is nothing in play mode, where there is no panel. The world itself is fixed
   behind everything, so it keeps the whole display. */
main:fullscreen #world-tree {
  position: fixed;
  top: var(--inset);
  left: var(--inset);
  bottom: var(--inset);
  z-index: 1;
}
main:fullscreen { padding-left: calc(var(--inset) + var(--tree-room)); }

/* Behind the floating panels rather than over them. A positioned element paints above the
   in-flow ones beside it whatever the markup order, and this one covers the
   screen. */
main:fullscreen .stack {
  position: fixed;
  inset: 0;
  z-index: -1;
}

/* The stage is `--stage-w` by `--stage-h`'s shape everywhere else and the
   display's shape here: dala fills the screen rather than sitting letterboxed
   in the middle of it. Without the height it would keep the stage's aspect
   ratio and hang off the bottom.
   The edge and the rounded corners are page chrome and there is no page here.
   The frame goes with it: over the whole display there is nothing outside the
   rectangle for a line to separate it from. */
main:fullscreen #stage { height: 100%; }
main:fullscreen .stack { border: 0; border-radius: 0; }

/* Over the world a box needs a ground to be read against, and the ones that
   get it are the readout sections: they carry a fainter one at every width and
   take the full one here.

   The tool panel and the tuning panel carry theirs at every width, not only
   this one. */
main:fullscreen .sections .section {
  background: var(--over);
}

/* And the pointer goes through it to the world.

   The panel fills the display here so the log can grow into the empty part of
   it — which makes it a screen-sized rectangle over the world, and one that
   took the pointer would swallow every click meant for what is under it.

   Everything but the controls: the switches and the address are what a pointer
   is for in here, and the readouts are text nobody clicks. Selecting that text
   goes with it, which is the cost — on a page the panel sits under the world
   and keeps it. */
main:fullscreen #debug { pointer-events: none; }
main:fullscreen #debug button { pointer-events: auto; }

/* Which leaves nothing for the panel around them to draw, and the log draws
   nothing either — a filled box round filled boxes is a ground drawn twice,
   and an empty bordered one is the hairline round nothing that `.panel` is on
   a page, where the page itself is behind it and there is something for a
   border to sit on.

   The panel takes everything under the toolbar and the log takes what the
   readouts leave, which with `#debug`'s `column-reverse` means it grows upward
   from them into the empty part of the display. */
main:fullscreen #debug {
  flex: 1;
  min-height: 0;
  background: none;
  border: 0;
  padding: 0;
}
/* And with no name on it, because a title is the top edge of a box and there
   is no box here — the log in fullscreen is lines over the world, and a word
   above them names nothing whose extent you can see. Its bar would also hang
   outside a panel with no padding, since a bar bleeds out to edges the padding
   is what puts there. */
main:fullscreen #log-panel > h2 { display: none; }

/* The band grows into what is left; the log grows into the band. */
main:fullscreen .foot { flex: 1; min-height: 0; }
main:fullscreen #log-panel {
  min-height: 0;
  /* Still the grid `.section` makes it — the heading keeps its own row and the
     log takes the rest, rather than a second layout written for this mode. */
  grid-template-rows: auto 1fr;
  background: none;
  border: 0;
  padding: 0;
}

/* The two boxes laid out against the display are kept clear of the tools,
   whether or not a pane is up: a box that changes width when a tool is picked
   is a box that moves the text in it.

   These two by name, and not every `large`. What this subtracts is measured
   from `100%` of the element it is on, so a `large` panel *inside* one of
   these — the log — would subtract the same column again from a width that
   has already lost it. */
main:fullscreen #toolbar, main:fullscreen #debug {
  max-width: calc(100% - var(--small) - var(--compact) - var(--inset));
}

/* The log fills the height its panel was given, and what it says sits at the
   bottom of it — against the readouts, which is where the eye already is.
   Filling and then floating at the top would put the newest line as far from
   everything else as the display allows.

   It scrolls rather than growing past its box: the panel's height is the
   display's, and a long enough log would otherwise push the readouts off the
   bottom of it. */
main:fullscreen #log {
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}
/* Slack on the first line rather than `justify-content: flex-end`, which puts
   the overflow past the top edge where nothing can scroll back to it. An auto
   margin takes the room when there is room and collapses to nothing when there
   is not. */
main:fullscreen #log > :first-child { margin-top: auto; }

/* It stands in the last band with the log, and the display is what bounds
   that, so all it wants of its own is somewhere to stop. */
main:fullscreen .tune { max-height: 60vh; overflow-y: auto; }

/* ---- wider than one column --------------------------------------------- */

/* One width. The fullscreen rules override what this sets: there the world is
 * the display and every box floats over it.
 *
 * 92rem is the world panel and its gutter, `--board-min`, the gutter,
 * `--small` and the body's padding either side, rounded up — where the right
 * column first fits beside the world rather than over it. Change any of those
 * and this number is what has to follow, since a media query cannot read them.
 *
 * The world narrows by `--small` and a gutter at this line, because that is
 * the room the panel stops borrowing from the world and starts taking beside
 * it. That step is the panel moving, and it is the only one: nothing is
 * capped on either side of it, so the page grows with the window throughout. */

@media (min-width: 92rem) {
  /* Three columns. The first is the world panel and the second the page — the
     toolbar, the world, the readouts under it. The third holds nothing of its
     own: it is the room `.tool-column` moves into, which is why the track is
     `--small` wide and its area empty. */
  main {
    grid-template-columns: var(--small) minmax(var(--board-min), 1fr) var(--small);
    grid-template-areas:
      "tree bar   ."
      "tree board ."
      "foot foot  foot";
  }
  /* Play mode has no world panel, so only the last two. */
  main[data-mode="play"] {
    grid-template-columns: minmax(var(--board-min), 1fr) var(--small);
    grid-template-areas:
      "bar   ."
      "board ."
      "foot  foot";
  }

  /* Out of the world and into that room. `100%` is `.board`'s right edge,
     which is the world's column's, and the gutter is the grid's own. The picker
     stays on the world, so the clearance it needed goes. */
  main .tool-column {
    top: 0;
    right: auto;
    left: calc(100% + var(--gutter));
    margin-right: 0;
  }
}

/* dala's toolbar: the top row of the page, placed by the grid like every
   other row. */
#toolbar {
  display: grid;
  gap: .35rem;
  padding: .45rem .5rem;
  border: 1px solid var(--edge);
  border-radius: var(--round);
  background: var(--over);
  user-select: none;
}

/* An author `display` outranks the user agent's `[hidden] { display: none }`,
   so the toolbar has to say it itself or `hidden` does nothing. */
#toolbar[hidden] { display: none; }

/* Each row is as wide as the toolbar rather than as wide as what is in it, so
   a font that leaves a row too little room narrows the row's name field
   instead of pushing the toolbar past the world. */
#toolbar > .line { min-width: 0; }

/* One field a row: a label in its column, and after it the controls that field
   is made of.

   A box that *owns* a label column sizes it to the widest label in it, so the
   controls get everything else and no width is written down. Everything
   directly in such a box spans both columns; a row, or a group of rows, takes
   that span and re-divides it with `subgrid`, which is what lines every row in
   the box up with every other. Not `display: contents`, which would line them
   up too and stop a row being a box that can carry a rule under it. */
/* Three roles, and a box says which it is rather than being found by where it
   sits.

   `label-columns` declares the two columns. `label-through` takes them and
   hands them on, for a box that holds rows rather than fields. `label-row`
   takes them and puts its own two children in them.

   A `.set` is `label-columns` or `label-through` depending on whether its
   labels line up with the pane's or stand apart from it — a decision about the
   pane, not a fact about how deep the markup happens to go. */
.label-columns {
  display: grid;
  grid-template-columns: max-content 1fr;
  align-content: start;
  row-gap: .25rem;
  /* The space between a name and what it names, which every other label column
     on the page already stands off by. A box that declares its own columns has
     to declare it too — the ones that take a pane's columns take its gap with
     them. */
  column-gap: var(--beside);
}
.label-through, .label-row {
  display: grid;
  grid-template-columns: subgrid;
  /* A row is a label and one line of controls, and the label sits *on* that
     line: baselines, not boxes. Centering lines up two boxes, and a control's
     box is not its text — a button carries padding, and a `select` keeps the
     browser's own `line-height: normal` whatever the sheet says. Anything that
     would be a second line is a second row of the grid — see `.below` — rather
     than a wrap inside the field. */
  align-items: baseline;
  row-gap: .25rem;
}
/* What is directly in a box that has the columns spans both of them — unless
   it is a row, whose two children are the two columns. */
.label-columns > *, .label-through > * { grid-column: 1 / -1; }
/* All three set `display`, and an author `display` outranks the user agent's
   rule for `[hidden]`, so each has to say it. */
.label-columns[hidden], .label-through[hidden], .label-row[hidden] { display: none; }
/* A group read down rather than across, for a set too long to be a row. Two of
   them stand side by side in the paint pane — what a stroke lays down, and what
   it lays it through — because the first is going to be longer than the panel
   is tall and the second is three words that would otherwise sit under all that
   empty air.

   The same segmented control, stacked vertically: one frame around the set, and
   the members sharing an edge. Written as the vertical mechanism rather than
   the horizontal one re-derived — the negative margin and the rounding are the
   axis the buttons are stacked on, and on the wrong axis they say nothing. */
.controls .group.tall {
  flex-direction: column;
  align-self: start;
}

/* The materials: a group a hardness, softest first, because which brush gets
   through which material is the rule this pane is about and the order is where
   that gets said without a sentence. Each group's name is in the label column
   beside it, under `material`, which is where every other name here is.

   The filter stands to the right of the lot rather than under it: it is the
   same question asked about the brush, and it is three words against a column
   that grows. */
/* Tighter than a label stands from a field: these name a run of the list they
   are beside rather than labeling a control, so they belong to it. */
.row.materials { row-gap: 0; column-gap: .3rem; }
/* The field column of this row is two columns of its own — the list, and the
   filter beside it — and it takes the row's rows so the names in the label
   column can stand beside the buttons they name. A track of the *pane* would
   have done it too, and would have taken its width off every field in the
   panel; this takes it off nothing but this row. */
.materials .pair {
  grid-column: 2;
  grid-row: 1 / -1;
  display: grid;
  grid-template-rows: subgrid;
  grid-template-columns: auto auto;
  align-items: start;
  /* The field gap is the space between two controls on a line. Down this
     column it would be the space between two buttons of one group, which is
     the one place there is not meant to be any. */
  row-gap: 0;
}
/* One column of buttons, taking the rows the names beside it stand on.

   A grid, so its rows are the row's rows — which is what puts a name beside
   the button it names. The buttons keep every border they have and close the
   seam by the pixel of negative margin the group already gives them: a track
   here is sized by what is in it, so a button an outer pixel shorter is a
   track an outer pixel shorter, and the two edges land on each other. Dropping
   the top border instead would leave whichever button is chosen missing a
   side. */
.materials .pair > .group.tall {
  grid-column: 1;
  grid-row: 2 / -1;
  display: grid;
  grid-template-rows: subgrid;
}


/* A column of things under the word that names them. Its own name, since
   `.stack` is the world's frame. */
.materials .aside {
  grid-column: 2;
  grid-row: 1 / -1;
  margin-left: var(--beside);
  display: grid;
  gap: .1rem;
  align-content: start;
}
/* The name over a group, at a panel's size and dimmed: it says what the buttons
   under it have in common, and the buttons are what is being read. */
.controls .caption {
  font-size: var(--fine);
  line-height: 1.15;
  color: var(--dim);
}
/* The ones in the label column end at the list they name, so the eye runs down
   one edge to the buttons rather than across a gap that changes width with the
   word.

   Each of them starts a run of the list — which is what `starts` says, and
   what a rule above it draws. The first is one too — the list begins at a line the same way each kind inside it does.
   No margin on it, so the rule lands on the grid row's own edge, which is
   where the button beside it starts. */
.materials .starts {
  /* The whole of the label column, so the rule above it runs the width of that
     column and reaches the list. Stretched and then set to the right, rather
     than shrunk to the word: a rule as wide as `soft` is four letters of line,
     which reads as an underline and not as a division. */
  justify-self: stretch;
  text-align: right;
  /* The same ink as the name under it, which is what a rule that belongs to
     the name should be. */
  border-top: 1px solid currentColor;
}
/* On the seam, not a pixel above it: the buttons overlap by their shared
   border, so a name after the first joins them there. */
.materials .starts + .starts { margin-top: -1px; }
.controls .group.tall button {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: .4rem;
  margin-left: 0;
  margin-top: -1px;
  text-align: left;
}
.controls .group.tall button:first-child {
  border-radius: var(--round-tight) var(--round-tight) 0 0;
  margin-top: 0;
}
.controls .group.tall button:last-child {
  border-radius: 0 0 var(--round-tight) var(--round-tight);
}
/* The color the ring draws this material in, so the panel and the world say
   the same thing about it. */
.controls .group.tall .swatch {
  flex: none;
  width: .7rem;
  height: .7rem;
  border-radius: 2px;
  border: 1px solid var(--edge);
}


/* Rows that are one answer between them, under the name of what they answer.
   The heading is a row, so the choice the group depends on sits beside the
   word that names it. What sets the group off from the rows above it is the
   `hr` before it — the same line every section here begins with. */
.set-head h3 { margin: 0; font-weight: 400; }

#tool-panel .fields,
.generation .fields {
  display: flex;
  /* The field's baseline is the row's, so it has to be a text baseline and not
     a synthesized one. A flex box takes its baseline from its first
     baseline-aligned item; a range input has no text to take one from, so it
     opts out and the figure beside it answers instead. */
  align-items: baseline;
  flex-wrap: wrap;
  gap: .3rem;
  min-width: 0;
}
#tool-panel .fields > input[type="range"],
#tool-panel .fields > input[type="checkbox"] { align-self: center; }

/* Separate elements are separated. `.group` is what makes a set of buttons
   read as one control, so everything a row holds outside a group is its own
   thing and gets the space to say so.

   A line inside a line is not a second layout — it is a handful of controls
   that come and go together, so it lays out as though it were not there.

   Its own name, apart from `.row`: a row in a panel is a label column and a
   field; a line in the toolbar is a flex of controls. */
.controls .line { display: flex; align-items: center; gap: .5rem; }
.controls .line .line { display: contents; }
/* As with the toolbar itself: the `display` above outranks the user agent's
   `[hidden] { display: none }`, so a line has to say it. */
.controls .line[hidden] { display: none; }

/* The far end of the toolbar's first row, away from the controls that are
   about the world. `.controls button` says how it looks. */
#toolbar #fullscreen { margin-left: auto; }

/* A name for the field beside it, with the key that does the same thing where
   there is one — set against that field rather than against
   the left edge of the panel. The column is as wide as the longest name in the
   box, so a short one left-set is a word adrift with an inch of nothing after
   it; ranged right, every name ends on the same line and the eye runs down it
   to the controls. The gap to the field is the column gap, which is the one
   place it is written. */
.controls .label { font-size: var(--fine); text-align: right; }

/* The line a section begins with, wherever the section is: the font under the
   words it sets, the simulation under the tile it runs on, the materials under
   whichever mode's own settings are up. One rule, so they are one line. */
#tool-panel hr {
  width: 100%;
  margin: .2rem 0 .1rem;
  border: 0;
  border-top: 1px solid var(--edge);
}

/* A control that belongs to the row above it rather than beside it — the
   shortcuts under a slider. A row of the grid and not a wrap inside the field:
   a group that wraps at some widths and not others is a row whose height
   depends on the words in it, and one that wraps drags the label down with it. */
.below {
  /* Under the control it belongs to, in that column: the label names the row
     and these are part of what the row holds, not a strip beneath it. Wrapping,
     because how many shortcuts there are is a property of the ladder rather
     than of the space there happens to be. */
  grid-column: 2;
  flex-wrap: wrap;
}

/* One choice out of a set, so they sit together and read as one control.
   Nothing here changes its label, so the group's width is fixed by its widest
   member and stays there. */
.controls .group { display: flex; }
.controls .group button {
  min-width: 5.5ch;
  border-radius: 0;
  margin-left: -1px;
  /* Stronger than the edge a lone button is drawn in: a segmented control is
     read as one thing, and the shared frame is what says so. */
  border-color: var(--dim);
}
.controls .group button:first-child { border-radius: var(--round-tight) 0 0 var(--round-tight); margin-left: 0; }
.controls .group button:last-child { border-radius: 0 var(--round-tight) var(--round-tight) 0; }
/* The pressed one is in front, so its border wins over its neighbor's. */
.controls .group button[aria-pressed="true"] { position: relative; z-index: 1; }

.controls .check {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  font-size: var(--fine);
  user-select: none;
}
.controls .check input { margin: 0; accent-color: currentColor; }
.controls .check:has(input:disabled) { opacity: .35; }

/* Which tool is in your hand: an icon and its name, top to bottom, over the
   world's right edge. Always there — at every width and in fullscreen —
   because picking a tool is part of pointing at the world.

   A column of glyphs with no words is a column you have to learn; the word
   makes the icon readable the first time, and it is small because after that
   the icon is what you aim at. */
.picker {
  position: absolute;
  top: var(--inset);
  right: var(--inset);
  z-index: 1;
  display: grid;
  gap: 2px;
  padding: .3rem;
  border: 1px solid var(--edge);
  border-radius: var(--round);
  background: var(--over);
}
.picker button {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 1px;
  width: 100%;
  padding: .32rem .1rem .28rem;
  border: 1px solid transparent;
  border-radius: var(--round-tight);
  /* Under `--fine`: the word under an icon is read once to learn what the icon
     means and never again, and it has a `compact` rail to do it in. */
  font-size: 8.5px;
  letter-spacing: .02em;
  line-height: 1.1;
  opacity: .55;
}
.picker button svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
/* The key that picks the tool, in its corner, at the size of the word under
   the icon and for the same reason. */
.picker button .key {
  position: absolute;
  top: 2px;
  left: 3px;
  font: inherit;
  line-height: 1;
}
/* And the key that picks a paint mode, in its button's corner the same way, a
   size under the word beside it. It fits in the button's own padding, so the
   word does not move for it. */
.controls #paint-modes button { position: relative; }
.controls #paint-modes .key {
  position: absolute;
  top: 1px;
  left: 2px;
  font: inherit;
  font-size: .75em;
  line-height: 1;
}
.picker button:hover { opacity: .85; }
.picker button[aria-selected="true"] {
  opacity: 1;
  color: var(--bright);
  border-color: var(--edge);
  background: var(--fill-over);
}

/* Over the world's top-left corner as the picker is over its top-right: snake
   mode's panel. */
.corner {
  position: absolute;
  top: var(--inset);
  left: var(--inset);
  z-index: 1;
  display: grid;
  justify-items: start;
  gap: var(--inset);
}
/* Drawn the way the picker is: its name over a line of words. */
.snake {
  display: grid;
  gap: 2px;
  padding: .3rem .5rem;
  border: 1px solid var(--edge);
  border-radius: var(--round);
  background: var(--over);
  font-size: var(--fine);
}
.snake[hidden] { display: none; }
/* The box that says whether a change drops the tiles somebody changed, in the
   label column: ending where the names above it end, and centered on the line
   of words beside it, which give the row its baseline since the box has no
   text to. */
.generation input[type="checkbox"] { justify-self: end; align-self: center; margin: 0; }
.generation label.fields { font-size: var(--fine); }
/* The lock over the section's controls, at its head rather than stretched
   across it, and the warning under them while it is open. */
#ground-lock { justify-self: start; }
.generation-warning { margin: 0; font-size: var(--fine); color: var(--warn); }
/* As wide as the largest seed, ten digits, so the button after it never moves. */
.controls #ground-seed { min-width: 10ch; font-variant-numeric: tabular-nums; }

/* What the picked tool offers. **One element with two anchors**, so nothing is
   ever reparented: over the world's right edge on a page with no room for a
   second column, and just outside the board on one that has — see the media
   query, where `100%` of `.board` is the world's column's right edge.

   Measured from `.board` rather than `.world`, because the second position is
   outside the world and the board is what the columns are made of. The world
   is the first thing in `.board`, so their top-right corners are the same.

   As tall as the board, and the track the tool panel moves down as the page
   scrolls. Only the panel takes the pointer: over the world, the rest of the
   track is world, and a press or a wheel there is the world's. */
.tool-column {
  position: absolute;
  top: var(--inset);
  right: var(--inset);
  bottom: var(--inset);
  z-index: 1;
  width: var(--small);
  pointer-events: none;
}
/* The panels in it are `small` and so is the column, from the same value. */

/* Room for the picker beside it, so the two never sit on top of each other —
   derived from the picker rather than measured off it once and left. */
.tool-column { margin-right: calc(var(--compact) + var(--inset)); }

/* Over the world it needs a ground of its own — `.panel` draws a border and no
   fill, which against terrain is a hairline around nothing. It keeps that
   ground in the second column, so the panel does not change because it moved.

   In view however far the page is scrolled, so the card for what a press
   picked is on screen with the press; and never taller than the window, so
   the foot of it is always reachable. */
#tool-panel {
  position: sticky;
  top: var(--inset);
  max-height: min(100%, calc(100vh - 2 * var(--inset)));
  overflow-y: auto;
  pointer-events: auto;
  display: grid;
  align-content: start;
  padding: .45rem;
  background: var(--over);
}

/* What a field is made of takes the column it is given: a slider grows into
   whatever is left beside the figure it sets, a name field beside the two
   buttons that act on it, and a list all of it. */
#tool-panel input[type="range"],
#tool-panel .field { flex: 1; min-width: 3rem; }
#tool-panel .list { width: 100%; }

/* The tuning panel wants the same ground as the boxes it sits with. */
#pane-tuning .tune {
  background: var(--over);
}
.controls .panes > * { align-content: start; }

/* The panes, of which only the one in use is drawn — see `[inert]` below. */
.controls .panes { display: grid; }
/* As with the toolbar and its lines: an author rule that sets `display`
   outranks the user agent's `[hidden] { display: none }`, so this has to say
   it. */
.controls .panes[hidden] { display: none; }
.controls .panes > * { gap: .35rem var(--beside); }

/* The one not in use is not drawn, so the panel is the size of what is in it.

   `inert` is what the controls write, and it is what a pane not in use *is*:
   out of the focus order and out of the accessibility tree. This stops drawing
   it as well. */
.controls .panes > [inert] { display: none; }

/* A field, and a list: what a thing you type into or pick from looks like
   here. Width is what differs, and is set per element. */
.controls .field,
.controls .list,
.sheet .list {
  box-sizing: border-box;
  border: 1px solid var(--edge);
  background: color-mix(in srgb, Canvas 40%, transparent);
}
.controls .field,
.sheet .field {
  padding: .1rem .3rem;
  border-radius: var(--round-tight);
  color: inherit;
  font: inherit;
  font-size: var(--fine);
}
.controls .list,
.sheet .list {
  overflow-y: auto;
  padding: .15rem;
  border-radius: var(--round-tight);
}

/* The link and the address are one control to look at: they share an edge, so
   the row's gap must not get between them. */
#toolbar .address { display: flex; align-items: stretch; }

/* The link, joined to the address it names. What it shows is the state's dot,
   drawn by `#link::before` for every page in the tree — the shape and the
   color are both up there, and this only sizes it. */
#toolbar #link {
  display: inline-flex;
  align-items: center;
  padding: .1rem .35rem;
  border-right: 0;
  /* Joined to the address on its right, so it is square on that side. */
  border-radius: var(--round-tight) 0 0 var(--round-tight);
}
/* The dot is the whole of this indicator, rather than a mark beside a word, so
   it is drawn a little larger than it is on a page where the word is there. */
#toolbar #link::before { width: .62rem; height: .62rem; }
/* The words `fixed` stacks in it are hidden: the dot is what shows, and
   `aria-label` carries the word. */
#toolbar #link .swap { display: none; }

/* The server you are connected to, by name, at a fixed width with an ellipsis
   so a long name does not move the row. It narrows, to a point, when the row
   has too little room. */
#toolbar #server-name {
  flex: 0 1 16ch;
  min-width: 8ch;
  overflow: hidden;
  white-space: nowrap;
  /* Joined to the light on its left, so it is square on that side. */
  border-radius: 0 var(--round-tight) var(--round-tight) 0;
  color: var(--dim);
  text-overflow: ellipsis;
}

/* The mode switch, set well apart from the connection it follows. */
#toolbar #doing { display: flex; align-items: center; gap: .5rem; margin-left: 1.5rem; }

/* The world's name, which is the world's and yours to change. A field rather
   than a readout, and sized rather than grown, so a long name does not move
   the row. It narrows, to a point, when the row has too little room. */
#toolbar #world-name { flex: 0 1 10ch; min-width: 4ch; }

/* A row whose field is a block rather than a line — a list, a picker — which
   is as tall as the rows it shows. There is no first line to sit on, so the
   label names the top of the block instead. Said on the row rather than found
   with `:has`, because it is a property of the row and not an exception to
   one. */
.row.block { align-items: start; }
/* The templates are the build's, so the list is as tall as its rows and that
   never changes under a hand. */
.controls #template-list { width: 9rem; }

/* One row of a list, wherever the list is. */
.list .option {
  padding: .15rem .3rem;
  border-radius: var(--round-tight);
  font-size: var(--fine);
  white-space: nowrap;
  cursor: default;
}
/* Only a row that does something when pressed: a package's does nothing yet,
   and neither does the one a list with nothing in it has. An island's row is
   the disclosure over its tiles, so it is marked current rather than
   selected. */
.list .option[role="option"]:hover, #tree-islands summary:hover { background: var(--fill-over); }
.list .option[aria-selected="true"], #tree-islands summary[aria-current="true"] {
  background: var(--fill-on);
  font-weight: 600;
}
/* A menu in a panel, wherever it stands: the page's own type and edge, sized
   like the buttons beside it so the row is one height. One of these is a
   choice from data rather than a fixed row of controls — what tunes a build
   has, what settings a world has — which is why it is not a group of
   buttons.

   Longhands, not the `font` shorthand: it carries `line-height`, and a select
   left on `normal` centers its text in a box a different height from the line
   the label beside it sits on. */
.controls select {
  font-family: inherit;
  font-size: var(--fine);
  line-height: inherit;
  padding: .15rem .3rem;
  color: inherit;
  background: transparent;
  border: 1px solid var(--edge);
  border-radius: var(--round-tight);
}
/* As wide as the longest name each can be asked to hold. */
.controls #music { max-width: 12rem; }
.controls #pick-island-sim, .controls #tile-sim { max-width: 9rem; }

.controls #island-new-name, .controls #sim-name {
  flex: none;
  width: 7rem;
}

/* The checkpoint arrows: a glyph each, so they want none of the width a word
   needs. The figure beside them is sized for the longest it can show —
   `none yet`, or `N of N` at `MAX_SAVES` — so the row does not move when the
   first checkpoint is marked. */
.controls #rewind, .controls #forward { min-width: 0; padding: .15rem .45rem; }
.controls #checkpoint-at { min-width: 8ch; }

/* The stamp's own field. It sits in a row that must not change width, so it
   is given one rather than grown to fill the row. */
.controls #text-say {
  flex: none;
  width: 9rem;
}

/* The frame every sheet shares: the same border, the same translucency and the
   same small type. Modal, so nothing a sheet does can move the page behind it.
   What differs is width, and `--sheet-width` sets it.

   What is *in* one is its own — the load sheet's foot is a choice to confirm,
   the settings sheet has nothing to confirm and no foot at all. */
.sheet {
  width: var(--sheet-width, 18rem);
  padding: .7rem .75rem .6rem;
  border: 1px solid var(--edge);
  border-radius: var(--round);
  color: inherit;
  background: color-mix(in srgb, Canvas 96%, transparent);
}
.sheet::backdrop { background: rgb(0 0 0 / .5); }

.sheet > h2,
.sheet-head h2 {
  margin: 0;
  font-size: var(--fine);
  font-weight: 600;
  letter-spacing: .06em;
}
/* A title with nothing beside it carries its own space to the list under it;
   one in a head has the head carry it. Two selectors that cannot both match, so
   neither has to outrank the other. */
.sheet > h2 { margin-bottom: .5rem; }

/* One choice out of a list, so the rows are the control and there is no second
   one drawn beside them. A fixed height rather than one that grows with the
   list: a chooser that is a different size for every build is one whose `load`
   button is somewhere else every time. */
#worlds-list { height: 7.5rem; }
#worlds-list:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; }

/* What the button below will do, on its own line above it rather than beside
   it: it is read once, while deciding, and the buttons are what the hand goes
   to. */
.sheet-note {
  margin: .5rem 0 .55rem;
  font-size: var(--fine);
  line-height: 1.4;
  letter-spacing: .04em;
  color: var(--dim);
}

/* A sheet's foot, where it has a choice to confirm: that, and the cancel
   beside it. */
.sheet-foot { display: flex; align-items: center; gap: .35rem; }
.sheet-foot .spacer { flex: 1; }
.sheet-foot button { padding: .18rem .6rem; font-size: var(--fine); opacity: .55; }
.sheet-foot button:disabled { opacity: .3; cursor: default; }
/* The one that does something, against the cancel beside it. */
.sheet-foot .confirm:enabled { opacity: 1; border-color: currentColor; }

/* The settings sheet. Wide enough to read across: the names on the left, what
   the picked one runs on the right, and one column wherever there is not room
   for two. */
#sim-sheet { --sheet-width: min(42rem, 92vw); }
/* A sheet's title with the button that closes it at the far end. */
.sheet-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: .5rem;
}
#sim-sheet .sheet-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  align-items: start;
  gap: .5rem 2.5rem;
}
#sim-sheet .sheet-col { row-gap: .25rem; column-gap: var(--beside); }
/* Fixed, so a world with more settings in it is not a sheet that grew. */
#sim-list { height: 9rem; }
#sim-list:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; }

/* Every button that swaps its label is sized by `.swap` — see ui.js — so the
   only widths written down here are for controls whose *value* changes. */

/* A button in a panel, whichever panel: small, dim until it is the one that is
   on, and then the page's own ink with an edge to match. One pair of states for
   a mode, a tool, and a readout you have switched up. */
.controls button, .switches button { padding: .15rem .5rem; font-size: var(--fine); opacity: .5; }
.controls button[aria-pressed="true"],
.switches button[aria-expanded="true"] { opacity: 1; color: var(--bright); border-color: currentColor; }
/* Dimmed rather than removed, like the tuning panel's: a control that comes
   and goes moves everything beside it. What it says is why it cannot be used. */
.controls button:disabled, .switches button:disabled { opacity: .35; cursor: default; }
.controls button:disabled:hover,
.switches button:disabled:hover { border-color: var(--edge); }

/* The shared `input` rule is for text fields — a slider wants none of it. */
.controls input[type="range"] {
  flex: none;
  width: 5rem;
  padding: 0;
  border: 0;
  background: none;
  accent-color: currentColor;
}
.controls input[type="range"]:disabled { opacity: .35; }

/* A figure, and every one of them is wide enough for the longest it can show
   so that nothing beside it ever moves — see the widths below. */
.controls .value {
  min-width: 2ch;
  text-align: right;
  font-size: var(--fine);
  color: var(--dim);
  white-space: nowrap;
}
.controls #tile-at { color: inherit; }
/* A name is as long as somebody typed it, and a card is as wide as the panel:
   cut with an ellipsis rather than wrapped, so a long name is not a taller
   card. */
.controls #pick-island-at {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.controls #size-value { min-width: 6ch; }   /* `129 px` */
.controls #flow-value { min-width: 4ch; }   /* `100%` */
.controls #period-value { min-width: 12ch; }  /* `1/8 tick (!)` */
.controls #spread-value { min-width: 5ch; }  /* `32 px` */
.controls #text-size-value { min-width: 10ch; }  /* `64 px (×8)` */

/* Which way a tile pulls. Arrows rather than words, so the buttons are the
   width of a glyph instead of the width of `right`. */
.controls #gravity button { min-width: 0; padding: .15rem .4rem; }
.controls #gravity:has(button:disabled) { opacity: .45; }

/* ---- the three sizes a panel comes in ----------------------------------
 *
 * See `--compact` and `--small` at the top for what each one holds. A panel
 * carries the class of the size it is built for, and that is the only place
 * its width is decided — the slots panels sit in read the same two values.
 *
 * `large` is a width and not a maximum: a log is read across, and one that
 * shrank to its longest line would move every time a line arrived.
 *
 * Border-box, because a size is what a panel takes up. A `small` panel with a
 * border and padding measured from its content is wider than the column made
 * for it, and the overflow lands on the world. */
.compact, .small, .large { box-sizing: border-box; }
.compact { width: var(--compact); }
.small { width: var(--small); }
.large { width: 100%; }

/* A bordered box, with no ground of its own. */
.panel {
  border: 1px solid var(--edge);
  border-radius: var(--round);
  padding: .55rem .75rem .65rem;
}

/* A heading in here names the box under it, at a panel's size like everything
   else in one — it is a name and not an announcement, and the bar it sits on
   is what makes it findable.

   What sets it apart from the labels under it is the face: this is `--ui` and
   they are `--mono`, which is the same difference as everywhere else on the
   page. Lowercase, as a switch naming the same thing is. */
.panel > h2, .section > h2 {
  margin: 0;
  font-size: var(--fine);
  line-height: 1.4;
  text-transform: lowercase;
  color: var(--bright);
}
/* A panel is not a grid, so the space under its heading is the heading's own.
   A section has a gap. */
.panel > h2 { margin-bottom: .35rem; }

/* A section's heading is a bar: its name at one end, and `remote` at the other
   when the thing it describes is not on this machine.

   Only `remote`. This machine is where a reader already is, so saying it is
   telling somebody something they cannot be wrong about — and a word on every
   section is a column of the same word, which is a column nobody reads. What
   is worth marking is the exception.

   Both ends are still named in `data-about`, because that is the markup saying
   what each section is about and half of it is not a statement. Whose end it
   is is a property of the section and not of the mode: the frame rate is this
   machine's whether or not there is a server, and how far behind a player is,
   is the far end's whether or not you are that player. */
.section > h2 {
  display: flex;
  justify-content: space-between;
  gap: .5rem;
  /* Out to the section's own edges, which is what makes it a bar across the
     top rather than a word with a line under it. */
  margin: calc(-1 * var(--pad-y)) calc(-1 * var(--pad-x)) 0;
  padding: var(--pad-y) var(--pad-x) .15rem;
  border-bottom: 1px solid var(--edge);
  border-radius: var(--round) var(--round) 0 0;
}

/* And tinted when what is under it is not on this machine.

   A tint and not a hue: the two hues in this panel already mean the two
   directions bytes go, and a third meaning for a color in the same box is a
   color nobody can read. This is the same ink the section is filled with, a
   little more of it — so it reads as "this one is marked" without saying
   anything a reader has to look up, and the word beside it says what. */
.section[data-about="remote"] > h2 {
  background: var(--fill-over);
}
.section[data-about="remote"] > h2::after { content: "remote"; }
.section > h2::after {
  font-weight: 400;
  color: var(--dim);
}

/* The bar that picks the sections, the sections, and the band with the log.
   Reversed in fullscreen, where the panel is pinned to the foot of the display
   — the bar goes to the edge it is anchored to and the log grows up off it. */
#debug { display: flex; flex-direction: column; gap: .4rem; }

/* The last band: the log, which takes what is left, and the tuning panel beside
   it while there is one. Beside and not across, because the tuning panel is a
   column of controls and a full-width one is a row of sliders as long as the
   world. Nothing mounted, no column — `.small` is a width, and an empty box
   that wide is a gap beside the log. */
.foot { display: flex; align-items: stretch; gap: .4rem; min-height: 0; }
.foot #log-panel { flex: 1; min-width: 0; }
#pane-tuning:empty { display: none; }
/* As with the toolbar: the `display` above outranks the user agent's
   `[hidden] { display: none }`, so the backtick does nothing unless the panel
   it puts away says this itself. */
#debug[hidden] { display: none; }
main:fullscreen #debug { flex-direction: column-reverse; }

/* Named the same as the titles they switch on, and otherwise a row of the same
   buttons every panel has — see `.controls button`, whose rules they share. */
.switches { display: flex; gap: .3rem; }
.switches button { text-transform: lowercase; }

/* `small` sections across a `large` panel, wrapping onto another row when the
   world's column runs out. Side by side rather than stacked, because they are
   read together — a stall is a link section and a world section — and stacked
   they are a page of scrolling to see the last. */
.sections {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  /* Each is its own height. Stretching them to the tallest on the row leaves a
     two-row readout in a box drawn for a four-row one. */
  align-items: start;
}

/* Each one a panel of its own: a ground faint enough to read text through and
   the corners taken off it. Blocks of monospace with nothing between them read
   as one long block, and which section a line belongs to is the first thing
   you need from it. A tint of the text color rather than a fixed gray,
   so it follows the light and dark schemes the way everything else here
   does. */
.section {
  /* Named because the title bar reads both back: it bleeds out to the
     section's own edges, so it has to know what they are. */
  --pad-x: .45rem;
  --pad-y: .3rem;
  display: grid;
  gap: .2rem;
  align-content: start;
  padding: var(--pad-y) var(--pad-x);
  border: 1px solid var(--edge-faint);
  border-radius: var(--round);
  background: var(--fill);
}
/* The `display` above outranks the user agent's `[hidden] { display: none }`,
   so a section has to say it or its switch does nothing. */
.section[hidden] { display: none; }

/* A section's readout: a row per subject, and in a row a label then the
 * facts, each its own item.
 *
 * Lists rather than a line with separators in it. A panel this narrow has to
 * wrap somewhere, and text only wraps where the browser finds a space — which
 * is the middle of `18 kB/s` as often as it is between two figures. Items wrap
 * between themselves and nowhere else.
 *
 * Monospace, and the gap is what the separators were: figures under figures
 * across rows, and enough space between two of them that neither reads as part
 * of the other. */
.readout {
  display: grid;
  gap: .1rem;
  font: 400 var(--fine)/1.55 var(--mono);
  color: var(--dim);
}
/* The written row is in the same face as the generated ones, or the label
   beside a URL would be a different size from the labels under it. */
.facts {
  font: 400 var(--fine)/1.55 var(--mono);
  color: var(--dim);
}
/* A row: the label, then the facts. `.facts` is the same row written into the
   page, for the one that holds a control instead of text. */
.readout ul, .facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--beside);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Nothing sticks out. A section is `small` and what goes in it is whatever the
   client had to say, so the guarantee has to be structural rather than a
   promise about content: every box shrinks to the panel, and an item that
   still will not fit breaks inside itself.

   `anywhere` and not `break-word`, which is the same break but leaves the
   longest word as the row's minimum width — the panel would widen to hold it
   and overflow the world instead. */
.readout, .readout ul, .readout li, .facts { min-width: 0; }
.readout li { overflow-wrap: anywhere; }

/* How wide a label column is, and what separates it from what it names. Every
   layout that has one reads both — a readout's rows, a section that stacks its
   values, the editor's fields, a sheet's — so a label is the same width and the
   same distance from its subject wherever one appears. The distance is on
   `:root` because a sheet is a dialog outside every panel, and inherits
   nothing from one.

   A readout's is wide enough for its longest label, `datagram`, and so is the
   row written above the server's, which lines up with it; the editor's is as
   wide as its longest label by the grid's own sizing. `auto` is the default,
   for the labels that are not in a column at all — the toolbar's sit in a row
   beside what they name. */
.readout, .facts { --label: 8ch; }

/* A label column, wherever a row has one: a readout's, the written row that
   holds the address, and the editor's fields.

   In a readout it is found by position — always the first item, where a class
   written per row per frame would only say what the position already does. In
   the editor it is a class, because the markup is written by hand and says so.

   The page's own ink against the dimmed figures beside it, which is what makes
   it the column a row is found by. Color and not weight: it is a whole
   column, and a column in bold is a page with nothing left to emphasize. It
   never shrinks or breaks. */
.readout li:first-child,
.facts > :first-child,
.controls .label {
  flex: none;
  min-width: var(--label);
  color: var(--bright);
  white-space: nowrap;
}

/* A row that names the rows under it rather than carrying facts of its own.
   Reads like the panel's own headings, because that is what it is — a heading
   one level in. Spaced above and not below, so it sits with what it names. */
.readout ul.heading {
  margin-top: .5rem;
  font: 400 var(--fine)/1.4 var(--ui);
  text-transform: lowercase;
  color: var(--bright);
}
/* Its one item is a name and not a label, so it wants no column to sit in. */
.readout ul.heading li { min-width: 0; }
.readout ul.heading:first-child { margin-top: 0; }

/* Both scroll rather than growing.

   The players are as many as have joined and the bindings are as many as the
   game has, so left alone either would decide how tall the whole panel is and
   the readouts beside it would sit in a row as tall as the longest of them.
   Tall enough to be worth opening, short enough that the sections stay a band
   across the bottom of the page.

   The height caps the box and not the panel: the section keeps whatever it
   needs when it is shorter than this. */
#readout-controls .readout {
  max-height: 12lh;
  overflow-y: auto;
}

/* As does the list of players, which is as long as the number of people
   connected. */
#readout-players .readout { max-height: 10lh; overflow-y: auto; }

/* The server's values stack instead of flowing across, at the size where they
   do not fit across.

   Only this section. Its values are three long strings and each is a whole
   answer — what the build calls itself, when it was made, which commit it came
   from — where every other section's items are short figures that read across
   and would be a page of one-item lines if they stacked. Only at `small`,
   because a wider one has the room to flow them.

   `controls` is the other, for the opposite reason and at every size: every
   one of its rows is a single binding, and what it wants from the grid is that
   a long action wraps under itself rather than back under the key.

   A grid and not a column flex, so the label keeps its own place beside the
   first value instead of sitting above all of them. */
#readout-server.small .readout ul,
#readout-controls .readout ul {
  display: grid;
  grid-template-columns: var(--label) 1fr;
  column-gap: var(--beside);
}
#readout-server.small .readout li + li { grid-column: 2; }

/* A graph and the numbers its lines reach, which the frame rate's readout and
   the wire's are both built from. The numbers are a fixed width so that a rate
   going from 60 to 120 cannot narrow the graph beside it. */
.fps, .net { display: flex; align-items: stretch; gap: .5rem; }
.fps .graph, .net .graph { flex: 1; min-width: 0; }
.means {
  flex: none;
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
  font: 400 var(--fine)/1 var(--mono);
  color: var(--dim);
}

/* Spread top to bottom, because each of these is the height of one of the
   graph's lines: a number beside a graph that does not line up with what it
   summarizes is a number you have to work out. Three letters, a space and the
   rate — wide enough for a display fast enough to need four digits. */
.fps .means {
  width: 8ch;
  justify-content: space-between;
}

/* Kept together, and grouped by direction. These do not each mark a height on
   the graph — two of them are readings of the same line — so pushing them to
   the corners of the box would read as three unrelated figures. A word, an
   arrow and a size: `avg ↓ 1.00 GB/s` is the longest it goes. */
.net .means { width: 15ch; }
.net .means .out { margin-top: .2rem; }
/* Which line each number belongs to, since that is what the color says on the
   graph and nothing else here would. */
.net .means .in { color: var(--in); }
.net .means .out { color: var(--out); }
.graph .in { stroke: var(--in); }
.graph .out { stroke: var(--out); }

/* Whatever the row has left, cut with an ellipsis wherever that lands — the
   address is long, the panel is narrow, and where to cut is a question about
   the width rather than about the URL. Left-aligned because a URL is read from
   the front. */
.address {
  flex: 1;
  min-width: 0;
  padding: .1rem .4rem;
  font: 400 var(--fine)/1.4 var(--mono);
  color: var(--dim);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.address:disabled { opacity: .5; cursor: default; }
.address:disabled:hover { border-color: var(--edge); }

/* `GRAPH_WINDOW_SECONDS` wide and three lines tall, stretched to whatever the
   row gives it — which is what `preserveAspectRatio="none"` on the element is for. The
   strokes do not stretch with it: without `non-scaling-stroke` a box ten units
   wide and a hundred tall draws horizontal lines ten times thinner than
   vertical ones.

   Framed, so a line near the floor reads as near the floor rather than as the
   only thing on screen. Square rather than rounded: an SVG paints to its
   viewport, which is the border box and not the rounding inside it, so a line
   that ran into a rounded corner would sit outside the frame. */
.graph {
  height: 3lh;
  box-sizing: border-box;
  border: 1px solid var(--edge);
}
/* The time scale, a mark a second, in the same hairline as the frame it stands
   on — it is what the lines are measured against and not one of them. */
.graph .ticks { stroke: var(--edge); }
.graph path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
/* One point a frame, so this line *is* the jitter rather than a summary that
   would have hidden it — and faint, because at a point a frame across a few
   hundred pixels it is a band, and a band drawn at full strength is
   the only thing you can see. The average over it is the line to read. */
.graph .rate { opacity: .3; }
.graph .average { opacity: .9; }

/* ---- the tuning panel -------------------------------------------------- */

/* Generated from a page's props by tune.js, so nothing here knows what any of
   the rows are. Tight on purpose: it is a workbench, and a value you have to
   scroll to is a value you will not try. */
.tune > h2 { display: flex; align-items: baseline; justify-content: space-between; }
.tune > h2 .actions { display: flex; gap: .35rem; }
.tune > h2 button { padding: .1rem .55rem; font-size: var(--fine); letter-spacing: 0; }
/* Nothing to put back and nothing to write. Dimmed rather than removed: a
   control that comes and goes moves everything beside it. */
.tune > h2 button:disabled { opacity: .35; cursor: default; }
.tune > h2 button:disabled:hover { border-color: var(--edge); }
/* A verdict as a color, not a word: a label that changes changes its width,
   and everything after it moves. */
.tune > h2 button[data-state="ok"] { border-color: var(--ok); color: var(--ok); }
.tune > h2 button[data-state="fail"] { border-color: var(--fail); color: var(--fail); }

/* Everything inside a group is stepped in from the group's own label — rows
   and nested groups alike, so it compounds with depth and the shape of the
   panel is the shape of the tree.
   Indenting the container instead would leave a group's rows flush with its
   own summary, which is what makes a list of them read as siblings of the
   thing they belong to.
   The line runs down the side of the contents rather than the container. Rows
   sit flush against each other, so their borders meet and read as one line. */
.tune .group > .row,
.tune .group > .group {
  margin-left: .3rem;
  padding-left: .6rem;
  border-left: 1px solid var(--edge);
}
.tune .group > summary {
  cursor: pointer;
  color: var(--dim);
  font-size: var(--fine);
  letter-spacing: .06em;
  padding: .1rem 0;
}
.tune .group[data-unsaved] > summary { color: var(--warn); }

/* One row per value: name, the low end, the control, the high end, the number
   it is at, and the unit. Every column but the slider is a fixed width, so
   they line up down the panel and a number growing a digit moves nothing.
   Eight characters for the field, because the longest value any slider here
   can land on is a rate like 0.0015 at six and its own padding costs more than
   one character.
   Columns are assigned explicitly below rather than left to auto-placement: a
   checkbox row has no ends to show, and its control still has to sit in the
   same column as every slider. */
.tune .row {
  display: grid;
  grid-template-columns: 7rem 4.5ch 1fr 5.5ch 8ch 2.6rem;
  align-items: center;
  gap: .4rem;
  font-size: var(--fine);
  padding: .05rem 0;
}

.tune .row .name { grid-column: 1; }
.tune .row .min { grid-column: 2; text-align: right; }
.tune .row .max { grid-column: 4; text-align: left; }
.tune .row input[type="range"] { grid-column: 3; }
.tune .row input[type="checkbox"], .tune .row select { grid-column: 3; }
.tune .row input[type="number"] { grid-column: 5; }
.tune .row .unit { grid-column: 6; }

/* The ends of the range are a scale, not values: present, readable, and not
   competing with the number the row is at. */
.tune .row .min, .tune .row .max {
  color: var(--dim);
  opacity: .55;
  font-size: var(--fine);
  overflow: hidden;
  white-space: nowrap;
}
/* Without this a field's padding and border are added *onto* its 100% width,
   so it laps over the unit beside it. */
.tune .row input, .tune .row select { box-sizing: border-box; }

.tune .row .name { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tune .row[data-unsaved] .name { color: var(--warn); }
.tune .row .unit { color: var(--dim); opacity: .6; }
.tune .row input[type="range"] { width: 100%; padding: 0; border: 0; background: none; accent-color: currentColor; }
.tune .row input[type="number"] {
  width: 100%;
  padding: .05rem .2rem;
  font-size: var(--fine);
  text-align: right;
  font-variant-numeric: tabular-nums;
  /* No spinner. It takes a third of the field, and the slider beside it is a
     better way to nudge a value than two arrows two pixels tall — the arrow
     keys still step it while it has focus. */
  appearance: textfield;
}
.tune .row input[type="number"]::-webkit-outer-spin-button,
.tune .row input[type="number"]::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}
.tune .row input[type="checkbox"] { justify-self: start; }
.tune .row select { padding: .05rem .2rem; font-size: var(--fine); }

/* A readout is the same row without a control, and its value sits where the
   number field would be. */
.tune .row.read .value { grid-column: 5; text-align: right; font-variant-numeric: tabular-nums; }

/* How many values inside a closed group no longer match the file. */
.tune .badge { margin-left: .5rem; color: var(--warn); }

/* A list, semantically, so each line is one statement and the container says
 * what the list is. Nothing about it should look like a bulleted list. */
#log {
  min-height: 1.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--dim);
  white-space: pre-wrap;
}

.fail { color: var(--fail); }
/* What `first-aid.js` puts at the top of a page that has no log to write in. */
pre.fail { margin: 0 0 1rem; white-space: pre-wrap; }
.ok { color: var(--ok); }

/* ---- the landing page and echo ----------------------------------------- */

/* The prototype cards. Scoped to that list rather than every `ul`, because
   `#log` is a list too and does not want to be a grid with a gap. */
#list { list-style: none; padding: 0; margin: 0; display: grid; gap: .75rem; }

/* Not `a.card`: a prototype that is down is rendered as a div, because there is
   nothing at the other end to link to. */
.card {
  display: grid;
  gap: .15rem;
  padding: .9rem 1.1rem;
  text-decoration: none;
  color: inherit;
  border: 1px solid var(--edge);
  border-radius: var(--round);
  transition: border-color .12s;
}
a.card:hover, a.card:focus-visible { border-color: currentColor; }
.card .name { font-weight: 600; }
.card .desc { color: var(--dim); }
/* The UDP port is not decoration — it is what identifies a prototype, and what
   you need to reach one from anything other than this page. */
.card .where { color: var(--dim); font-size: .85em; opacity: .75; }

/* Off, but still listed: that a prototype exists and is down is worth knowing,
   and is different from it never having existed. Dashed rather than merely
   faint, so it reads as unavailable and not as low-priority. */
.card[data-down] { opacity: .6; border-style: dashed; }
.card .note { color: var(--warn); font-size: .85em; margin-top: .15rem; }

.note { color: var(--dim); max-width: 62ch; margin: 0; }

form { display: flex; gap: .5rem; flex-wrap: wrap; }
input, button {
  font: inherit;
  padding: .45rem .6rem;
  color: inherit;
  background: transparent;
  border: 1px solid var(--edge);
  border-radius: var(--round-tight);
}
input { flex: 1 1 24rem; min-width: 0; }
button { padding-inline: 1.1rem; cursor: pointer; }
button:hover { border-color: currentColor; }


/* ---- web/web-music.html ------------------------------------------------------ */

.web-music { display: grid; gap: .5rem; }
.web-music .row { display: flex; align-items: center; gap: .5rem; }
.web-music .spacer { flex: 1; }
.web-music .dim { color: var(--dim); }
.web-music button:disabled { opacity: .35; cursor: default; }
.web-music button:disabled:hover { border-color: var(--edge); }
.web-music .check { display: flex; align-items: center; gap: .35rem; color: var(--dim); }
.web-music .check input { margin: 0; accent-color: currentColor; }

/* A line that is usually blank: the reason a control could not do what it says.
   It holds its height whether or not there is anything in it, because a panel
   that grows a line when something fails moves every panel below it. */
.web-music .said { margin: 0; min-height: 1lh; color: var(--warn); }

.web-music .rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .1rem;
  font-variant-numeric: tabular-nums;
}

/* The window on what is arriving. A fixed height rather than one that follows
   the list, so a keyboard sending three messages a second does not resize the
   page around itself. */
.web-music .rows.tall {
  height: 13lh;
  overflow-y: auto;
  white-space: pre;
  font-family: var(--mono);
  font-size: .9em;
}

.web-music .rows .empty, .web-music tbody .empty td { color: var(--dim); }
.web-music .rows li[data-gone] { color: var(--dim); text-decoration: line-through; }

.web-music table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.web-music th {
  text-align: right;
  font-weight: 400;
  font-size: var(--fine);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
  border-bottom: 1px solid var(--edge);
  padding: .1rem .4rem;
}
.web-music td { text-align: right; padding: .1rem .4rem; }
/* The note is what a row is *about*; the three numbers are what it measures. */
.web-music th:nth-child(2), .web-music td:nth-child(2) { text-align: left; }
.web-music tbody .empty td { text-align: left; }

/* The file button. A bare file input is a control whose width depends on the
   name of whatever was last chosen, so the input is hidden and its label is
   the button — which is what a label is for. */
.web-music .file {
  padding: .45rem 1.1rem;
  border: 1px solid var(--edge);
  border-radius: var(--round-tight);
  cursor: pointer;
}
.web-music .file:hover { border-color: currentColor; }
.web-music input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.web-music input[type="text"] { flex: 0 1 12rem; padding: .45rem .6rem; }

/* A world in the chooser is one of two kinds, and the row says which in its own
   narrow column: a sentence in one row would be the end of reading the list
   down. */
#worlds-list .option { display: flex; align-items: baseline; gap: .5rem; }
#worlds-list .option .who { flex: 1; }
#worlds-list .option .kind { color: var(--dim); font-size: .85em; }
#worlds-list .option.empty { color: var(--dim); display: block; }
