/*
 * 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 nearly everything on this page is a
 * machine-produced fact.
 *
 * 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;
  /* What a field here is filled with and padded by, for the lists at the end
     of this file: this page's fields are the page's colour, not the card's. */
  --field: var(--page);
  --field-pad: var(--gap-s);
  --accent: #1a5fb4;
  --bad: #b3261e;

  /* One colour per kind of byte, and the legend is generated from this list
     rather than written twice. Chosen to stay apart for the commonest colour
     blindness: blue/amber/red rather than red/green. */
  --link: #cfe0f5;
  --link-ink: #10396e;
  --header: #ded3f2;
  --header-ink: #4a2f86;
  --record: #cceadd;
  --record-ink: #14563d;
  --record-alt: #e6f4ee;
  --filler: #e6e8ec;
  --filler-ink: #5c626b;
  --encrypted: #fbe3c0;
  --encrypted-ink: #7a4c05;
  --trailer: #dde3ea;
  --trailer-ink: #3c4855;
  --unclaimed: #f7cfcc;
  --unclaimed-ink: #8c1d17;

  /* What differs between two telegrams, in the Compare card. The accent,
     solid, because it is the one thing on that card the eye should find; a
     row that changed gets a soft band of it. */
  --changed: #1a5fb4;
  --changed-ink: #ffffff;
  --changed-soft: #e8f0fb;

  --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 the form's bottom row. The fields reach it
     through their padding anyway; saying it once is what stops the button
     drifting from them again, as it had at 38px against their 34. */
  --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;
}

@media screen and (prefers-color-scheme: dark) {
  :root {
    --ink: #e6e8ec;
    --ink-quiet: #b0b6bf;
    --ink-faint: #858c96;
    --page: #14161a;
    --card: #1c1f24;
    --edge: #2e333a;
    --accent: #78aeff;
    --bad: #ff8a80;

    --link: #1d3a5f;
    --link-ink: #bcd8ff;
    --header: #3a2f5c;
    --header-ink: #d8c9ff;
    --record: #1c4a38;
    --record-ink: #b6e8d2;
    --record-alt: #16332a;
    --filler: #2b2f36;
    --filler-ink: #a0a6b0;
    --encrypted: #5a3f10;
    --encrypted-ink: #ffd79a;
    --trailer: #2a323b;
    --trailer-ink: #c2cbd6;
    --unclaimed: #5c1f1b;
    --unclaimed-ink: #ffc4bf;

    --changed: #78aeff;
    --changed-ink: #0b1a2e;
    --changed-soft: #1a2940;
  }
}

* { box-sizing: border-box; }

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

/* Wide, because a telegram is two tables and a map of its bytes side by side,
   and 1100px gave each of them a third of a screen. The map alone wants 16
   bytes to a row without scrolling. */
main, .masthead { max-width: 1600px; margin: 0 auto; }

/* **Indented to where the cards' text starts, not to where their edges do.**
   Everything below the title sits inside a box with a border and a gap of its
   own, so a title flush with the page's own padding began one gap and one
   border to the left of every other word on the page, and read as misaligned
   -- which it was. `border-box` keeps the width the same as `main`'s. */
.masthead { padding: 0 calc(var(--gap-l) + 1px); }

/* The link to the manual, at the right of the title's line. It is out of the
   flow so the title and the lines under it stay where they were. */
.masthead { position: relative; }
.manual { position: absolute; top: 0.35em; right: calc(var(--gap-l) + 1px); }
.manual[hidden] { display: none; }
.manual { color: var(--accent); text-decoration: none; }
.manual:hover { text-decoration: underline; }

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

/* ---------------------------------------------------------------- the form */

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

/**
 * `hidden` beats a `display` rule, everywhere.
 *
 * The attribute is only a `display: none` in the browser's own sheet, so any
 * rule here that gives an element a `display` quietly cancels it -- which is
 * what `.field` did to the link box: it was marked hidden and drawn anyway.
 * Said once, with the one `!important` on the page, because the alternative is
 * remembering it at every rule that sets a `display`.
 */
[hidden] { display: none !important; }

.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=email], select {
  width: 100%;
  padding: var(--gap-s);
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-small);
}
/**
 * `display: block`, and it is not decoration.
 *
 * A textarea is inline by default, so the block that holds it reserves a line
 * box under it for descenders -- **5.8px measured here** -- and the box then
 * ends above the bottom of its own label. Nothing showed that while the only
 * textarea on the page sat alone; the key box sits on a row beside a select and
 * two buttons aligned to the bottom, and it was the one control 5.8px high.
 * Reported from a screenshot on 2026-09-03.
 *
 * A `select` in the same place has no such gap, which is why the row was right
 * before this box arrived.
 */
textarea { resize: vertical; display: block; }

/* A file being dragged over a box that will take it. Dashed, where the focus
   ring above is solid: the two can be on one box at once -- a drag over the box
   the caret is already in -- and they have to be tellable apart. */
textarea.dropping {
  outline: 2px dashed var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

/* Turning `appearance` off can take the focus ring with it, and a control you
   cannot see the focus on is one a keyboard cannot be driven round. Said once
   for all three, so they cannot come to differ. */
textarea:focus-visible, input[type=text]:focus-visible, input[type=password]:focus-visible, input[type=email]:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

/* **Links, quietly.** The only ones here are the index's, and the browser's own
   blue-then-purple was the loudest thing on a page of grey and hex. Accent, no
   rule under it until it is pointed at, and no `visited` colour at all: which
   telegram somebody read first is not a fact about the paste, and half an index
   in purple looked like it meant something. */
a {
  color: var(--accent);
  text-decoration: none;
  border-radius: 3px;
}
a:hover { text-decoration: underline; text-underline-offset: 3px; }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

input[type=text], input[type=password], input[type=email], select, button { height: var(--control); }

/* The key box is a textarea so that a pasted key file keeps its line breaks,
   and it sits on the form's bottom row, so it has to be one control high and
   not wrap. It can still be dragged taller to read a long list. */
textarea.one-line {
  height: var(--control);
  min-height: var(--control);
  white-space: pre;
  overflow-x: auto;
}

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

button {
  padding: var(--gap-s) var(--gap-l);
  border: 0;
  border-radius: 6px;
  background: var(--accent);
  color: #fff;
  font-size: var(--text-body);
  font-weight: 500;
  cursor: pointer;
}
button:hover { filter: brightness(1.08); }
button[disabled] { opacity: 0.6; cursor: default; }

/**
 * The second button, made quieter than the first.
 *
 * Two solid accent buttons side by side ask the reader which one the form is
 * for, every time. This one keeps the shape and the height and gives up the
 * fill, so Decode stays the obvious thing to press.
 *
 * `filter: brightness()` on a transparent background does nothing, so the
 * hover has to be a real colour rather than the rule above.
 */
button.quiet {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--edge);
}
button.quiet:hover { filter: none; border-color: var(--accent); background: var(--card); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.privacy { margin: var(--gap-m) 0 0; font-size: var(--text-caption); color: var(--ink-faint); }
/* How many models are known, and the button that replaces them. Hidden where
   no upload is taken. */
.known {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--gap-s) var(--gap-m);
  margin: var(--gap-m) 0 0;
  font-size: var(--text-caption);
  color: var(--ink-quiet);
}
.known[hidden] { display: none; }
.shared { margin: var(--gap-m) 0 0; font-size: var(--text-caption); 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 results */

.summary {
  display: flex;
  gap: var(--gap-l);
  flex-wrap: wrap;
  margin-bottom: var(--gap-l);
  padding: var(--gap-m) var(--gap-l);
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
}
/* The two buttons that write a file, pushed to the right of the row the facts
   are on. `center` against the row's own stretch: a button stretched to the
   height of a two-line fact is a button the size of the box it sits in.

   **They take a line each rather than share one badly.** On a phone the two
   do not fit one line: reported with a picture on 2026-09-19, where each label
   wrapped to two lines and the second was drawn outside its button -- every
   control here has one height, which is right for a field and wrong for a
   button whose label does not fit. Measured at 375px, 32px of button for 42px
   of label.

   Both halves are needed. `flex-wrap` alone does not settle it, because a flex
   item shrinks below its content before the line wraps, so the browser fits
   both on the line and wraps the words instead; `nowrap` on the label is what
   makes the button ask for its whole width, and then the row wraps. The labels
   themselves are kept: `Download readings` says what it writes, and
   `Readings` would not. Nothing changes above 640px, where the two have always
   fitted one line. */
.exports {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-s);
  align-items: center;
}
.exports button { white-space: nowrap; }

.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 survey's numbers: the summary row's shape, without its box -- it is
   inside a card already, and a box inside a box reads as two things. */
.counts {
  display: flex;
  gap: var(--gap-l);
  flex-wrap: wrap;
  margin-bottom: var(--gap-m);
}

/* A telegram drawn closed, which is what a capture of hundreds is drawn as.
   The line carries what the index line carries, so the two agree. Compare,
   Models and Readings are always drawn closed, with the same line. */
details.folded { margin-bottom: var(--gap-m); }
details.folded > summary {
  cursor: pointer;
  padding: var(--gap-s) var(--gap-l);
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  font-size: var(--text-small);
  color: var(--ink-quiet);
}
details.folded > summary::marker { color: var(--ink-faint); }

/* **Opened, the line is gone and the telegram is in its place.** The line was
   left above the open telegram until 2026-09-10, and it read as a click that
   opened a new box under the one clicked. Now the heading of the telegram
   takes the line's place and its triangle, and closes it again. */
details.folded[open] > summary { display: none; }
h2 > .fold {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
}
h2 > .fold:hover { filter: none; }
/* The triangle the line carried, at the line's size and in its colour. */
h2 > .fold::before {
  content: '▼';
  display: inline-block;
  margin-right: var(--gap-xs);
  font-size: var(--text-small);
  font-weight: 400;
  color: var(--ink-faint);
  vertical-align: middle;
}

.card {
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  padding: var(--gap-l);
  margin-bottom: var(--gap-l);
}
/* Descendant, not child. Two of these headings are built inside a wrapper div
   -- the addresses and the status byte -- so `.card > h3` missed them and the
   browser's own h3 showed through: 17.8px and near-black beside 15.2px and
   grey, which read as a deliberate hierarchy and was not one. Measured. */
.card h2, .telegram-head h2 { font-size: var(--text-lead); margin: 0 0 var(--gap-xs); font-weight: 600; }

/* The Readings heading and its two buttons on one line, the buttons at the
   right. The chosen one is filled, like a pressed key. */
.card-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--gap-s); }
.reading-rows { display: flex; gap: var(--gap-xs); }
/* A tip makes the pointer a question mark, and these are still buttons. */
.reading-rows button { padding: var(--gap-xs) var(--gap-m); font-size: var(--text-caption); cursor: pointer; }
.reading-rows button[aria-pressed="true"],
.reading-rows button[aria-pressed="true"]:hover { background: var(--accent); border-color: var(--accent); color: #fff; }

/* A telegram's name and its two copy buttons on one line, the buttons pushed to
   the right of it -- the shape the summary row's own buttons already have.

   `baseline`, not `center`: the heading carries a bottom margin and the button
   does not, so centring the pair on the box drew the button a few pixels high
   against the word beside it. On the baseline the two runs of text sit on one
   line, which is what the eye is reading them as. */
.telegram-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-m);
}
/* A squeezed button wraps its two words, so these must never be squeezed. They
   were held at their own width with `flex: none`, which did that and one thing
   more: a row that cannot shrink cannot wrap either, so on a phone it ran off
   the side. Measured at 375px, `Copy link` ended 41px past the edge and a copy's
   `Close` 52px past it, and the whole page scrolled sideways to reach them --
   worse than a table doing it inside its own box, which is the rule this page
   already keeps for its tables and its picture of the bytes.

   **Wrapping protects them instead, and protects them better.** The line above
   wraps, so a heading too long to share a line drops the buttons to their own
   rather than squeezing them; this row wraps, so buttons too many for one line
   take two. Neither can squeeze a button, which is what `flex: none` was for.
   Nothing moves above 640px, where they have always fitted one line. */
.telegram-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-s);
}


.card h3 { font-size: var(--text-body); margin: var(--gap-l) 0 var(--gap-s); font-weight: 600; color: var(--ink-quiet); }

/* The Readings card is one table, so every meter's columns line up, and each
   meter's name and status line are rows of it. They should look like the
   heading and the paragraph they were: no rule under them, no cell padding. */
table.readings td.heading,
table.readings td.status { padding: 0; border-bottom: none; }
table.readings td.status { padding-top: var(--gap-s); }

/* The last row of a data point's group. Heavier and darker than the line
   between two rows, so the groups are seen before the rows are. */
table.readings tr.group-end td { border-bottom: 2px solid var(--ink-faint); }

/* A button inside a table row, as tall as the text beside it. The form's
   height and padding would make its row twice as tall as the others. */
button.chart-open {
  height: auto;
  padding: 0 var(--gap-s);
  font-size: var(--text-caption);
  line-height: inherit;
}

/* ---------------------------------------------------------------- the chart */

dialog.chart {
  width: min(56rem, calc(100vw - 2 * var(--gap-l)));
  max-width: none;
  padding: var(--gap-l);
  border: 1px solid var(--edge);
  border-radius: 8px;
  background: var(--card);
  color: var(--ink);
}
dialog.chart::backdrop { background: rgb(0 0 0 / 0.35); }
dialog.chart h2 { font-size: var(--text-lead); margin: 0 0 var(--gap-xs); font-weight: 600; }

/* The four time ranges. The chosen one is filled, like a pressed key. */
.chart-ranges { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gap-xs); margin: var(--gap-m) 0; }
.chart-ranges-note { margin-left: var(--gap-s); font-size: var(--text-caption); color: var(--ink-faint); }
.chart-ranges button { padding: var(--gap-xs) var(--gap-m); font-size: var(--text-caption); }
.chart-ranges button[aria-pressed="true"],
.chart-ranges button[aria-pressed="true"]:hover { background: var(--accent); border-color: var(--accent); color: #fff; }

.chart-box { width: 100%; margin-bottom: var(--gap-m); }
.chart-box svg { display: block; }
.chart-label { font-family: var(--font-mono); font-size: var(--text-caption); fill: var(--ink-faint); }
.chart-grid { stroke: var(--edge); }
.chart-axis { stroke: var(--ink-faint); }
.chart-line { fill: none; stroke: var(--accent); stroke-width: 1.5; }
.chart-point { fill: var(--accent); }
.chart-point.differs { fill: var(--bad); }
/* A value the meter stored rather than what it reads now: a dashed line and a
   hollow point, so the two kinds are told apart without relying on colour. */
.chart-line.stored { stroke-dasharray: 5 4; }
.chart-point.stored { fill: var(--card); stroke: var(--accent); stroke-width: 1.5; }
.chart-point.stored.differs { fill: var(--card); stroke: var(--bad); }
.chart-legend { margin: 0 0 var(--gap-m); }
.chart-guide { stroke: var(--ink-faint); stroke-dasharray: 3 3; pointer-events: none; }
.chart-marker { fill: var(--card); stroke: var(--accent); stroke-width: 2; pointer-events: none; }
.chart-area { fill: transparent; }

/* The four numbers under the chart, two to a line. */
/* The figures under the chart, in a column of a meaning each: the latest
   reading and how many there are, the two extremes, and -- for a counter --
   how much it rose and how fast.

   **A column wraps whole or not at all**, which is why they are three lists
   in a row rather than one grid of six cells. A grid pairs its cells by the
   number of columns it happens to have, so the highest would sit above the
   lowest at three columns and above something else at two. `14rem` is the
   width below which a column gives up its place on the line and takes one of
   its own; three of them need 42rem and the dialog holds 56rem, so all three
   fit on a screen of ordinary width and fall to two, then one, on a narrow
   one. */
.chart-summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-xs) var(--gap-m);
  margin: 0 0 var(--gap-m);
  font-size: var(--text-small);
}
.chart-summary dl {
  flex: 1 1 14rem;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--gap-xs) var(--gap-m);
  margin: 0;
}
.chart-summary dt { color: var(--ink-faint); }
.chart-summary dd { margin: 0; }
.chart-actions { display: flex; justify-content: flex-end; }

/* The hidden row that holds the widest cell of each column, so that tables of
   one kind in different cards get the same widths (`widestRow` in app.js). It
   must keep its width and lose its height. The box inside each cell has no
   height, and the cell no padding or border, which takes the height; a box's
   width does not depend on its height, so the width stays. `visibility:
   hidden` keeps the width too, which `display: none` would not. */
tr.widest td {
  visibility: hidden;
  padding-top: 0;
  padding-bottom: 0;
  border: none;
}
tr.widest td > div { height: 0; overflow: hidden; }
.subtitle { margin: 0 0 var(--gap-m); color: var(--ink-quiet); font-size: var(--text-small); }
.subtitle code { font-family: var(--font-mono); }

.problem { color: var(--bad); font-size: var(--text-small); margin: 0 0 var(--gap-m); }

/* A telegram waiting for a key, which is not a fault: it is intact and nothing
   was given to open it with. Drawn in the accent rather than in `--bad`, so the
   red on this page keeps meaning "something is wrong with these bytes". */
.asking { color: var(--accent); font-size: var(--text-small); margin: 0 0 var(--gap-m); }

/* What the decoder itself said, under the sentence written for a reader. Kept
   small and grey: it is evidence rather than a message. */
.aside {
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  margin: calc(0px - var(--gap-s)) 0 var(--gap-m);
  white-space: pre-wrap;
}

/* ------------------------------------------------------------ the byte grid
 *
 * The picture. One cell per byte, sixteen to a row, each carrying the colour of
 * the field that claimed it -- and every byte is claimed by exactly one, which
 * is a property of the report rather than of this stylesheet.
 */

.region { margin-bottom: var(--gap-l); }

/* **The sentence under a map is two lines high, whatever it says, where the
   map is above the tables.** It changes as the pointer moves over a record's
   row, and a sentence that grew from one line to two pushed the table down a
   line -- so the row under the pointer changed, which changed the sentence
   again. A row with a Delete button in it could not be pressed. Found when the
   edited copy's buttons were driven in a browser (MBUS_TOOL_DESIGN.md
   §6sexies). Beside the tables, at the full width, the map is in a column of
   its own and moves nothing. Every byte still names its field in full on its
   own tooltip. */
@media screen and (max-width: 1639px) {
  .region [data-role="reading"] {
    height: 3em;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
  }
}
.region-name { font-size: var(--text-small); color: var(--ink-quiet); margin-bottom: var(--gap-s); }
.region-name .decrypted { color: var(--encrypted-ink); }

.grid {
  display: grid;
  grid-template-columns: auto repeat(16, 1fr);
  gap: 2px;
  font-family: var(--font-mono);
  font-size: var(--text-small);
  max-width: 720px;
}
.offset {
  color: var(--ink-faint);
  padding-right: var(--gap-s);
  text-align: right;
  font-size: var(--text-caption);
  align-self: center;
}
.byte {
  padding: 3px 0;
  text-align: center;
  border-radius: 3px;
  cursor: default;
  border: 1px solid transparent;
}
.byte.link      { background: var(--link);      color: var(--link-ink); }
.byte.header    { background: var(--header);    color: var(--header-ink); }
.byte.record    { background: var(--record);    color: var(--record-ink); }
.byte.data      { background: var(--record-alt); color: var(--record-ink); }
.byte.filler    { background: var(--filler);    color: var(--filler-ink); }
.byte.encrypted { background: var(--encrypted); color: var(--encrypted-ink); }
.byte.trailer   { background: var(--trailer);   color: var(--trailer-ink); }
.byte.unclaimed { background: var(--unclaimed); color: var(--unclaimed-ink); }
.byte.lit { border-color: currentColor; font-weight: 700; }

/* The Compare card's two telegrams, one line above the other. A byte that is
   the same in both is quiet, so the ones that differ are what is seen. The
   space under each second line keeps one pair apart from the next. */
.byte.same    { background: var(--filler);  color: var(--ink-faint); }
.byte.changed { background: var(--changed); color: var(--changed-ink); font-weight: 600; }
.pairs .byte.second { margin-bottom: var(--gap-s); }
.compare .row { margin-bottom: var(--gap-s); }
.compare tr.changed td { background: var(--changed-soft); }

/* The models that read one meter equally well, side by side: the reading on the
   left in the Readings table's own columns, then a column per candidate model.
   A row the candidates do not agree about gets the same band Compare uses for a
   row that changed -- one colour, one meaning: *this is where they differ*. */
.candidates tr.changed td { background: var(--changed-soft); }
/* The frame's maker, version and medium, above the readings and set apart
   from them by the line the Readings table draws under a group. */
.candidates tr.header-end td { border-bottom: 2px solid var(--ink-faint); }
/* The count and the matcher's verdict under the model's name, not beside it:
   run together on one line they read as one word (`heat3 of 3 · agrees`). */
.candidates th span.faint { display: block; font-weight: 400; text-transform: none; }
/* The button that draws the table without one model. Under the count, so a
   column heading is one thing read downwards: the name, what it read, and the
   way to be rid of it. */
.candidates th .drop-model { display: block; margin-top: var(--gap-xs); font-weight: 400; }
/* Room above the table for the picker, which is one control on a line of its
   own. */
.picker { margin: var(--gap-m) 0; }
.picker .choices { display: inline-block; width: auto; min-width: 18rem; max-width: 100%; }
.compare tr.same td { color: var(--ink-faint); }

.legend { display: flex; flex-wrap: wrap; gap: var(--gap-m); margin: var(--gap-m) 0; font-size: var(--text-caption); }
.legend span { display: inline-flex; align-items: center; gap: var(--gap-xs); color: var(--ink-quiet); }
.swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }

/* ---------------------------------------------------------------- the lists */

table { width: 100%; border-collapse: collapse; font-size: var(--text-small); }
th {
  text-align: left;
  font-weight: 600;
  color: var(--ink-quiet);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--edge);
  padding: var(--gap-xs) var(--gap-s) var(--gap-xs) 0;
}
td { padding: var(--gap-xs) var(--gap-s) var(--gap-xs) 0; border-bottom: 1px solid var(--edge); vertical-align: top; }
tr.selectable { cursor: default; }
tr.selectable:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
/* The row somebody is not pointing at, lit because the row they *are* pointing
   at is about it: a model's variable and the reading it was read from are the
   same thing seen from two sides, and they are two tables apart. The same
   accent as the hover and stronger, so the pair reads as one colour at two
   strengths -- which of the two is which is answered by where the pointer is.

   The field list beside the byte picture has always toggled this class and
   nothing ever drew it, so that half was a decoration until now. */
tr.lit { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.mono { font-family: var(--font-mono); }
/* A column holding a fact is as wide as the fact, so the sentence beside it
   gets the rest. Every one of them is pinned now: the one cell that had to be
   free to wrap was the Model table's whole locator, and that column is gone --
   the address is on the variable's tooltip and in the Readings row that lights
   beside it. `td.when` survives for `print.css`, which unpins the lot and has
   to spare a timestamp. */
td.mono { white-space: nowrap; }
/* For a run of text inside a cell rather than the cell itself: a date, which
   reads as two things when it is broken across two lines. */
.nowrap { white-space: nowrap; }

/* A cell of hex that lights its own bytes in the picture above when pointed at.
   Underlined rather than coloured: the colour in this table already means which
   kind of byte it is, and a second meaning on the same channel would be one
   meaning too many. */
/* The page's own tooltip, shown the moment something is pointed at rather than
   after whatever delay a browser gives a `title`. Fixed to the viewport and
   hung off body, since every table here sits in a scroll box that would clip
   it. `pointer-events: none` so it can never sit between the pointer and the
   thing being pointed at. */
/* Fixed to the window: it is placed with what `getBoundingClientRect` says,
   which is measured from the window. It was `absolute` until 2026-10-06, so on
   a page that was scrolled the tooltip was drawn that far below its cell. */
.tip {
  position: fixed;
  z-index: 10;
  max-width: min(46ch, calc(100vw - 16px));
  padding: var(--gap-xs) var(--gap-s);
  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; }

/* Anything that has one says so, quietly. */
[data-tip] { cursor: help; }

td.points { text-decoration: underline dotted var(--ink-faint); text-underline-offset: 3px; }
/* A value whose words are in its tooltip: a date the page knows more about. */
.noted { text-decoration: underline dotted var(--ink-faint); text-underline-offset: 3px; }
td.points:hover { text-decoration-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.quiet { color: var(--ink-quiet); }
.faint { color: var(--ink-faint); font-size: var(--text-caption); }
.scroll { overflow-x: auto; }

/* The kind of a record used to be a coloured chip in the value column --
   `reading`, `time`, `register`, `other`. The quantity column says the same
   thing better now that it carries the name a locator is written in: `Date` is
   a date and `ErrorFlags` is the alarms register, so the chip repeated the
   neighbouring cell in a second colour. */

.bits { display: flex; flex-wrap: wrap; gap: var(--gap-s); margin: 0 0 var(--gap-m); padding: 0; list-style: none; }
.bit {
  border: 1px solid var(--edge);
  border-radius: 6px;
  padding: var(--gap-xs) var(--gap-s);
  font-size: var(--text-caption);
  color: var(--ink-faint);
}
.bit.set { border-color: var(--bad); color: var(--bad); font-weight: 600; }
/* A past fault: the meter kept the bit after the fault stopped. Set, but not
   a reason to visit, so it is not red. */
.bit.past { border-style: dashed; color: var(--ink-quiet); }
.bit .mask { font-family: var(--font-mono); }

.notes { margin: 0; padding-left: var(--gap-l); color: var(--ink-quiet); font-size: var(--text-small); }

/* The model that fits one meter. The serial and the model name are an `h3`
   and the count below it a subtitle, which is the shape Readings already has
   -- so the two sections read as the same kind of thing rather than as two
   inventions. Nothing here draws a border of its own: the headings separate
   the meters, as they do up there. */
.model > .faint.mono { margin-bottom: var(--gap-xs); }

/* The address a model marks as the value its counter froze, rather than what
   it reads now. A badge and not a column: one row in a table of four carries
   it, and a column would be empty on the rest. The class is named for the
   concept and the badge reads `reset`, which is the word every other screen
   uses for the event. */
.reset-value {
  margin-left: var(--gap-s);
  font-family: var(--font);
  font-size: var(--text-caption);
  padding: 1px 6px;
  border-radius: 10px;
  background: var(--filler);
  color: var(--filler-ink);
}
/* Beside the value it is about, and a mark rather than the sentence: the
   sentence is a line and a half, and this table's rows are one value each. It
   is the tooltip instead, which is why the cursor says there is one.

   The ! is drawn, not typed. A typed ! sits where the font puts it: it stands
   on the baseline and leaves the room for descenders empty, so it was always
   a little high, by an amount that changed 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. */
.refused {
  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);
}
.refused::after {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  background: var(--page);
  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;
}

/* Sixteen bytes to a row at every width, because the offsets down the left are
   only useful if a row is always the same length. On a narrow screen the grid
   scrolls inside its own box rather than reflowing to eight, which would make
   the picture a different shape on a phone from the one in the manual. */
/* A way into a long paste: one line per telegram, no bullet, the model or what
   the frame says it is beside the link. */
/* Two columns, so eleven descriptions start at one place rather than stepping
   right when the ordinals reach two digits. `display: contents` on the row is
   what puts the link and its description into the grid: the columns are shared
   by every line only if they are the *list's*, not each item's. */
.index {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--gap-m);
  row-gap: 4px;
}
.index li { display: contents; }
/* The sections at the foot of the page, set apart from the telegrams above
   them: they are about the whole paste rather than about one meter. */
.index.whole { margin-top: var(--gap-m); }
.index a { font-weight: 600; }

/* The filter over a long capture, between the heading and the lines it
   narrows. The count under it says how many are left, and is gone when
   nothing is hidden. */
.filter { margin-bottom: var(--gap-m); }
.filter .subtitle { margin: var(--gap-s) 0 0; }
/* Wide enough for the longest state and its count, "No key here opens them
   (123)", which the form's `.narrow` is not. */
.filter .state { width: 240px; }
@media screen and (max-width: 640px) {
  .filter .state { width: 100%; }
}

/* Room above anything the index jumped to, so it does not sit flush against
   the top of the window. */
.telegram, .card { scroll-margin-top: var(--gap-m); }

/* **One telegram is one card here, and two cards where there is room.**
   Narrow, the card is the telegram itself and its three parts -- heading,
   picture, tables -- are plain boxes inside it. So the heading is inside a card
   rather than floating above one, and the picture is under the line saying how
   many bytes there are instead of several screens below it.

   The three are separate elements only so that the wide layout can put them in
   two columns; here they are simply stacked, in the order they are written. */
.telegram {
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  padding: var(--gap-l);
  margin-bottom: var(--gap-l);
}
.telegram > .card {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  margin-bottom: 0;
}
/* The gap between the picture and the tables under it, which the card each of
   them used to be gave them for free. */
.telegram > .telegram-figure { margin-bottom: var(--gap-l); }

/* The picture beside the tables rather than above them, **where there is room
   for both**.

   **1640px is the page at its full width**: 1600 of content and the body's own
   20 on each side. Below it `main` is narrower than 1600, so splitting would
   take the width out of the tables -- and the tables are what somebody reads.
   At 1280 the left column came out at 629px and "What it is" wrapped on nearly
   every row. So: two columns only where both can have their own width, and one
   column everywhere else.

   In one column the order is the DOM's -- heading, picture, tables -- so the
   picture is under the line that says how many bytes there are. The placement
   below is what moves it right, not `order`, so the reading order never
   changes.

   `sticky` inside the telegram's own block is the whole of "until the next
   telegram": a sticky element cannot leave its containing block, so it follows
   its own telegram down the page and stops where the pair ends.

   `align-self: start` and a `max-height`, because a sticky box stretched to the
   row's height has nothing to stick to, and a map taller than the screen would
   scroll its own bottom out of reach.

   **`screen and`, or a printed page comes out in two columns it has no room
   for.** Safari lays a page out for paper and still answers a width query with
   the window's width, so a telegram printed from a window wider than 1640px got
   this grid on a page 180mm across: the map took its 36rem, the column beside it
   was left about 60px, and every sentence in it came out one word to a line.
   Reported from a printed PDF on 2026-09-03 and reproduced by forcing this
   query on. A width is a fact about a window, so it is asked about a screen. */
@media screen and (min-width: 1640px) {
  /* The single card is taken apart into two, and the border it drew goes to
     the two halves below. */
  .telegram {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 36rem);
    column-gap: var(--gap-l);
    align-items: start;
    background: none;
    border: 0;
    padding: 0;
  }

  /* **The left card, drawn by the grid rather than by an element.** The heading
     and the tables are two boxes in one column, and a card has to be drawn
     around *both* -- so this is an empty grid item behind them, spanning the two
     rows. It is the first child, so everything else paints over it and no
     stacking order has to be declared.

     The alternative was a heading floating above the card, which is what this
     replaced. */
  .telegram::before {
    content: '';
    grid-column: 1;
    grid-row: 1 / 3;
    /* **`stretch`, against the row's `align-items: start`.** An empty box is
       0px tall unless something stretches it, and `start` is on the row so
       that the map card can stick. Measured at 0px, which drew the card as a
       hairline and left the tables sitting on the page. */
    align-self: stretch;
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: 8px;
  }
  /* The card's padding, split between the two boxes that sit on it -- and its
     border too, since they are placed on the grid rather than inside it, so
     `--gap-l` alone left every word 1px to the left of where the summary card
     above puts its own. Measured: 63 against 64.

     `.telegram > .telegram-main`, not `.telegram-main`: the rule stripping the
     cards above is `.telegram > .card`, which is the more specific of the two
     and wins inside a media query as much as outside one. The heading is not a
     `.card`, so it needs no such help -- and that is exactly why only the
     tables came out unpadded. */
  .telegram-head {
    grid-column: 1;
    grid-row: 1;
    padding: calc(var(--gap-l) + 1px) calc(var(--gap-l) + 1px) 0;
  }
  .telegram > .telegram-main {
    grid-column: 1;
    grid-row: 2;
    padding: 0 calc(var(--gap-l) + 1px) calc(var(--gap-l) + 1px);
    min-width: 0;
  }

  /* The right card is a card again, and starts level with the heading rather
     than below it. */
  .telegram > .telegram-figure {
    grid-column: 2;
    grid-row: 1 / 3;
    background: var(--card);
    border: 1px solid var(--edge);
    border-radius: 8px;
    padding: var(--gap-l);
    margin-bottom: 0;
    position: sticky;
    top: var(--gap-m);
    align-self: start;
    max-height: calc(100vh - 2 * var(--gap-m));
    overflow: auto;
  }
}

/* A phone, and `screen and` for the reason the block above gives: printed from
   a narrow window this held the byte map to a 460px minimum, which is wider
   than the paper leaves it. */
@media screen and (max-width: 640px) {
  body { padding: var(--gap-m); }
  .grid { font-size: var(--text-caption); min-width: 460px; }
}

/* A record whose address bytes are not on the wire: the two cells that name a
   DIF and a VIF are shown as what they mean rather than what was read. */
tr.inferred td.not-on-wire {
  font-style: italic;
  opacity: 0.65;
}

/* ------------------------------------------------------------ an edited copy */

/* A copy that can be changed is drawn like any telegram, with the accent on
   its edge, so it is not read as one the meter sent. The wide layout draws the
   card on `::before` and the picture's card separately, so both are named. */
.telegram.edited,
.telegram.edited::before,
.telegram.edited > .telegram-figure { border-color: var(--accent); }

/* A telegram described in words, under the heading. Ordinary text, kept to a
   line length that can be read. */
.description { max-width: 70ch; margin: 0 0 var(--gap-m); }
.description h3 { margin: 0 0 var(--gap-s); }
.description p, .description ul { margin: 0 0 var(--gap-s); }
.description ul { padding-left: var(--gap-l); }

/* What was changed so far, one line each, under the heading. */
ol.changes {
  margin: var(--gap-s) 0 0;
  padding-left: var(--gap-l);
  font-size: var(--text-small);
  color: var(--ink-quiet);
}

/* The Edit button at the end of a copy's record row, as tall as the text
   beside it, like the chart button. */
button.inline {
  height: auto;
  padding: 0 var(--gap-s);
  font-size: var(--text-caption);
  line-height: inherit;
}

/* The Edit column stays at the right edge of the table's scroll box. The table
   is wider than its box at most widths, and a last column that scrolled with
   it was off the screen. The cell needs a ground of its own, or the columns
   under it show through; the row's tint is mixed into that ground, so a row
   under the pointer is still one colour to its end. */
td.edit-cell,
th.edit-cell {
  position: sticky;
  right: 0;
  background: var(--card);
  padding-left: var(--gap-s);
  padding-right: 0;
}
tr.selectable:hover td.edit-cell { background: color-mix(in srgb, var(--accent) 8%, var(--card)); }
tr.lit td.edit-cell { background: color-mix(in srgb, var(--accent) 18%, var(--card)); }

/* The Data records heading of a copy, with Add a record at the end of its
   line. The line takes the heading's margins, so the table under it starts
   where it does on any other telegram. */
.records-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-m);
  margin: var(--gap-l) 0 var(--gap-s);
}
.card .records-head h3 { margin: 0; }

/* The two dialogs of a copy: one record, and a new record. The chart's box,
   narrower. */
dialog.edit-dialog {
  width: min(34rem, calc(100vw - 2 * var(--gap-l)));
  max-width: none;
  padding: var(--gap-l);
  border: 1px solid var(--edge);
  border-radius: 8px;
  background: var(--card);
  color: var(--ink);
}
dialog.edit-dialog::backdrop { background: rgb(0 0 0 / 0.35); }
dialog.edit-dialog h2 { font-size: var(--text-lead); margin: 0 0 var(--gap-xs); font-weight: 600; }
dialog.edit-dialog .subtitle { overflow-wrap: anywhere; }
.edit-dialog-body { margin: var(--gap-m) 0; }
.edit-dialog-body .faint { margin: 0; }
/* The line above the two fields of an upload: it is followed by a field, so
   it keeps a gap the other lines here do not need. */
.edit-dialog-body .upload-about { margin-bottom: var(--gap-m); }
.edit-dialog-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--gap-m);
}
.edit-dialog-grid .wide { grid-column: 1 / -1; }
.edit-dialog-actions { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: var(--gap-s); }
/* Delete is at the other end of the row from the button that saves, and red,
   because it removes a record. Undo on the copy brings the record back. */
.edit-dialog-actions .danger { margin-right: auto; color: var(--bad); }
.edit-dialog-actions .danger:hover { border-color: var(--bad); }
@media screen and (max-width: 640px) {
  .edit-dialog-grid { grid-template-columns: minmax(0, 1fr); }
}

/* The Export dialog: the line about two of its fields, the two columns that
   can be added, and how many rows the file will have. */
.edit-dialog-body .export-about { margin-bottom: var(--gap-m); }
.export-columns { display: flex; flex-wrap: wrap; gap: var(--gap-s) var(--gap-l); margin-bottom: var(--gap-m); }
.export-columns .check { display: inline-flex; align-items: center; gap: var(--gap-xs); cursor: pointer; }
.export-columns input[type=checkbox] { width: 1rem; height: 1rem; margin: 0; accent-color: var(--accent); }
.edit-dialog-grid [hidden] { display: none; }

/* A part of a copy's dialog with a Set button of its own: the access number,
   an address, or where a record sits. */
.header-part { padding-top: var(--gap-m); border-top: 1px solid var(--edge); margin-top: var(--gap-m); }
.header-part:first-child { padding-top: 0; border-top: 0; margin-top: 0; }
.header-part h3 { margin: 0 0 var(--gap-s); }
.header-part .faint { margin: var(--gap-xs) 0 var(--gap-s); }

/* The buttons at the end of a copy's Faults line. */
.edit-bars { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--gap-s); }

/* The Status byte and Alarms register dialogs: one line for each bit, the mask
   before its words. */
.status-bits { display: grid; gap: var(--gap-xs); margin-bottom: var(--gap-m); }
.status-bits .check { display: flex; align-items: baseline; gap: var(--gap-xs); cursor: pointer; }
.status-bits input[type=checkbox] { width: 1rem; height: 1rem; margin: 0; align-self: center; accent-color: var(--accent); }
.status-bits .mask { font-family: var(--font-mono); }
/* A register's current bits and its past ones, each under its own heading. */
fieldset.status-bits { border: 0; padding: 0; margin-inline: 0; min-inline-size: 0; }
fieldset.status-bits legend { padding: 0; margin-bottom: var(--gap-xs); font-weight: 600; font-size: var(--text-small); }

/* ==== 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 ==== */

/* A box and its sentence. The box is 1rem, like the boxes of the export
   columns, and it is pushed down by half of what the line is taller than the
   box, so its centre is the centre of the first line of the sentence. The line
   height is said here, and not left to the page, because that sum depends on
   it. The box takes the font size of the row: a form control does not, by
   default, and `em` would then count in a smaller size than the sentence's. */
/* The report is wider than the other dialogs here: it is a whole form. */
dialog.report-dialog { width: min(44rem, calc(100vw - 2 * var(--gap-l))); }
/* A note that is followed by a field. The notes of a dialog have no margin, so
   without this the label of the next field sits against the note and is lost. */
.report-dialog .report-note { margin-bottom: var(--gap-m); }
.report-check {
  display: flex;
  gap: var(--gap-s);
  align-items: flex-start;
  margin-top: var(--gap-s);
  /* The size of the labels of the fields, so a sentence is not larger than
     the name of a field. */
  font-size: var(--text-small);
  line-height: 1.5;
}
.report-check input[type=checkbox] {
  flex: none;
  width: 1rem;
  height: 1rem;
  margin: calc((1.5em - 1rem) / 2) 0 0;
  font-size: inherit;
  accent-color: var(--accent);
}
/* Out of sight and out of the way for a person, still in the form for a program. */
.report-trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
