/*
 * One page, one stylesheet.
 *
 * Sizes, dims and gaps are three scales: pick a step, never write a fresh
 * number. The one rule that carries meaning is the face -- a machine-produced
 * fact is set in the mono one, and most of this page is machine-produced facts.
 *
 * This file is downloaded by whoever opens the page, comments included.
 */

:root {
  --ink: #17191c;
  --ink-quiet: #4a4f57;
  --ink-faint: #767c86;
  --page: #f7f8fa;
  --card: #ffffff;
  --edge: #dfe3e8;
  --accent: #1a5fb4;
  --bad: #b3261e;
  --good: #1c7c48;
  --waiting: #8a5a00;

  --text-caption: 0.75rem;
  --text-small: 0.85rem;
  --text-body: 0.95rem;
  --text-lead: 1.1rem;
  --text-title: 1.5rem;

  /* One height for everything on a form row. Saying it once is what stops a
     button drifting away from the fields beside it. */
  --control: 34px;

  --gap-xs: 4px;
  --gap-s: 8px;
  --gap-m: 12px;
  --gap-l: 20px;
  --gap-xl: 32px;

  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  padding: var(--gap-xl) var(--gap-l);
  background: var(--page);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-body);
  line-height: 1.5;
}

main, .masthead { max-width: 1400px; margin: 0 auto; }
.masthead { padding: 0 calc(var(--gap-l) + 1px); }
.masthead h1 { font-size: var(--text-title); margin: 0 0 var(--gap-xs); font-weight: 600; }
.lede { margin: 0 0 var(--gap-l); color: var(--ink-quiet); max-width: 70ch; }

.panel {
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  padding: var(--gap-l);
  margin-bottom: var(--gap-l);
}

.field { display: block; margin-bottom: var(--gap-m); }
.label { display: block; font-size: var(--text-small); color: var(--ink-quiet); margin-bottom: var(--gap-xs); }
.hint { color: var(--ink-faint); font-weight: 400; }

textarea, input[type=text], input[type=password], input[type=number], select {
  width: 100%;
  font-family: var(--font-mono);
  font-size: var(--text-small);
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 6px;
  padding: var(--gap-s) var(--gap-m);
}
textarea { resize: vertical; display: block; }
input[type=text], input[type=password], input[type=number], select, button { height: var(--control); }
select { font-family: var(--font); cursor: pointer; }

textarea:focus-visible, input:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.row { display: flex; gap: var(--gap-m); align-items: flex-end; flex-wrap: wrap; }
.row .field { margin-bottom: 0; }
.narrow { width: 160px; }
.grow { flex: 1 1 240px; }

/* A checkbox is not a text field and must not be stretched to look like one:
   the box keeps its own size and the word sits beside it, on the same baseline
   as the labels of the fields it shares a row with. */
.tick { display: flex; align-items: center; gap: var(--gap-s); height: var(--control); }
.tick input { width: auto; height: auto; }
.tick span { font-size: var(--text-small); color: var(--ink-quiet); }

button {
  font: inherit;
  font-size: var(--text-small);
  padding: 0 var(--gap-l);
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}
button:hover { filter: brightness(1.08); }
button[disabled] { opacity: 0.6; cursor: default; }
button[disabled]:hover { filter: none; }
button.quiet {
  background: var(--card);
  color: var(--accent);
  border-color: var(--edge);
}
button.quiet:hover { filter: none; border-color: var(--accent); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.privacy { margin: 0; font-size: var(--text-caption); color: var(--ink-faint); max-width: 60ch; }
.detail { margin: calc(-1 * var(--gap-s)) 0 var(--gap-l); font-size: var(--text-small); color: var(--ink-quiet); }

/* Not a refusal: nothing went wrong, the watching simply stopped. Amber, the
   colour this page already uses for waiting, rather than the red of a fault. */
.ended {
  display: flex;
  align-items: center;
  gap: var(--gap-m);
  flex-wrap: wrap;
  background: var(--card);
  border: 1px solid var(--waiting);
  border-left-width: 4px;
  border-radius: 6px;
  padding: var(--gap-s) var(--gap-l);
  margin-bottom: var(--gap-l);
  color: var(--waiting);
  font-size: var(--text-small);
}

/* The same box as a refusal, in the ordinary ink: something happened and it is
   worth saying, and nothing went wrong. */
.note {
  background: var(--card);
  border: 1px solid var(--edge);
  border-left: 4px solid var(--accent);
  border-radius: 6px;
  padding: var(--gap-m) var(--gap-l);
  margin-bottom: var(--gap-l);
  color: var(--ink-quiet);
}

.refusal {
  background: var(--card);
  border: 1px solid var(--bad);
  border-left-width: 4px;
  border-radius: 6px;
  padding: var(--gap-m) var(--gap-l);
  margin-bottom: var(--gap-l);
  color: var(--bad);
}

/* ---------------------------------------------------------------- the live */

.summary {
  display: flex;
  gap: var(--gap-l);
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: var(--gap-l);
  padding: var(--gap-m) var(--gap-l);
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
}
.exports { margin-left: auto; display: flex; gap: var(--gap-s); align-items: center; }

.fact { display: flex; flex-direction: column; }
.fact .name { font-size: var(--text-caption); color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.04em; }
.fact .value { font-family: var(--font-mono); font-size: var(--text-lead); }

/* The connection's own word, coloured. Three states and three colours: it is
   working, it is trying, it is not. */
.value.is-connected { color: var(--good); }
.value.is-failed, .value.is-closed { color: var(--bad); }
.value.is-connecting { color: var(--waiting); }

.readout { padding: var(--gap-m) var(--gap-l); }
.booking { margin: 0; font-size: var(--text-small); color: var(--ink-quiet); }
/* The state that surprises people, so it is the one that is coloured: a gateway
   is asleep, and a readout asked for is a readout waiting rather than one
   running. */
.booking.is-waiting { color: var(--waiting); }
.booking.is-running { color: var(--good); }

.heading { font-size: var(--text-lead); font-weight: 600; margin: var(--gap-xl) 0 var(--gap-m); }

.meters {
  width: 100%;
  border-collapse: collapse;
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  font-size: var(--text-small);
}
.meters th, .meters td {
  text-align: left;
  padding: var(--gap-s) var(--gap-m);
  border-bottom: 1px solid var(--edge);
  vertical-align: top;
}
.meters th {
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  font-weight: 600;
  white-space: nowrap;
}
.meters tbody tr:last-child td { border-bottom: none; }
.meters .right { text-align: right; }
.meters td.serial, .meters td.gateway, .meters td.mono, .meters td.right { font-family: var(--font-mono); }

/* A meter that is transmitting and cannot be read is the row somebody has to
   act on, so it says what to fetch rather than showing an empty column. */
.want-key { color: var(--waiting); }
.damaged { color: var(--bad); }
.quiet-cell { color: var(--ink-faint); }

.reading { display: block; white-space: nowrap; }
.reading .what { color: var(--ink-quiet); }
.reading .amount { font-family: var(--font-mono); }
/* What the column is not showing. Quiet, because it is a caveat rather than a
   reading, and it must not be mistaken for one. */
.reading.more { color: var(--ink-faint); font-size: var(--text-caption); }

/* The gateway's own messages, four columns of them.
 *
 * The box scrolls rather than the page, and the head stays put while it does:
 * a payload is the widest thing on this page and the column it belongs to is
 * the one a reader loses first.
 */
.log {
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  font-size: var(--text-small);
  max-height: 22rem;
  overflow: auto;
}
.log table { width: 100%; border-collapse: collapse; }
.log th, .log td {
  text-align: left;
  padding: var(--gap-xs) var(--gap-m);
  border-bottom: 1px solid var(--edge);
  vertical-align: top;
}
.log tr:last-child td { border-bottom: none; }
.log th {
  position: sticky;
  top: 0;
  /* Opaque, or the rows show through it as they pass underneath. */
  background: var(--card);
  /* **And an explicit one, or being opaque is not enough.** A sticky cell is a
     positioned box, but under `border-collapse: collapse` the browser paints
     cells in the table's own order and the scrolling rows came out on top: the
     header had text bleeding through it, found by looking closely at a log long
     enough to scroll. */
  z-index: 1;
  /* The rule under the head, as a shadow rather than a border: a collapsed
     border belongs to the table rather than to the cell, so it stays behind
     while the cell travels. */
  box-shadow: inset 0 -1px 0 var(--edge);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  font-weight: 600;
  white-space: nowrap;
}
.log td.gw, .log td.at, .log td.topic {
  font-family: var(--font-mono);
  color: var(--ink-faint);
  white-space: nowrap;
}
/* The payload takes the width that is left and wraps: a repeater table is long,
   and a line that scrolled sideways would hide the end of the one thing the row
   carries. */
.log td.payload { font-family: var(--font-mono); word-break: break-all; width: 100%; }

.empty { color: var(--ink-faint); font-size: var(--text-small); margin: var(--gap-s) 0 0; }

/* The per-row button. Quiet and narrow: it is an errand from a row, not the
   point of the row, and eight of them down a table must not shout. */
.meters td.explore { text-align: right; white-space: nowrap; }
.meters button.explore {
  height: auto;
  padding: 2px var(--gap-s);
  font-size: var(--text-caption);
  background: var(--card);
  color: var(--accent);
  border-color: var(--edge);
}
.meters button.explore:hover { filter: none; border-color: var(--accent); }

/* `[hidden]` is only a `display: none` in the browser's own sheet, so
   `.field { display: block }` cancels it silently and the link box is drawn on
   an untouched page. The frame bench was caught by exactly this. */
[hidden] { display: none !important; }

/* ------------------------------------------------------------- narrowing */

/* One filter to a row: a label in the first column and its control in the
 * second, so the labels line up against the controls however wide the chips
 * grow. The medium list is nine entries on a real gateway, and side by side
 * they pushed every other filter off to the right.
 */
.sift {
  display: grid;
  /* The label, its control, and the one button that belongs to all of them. */
  grid-template-columns: max-content 1fr max-content;
  gap: var(--gap-s) var(--gap-m);
  align-items: baseline;
  margin-bottom: var(--gap-m);
}
/* The two cells of a row that comes and goes, sitting in the grid's own columns
   rather than in a box of their own. `[hidden]`'s `display: none` wins over
   this, which is what hides the pair as a unit. */
.sift-row { display: contents; }
/* **Every cell says which column it is in.** With only two columns the grid
   filled them in order and a pair landed on a row of its own; the third column
   for the button broke that, and the labels and chips began flowing three to a
   row -- `Maker:` ended up beside the medium chips. Found by looking at it. */
.sift-label {
  grid-column: 1;
  text-align: right;
  font-size: var(--text-small);
  color: var(--ink-quiet);
  white-space: nowrap;
}
/* A name, not a paragraph: as wide as the number it holds and no wider.
 *
 * `.sift input` rather than `.sift-serial`: `input[type=text]` above is a class
 * *and* an attribute, so it outranks a bare class and the box stretched the
 * whole row. Found by looking at it. */
.sift input[type=text] { grid-column: 2; width: 14ch; }

/* Top right of the block, level with the first filter. */
#sift-clear { grid-column: 3; grid-row: 1; justify-self: end; align-self: center; }
/* **Invisible rather than gone**, so the column keeps its width and the chips
   beside it do not shift the moment the first filter is chosen. `visibility`
   also takes it out of the tab order, which `opacity` would not. */
#sift-clear.idle { visibility: hidden; }

.chips { grid-column: 2; display: flex; gap: var(--gap-xs); flex-wrap: wrap; align-items: center; }

/* A chip is a toggle, so it says which it is by looking pressed rather than by
   a tick beside it: there is no room for one, and nine ticks read as a list. */
.chip {
  font: inherit;
  font-size: var(--text-caption);
  font-family: var(--font-mono);
  height: auto;
  padding: 2px var(--gap-s);
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink-quiet);
  cursor: pointer;
}
.chip:hover { filter: none; border-color: var(--accent); }
.chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A table of eight columns does not fit a phone, and a page that scrolls
   sideways as a whole is worse than one whose table does. */
@media (max-width: 900px) {
  body { padding: var(--gap-l) var(--gap-m); }
  .meters { display: block; overflow-x: auto; white-space: nowrap; }
  .exports { margin-left: 0; }
}
