/*
 * 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; }

/* The link to the manual, in the title's row. Hidden until the manual
   answers, so the rule that shows it must not undo `hidden`.

   It shares a box with the account, and the box centres them. As a sibling of
   the account it sat at the top of the row, a few pixels above the name and
   the buttons beside it. The size is the account's own. */
.corner { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: var(--gap-s) var(--gap-m); }
.manual { white-space: nowrap; font-size: var(--text-small); }
.manual[hidden] { display: none; }
.manual { color: var(--accent); text-decoration: none; }
.manual:hover { text-decoration: underline; }
.lede { margin: 0 0 var(--gap-l); color: var(--ink-quiet); max-width: 70ch; }
/* The masthead's one row: the title and the lede on the left, who you are on
   the right. `.row` ends its children at the bottom, which would drop the
   account block to the foot of a two-line title. */
.row.top { align-items: flex-start; }
/* **On a phone it goes under the title instead.** A name, a role and two
   buttons do not fit beside a title at 375px: a flex item shrinks before it
   wraps, so without this the row kept them all on one line and each button's
   label wrapped inside a box of one height, which is a label spilling out of
   its button. Giving the title the whole line is what makes the row wrap.
   The wired bench met the same thing on the same day. */
@media (max-width: 700px) {
  .masthead .grow { flex: 1 1 100%; }
  /* The space under goes on the corner, not on the account: `.corner`
     centres its items, so a margin on the account alone lifted Sign in
     half a margin above Help (seen on a phone, 2026-10-15). */
  .account { flex-wrap: wrap; }
  .corner { margin-bottom: var(--gap-m); }
}

/* **The account as one button and a menu, on a phone** (asked for on
   2026-10-04). The rule above gives a visitor's two words a line of their
   own, which is right for a visitor. Somebody signed in had four lines there:
   Help, the name and role, and two buttons that wrapped. Now the title and
   one button share a line, and the rest opens under the button.

   `.corner-items` wraps Help and the account. On a wide screen it has no box
   of its own, so the row is laid out as it was. */
.corner-items { display: contents; }
.account-open { display: none; }
@media (max-width: 700px) {
  /* The title lines up with the words of the tabs under it. */
  .masthead { padding: 0 var(--gap-m); }
  .masthead.signed-in .row.top { flex-wrap: nowrap; align-items: center; margin-bottom: var(--gap-s); }
  .masthead.signed-in .grow { flex: 1 1 0; min-width: 0; }
  .masthead.signed-in h1 { margin: 0; }
  /* The sentence under the title is for somebody who has not seen the page. */
  .masthead.signed-in .lede { display: none; }
  /* Signed in, the corner shares its line with the title, so the space
     under it that a visitor's corner has would lift it off the title's
     centre. */
  .corner.has-menu { position: relative; flex-wrap: nowrap; margin-bottom: 0; }
  .corner.has-menu .account-open { display: inline-flex; align-items: center; gap: var(--gap-s); white-space: nowrap; }
  .corner.has-menu .corner-items {
    display: none;
    position: absolute;
    right: 0;
    top: calc(100% + var(--gap-xs));
    z-index: 20;
    flex-direction: column;
    align-items: stretch;
    min-width: 13rem;
    padding: var(--gap-xs);
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(23, 25, 28, 0.14);
  }
  .corner.has-menu.is-open .corner-items { display: flex; }
  /* The account's own box goes, so its parts and Help are ordered as one list. */
  .corner.has-menu .account { display: contents; }
  .corner.has-menu .who {
    order: 1;
    margin: 0 0 var(--gap-xs);
    padding: var(--gap-s) var(--gap-m);
    border-bottom: 1px solid var(--edge);
  }
  .corner.has-menu #my-password-open { order: 2; }
  .corner.has-menu .manual { order: 3; }
  .corner.has-menu #sign-out { order: 4; }
  .corner.has-menu .corner-items button,
  .corner.has-menu .corner-items .manual {
    display: block;
    width: 100%;
    height: auto;
    padding: var(--gap-s) var(--gap-m);
    border: none;
    border-radius: 6px;
    background: none;
    color: var(--accent);
    font-size: var(--text-body);
    text-align: left;
  }
}

.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; }

/* **`email` is in the list because it was left out of it once**, and the
   symptom was not obvious: an unlisted type keeps a browser's own intrinsic
   width, so the field came out about 120px wide inside a row that had given it
   half the panel, and the label above it sat over nothing. Found by looking at
   the invitation form (2026-09-19). */
textarea, input[type=text], input[type=email], input[type=password], input[type=number], input[type=date], input[type=time], 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; }
/* **`button` stays in this list.** Splitting it out to a `min-height` looked
   tidier and grew every row that has a small button in it: three variants below
   say `height: auto` to escape this rule, and a minimum they could not escape
   made the Explore button on each meter 34px again. Measured on 2026-09-19,
   where the Meters picture went from 876px tall to 936. */
input[type=text], input[type=email], input[type=password], input[type=number], input[type=date], input[type=time], 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; }

/* A field with its usual value a click away, inside the box on the right. The
   input keeps room for the button, so a long value does not run under it. */
/* As wide as the box it holds, so the button is inside it on a field that is
   narrower than the dialog -- the idle timeout is 10rem, and the button sat
   beside the box rather than in it until this was measured. */
.with-default { position: relative; display: inline-block; max-width: 100%; }
.narrow-field .with-default { width: 10rem; }
.with-default input { padding-right: 34px; width: 100%; }
/* The icon alone: no box of its own, because the box it sits in is the
   field's. It is a control all the same, so it takes the pointer and the
   focus ring like any other. */
button.in-field {
  position: absolute;
  right: var(--gap-xs);
  top: 50%;
  transform: translateY(-50%);
  height: auto;
  padding: 2px var(--gap-s);
  font-size: var(--text-caption);
}
button.icon-only {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  border: none;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
}
button.icon-only:hover { color: var(--ink); }
/* Copies the serial number it is beside. Close to the number, so the two read
   as one thing; the tick after a copy is the colour of a good answer.
   **Its line sees the drawing alone**: the padding is the area that takes a
   press, and the negative margin takes the same amount back. A box taller than
   the drawing made its line taller, and the number in a table row then sat 3px
   lower than the cells beside it. */
button.copy { padding: 6px 4px; margin: -6px -2px -6px 2px; vertical-align: -3px; }
button.copy.copied, button.copy.copied:hover { color: var(--good); }
/* On a card the head's own gap is wider than that, so take most of it back. */
.gateway-head button.copy { margin-left: calc(var(--gap-xs) - var(--gap-m)); }
/* The one control on a card that takes something away. */
button.bin { color: var(--bad); margin-left: var(--gap-xs); vertical-align: middle; }
button.bin:hover { color: var(--bad); opacity: 0.8; }
/* **A heading's line sees less than the drawing**, as the copy button's does:
   the negative margin takes back the padding and more. The drawing is taller
   than the heading's letters, so it made the Status heading taller than the
   three beside it, and the rows of that part sat lower. */
.gateway-part h3 button.bin, .gateway-repeaters h3 button.bin { height: auto; padding: 4px 2px; margin-top: -8px; margin-bottom: -8px; }
.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); }
/* A button that is an errand from a table row, not an action of the page:
   Change, Delete, Open it, Reset the password. **The wired bench's rule, word
   for word** (MBUS_WIRED_TOOL_DESIGN.md), so the two benches' tables look the
   same -- and every one of these buttons has said `small` since it was
   written, with nothing here to make it so. They came out full height in rows
   half again as tall as their text (2026-09-20). */
button.small { height: 28px; padding: 0 var(--gap-m); }
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; }
/* A quiet line of explanation.
 *
 * **The pull up belongs to a heading, not to the class.** It was on `.detail`
 * itself, and only two of the five on this page follow a heading: the other
 * three follow a row of fields or a bar of buttons, where -8px lifts the
 * sentence *into* what is above it. Reported on 2026-09-19 against the
 * sign-in panel, where "Nobody has an account on this bench yet" crossed the
 * Email box's bottom border; measured at 8px of overlap before and 0 after.
 *
 * `:is()` and not `:where()`, because this is a preference over the default
 * and has to win the tie rather than lose it on source order. */
.detail { margin: var(--gap-m) 0 var(--gap-l); font-size: var(--text-small); color: var(--ink-quiet); }
:is(h1, h2, h3) + .detail { margin-top: calc(-1 * var(--gap-s)); }
/* First in a panel: the panel's own padding is the space, and a margin on top
   of it reads as a gap nobody put there. */
.detail:first-child { margin-top: 0; }

/* 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;
}
/* The four buttons for the whole session. They wrap rather than run off the
   side: on a phone they do not fit one line, and a page that scrolls sideways
   as a whole is worse than one whose table does -- the same rule the Meters
   table already keeps below. Measured at 375px, where the fourth button ended
   47px past the edge. */
.exports { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--gap-s); align-items: center; }

/* A button that opens a short list of links under it, such as Export. The
   wired bench's rule, word for word, so the two menus look the same. */
.menu { position: relative; }
.menu summary {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-s);
  font-size: var(--text-small);
  padding: 0 var(--gap-l);
  height: var(--control);
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: var(--card);
  color: var(--accent);
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.menu summary::-webkit-details-marker { display: none; }
.menu summary:hover, .menu[open] summary { border-color: var(--accent); }
.menu summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.menu .caret { font-size: var(--text-caption); }
.menu-items {
  position: absolute;
  right: 0;
  top: calc(100% + var(--gap-xs));
  z-index: 10;
  min-width: 18rem;
  display: flex;
  flex-direction: column;
  padding: var(--gap-xs) 0;
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
}
.menu-items a {
  display: flex;
  flex-direction: column;
  padding: var(--gap-s) var(--gap-m);
  color: var(--ink);
  text-decoration: none;
  font-size: var(--text-small);
}
.menu-items a .hint { font-size: var(--text-caption); }
.menu-items a:hover, .menu-items a:focus-visible { background: var(--page); outline: none; }

.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); }

/* How many listed meters have been heard: a bar and the count beside it. */
.listed { display: flex; align-items: center; gap: var(--gap-m); padding: var(--gap-m) var(--gap-l); }
.listed progress { flex: 1; height: var(--gap-s); accent-color: var(--good); }
.listed span { font-size: var(--text-small); white-space: nowrap; }
.meters td .badge { white-space: nowrap; }

/* The signal map: listed meters against gateways. One blue, light to dark
   for weak to strong, five steps of 10 dB. The lightest step still stands
   out from the card (2:1), and the text is ink on the two light steps and
   white on the three dark ones. */
.signal-scroll { overflow-x: auto; }
.signal-map { border-collapse: separate; border-spacing: 2px; font-family: var(--font-mono); font-size: var(--text-small); }
.signal-map th { font-weight: 600; color: var(--ink-quiet); padding: var(--gap-xs) var(--gap-s); text-align: center; white-space: nowrap; }
.signal-map th.serial { text-align: left; position: sticky; left: 0; background: var(--page); }
.signal-map td { min-width: 4.5rem; padding: var(--gap-xs) var(--gap-s); text-align: center; border-radius: 4px; white-space: nowrap; cursor: default; }
.signal-map td[title] { cursor: help; }
:is(.signal-map td, .swatch).is-none { background: var(--card); color: var(--ink-faint); box-shadow: inset 0 0 0 1px var(--edge); }
:is(.signal-map td, .swatch).is-s1 { background: #86b6ef; color: var(--ink); }
:is(.signal-map td, .swatch).is-s2 { background: #3987e5; color: var(--ink); }
:is(.signal-map td, .swatch).is-s3 { background: #256abf; color: #ffffff; }
:is(.signal-map td, .swatch).is-s4 { background: #184f95; color: #ffffff; }
:is(.signal-map td, .swatch).is-s5 { background: #0d366b; color: #ffffff; }
/* A repeater carried the meter's telegrams. Beside the number, or alone where
   nothing hears the meter itself. In the signal map only while there are no
   repeater columns to say it instead; in the Meters table always. */
.via { font-size: var(--text-caption); font-weight: 700; margin-left: var(--gap-xs); }
/* Alone in its cell, it stands where the number would. A text node is not a
   child for :only-child, so the class is set where the cell is written. */
.via.alone { margin-left: 0; color: var(--ink-quiet); }
.meters td .via { color: var(--ink-quiet); }
/* The number, its unit and the R stay on one line: the column is narrow, and
   a value broken over two lines reads as two values. */
.meters td.signal { white-space: nowrap; }
/* The R in the scale under the map, which is a letter rather than a colour. */
.via-key { font-family: var(--font-mono); font-size: var(--text-caption); color: var(--ink-quiet); }
.signal-map td.blank { background: none; }
.signal-map tfoot th, .signal-map tfoot td { border-top: 1px solid var(--edge); }
/* The redundancy score above the map, and the column of how many gateways
   deliver each meter. Amber for one gateway, red for none. */
.redundancy { margin-bottom: var(--gap-m); }
.redundancy p { margin: 0 0 var(--gap-xs); }
.redundancy-score { font-weight: 600; }
.redundancy-detail { font-size: var(--text-small); color: var(--ink-quiet); }
.signal-map td.delivered { background: none; font-weight: 600; }
.signal-map td.delivered.is-single { color: var(--waiting); }
.signal-map td.delivered.is-lost { color: var(--bad); }
.signal-scale { display: flex; flex-wrap: wrap; gap: var(--gap-m); margin-top: var(--gap-m); font-size: var(--text-caption); color: var(--ink-quiet); }
.signal-step { display: inline-flex; align-items: center; gap: var(--gap-xs); }
.swatch { display: inline-block; width: 14px; height: 14px; border-radius: 3px; }

/* A meter not in the list of meters to expect: a white ! in a red circle,
   beside its serial. The M-Bus Explorer's mark for a refused reading, and
   drawn the same way. The ! is a mask, not a typed character, because a typed
   one stands on the baseline and sits high by an amount that changes with the
   font. The text stays in the element for the tests, at size zero; the
   aria-label is what is read. `overflow: hidden` puts the box's baseline at
   its bottom edge, so 2 px below the text's baseline centres the circle on the
   digits. */
.unexpected {
  margin-left: var(--gap-s);
  display: inline-block;
  overflow: hidden;
  vertical-align: -2px;
  width: 14px;
  height: 14px;
  font-size: 0;
  border-radius: 50%;
  background: var(--bad);
  cursor: help;
}
.unexpected::after {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  background: var(--card);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Crect x='6' y='2.85' width='2' height='5.3' rx='1'/%3E%3Ccircle cx='7' cy='10.05' r='1.1'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}

/* 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); }
/* The firmware button of a session that watches a whole broker is not a
   readout's, so it stands apart at the right end of the row (2026-10-16). */
.readout .row > .at-end { order: 1; margin-left: auto; }
.readout-options { gap: var(--gap-l); flex-wrap: wrap; }
/* The rest of the readout options, folded under one line. */
.readout-more { margin-top: var(--gap-s); }
.readout-more > summary { cursor: pointer; font-size: var(--text-small); color: var(--accent); width: max-content; }
.readout-more > summary .hint { color: var(--ink-faint); margin-left: var(--gap-xs); }
.readout-more-body { display: flex; flex-wrap: wrap; gap: var(--gap-m) var(--gap-xl); align-items: flex-start; margin-top: var(--gap-s); }
/* The six lists of the readout options: the radio mode and the five options
   with three values. Two rows of three where there is room, then three rows
   of two, then one column on a phone. */
.readout-flags { display: grid; grid-template-columns: repeat(3, minmax(0, 15rem)); gap: var(--gap-s) var(--gap-l); }
.readout-flags .field { margin-bottom: 0; }
@media (max-width: 900px) { .readout-flags { grid-template-columns: repeat(2, minmax(0, 15rem)); } }
@media (max-width: 640px) { .readout-flags { grid-template-columns: minmax(0, 15rem); } }
.kinds .tick { height: auto; padding: 2px 0; }
.kinds { border: 0; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, max-content); column-gap: var(--gap-l); }
.kinds legend { font-size: var(--text-small); color: var(--ink-quiet); padding: 0; margin-bottom: var(--gap-xs); }
.readout-more .tick:has(input:disabled) span { color: var(--ink-faint); }
.readout-options .tick:has(input:disabled) span { color: var(--ink-faint); }
.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); }

/* A section that folds. The heading is the control, with a triangle before it
   that points down when the section is open and right when it is closed. The
   heading's own margin moves to the summary, so the space above a closed
   section is the same as above an open one. */
.section > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--gap-s);
  width: max-content;
  margin: var(--gap-xl) 0 var(--gap-m);
  cursor: pointer;
}
.section > summary::-webkit-details-marker { display: none; }
.section > summary::before {
  content: '';
  border-style: solid;
  border-width: 5px 0 5px 7px;
  border-color: transparent transparent transparent var(--ink-faint);
  transition: transform 0.1s;
}
.section[open] > summary::before { transform: rotate(90deg); }
.section > summary:hover::before { border-left-color: var(--accent); }
.section > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.section > summary .heading { margin: 0; }
/* Notify, after the Gateways heading and its ?. */
.section > summary #notify { margin-left: var(--gap-m); }
.section:not([open]) > summary { margin-bottom: 0; }
/* The ? after a section's heading. What the section shows is its tooltip. */
.hint-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.25rem; height: 1.25rem; border: 1px solid var(--ink-faint); border-radius: 50%;
  font-size: var(--text-small); color: var(--ink-faint); cursor: help;
}
.hint-mark:hover, .hint-mark:focus-visible { color: var(--accent); border-color: var(--accent); outline: none; }

/* ----------------------------------------------------------- gateway cards */

/* One card to a gateway, as wide as the column: its five parts sit side by
   side on a wide screen and stack on a narrow one. */
.gateways { display: grid; gap: var(--gap-m); }

.gateway-card {
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  padding: var(--gap-m) var(--gap-l);
  /* **A grid item is as wide as its widest content unless told otherwise.**
     The repeater table is wider than a phone, and its own box scrolls -- but
     without this the card grew to fit it and took the whole page sideways with
     it. Measured on a 375px screen with one repeater. */
  min-width: 0;
}
.gateway-head { display: flex; align-items: baseline; gap: var(--gap-m); flex-wrap: wrap; margin-bottom: var(--gap-s); }
.gateway-id { font-family: var(--font-mono); font-size: var(--text-lead); font-weight: 600; }
.gateway-heard { font-size: var(--text-small); color: var(--ink-faint); margin-left: auto; }
.gateway-head button.show-meters, .gateway-head button.fold { height: auto; padding: 2px var(--gap-s); font-size: var(--text-caption); }
/* A folded card's one line: its charge, RSRP and meters, beside its status. */
.gateway-brief { font-family: var(--font-mono); font-size: var(--text-small); color: var(--ink-quiet); }
/* The two readout sentences on a card's head: what the gateway last said, and
   when this session asked. A class each, and not one shared by both -- a
   locator naming the class matched two elements the moment the second arrived,
   which is four browser tests failing on ambiguity rather than on anything
   being wrong. */
.gateway-readout, .gateway-asked { font-size: var(--text-small); color: var(--ink-quiet); }
.gateway-card:has(.gateway-details[hidden]) .gateway-head { margin-bottom: 0; }
/* **On a phone the head is three rows, each from the left**: the id and its
   badges, then what the gateway said, then the buttons. Left to wrap freely,
   a button landed at the end of a line of text on one card and alone on a
   line on the next, so no two cards looked alike (asked for on 2026-10-06).
   The head has no element per row, so the two pseudo-elements are the row
   ends: each is a full line of no height. Such a line still takes a row gap,
   which is why the row gap here is half of the gap it draws. */
@media (max-width: 700px) {
  .gateway-head { gap: var(--gap-xs) var(--gap-m); }
  .gateway-head::before, .gateway-head::after { content: ''; flex-basis: 100%; }
  .gateway-head::before { order: 1; }
  .gateway-brief, .gateway-readout, .gateway-asked, .gateway-heard { order: 2; }
  .gateway-heard { margin-left: 0; }
  .gateway-head::after { order: 3; }
  /* Not the copy button: it stays beside the id it copies. */
  .gateway-head button:not(.copy) { order: 4; }
  /* A session that names one gateway shows no button, so it has no third row. */
  .gateway-head:not(:has(button:not(.copy):not([hidden])))::after { display: none; }
}

/* The connection's word, coloured the way the summary colours its own. */
.badge {
  font-size: var(--text-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 1px var(--gap-s);
  border-radius: 999px;
  border: 1px solid currentColor;
}
.badge.is-online { color: var(--good); }
/* Offline is the ordinary state of a battery gateway, so it is not red. */
.badge.is-offline { color: var(--waiting); }
.badge.is-unknown { color: var(--ink-faint); }
/* A readout's tag: amber while it waits, green while it runs. */
.badge.is-waiting { color: var(--waiting); }
.badge.is-running { color: var(--good); }
/* The gateway refused the list, so nothing was sent: red, because it needs
   somebody to look at the card's error. */
.badge.is-refused { color: var(--bad); }
.badge[title] { cursor: help; }

/* **Nearly equal parts: Status 30, Battery 27, Network 26, Device 26**
   (asked for on 2026-10-03; they were 30, 20, 20 and 30 per cent from
   2026-09-15). Every part ends with a date and a time, and the two narrow
   parts could not hold that on one line. The shares are measured, not chosen
   by eye: Status and Battery have the longest names, so they get a little
   more, and at 1280 px every part then has about 12 px to spare.
   `minmax(0, …)` holds a part to its share, so a long value wraps or is cut
   short rather than widening its column -- the columns must not move when the
   values arrive, and every part has all its rows from the start, with a dash
   where there is no value. Signal quality was a part of its own beside
   Network and is in it again. */
.gateway-parts {
  display: grid;
  grid-template-columns: minmax(0, 30fr) minmax(0, 27fr) minmax(0, 26fr) minmax(0, 26fr);
  gap: var(--gap-m) var(--gap-xl);
}
/* Too narrow for four side by side: two and two, in two rows. Under 1280 px
   the date and the time no longer fit on one line in a quarter of the card. */
@media (max-width: 1279px) {
  .gateway-parts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gap-m) var(--gap-l); }
}
/* And one under another on a phone. */
@media (max-width: 700px) {
  .gateway-parts { grid-template-columns: minmax(0, 1fr); }
}
.gateway-part h3 {
  margin: 0 0 var(--gap-xs);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  font-weight: 600;
}
/* A name and its value on one line, the names in a column of their own. */
.gateway-part dl {
  display: grid;
  /* `minmax(0, …)`, or a long value widens its column past the part rather
     than being cut short. */
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 2px var(--gap-m);
  margin: 0;
  font-size: var(--text-small);
}
.gateway-part dt { color: var(--ink-quiet); }
/* A term with a meaning in its tooltip says so, as the page's other help does. */
.gateway-part dt[title] { cursor: help; text-decoration: underline dotted var(--ink-faint); text-underline-offset: 3px; }
.gateway-part dd { margin: 0; overflow-wrap: anywhere; }
.gateway-part dd.mono { font-family: var(--font-mono); }
.gateway-part dd.bad { color: var(--bad); }
/* A date and a time, on one line. */
.gateway-part dd.stamp { white-space: nowrap; overflow-wrap: normal; }
/* One line, cut short with an ellipsis; the whole value is in the tooltip. */
.gateway-part dd.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal; }

/* The repeaters, across the whole card under its parts. */
.gateway-repeaters { margin-top: var(--gap-m); padding-top: var(--gap-m); border-top: 1px solid var(--edge); overflow-x: auto; }
.gateway-repeaters h3 {
  margin: 0 0 var(--gap-xs);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  font-weight: 600;
}
.gateway-repeaters .not-sent { margin: 0; font-size: var(--text-caption); color: var(--ink-faint); }
table.repeaters { border-collapse: collapse; font-size: var(--text-small); }
table.repeaters th {
  text-align: left;
  font-size: var(--text-caption);
  font-weight: 400;
  color: var(--ink-quiet);
  padding: 0 var(--gap-l) var(--gap-xs) 0;
  white-space: nowrap;
}
table.repeaters td { padding: 2px var(--gap-l) 2px 0; white-space: nowrap; }
table.repeaters td.mono { font-family: var(--font-mono); }
table.repeaters td.bad { color: var(--bad); }

/* The three tabs of the home page (QUNDIS_TOOL_DESIGN.md 4bis.3d). The wired
   bench's own, word for word, so somebody who uses both benches meets one
   control rather than two. */
.tabs { display: flex; gap: var(--gap-xs); margin-bottom: var(--gap-l); border-bottom: 1px solid var(--edge); }
.tab {
  background: none;
  color: var(--ink-quiet);
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 0 var(--gap-m);
}
.tab:hover { filter: none; color: var(--accent); }
.tab.is-active { color: var(--ink); border-bottom-color: var(--accent); font-weight: 600; }

/* The way back from a session to the home page. Its own line above everything
   the session draws, so it is in the same place whatever that session is
   doing. */
.live-head { margin-bottom: var(--gap-m); }

/* The schedule dialog. */
.schedule-dialog {
  border: 1px solid var(--edge);
  border-radius: 8px;
  padding: var(--gap-l);
  width: min(36rem, calc(100vw - 2 * var(--gap-l)));
  color: var(--ink);
  background: var(--card);
}
.schedule-dialog::backdrop { background: rgba(23, 25, 28, 0.35); }
/* Wider, for a dialog that holds a table as well as a form. */
.wide-dialog { width: min(44rem, calc(100vw - 2 * var(--gap-l))); }
/* The messages kept for a gateway. As wide as the page lets it be, because a
   payload is the widest thing here, and tall enough to read a day of them. */
.messages-dialog { width: min(76rem, calc(100vw - 2 * var(--gap-l))); }
.messages-dialog .log { max-height: 60vh; }
.messages-dialog .log[hidden] { display: none; }

/* What one contractor was given: a row for each, with its own way to take it
   away, so nothing removes them all by being left empty. */
table.given { width: 100%; border-collapse: collapse; font-size: var(--text-small); margin-top: var(--gap-s); }
table.given th {
  text-align: left;
  font-size: var(--text-caption);
  font-weight: 600;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0 var(--gap-s) var(--gap-xs) 0;
}
table.given td { padding: var(--gap-xs) var(--gap-s) var(--gap-xs) 0; border-top: 1px solid var(--edge); vertical-align: top; }
table.given td.given-meters { color: var(--ink-quiet); }
table.given td.given-gateway { font-family: var(--font-mono); }
table.given td.action { text-align: right; white-space: nowrap; }
/* A date and its time stay on one line. A cell of several addresses has one
   on each line, and a long one may break anywhere. */
table.given td.stamp { white-space: nowrap; }
table.given td.lines { white-space: pre-line; overflow-wrap: anywhere; }
.given-heading { font-size: var(--text-small); color: var(--ink-quiet); margin: var(--gap-l) 0 var(--gap-s); }
.schedule-dialog h2 { font-size: var(--text-lead); margin: 0 0 var(--gap-s); }
/* A question is a sentence or two, so its dialog is the narrow one. */
.ask-dialog { width: min(28rem, calc(100vw - 2 * var(--gap-l))); }
.ask-dialog h2 { overflow-wrap: anywhere; }
.schedule-dialog .privacy { margin-bottom: var(--gap-m); }
/* A row carries no margin of its own, so in a dialog whose fields sit in rows
   the rows touch each other and the buttons sit against the last input. The
   buttons stand further away than a row does, because they are not one more
   field. Margins collapse, so a stacked field's own margin is not added to
   this: every dialog ends up with the same space above its buttons. */
.schedule-dialog .row { margin-top: var(--gap-m); }
.schedule-dialog .row.actions { margin-top: var(--gap-l); }
.narrow-field input { width: 10rem; }
.schedule-preview { margin: calc(-1 * var(--gap-s)) 0 var(--gap-m); font-size: var(--text-small); color: var(--ink-quiet); white-space: pre-line; min-height: 1.5em; }
.schedule-preview.bad { color: var(--bad); }
/* Under a row of fields and not under one field: the pull up above is for a
   preview that follows its own label, and here it would draw over the boxes. */
.row + .schedule-preview { margin-top: var(--gap-xs); }
/* The period of a logger's files and its two buttons, on one line. On a
   phone there is no room for four in a line, and they wrap. */
.logger-files { flex-wrap: nowrap; }
.logger-files .field { flex: 1 1 0; min-width: 0; }
.logger-files .field input { width: 100%; }
.logger-files button { white-space: nowrap; }
@media (max-width: 700px) { .logger-files { flex-wrap: wrap; } }
/* Under the two fields of a readout at a chosen time, not pulled up to them. */
#readout-says { margin-top: var(--gap-m); }

/* A card's own buttons: smaller than the panel's, because there is one set
   for each gateway. */
.gateway-part .booking { margin: var(--gap-s) 0 0; font-size: var(--text-caption); }
.card-actions { display: flex; gap: var(--gap-s); flex-wrap: wrap; margin-top: var(--gap-s); }
.card-actions button { height: auto; padding: 3px var(--gap-m); font-size: var(--text-caption); }
/* The card's second section: one row across the whole card, under its parts. */
.gateway-actions { margin-top: var(--gap-m); }
.gateway-actions .card-actions { margin-top: 0; }
.gateway-actions .booking { margin: 0 0 var(--gap-xs); }
/* A heading whose tooltip says when its message was received. */
.gateway-part h3[title] { cursor: help; }

/* **`.listing` is the administration tables, and it is not `.meters`.** They
   borrowed that class for a day and broke four scripts at once: `table.meters`
   had been one table on this page since it was written, and every locator that
   said so suddenly found four. The look is shared by naming both here; the
   rules that mean something about meters -- a row that opens, a column that
   scrolls on a phone -- stay where they belong. */
.meters, .listing {
  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, .listing th, .listing td {
  text-align: left;
  padding: var(--gap-s) var(--gap-m);
  border-bottom: 1px solid var(--edge);
  vertical-align: top;
}
/* **But an administration row is centred, where a meter's is not.** A meter's
   row opens and grows a column of quantities under it, so its cells must start
   at the top or the serial would float in the middle of a tall row. Nothing in
   these tables grows: a row is one line of text and a button, and the button
   is the taller of the two -- so aligning to the top left the text 7px above
   the middle of the button beside it, measured. */
.listing th, .listing td { vertical-align: middle; }
.meters th, .listing 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, .listing tbody tr:last-child td { border-bottom: none; }
/* The bin of a queued action: at the end of its row, and as narrow as it is. */
.listing td.queued-remove { text-align: right; width: 1%; }
.listing td.queued-remove button.bin { margin-left: 0; }
.listing td.mono { font-family: var(--font-mono); }
/* The reason somebody gave for asking for an account: their own lines, kept,
   and a long word that cannot push the table wider than the page. */
.listing td.reason { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 32em; }

/* A header that orders the table. It is a button, so the keyboard reaches it,
   and it is drawn as the header beside it: the face, the size, the letters and
   the colour are the header's, and only the pointer and the arrow say it does
   something. A button brings its own font and padding, which is what the first
   three lines take away. */
.meters th[data-sort] { padding: 0; }
.meters th[data-sort] button {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  background: none;
  border: none;
  border-radius: 0;
  width: 100%;
  height: auto;
  padding: var(--gap-s) var(--gap-m);
  text-align: inherit;
  white-space: nowrap;
  cursor: pointer;
}
.meters th[data-sort] button:hover { color: var(--ink); filter: none; }
/* The column the table is ordered by keeps the darker ink whether or not the
   pointer is on it, so the arrow is never the only thing saying so. */
.meters th.is-ordering button { color: var(--ink); }
/* The arrow sits after the name and takes no width when there is none, so a
   header does not move as the ordering changes. */
.meters th[data-sort] .caret { display: inline-block; width: 1em; text-align: left; }
.meters th.right[data-sort] .caret { text-align: right; }
.meters .right { text-align: right; }
.meters td.serial, .meters td.gateway, .meters td.mono, .meters td.right { font-family: var(--font-mono); }
/* The number, its copy button and its marks stay on one line. A mark alone on
   a second line made that row taller than the rows beside it. */
.meters td.serial { white-space: nowrap; }

/* The meter's own mark, beside its serial: orange when any counter on it fell,
   green when one rose, and a green tilde for a gauge that moved. The same three
   the quantities under it carry, and the same colours. */
.meters .meter-mark { margin-left: var(--gap-s); cursor: default; font-weight: 600; }
.meters .meter-mark.rises { color: var(--good); }
.meters .meter-mark.falls { color: var(--waiting); }
.meters .meter-mark.moved {
  display: inline-block;
  vertical-align: middle;
  font-size: 1.5em;
  line-height: 0;
}

/* A meter with readings opens them on a click. One with none does not invite
   the click, since a row that answered it with an empty space would be worse
   than a row that stayed still. */
.meters tr.opens { cursor: pointer; }
.meters tr.opens:hover td { background: var(--page); }
.meters tr.is-open td { border-bottom-color: transparent; }
/* The cell holding them keeps no padding of its own, so a quantity's row
   reaches every edge of the table and is marked the way the meter's row above
   it is. The padding moves into the table inside: to the first cell of a row,
   which is what puts a quantity's name under the serial, and to the last row,
   below. */
.meters tr.detail > td { padding: 0; background: var(--page); }

/* The quantities under a meter: a name, what it reads, and how far it has
   moved. Its own grid, because the columns above are facts about the meter and
   a quantity has none of them.

   It is as wide as the row it sits in. A table that took only the width of its
   words left the pointer marking the words rather than the line, which read as
   a highlighted piece of text instead of the row a click would open. */
table.quantities { border-collapse: collapse; font-size: var(--text-small); width: 100%; }
table.quantities td {
  padding: 2px var(--gap-l) 2px 0;
  border-bottom: none;
  white-space: nowrap;
  vertical-align: top;
}
table.quantities td:first-child { padding-left: var(--gap-m); }
/* The space under the last quantity belongs to that row. Held by the cell
   around them instead, it stayed the colour of the block while the row above
   it turned white, and read as a gap between a marked row and the line under
   it. */
table.quantities tr:last-child td { padding-bottom: var(--gap-s); }
table.quantities td.amount { font-family: var(--font-mono); }
/* The last column takes whatever width is left, so the widths of the two
   before it are still the widths of their words. Its mark stays at the left of
   it, where the column began. */
table.quantities td.change { min-width: 7rem; width: 100%; }
.quantities .subunit { color: var(--ink-faint); font-size: var(--text-caption); }
.quantities tr.quantity { cursor: pointer; }
.quantities tr.quantity:hover td { background: var(--card); }

/* A value that has moved since the first reading. A counter only rises while a
   meter works, so green is a rise and orange is a fall, and the number is how
   far. A gauge moves either way in normal use, so it carries a tilde and no
   number. */
.quantities .rises { color: var(--good); font-family: var(--font-mono); font-weight: 600; }
.quantities .falls { color: var(--waiting); font-family: var(--font-mono); font-weight: 600; }
/* A gauge's mark is a tilde, drawn larger than the table's text: at this size
   it is a faint stroke near the top of the line, where a counter's triangle is
   solid and easy to see. `line-height: 0` stops the larger glyph from making
   its row taller than a row carrying no mark, so only the rows that say
   something would have grown. The wired bench draws it the same way. */
.quantities .moved {
  display: inline-block;
  vertical-align: middle;
  font-size: 1.5em;
  line-height: 0;
}

.chart { padding: var(--gap-xs) 0 var(--gap-m); max-width: 640px; }
.chart svg { display: block; width: 100%; height: 7.5rem; }
.chart polyline { fill: none; stroke: var(--accent); stroke-width: 2; vector-effect: non-scaling-stroke; }
.chart-scale {
  display: flex;
  justify-content: space-between;
  gap: var(--gap-l);
  font-size: var(--text-caption);
  color: var(--ink-faint);
}
.chart-scale .mono { font-family: var(--font-mono); }
.aside { color: var(--ink-faint); font-size: var(--text-caption); margin: var(--gap-xs) 0 0; }

/* 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); }

/* 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%; }
/* **On a phone the gateway, the time and the topic share one column**, one
   under the other, so the message keeps most of the width (asked for on
   2026-10-15: four columns side by side left the message a few letters
   wide). On a wide screen they are three columns, as before. */
#raw-section .log :is(th, td).where { display: none; }
/* **Gateway raw data has no scrollbar of its own** (asked for on 2026-10-16):
   it is as long as its lines, the page scrolls, and its head sticks to the top
   of the window. `clip` keeps the rounded corners without a box that scrolls,
   which would hold the sticky head inside it. The dialog's log still scrolls. */
#raw-section .log { max-height: none; overflow: clip; }
@media (max-width: 700px) {
  #raw-section .log :is(th, td):is(.gw, .at, .topic) { display: none; }
  #raw-section .log :is(th, td).where { display: table-cell; }
  #raw-section .log td.where { font-family: var(--font-mono); color: var(--ink-faint); white-space: nowrap; }
  #raw-section .log td.where span { display: block; }
}

/* The (i) of a line. As narrow as the icon, and no taller than the text of
   the row, so a log of one-line messages keeps its height. */
.log :is(th, td).explain { padding-left: 0; padding-right: 0; }
.log button.explains { height: auto; padding: 1px 2px; vertical-align: top; }
.log button.explains:hover { color: var(--accent); }
/* The (i) after a field's name: how the value is written. */
.label button.explains { height: auto; padding: 0 2px; vertical-align: text-bottom; }
.label button.explains:hover { color: var(--accent); }

/* The page's own tooltip: a name and a value on each row. Fixed to the
   window, because the log scrolls and would cut it. It takes no pointer, so
   it can never be in the way of the row under it. */
.tip {
  position: fixed;
  z-index: 20;
  max-width: min(34rem, calc(100vw - 16px));
  padding: var(--gap-s) var(--gap-m);
  border-radius: 6px;
  background: var(--ink);
  color: var(--page);
  font-size: var(--text-caption);
  line-height: 1.45;
  pointer-events: none;
  box-shadow: 0 2px 10px rgb(0 0 0 / 0.25);
}
.tip[hidden] { display: none; }
/* In the top layer a browser centres a box and gives it a border and a
   scrollbar. This one is placed by the page and has neither. */
.tip[popover] { inset: auto; margin: 0; border: 0; overflow: visible; }
.tip p { margin: 0; }
/* What a raw data message is, in one line, over the rows of what it carries. */
.tip p.about { font-weight: 600; }
.tip p.about:not(:last-child) { margin-bottom: var(--gap-xs); }
/* Words an element says of itself. Some are several lines. */
.tip p.said { white-space: pre-line; max-width: 26rem; }
.tip dl { display: grid; grid-template-columns: auto 1fr; gap: 2px var(--gap-m); margin: 0; }
.tip dt { opacity: 0.7; white-space: nowrap; }
.tip dd { margin: 0; overflow-wrap: anywhere; }
/* How a raw data message was delivered, under what it carries: its QoS, and
   whether the broker kept it. Quieter than the rows, as the names are. */
.tip p.delivery { opacity: 0.7; }
.tip :not(.delivery) + p.delivery { margin-top: var(--gap-xs); }

.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, #raw-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, #raw-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.
   **`.listing` as well as `.meters`, since 2026-09-19.** The administration
   tables were written after this rule and nobody thought of it, so on a phone
   Brokers was 641px wide in 375 and took the *page* sideways with it -- the
   exact thing the sentence above says is the worse outcome. The Change and
   Delete buttons were off the right of the screen with no way to reach them.
   `phone.spec.ts` could not see it: it is a visitor, and a visitor has no
   Administration. */
/* A button under a table that opens a dialog to add a row to it. It needs the
   space below: the one under People is followed straight by the invitations
   table, and without this the two touch -- which is what was reported on
   2026-09-20, when the invite form itself stood there. */
.adds { margin-bottom: var(--gap-m); }

/* Only a phone folds the filters, so the button exists only there. */
#sift-fold { display: none; }

@media (max-width: 900px) {
  body { padding: var(--gap-l) var(--gap-m); }
  #sift-fold { display: inline-flex; margin-bottom: var(--gap-m); }
  /* **In the media query, so a wider window always shows them.** The class
     stays on the element when the window grows; the rule that acts on it does
     not, which is what keeps a resized window from hiding the filters for
     good. */
  .sift.is-folded { display: none; }
  .meters, .listing {
    display: block;
    overflow-x: auto;
    white-space: nowrap;

    /* **A shadow at whichever edge has more table behind it.** These tables
       are about three times a phone's width and they scroll, but nothing said
       so: seven of the eleven columns are off the right and a reader has no
       reason to think of dragging sideways (2026-09-19).

       Four layers, and the trick is the attachment. The two covers are
       `local`, so they travel with the content and sit over a shadow when that
       end is reached; the two shadows are `scroll`, so they stay at the edges
       of the box. At rest the left cover hides the left shadow and only the
       right one shows; scrolled to the end, the reverse. A table that fits
       shows neither, which is why this needs no width test of its own.

       The colour is `background-color` and not a fifth layer, because a colour
       is painted under every layer whatever they do, so the card stays solid
       across the whole width.

       The ink is the one the dialog's backdrop already uses, at a weight that
       reads as an edge rather than as a border. This page is drawn light only,
       so a dark shadow is safe on it. */
    background-color: var(--card);
    background-image:
      linear-gradient(to right, var(--card), transparent),
      linear-gradient(to left, var(--card), transparent),
      linear-gradient(to right, rgba(23, 25, 28, 0.13), transparent),
      linear-gradient(to left, rgba(23, 25, 28, 0.13), transparent);
    background-position: left center, right center, left center, right center;
    background-repeat: no-repeat;
    background-size: 28px 100%, 28px 100%, 10px 100%, 10px 100%;
    background-attachment: local, local, scroll, scroll;
  }
  .exports { margin-left: 0; }
  /* **The Export menu as wide as the row of buttons, under the whole row.**
     Hung from the right-hand edge of Export, as on a wide screen, its 18rem
     started 61px off the left of a phone: Export is the second of four
     buttons, and the row wraps (2026-09-21). The row is the list's box here,
     so the list cannot be wider than the page. */
  .exports { position: relative; }
  .exports .menu { position: static; }
  .exports .menu-items { left: 0; right: 0; min-width: 0; }
}

/* Who you are, in the corner of the masthead. Two words for a visitor, because
   signing in is optional and the page must not read as though it is not. */
/* **Who you are, opposite the title** (the wired bench's masthead). No bottom
   margin any more: it sits beside the lede, which carries the space under the
   whole header. */
.account { display: flex; align-items: center; gap: var(--gap-s); }
.account .who {
  display: flex;
  align-items: center;
  gap: var(--gap-s);
  font-size: var(--text-small);
  color: var(--ink-quiet);
  margin-right: var(--gap-xs);
}
/* The role, as a chip rather than as more of the same sentence.
   **`.role` and not `.badge`**, which this page already has: `.badge` is a
   gateway's ONLINE / OFFLINE chip, upper case and coloured through
   `currentColor`. Borrowing it uppercased the role and, worse, repainted every
   gateway's status grey -- the second-definition trap 4bis.3b already records
   twice, met a third time and caught by the word coming out shouting. */
.role {
  font-size: var(--text-caption);
  color: var(--ink-quiet);
  border: 1px solid var(--edge);
  border-radius: 999px;
  padding: 0 var(--gap-s);
  white-space: nowrap;
}

/* A field that cannot be changed, because a listed broker was chosen: it
   shows that broker's own value. Dimmed rather than hidden, so the reader can
   see what will be connected to, and less than it was before these fields
   showed the chosen broker (2026-09-22), because they are now worth reading. */
.is-put-away { opacity: 0.6; }

.heading-small {
  font-size: var(--text-body);
  font-weight: 600;
  margin: var(--gap-l) 0 var(--gap-xs);
}
/* **No rule for `.refusal` here.** One already exists above, with a box and a
   margin, and the new panels use it as it is -- a second definition further
   down the file silently took the margin off every refusal on the page, which
   is what the second class collision of the day looked like. */
/* **The buttons of a row sit at its right-hand edge.** They were at the left
   of whatever width the last column happened to take, which put them 44px
   short of the table's edge in Brokers and 101px short in People -- a ragged
   right, and a different one on every page. `width: 1%` makes the column as
   narrow as its buttons, so the columns that carry something take the rest.
   The wired bench's `.rows .action`, word for word.

   **Between the buttons and not after the last one.** The margin was on every
   button, so right-aligning left the last one 8px off the edge. Measured. */
.listing .action { text-align: right; width: 1%; white-space: nowrap; }
.listing td button + button { margin-left: var(--gap-s); }
.listing { margin-bottom: var(--gap-m); }
/* A person whose account is disabled, faded as a muted meter is. */
.listing tr.is-skipped td { opacity: 0.5; }

/* The calendar of a date box and the list of times of a time box, drawn by
   the page (app.js, "a calendar and a list of times"). Fixed to the window,
   and placed by the script under the box it is for. */
.logger-files .with-default { display: block; }
.chooser {
  position: fixed;
  z-index: 1000;
  box-sizing: border-box;
  padding: var(--gap-s);
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
}
.chooser[hidden] { display: none; }
/* In the top layer a browser centres a box. This one is placed by the page. */
.chooser[popover] { inset: auto; margin: 0; overflow: visible; }
.chooser-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--gap-xs); }
.chooser-month { font-size: var(--text-small); font-weight: 600; }
.chooser-heading {
  padding: var(--gap-xs) var(--gap-xs) 2px;
  font-size: var(--text-caption);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.chooser-grid { display: grid; gap: 2px; }
.chooser-days { grid-template-columns: repeat(7, 36px); }
.chooser-times { grid-template-columns: repeat(6, 36px); }
.chooser-weekday {
  height: 24px;
  line-height: 24px;
  text-align: center;
  font-size: var(--text-caption);
  color: var(--ink-faint);
}
/* A square large enough for a finger. No box of its own until it is chosen. */
.chooser button {
  width: 36px;
  height: 36px;
  padding: 0;
  font-family: var(--font);
  font-size: var(--text-small);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: none;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
}
.chooser button:hover { filter: none; background: var(--page); }
.chooser button:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; }
.chooser button.today { border-color: var(--edge); }
.chooser button[aria-pressed=true], .chooser button[aria-pressed=true]:hover { color: #fff; background: var(--accent); font-weight: 600; }
.chooser button.chooser-step { font-size: var(--text-lead); color: var(--ink-quiet); }
.chooser-foot { display: flex; justify-content: flex-end; margin-top: var(--gap-xs); }
.chooser button.chooser-today { width: auto; height: 28px; padding: 0 var(--gap-s); color: var(--accent); }

/* ==== own lists: begin ==== */
/* Every select is drawn by the page: a button that looks like a field, and a
   list that opens from it. Safari on a phone or a tablet draws a select's
   options in a narrow popover of its own, which no rule here can reach. The
   select itself stays in the page, hidden, and holds the value.

   This block is the same, byte for byte, in every bench. `--field` and
   `--field-pad` are what a bench sets where its own fields differ. */
select[data-own-list] { display: none !important; }
.choices { position: relative; display: block; width: 100%; }
button.choices-button {
  width: 100%;
  height: var(--control);
  display: flex;
  align-items: center;
  gap: var(--gap-s);
  padding: 0 var(--field-pad, var(--gap-m));
  font-family: var(--font);
  font-size: var(--text-small);
  font-weight: 400;
  color: var(--ink);
  background: var(--field, var(--card));
  border: 1px solid var(--edge);
  border-radius: 6px;
  text-align: left;
  cursor: pointer;
}
button.choices-button:hover { filter: none; border-color: var(--accent); }
button.choices-button[aria-expanded=true] { border-color: var(--accent); }
button.choices-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
button.choices-button[disabled] { cursor: default; border-color: var(--edge); }
/* `width: 0` and then grown: a long line must not make its field wider. With
   `min-width: 0` alone, a row of fields took the width of the chosen words
   and a phone scrolled sideways. */
.choices-now { flex: 1 1 0; width: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.choices-caret { flex: none; color: var(--ink-quiet); }
.choices-list {
  position: fixed;
  z-index: 1000;
  box-sizing: border-box;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--gap-xs) 0;
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
}
.choices-group {
  padding: var(--gap-s) var(--gap-m) var(--gap-xs);
  font-size: var(--text-caption);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* A line wraps and is never cut: its end is often what tells two lines apart. */
.choices-list button {
  display: block;
  width: 100%;
  height: auto;
  min-height: var(--control);
  padding: var(--gap-s) var(--gap-m);
  font-family: var(--font);
  font-size: var(--text-small);
  font-weight: 400;
  line-height: 1.35;
  color: var(--ink);
  background: none;
  border: none;
  border-radius: 0;
  text-align: left;
  overflow-wrap: anywhere;
  cursor: pointer;
}
.choices-list button:hover, .choices-list button:focus-visible { filter: none; background: var(--page); outline: none; }
.choices-list button[aria-selected=true] { color: var(--accent); font-weight: 600; }
.choices-list button[disabled] { color: var(--ink-faint); cursor: default; background: none; }
/* ==== own lists: end ==== */
