/*
 * One page, one stylesheet.
 *
 * The same vocabulary as the two benches next door -- the same tokens, the same
 * three scales, the same rule about the face. Sizes, dims and gaps are 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 an ICCID, an
 * address and an IMEI are all 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;
  /* The accent, darker: a bar under the pointer. Black was too heavy beside
     the blue. */
  --accent-deep: #0d3c78;
  --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; }
/* `hidden` is how the page hides things (`show` in app.js), and any rule that
   sets `display` beats it. The forecast's key is `display: flex`, so it stayed
   on screen under "Shown once this SIM has 3 months of traffic on record". */
[hidden] { display: none !important; }

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

/*
 * **This bench is wider than the two next door, on purpose.**
 *
 * They cap at 1400px because what they show is prose and a few columns, and a
 * long line of prose is hard to read. This shows an account: as many columns as
 * the provider sends, which was twelve on the first real one. Capping it would
 * push half of them behind a sideways scroll on a screen with room for them.
 *
 * The reading width still applies to the prose: `.lede` keeps its own 70ch.
 */
main, .masthead { max-width: none; 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`. */
.manual { margin-left: auto; white-space: nowrap; }
.manual[hidden] { display: none; }
.manual { color: var(--accent); text-decoration: none; }
.manual:hover { text-decoration: underline; }
/* The title, and on a SIM's page that SIM's name and buttons beside it. On a
   narrow window the name goes to a line of its own under the title. */
.masthead-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--gap-s) var(--gap-xl); }
.masthead-row:has(> .detail-head:not([hidden])) { margin-bottom: var(--gap-l); }
.masthead-row:has(> .detail-head:not([hidden])) h1 { margin: 0; }
.lede { margin: 0 0 var(--gap-l); color: var(--ink-quiet); max-width: 70ch; }

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

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

input[type=text], input[type=password], input[type=search], 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);
  height: var(--control);
}
select { font-family: var(--font); cursor: pointer; }

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

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

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

.privacy {
  margin: var(--gap-m) 0 0;
  font-size: var(--text-caption);
  color: var(--ink-faint);
  max-width: 70ch;
}

/*
 * The sign-in, which is the one narrow thing on a page that is otherwise as
 * wide as the screen.
 *
 * **Two fields and a button do not want the width of an account.** Stretched
 * across a large screen they sit a hand's width apart with nothing between
 * them, and the eye has to travel the whole page to fill two boxes. So this
 * column is sized by what is in it -- a field, then a field, then the button
 * -- and centred, while the table next door still gets everything.
 *
 * The heading and the lede join it, because a left-aligned title over a
 * centred card reads as a mistake. They go back to the full width the moment
 * somebody signs in, which is what `body.signed-in` says.
 */
#signin, #signin-error { max-width: 24rem; margin-left: auto; margin-right: auto; }
#signin .field:last-of-type { margin-bottom: var(--gap-l); }
button.wide { width: 100%; }

body:not(.signed-in) .masthead {
  max-width: 24rem;
  margin: 0 auto;
  padding: 0;
}

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

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

/* ------------------------------------------------------------- the account */

.summary {
  display: flex;
  gap: var(--gap-xl);
  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;
}

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

.heading { font-size: var(--text-lead); font-weight: 600; margin: var(--gap-xl) 0 var(--gap-m); }
.panel .heading { margin-top: 0; }
/* **A chart's card puts its title, its choice and its figures on one line**,
   so the chart starts as high as it can. On a narrow card the line wraps, and
   the title stays first. */
.panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-m) var(--gap-xl);
}
.panel-head .heading { margin: 0; }
.panel-head .spacer { flex: 1 1 auto; }
.panel-head .key { margin: 0; }

/*
 * The account, as many columns wide as the provider sends.
 *
 * The box scrolls rather than the page, and the head stays put while it does: a
 * label is the widest thing here and the ICCID beside it is the column a reader
 * loses first.
 */
.sheet {
  background: var(--card);
  border: 1px solid var(--edge);
  border-radius: 8px;
  overflow: auto;
  max-height: 70vh;
}
/*
 * **The list is read by scrolling the page, not a box inside it**, and its head
 * stays at the top of the window while it does.
 *
 * Fifty rows are taller than any window, so the box above always had a
 * scrollbar of its own beside the page's, and people asked for it to go
 * (2026-09-10). Taking only its height away kept the box a *scrolling* box,
 * and a sticky head sticks to the nearest scrolling box -- so the head left
 * with the rows, and they asked for it back the same morning.
 *
 * So this box does not scroll at all. `visible` is what makes the window the
 * thing the head sticks to. `fit-content` with a floor of the full width is
 * what keeps a wide account whole: the table is as wide as the page while its
 * columns fit, the label column wrapping first as it always has, and grows
 * past it only when they cannot -- the card growing with it and the page
 * scrolling sideways, rather than a column being cut off.
 */
#account > .sheet {
  overflow: visible;
  max-height: none;
  width: fit-content;
  min-width: 100%;
}
/* Nothing clips this box now, so the head's own ground would paint over the
   card's two top corners. It carries them instead. */
#account > .sheet .rows th:first-child { border-top-left-radius: 7px; }
#account > .sheet .rows th:last-child { border-top-right-radius: 7px; }
/* A phone: the label and the state's mark, and no other column (`SMALL` in
   app.js). The label gives up its minimum width, which alone was wider than
   the screen. */
@media screen and (max-width: 640px) {
  #sims th:not(.small-kept), #sims td:not(.small-kept) { display: none; }
  #sims td.label-cell { min-width: 0; }
  /* The state is the last column left, so it carries the card's corner. */
  #account > .sheet .rows th.small-kept:not(:first-child) { border-top-right-radius: 7px; }
}
/* The line under the head, drawn by the head. In a table whose borders are
   collapsed a border belongs to the table rather than to the cell, so a stuck
   head can leave its line behind and let the rows show through under it. */
#account > .sheet .rows th { box-shadow: inset 0 -1px 0 var(--edge); }

/*
 * A sheet inside a panel is not a second card -- the panel is the card.
 *
 * It keeps what it is for, which is the scrolling and the head that stays put,
 * and gives up the border, the ground and the corners it would otherwise draw
 * a second time inside them. It reaches the panel's two side edges, so the
 * table's cell padding is the only inset there, exactly as where a sheet
 * stands on its own.
 *
 * **The sides only, and not the bottom.** It reached the bottom edge too for a
 * day, with a negative margin -- and that margin pulled whatever came after it
 * up by the same amount, so the sentence that replaces an empty history was
 * drawn over the table's head. The panel's own padding below the last row is
 * what every other card on the page has anyway.
 */
.panel > .sheet {
  background: none;
  border: none;
  border-radius: 0;
  margin: 0 calc(-1 * var(--gap-l));
  width: auto;
}

.rows {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-small);
}
.rows th, .rows td {
  text-align: left;
  padding: var(--gap-s) var(--gap-m);
  border-bottom: 1px solid var(--edge);
  white-space: nowrap;
}
.rows th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--card);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  font-weight: 600;
  cursor: pointer;
  user-select: none;
}
.rows th:hover { color: var(--accent); }
.rows th .arrow { color: var(--accent); }
.rows tbody tr:last-child td { border-bottom: none; }
.rows tbody tr { cursor: pointer; }
.rows tbody tr:hover { background: var(--page); }

/* A machine-produced fact in the mono face; a label is prose somebody typed,
   and it is the one column allowed to wrap. */
.rows td.mono { font-family: var(--font-mono); }
.rows td.label-cell { white-space: normal; min-width: 22rem; }
/* The label is a link so the browser can open it in a tab, but it is not
   painted like one: the whole row is already the target, and a blue underline
   on every row would read as a column of them. */
.rows td.label-cell a { color: inherit; text-decoration: none; }
.rows tbody tr:hover td.label-cell a { text-decoration: underline; }
.rows td.blank { color: var(--ink-faint); }

/* 1NCE's own three words for what a SIM has left. The one that means somebody
   has to act is the one that is coloured. */
.rows td.is-exceeded, .is-exceeded { color: var(--bad); }
.rows td.is-low, .is-low { color: var(--waiting); }
.rows td.is-disabled { color: var(--ink-faint); }

/* A SIM's state in the list: a filled dot when enabled, a hollow ring when
   disabled. The shape carries it as well as the colour. */
.state {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  vertical-align: middle;
}
.state.is-on { background: var(--good); }
.state.is-off { border: 1.5px solid var(--ink-faint); }
/* A cross, from two bars: no icon font on this page, and none needed. */
.state.is-stopped { position: relative; border-radius: 0; }
.state.is-stopped::before,
.state.is-stopped::after {
  content: '';
  position: absolute;
  left: 4px;
  top: -1px;
  width: 2px;
  height: 12px;
  background: var(--bad);
  transform: rotate(45deg);
}
.state.is-stopped::after { transform: rotate(-45deg); }
/* A history row's severity: a grey ring with an i, an amber triangle, a red
   disc. The shapes differ as well as the colours, as with the states. */
.severity {
  display: inline-grid;
  place-items: center;
  width: 16px;
  height: 16px;
  vertical-align: middle;
  font: 700 11px/1 var(--font);
  color: var(--card);
}
.severity.is-info { border: 1.5px solid var(--ink-faint); border-radius: 50%; color: var(--ink-faint); }
.severity.is-info::before { content: 'i'; }
.severity.is-warning { background: var(--waiting); clip-path: polygon(50% 0, 100% 100%, 0 100%); padding-top: 4px; }
.severity.is-critical { background: var(--bad); border-radius: 50%; }
.severity.is-warning::before, .severity.is-critical::before { content: '!'; }
/* A severity this page does not know: its first letter, and no shape. */
.severity.is-other { color: var(--ink-quiet); font-size: var(--text-small); }
/* A cell holding only a mark asks for no width beyond it and its heading. The
   table is as wide as the page, so without this the spare room is shared out
   and the mark sits in a column as wide as the word it replaced. */
.rows td.marked { width: 1%; }

/* Read by a screen reader and carried by a copy, and not drawn. */
.unseen {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.pager { display: flex; gap: var(--gap-m); align-items: center; margin-top: var(--gap-m); }
.pager span { color: var(--ink-quiet); font-size: var(--text-small); }

/* ------------------------------------------------------------------ one SIM */

/* The heading takes the room and the way back sits at the right edge, where it
   is out of the way of the thing somebody came to read. It is on the title's
   line (`.masthead-row`), so it takes the rest of that line; below 20rem it
   goes to a line of its own. */
.detail-head {
  flex: 1 1 20rem;
  min-width: 0;
  align-items: center;
  flex-wrap: nowrap;
}
/* Sized to the name, so the pencil sits against it. The spacer after them is
   what pushes the two buttons to the right edge. */
.detail-head h2 { font-size: var(--text-lead); font-weight: 600; margin: 0; flex: 0 1 auto; min-width: 0; }
.detail-head button { flex: 0 0 auto; }

/* `stretch`, not `flex-start`: the two short boxes beside the SIM's own fields
   are meant to reach the same depth as it, so the row has one bottom edge
   rather than a tall card with a ragged stack of small ones next to it. On a
   narrow window each is on a line of its own and there is nothing to match, so
   this costs nothing there. */
/* The bottom margin is the gap every panel keeps below it. Its children give
   theirs up so the gap inside the row is `gap` alone, and the row has to put
   it back: Traffic sat against it once its heading moved inside its own card,
   because that heading's top margin was the only thing that had kept them
   apart. */
.panels { display: flex; flex-wrap: wrap; gap: var(--gap-l); align-items: stretch; margin-bottom: var(--gap-l); }
.panels > .panel, .panels > .column { flex: 1 1 22rem; margin-bottom: 0; }
/* Quota and Network are each short. Stacked in one column they read as one
   side of the page rather than as two loose boxes. */
.column { display: flex; flex-direction: column; gap: var(--gap-l); }
/* Sharing whatever height the row has: two boxes of one size read as a pair,
   and the alternative is one of them carrying all the empty room. */
.column .panel { margin-bottom: 0; flex: 1 1 0; }

/* The name takes the room; the spacer pushes the two buttons to the right
   edge, where the way back already was. */
.detail-head .spacer { flex: 1 1 auto; }
/* A phone: the buttons go to a line of their own under the name. Beside it they
   left the name a few letters wide, one word to a line. The spacer takes a
   whole line to force the break, so the row gap is zero and the buttons keep
   the space above them themselves. */
@media screen and (max-width: 640px) {
  .detail-head { flex-wrap: wrap; row-gap: 0; }
  .detail-head .spacer { flex-basis: 100%; }
  .detail-head #switch, .detail-head #back { margin-top: var(--gap-m); }
}

/* A pencil beside the name, sized to the text rather than to a button: it is
   an affordance on the heading and not a third control competing with the two
   at the other end. */
button.pencil {
  background: none;
  border: none;
  color: var(--ink-faint);
  height: auto;
  padding: 0 var(--gap-xs);
  font-size: var(--text-lead);
  line-height: 1;
}
button.pencil:hover { filter: none; color: var(--accent); }

dialog {
  border: 1px solid var(--edge);
  border-radius: 8px;
  padding: var(--gap-l);
  max-width: 34rem;
  width: calc(100% - 2 * var(--gap-l));
  color: var(--ink);
  background: var(--card);
}
/* The list of columns: a box to tick, the name, and two arrows for the order. */
ul.columns {
  list-style: none;
  margin: 0 0 var(--gap-l);
  padding: 0;
  max-height: 60vh;
  overflow-y: auto;
  border: 1px solid var(--edge);
  border-radius: 6px;
}
ul.columns li {
  display: flex;
  align-items: center;
  gap: var(--gap-s);
  padding: var(--gap-xs) var(--gap-m);
  border-bottom: 1px solid var(--edge);
}
ul.columns li:last-child { border-bottom: none; }
ul.columns .grip { color: var(--ink-faint); cursor: grab; user-select: none; }
ul.columns li.is-dragged { opacity: 0.4; }
ul.columns li.drop-before { box-shadow: inset 0 2px 0 var(--accent); }
ul.columns li.drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
ul.columns label {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--gap-m);
  font-size: var(--text-small);
  cursor: pointer;
}
ul.columns li.is-hidden label { color: var(--ink-faint); }
ul.columns input[type=checkbox] { accent-color: var(--accent); margin: 0; }
ul.columns button.quiet { padding: 0 var(--gap-m); }

dialog::backdrop { background: rgb(23 25 28 / 40%); }
dialog .heading { margin-top: 0; }
dialog .privacy { margin-top: 0; margin-bottom: var(--gap-m); }
dialog .field { margin-bottom: var(--gap-l); }
dialog .refusal { margin-bottom: var(--gap-m); }

/* The provider's own sentence under the number it belongs to. Quiet, because
   the number is the thing being read and this is what 1NCE calls it. */
dd.quiet-line {
  color: var(--ink-faint);
  font-family: var(--font);
  font-size: var(--text-caption);
  margin-top: calc(-1 * var(--gap-xs));
}
dd.quiet-line.is-exceeded { color: var(--bad); }
dd.quiet-line.is-low { color: var(--waiting); }

/* ----------------------------------------------------------------- traffic */

/* Three buttons that are one choice, so they read as a switch rather than as
   three things that might each do something. */
.choice { display: flex; }
/* A label is two words at most ("12 months") and is never split over two
   lines: on a phone the traffic chart's four split into "3 / days". */
.choice button {
  white-space: nowrap;
  border-radius: 0;
  border: 1px solid var(--edge);
  background: var(--card);
  color: var(--accent);
  margin-left: -1px;
}
.choice button:first-child { border-radius: 6px 0 0 6px; margin-left: 0; }
.choice button:last-child { border-radius: 0 6px 6px 0; }
.choice button.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.choice button:hover { filter: none; }
.choice button:not(.on):hover { border-color: var(--accent); z-index: 1; }

/* The traffic chart has four choices, and on a phone they are about 60px too
   wide for the card. Two rows of two, with the corners moved to match. */
@media screen and (max-width: 640px) {
  #every { display: grid; grid-template-columns: 1fr 1fr; }
  #every button { margin-left: 0; }
  #every button:nth-child(even) { margin-left: -1px; }
  #every button:nth-child(n + 3) { margin-top: -1px; }
  #every button:nth-child(1) { border-radius: 6px 0 0 0; }
  #every button:nth-child(2) { border-radius: 0 6px 0 0; }
  #every button:nth-child(3) { border-radius: 0 0 0 6px; }
  #every button:nth-child(4) { border-radius: 0 0 6px 0; }
}

/* The chart is one inline SVG this page draws itself: no bundler, no library,
   and nothing to keep in step. It scales to its box rather than being measured
   -- a chart drawn from a measurement is a chart that is empty whenever the box
   is hidden when it is drawn. */
/* **It fits the box: no scrollbar, and nothing stretched.** The width is
   measured and the bars are laid out inside it, so a narrow window gets
   narrower bars and fewer labels rather than a sideways scroll or a picture
   squeezed until no label can be read. */
/* The data stream's own state, under the chart: an offer to set one up, or
   the credential to paste, shown once. Set apart from the chart because it is
   about the *stream* rather than about this SIM. */
.offer {
  margin-top: var(--gap-l);
  padding-top: var(--gap-l);
  border-top: 1px solid var(--edge);
}
.offer dd { user-select: all; }

#chart { margin-top: var(--gap-m); }
#chart svg { display: block; }
#chart .bar, .plot .bar { fill: var(--accent); }
#chart .bar:hover, .plot .bar:hover { fill: var(--accent-deep); }
#chart .axis, .plot .axis { stroke: var(--edge); stroke-width: 1; }
#chart .axis.faint, .plot .axis.faint { stroke-dasharray: 3 4; }
/* The horizontal rules, one per round step up the scale. Dashed and paler than
   the axis: they are there to read a bar's height against, not to be looked
   at. */
#chart .grid, .plot .grid { stroke: var(--edge); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.7; }
#chart .tick, .plot .tick { fill: var(--ink-faint); font-size: 10px; font-family: var(--font); }
/* The first hour of a day, or the first day of a month: the label that says
   which one, rather than which number within it. */
#chart .tick.strong, .plot .tick.strong { fill: var(--ink-quiet); font-weight: 600; }
/*
 * The statistics: two pies, two bar charts and a forecast.
 *
 * The pies share five colours for the five longest networks and two greys --
 * a darker one for "the rest", a light one for time nobody named or time not
 * connected. A sixth colour is one nobody can tell from the fifth.
 */
.plot { margin-top: var(--gap-m); overflow: hidden; }

/* **Each chart's room is kept while it is being read**, so the page does not
   grow under the reader as the answers arrive on a slow connection. Each
   height is what the chart measures once drawn (app.js): the traffic chart is
   `CHART` (14 + 300 + 48), a card of days 218 and its "From ... to today"
   line, a pie 160 and that line, the forecast 14 + 220 + 30. A card that
   draws a sentence instead keeps the same room, which is the point: one SIM
   and the next have cards of one size. */
#chart { min-height: 362px; }
/* "reading…" inside the chart's box, and not pushing it down by its margin. */
#chart > .empty { margin: 0; }
#stat-connected, #stat-operators { min-height: 190px; }
#stat-month, #stat-week { min-height: 248px; }
#forecast { min-height: 264px; }
/* A note under a chart is one line, and empty until its answer arrives. */
#traffic-note, #forecast-note { min-height: 1lh; }
/* The forecast's key keeps its room until there is a forecast. Not `.unseen`,
   which is the screen-reader class above and makes a thing 1px. */
.key.not-yet { visibility: hidden; }
/* A phone: a note is two lines. The legend goes under the pie, and its length
   depends on how many operators the SIM used; this is a SIM that used three
   and a part with no name. The forecast's key is three lines. */
@media screen and (max-width: 640px) {
  #traffic-note, #forecast-note { min-height: 2lh; }
  #stat-connected, #stat-operators { min-height: 316px; }
}
.plot svg { display: block; }
/* A chart is drawn to the width of its card, so a card must not take its width
   from the chart: two cards side by side would come out unequal, the one whose
   chart was drawn wider pushing the other. */
.panels > .panel { min-width: 0; }

/* What each of the forecast's lines is, drawn as the lines themselves. */
.key { list-style: none; margin: var(--gap-s) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--gap-s) var(--gap-l); font-size: var(--text-small); color: var(--ink-quiet); }
.key li { display: flex; align-items: center; gap: var(--gap-s); }
.key .sample { display: inline-block; width: 22px; height: 0; }
.key .sample.past { border-top: 2px solid var(--accent); }
.key .sample.trend { border-top: 1px solid var(--ink-quiet); }
.key .sample.ahead { border-top: 1.5px dashed var(--ink-quiet); }
.key .sample.now-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); }
.pie { display: flex; align-items: center; gap: var(--gap-l); flex-wrap: wrap; }
.slice.s0, .swatch.s0 { fill: var(--accent); background: var(--accent); }
.slice.s1, .swatch.s1 { fill: #2e9d8f; background: #2e9d8f; }
.slice.s2, .swatch.s2 { fill: var(--waiting); background: var(--waiting); }
.slice.s3, .swatch.s3 { fill: #7b5cc4; background: #7b5cc4; }
.slice.s4, .swatch.s4 { fill: var(--good); background: var(--good); }
.slice.other, .swatch.other { fill: var(--ink-faint); background: var(--ink-faint); }
.slice.idle, .swatch.idle { fill: var(--edge); background: var(--edge); }
.slice { stroke: var(--card); stroke-width: 1.5; }
/* One grid for the whole legend, so a share and a length line up from row to
   row; each row takes no box of its own. */
.legend {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--text-small);
  display: grid;
  grid-template-columns: 12px auto auto auto;
  gap: var(--gap-s) var(--gap-m);
  align-items: center;
}
.legend li { display: contents; }
.legend .swatch { width: 12px; height: 12px; border-radius: 3px; }
.legend .share { font-family: var(--font-mono); text-align: right; }
.legend .said-by { font-family: var(--font-mono); color: var(--ink-faint); }
.plot .past { fill: none; stroke: var(--accent); stroke-width: 2; }
.plot .trend { fill: none; stroke: var(--ink-quiet); stroke-width: 1; }
.plot .ahead { fill: none; stroke: var(--ink-quiet); stroke-width: 1.5; stroke-dasharray: 6 5; }
.plot .now-dot { fill: var(--accent); stroke: var(--card); stroke-width: 2; }
.plot .out-dot { fill: var(--bad); }
/* The plan's end, in the colour this page uses for a warning. */
.plot .plan-end { stroke: var(--waiting); stroke-width: 1.5; }
.plot .plan-end-said { fill: var(--waiting); font-size: 11px; font-weight: 600; font-family: var(--font); }
.plot > .empty { margin: 0; }

#chart .nothing { fill: var(--ink-faint); font-size: 12px; font-family: var(--font); }

/*
 * The window's totals, and the bar the pointer is over, in one set of columns.
 *
 * **Two rows sharing a grid rather than two shapes.** The point is to compare
 * them: an hour's 0.4 MB against the month's 812 MB says something, and it says
 * it only if the digits line up. Right-aligned and monospaced for the same
 * reason.
 *
 * **The second row keeps its height when it is empty.** It was a line of its
 * own under the row before, which grew and shrank as the pointer crossed the
 * chart -- and a long enough sentence wrapped to two lines, so the chart moved
 * under the pointer that was pointing at it. Nothing here changes height: only
 * the text in the cells changes.
 */
.totals {
  display: grid;
  /*
   * **The first column is a fixed width, and that is not a cosmetic choice.**
   *
   * Left to size itself, it was as wide as whatever was in it -- so the moment
   * a date appeared the whole block of numbers slid 30px sideways, and slid
   * back when the pointer left. Measured in a browser; it is the same flicker
   * as the vertical one, in the other direction. 10.5rem holds the widest
   * thing that goes there, an hour in the Hour view, which measured 9.7rem.
   */
  grid-template-columns: 10.5rem repeat(3, minmax(6rem, auto));
  gap: 0 var(--gap-l);
  align-items: baseline;
}
.totals .name {
  font-size: var(--text-caption);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  text-align: right;
}
.totals .mark,
.totals .value {
  font-family: var(--font-mono);
  font-size: var(--text-small);
  text-align: right;
  /*
   * **The two together, and neither on its own is enough.**
   *
   * Grid items are blockified, so `min-height` applies -- but an empty cell is
   * then exactly this tall while a filled one is as tall as the font's own
   * line box, and the difference moved the chart 2.7px every time the pointer
   * touched a bar. Measured in a browser; `min-height: 1.3em` alone looked
   * right and was not. Naming the line height makes the filled cell exactly as
   * tall as the empty one.
   */
  line-height: 1.4;
  min-height: 1.4em;
}
.totals .mark { color: var(--ink-quiet); }
/* A phone: the three amounts take the width, and what a row is about -- All,
   or the bar that was touched -- is a line of its own above its amounts. Side
   by side the four columns needed about 520px, and the page scrolled sideways.
   An empty line keeps its height, as above, so the chart still does not move. */
@media screen and (max-width: 640px) {
  .totals { flex: 1 1 100%; grid-template-columns: repeat(3, 1fr); gap: 0 var(--gap-m); }
  .totals .name:first-child { display: none; }
  .totals .mark { grid-column: 1 / -1; text-align: left; }
}

/* What a write came to, kept apart from a refusal: this is the provider
   agreeing, and it goes away on the next thing somebody does. */
.said {
  background: var(--card);
  border: 1px solid var(--good);
  border-left-width: 4px;
  border-radius: 6px;
  padding: var(--gap-m) var(--gap-l);
  margin: 0 0 var(--gap-l);
  color: var(--good);
}
.said.waiting { border-color: var(--waiting); color: var(--waiting); }

dl { margin: 0; }
dt { font-size: var(--text-caption); color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.04em; }
dd {
  margin: 0 0 var(--gap-m);
  font-family: var(--font-mono);
  font-size: var(--text-small);
  word-break: break-all;
}
dd:last-child { margin-bottom: 0; }
dd.blank { color: var(--ink-faint); font-family: var(--font); }

/* **On a SIM's page each value sits beside its label**, not under it. Under
   it, the three cards took most of a Full HD screen and the traffic chart
   began at its bottom edge; beside it, the whole chart is on screen. The label
   column is as wide as the widest label. A provider's own sentence under a
   quota (`quiet-line`) stays in the value column. On a phone the label goes
   back above the value, which leaves a long value the whole width. */
@media screen and (min-width: 641px) {
  #detail .panels dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: var(--gap-l);
    row-gap: var(--gap-s);
    align-items: baseline;
  }
  #detail .panels dt { grid-column: 1; }
  /* Broken between words first. `break-all` split a building's name inside
     a word ("S / cala A") once the value had only part of the card. */
  #detail .panels dd { grid-column: 2; margin: 0; word-break: normal; overflow-wrap: anywhere; }
  #detail .panels dd.quiet-line { margin-top: calc(2px - var(--gap-s)); }
}

#events tbody tr { cursor: default; }
#events tbody tr:hover { background: none; }
#events td { white-space: normal; }
#events td:nth-child(2) { white-space: nowrap; font-family: var(--font-mono); }

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

/* ==== own tooltips: begin ==== */
/* The page's own tooltip, the same text in three benches (see app.js). Fixed
   to the window, because a table that scrolls would cut it. It takes no
   pointer, so it is never in the way of what is under it. */
.own-tip {
  position: fixed;
  z-index: 1100;
  max-width: min(26rem, calc(100vw - 16px));
  padding: var(--gap-s) var(--gap-m);
  border-radius: 6px;
  background: var(--ink);
  color: var(--page);
  font-family: var(--font);
  font-size: var(--text-caption);
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  white-space: pre-line;
  pointer-events: none;
  box-shadow: 0 2px 10px rgb(0 0 0 / 0.25);
}
.own-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. */
.own-tip[popover] { inset: auto; margin: 0; border: 0; overflow: visible; }
/* ==== own tooltips: end ==== */
