/* PitWatch styling.
 *
 * One stylesheet, no build step, no framework. The page it has to render well
 * is a wall tablet in a basement and a phone at two in the morning, so contrast
 * is high, hit targets are large, and nothing important is conveyed by color
 * alone: every state carries a shape or a word as well.
 */

:root {
  color-scheme: light dark;

  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-2: #eceef1;
  --border: #d3d7dd;
  --text: #16181d;
  --text-muted: #5c6371;

  --accent: #1f6feb;
  --ok: #1a7f37;
  --warn: #9a6700;
  --crit: #b42318;

  --series-1: #1f6feb;
  --series-2: #8250df;

  /* Water, for the one section that is about water and nothing else. The
     alarm colors above are spoken for: a float rising is not a fault, and
     painting its heading in the same red as an overload would say it was. */
  --water: #0e7490;
  /* The pump that answers the next call. Its own hue on purpose: it has to be
     told apart at a glance from the green of one that is running and from the
     water blue of one sitting out, and green against green was the version
     that could not be. Violet is nowhere else on this page, carries no
     inherited meaning of good or bad, and reads as "designated". */
  --lead: #6d28d9;

  --radius: 10px;
  --gap: 1rem;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14161a;
    --surface: #1c1f25;
    --surface-2: #24282f;
    --border: #333942;
    --text: #e7eaef;
    --text-muted: #9aa3b2;

    --accent: #4c8dff;
    --ok: #3fb950;
    --warn: #d29922;
    --crit: #f85149;

    --series-1: #4c8dff;
    --series-2: #b083f0;

    --water: #4bbcd8;
    --lead: #a78bfa;
  }
}

* { box-sizing: border-box; }

/* Some elements, svg among them, do not reliably take the hidden attribute. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.5;
  background: var(--bg);
  color: var(--text);
  display: flex;
  flex-direction: column;
  /* svh, not vh and not dvh.
   *
   * A phone has three viewport heights. lvh is the tallest, with the browser
   * chrome retracted, and 100vh resolves to it, which is why a page sized in vh
   * overflows by roughly the height of the URL bar. dvh is whichever is showing
   * right now, which sounds like the answer and is not: at first paint the
   * chrome is expanded, and a layout sized to the current viewport can still
   * end up a little taller than the space left once everything has settled.
   *
   * svh is the smallest, the viewport with the chrome fully expanded. Sizing to
   * it means the page fits in the worst case, which is exactly the requirement
   * here: no scroll when the content fits. The cost is a little space at the
   * bottom once the URL bar retracts, and on a page that never scrolls it never
   * does. */
  min-height: 100vh;
  min-height: 100svh;
}

/* Header ------------------------------------------------------------------ */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--gap);
  padding: 0.75rem 1.25rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* The name gives way before the icons do. On a screen too narrow for both, a
   header that wraps onto two rows costs every page a row of height; a name
   that ends in an ellipsis costs nothing anybody needs. */
.brand {
  display: inline-flex;
  align-items: center;
  min-width: 0;
  gap: 0.6rem;
  font-weight: 650;
  font-size: 1.1rem;
  letter-spacing: 0.01em;
  color: var(--text);
  text-decoration: none;
}

.brand-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The mark. It is drawn in currentColor, so the one color named here paints
   the badge and the white inside it stays white in either scheme. */
.brand-logo {
  flex: none;
  width: 1.75rem;
  height: 1.75rem;
  color: var(--accent);
}

.topbar nav {
  display: flex;
  gap: 0.25rem;
  flex-wrap: wrap;
  flex: none;
  /* `flex: none` is `flex: 0 0 auto`, so this never shrinks and its width is
     whatever its contents add up to. With the building picker in it that came
     to 377px, which runs off any screen narrower than about 395 and sat 4px
     from the edge at 390. The cap lets it wrap instead of overflow. */
  max-width: 100%;
}

/* Which building, on installations that have more than one. Sized to the name
   rather than to the row: a select that stretched would push the icons off a
   phone, and one clipped to nothing would make two addresses on the same
   street indistinguishable. */
/* Sits before the nav in the markup and after it on screen.

   The markup order is what a phone uses: the header wraps, and a picker that
   came after seven icons took a third row of its own, 111px of an 844px
   screen on every page. Before them it shares the brand's row, which has the
   space, and the header stays at two. `order` puts it back on the right on a
   screen wide enough for one row, so nothing moves on a desktop. */
.site-switch {
  display: flex;
  align-items: center;
  order: 3;
  margin-left: 0.5rem;
  min-width: 0;
}

/* Pushed right so the picker and the icons stay together against the edge
   rather than the three children spreading across the bar. */
.topbar nav { margin-left: auto; }

/* The no JavaScript fallback. Hidden by static/switcher.js the moment it
   runs, so in a normal browser this is never seen; sized anyway, because the
   one person who does see it is reading the header of a page in a basement. */
.site-switch button {
  padding: 0.3rem 0.6rem;
  margin-left: 0.3rem;
  font-size: 0.85rem;
}

.site-switch select {
  max-width: 12rem;
  padding: 0.3rem 0.5rem;
  font-size: 0.9rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* A select is drawn by the browser, not by this stylesheet, and a background
   set here does not change the widget it paints: on a dark header this one
   came out white with the whole list white behind it. `color-scheme` is the
   property it listens to, the same fix the model picker on the chat page
   needed. Only in the dark, because in the light the light one is right. */
@media (prefers-color-scheme: dark) {
  .site-switch select { color-scheme: dark; }
}

/* The public header's links, which are words rather than icons.

   Written as `.topbar nav a` once, back when the signed in header was words
   too. That outweighed every rule `.icon-link` had, so the signed in header
   quietly took its colors from here instead: the page you were on was the
   same gray as the ones you were not, and had been for as long as the nav has
   been icons. Named after the nav it is actually for, so the two headers stop
   sharing rules that only ever suited one of them. */
.public-nav a {
  padding: 0.45rem 0.8rem;
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
}

.public-nav a:hover { background: var(--surface-2); color: var(--text); }

/* Layout ------------------------------------------------------------------ */

main {
  flex: 1;
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.25rem 1.5rem;
  margin-bottom: var(--gap);
}

.card h1 { margin-top: 0; font-size: 1.4rem; }
.card h2 { margin-top: 0; font-size: 1.15rem; }

.notice { border-left: 4px solid var(--accent); }

.muted { color: var(--text-muted); }

.footer {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 1rem 1.25rem;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* Controls ---------------------------------------------------------------- */

.button {
  display: inline-block;
  padding: 0.55rem 1.1rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--accent);
  color: #ffffff;
  font-weight: 550;
  text-decoration: none;
  cursor: pointer;
}

.button:hover { filter: brightness(1.08); }

/* A button with nothing to do. Gray and unpressable rather than hidden: the
   thing it does is worth knowing about even in the hour it cannot be done, and
   a control that vanishes and comes back is a control nobody learns. Whatever
   is standing in its way is said in a line underneath. */
.button:disabled,
.button[disabled] {
  background: var(--border);
  color: var(--text-muted);
  cursor: not-allowed;
  filter: none;
}

.button.small {
  padding: 0.3rem 0.7rem;
  font-size: 0.82rem;
  font-weight: 500;
}

/* The two that press the panel's own button, on the dashboard where somebody
   whose phone has just gone off is already looking. They were on the settings
   page while the wiring was being proved, which was the right place to test
   them and the wrong place to reach for at two in the morning. */
.press-buttons {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* A role that is not a checkbox. Owner is set on the form rather than by
   clicking a cell, so the list says what somebody is without offering to
   change it here. */
.role-tag {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 2rem;
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 600;
  white-space: nowrap;
}

.button.secondary {
  background: var(--surface-2);
  color: var(--text);
  border-color: var(--border);
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="email"],
input[type="tel"],
input[type="time"],
select {
  width: 100%;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

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

code, .mono { font-family: var(--mono); font-size: 0.92em; }

/* The board --------------------------------------------------------------- */

/* Four sections down the page: pump 1, pump 2, alerts, floats.
 *
 * It was one box holding all of it. Two pump columns in half a screen each and
 * three lists of lamps side by side underneath, everything sized down until the
 * whole panel fit a phone without scrolling. It fit, and it was unreadable: a
 * quarter of a phone is not a column, and the people who read this page are
 * standing in a boiler room and are not twenty five. A page that fits on one
 * screen and cannot be read has saved nobody anything.
 *
 * So the arrangement is one section per row now and the type is the size it
 * needs to be. The cost is a scroll, which is a thing a thumb does without
 * looking, and the gain is a heading somebody can read from further away than
 * arm's length.
 *
 * Boxes again rather than rules between blocks. A rule is enough to separate
 * two columns inside one card; it is not enough between four full width
 * sections, where the eye needs to know where a section starts more than it
 * needs the pixel of height back.
 *
 * A lamp is dark unless it is on. That is the point of a lamp, and it is why an
 * unassigned one is dark too rather than being drawn in some third color that
 * has to be learned.
 */

/* Capped and centered. The page is wide enough for a table of users, and this
   is not one: a label at one end of a desktop and its number at the other is
   two things to read rather than one line.

   The banner and the device dots take the same cap, so the three of them line
   up down one edge instead of the sections sitting inside a wider column. */
.board,
.banner,
.links {
  max-width: 46rem;
  margin-left: auto;
  margin-right: auto;
}

.board {
  display: grid;
  gap: 0.85rem;
  margin-bottom: 1.25rem;
}

.board-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.95rem 1.15rem 1.05rem;

  /* A grid item will not shrink below the widest unbreakable thing inside it
     unless it is told it may, because `min-width` defaults to `auto` rather
     than to zero. The panel heading holds a line of nowrap text that says what
     the alarm is doing, and when that became "alarm, nobody has acknowledged
     it" the card refused to go under 426px: on a 390px phone the whole page
     grew to 440 and scrolled sideways, then fixed itself when the alarm
     cleared and the word went back to "quiet".

     The ellipsis on the heading could not save it. An element can only clip
     what it is allowed to be narrower than. */
  min-width: 0;
}

/* The icon, the name, and whatever the section has to say about itself, on one
   line. The icon is there to be found by shape: four headings down a page are
   quicker to tell apart by their drawing than by reading them. */
.card-top {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.3rem;
  /* Same argument one level down: a flex item has the same `auto` minimum. */
  min-width: 0;
}

/* Each of the four headings carries its own color, in the icon and in a tint
   behind it. Four gray drawings in a column are told apart by reading them,
   which is the job the icon was there to save; a color is the thing an eye
   finds before it has read anything.

   The hue is the section's own and it does not change with the state of
   anything, which is what keeps it clear of the lamps below it. A red bulb
   means an alarm is up right now. A red heading on Alerts means that section
   is Alerts. The one exception is a running pump, which turns its whole
   section green, icon included, and is the only place on this page where a
   color moves. */
.card-icon {
  flex: none;
  box-sizing: border-box;
  width: 2.3rem;
  height: 2.3rem;
  padding: 0.3rem;
  border-radius: 9px;
  color: var(--icon, var(--text-muted));
  background: color-mix(in srgb, var(--icon, var(--text-muted)) 13%, transparent);
}

.pump-card { --icon: var(--accent); }
.alert-card { --icon: var(--crit); }
.water-card { --icon: var(--water); }

.card-title {
  margin: 0;
  min-width: 0;
  font-size: 1.3rem;
  font-weight: 650;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-top .info-mark { margin-left: 0.1rem; }

/* The word off the controller's screen, beside the pump it is about: LEAD,
   LAG, ON or ERROR. Pushed to the end of the line, so the four of them line up
   down the right hand edge of the two pump sections however long the names
   are.

   Every one of them is a word as well as a color, which is the rule the rest
   of this page follows: nothing important is said in color alone. */
.status {
  flex: none;
  margin-left: auto;
  padding: 0.2rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 2rem;
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  font-variant-numeric: tabular-nums;
}

/* The pump that answers the next call.

   Three states have to be told apart at a glance and there are only ever two
   badges on the page, so the colors carry the whole distinction. This was the
   brand blue, which read as decoration; then green, which was worse, because
   green is what a running pump wears and LEAD next to ON was two greens
   meaning different things. Violet appears nowhere else here, has no
   inherited sense of good or bad, and says "this is the one" without saying
   anything is happening. */
.status-lead {
  color: var(--lead);
  border-color: color-mix(in srgb, var(--lead) 50%, transparent);
  background: color-mix(in srgb, var(--lead) 14%, transparent);
}

/* Sitting this one out, which is the pump that went last. Not a warning and
   not a fault, so it takes the water blue rather than anything with an opinion:
   a color that says "this is a state I have been told about" next to a green
   that says "this one is next". */
.status-lag {
  color: var(--water);
  border-color: color-mix(in srgb, var(--water) 45%, transparent);
  background: color-mix(in srgb, var(--water) 12%, transparent);
}

/* Running right now. The strongest thing on the card, because it is the answer
   to the question somebody opened the page with. */
.status-on {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 70%, transparent);
  background: color-mix(in srgb, var(--ok) 22%, transparent);
  font-weight: 800;
}

/* Off and staying off, and the alerts section with something raised. Two
   different sentences and the same news: a red word where a quiet one usually
   is. */
.status-error,
.status-alarm {
  color: var(--crit);
  border-color: color-mix(in srgb, var(--crit) 55%, transparent);
  background: color-mix(in srgb, var(--crit) 14%, transparent);
}

/* Nothing raised, which is what this says nearly all of the time.
   
   Green, and it was muted gray on the reasoning that the page already had a
   green for a running pump. Gray is what everything undecided wears here, so
   the one badge that is genuinely reporting good news looked like a badge with
   no opinion. Red and green on the same spot is the pairing everybody already
   reads without being taught, and this badge is the only place on the board
   that answers yes or no. */
.status-clear {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 45%, transparent);
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  letter-spacing: 0.02em;
}

/* Label on the left, answer on the right, four to a section. A definition list
   rather than a table: these are four different facts about one motor, not
   rows of the same kind of thing.

   A rule between the rows, because the line is the width of the section now
   and a label at one end with its number at the other needs a track for the
   eye to follow. The numbers are tabular and right aligned for the same
   reason: four answers in a column somebody can compare down.

   One size for all four rows. It was three sizes once: the label, the answer a
   step up from it, and the amps a step up again, which made a reading that is
   zero almost all the time the largest thing on the page. The label is muted
   and the answer is not, and that is the whole difference between them. */
.facts { margin: 0.35rem 0 0; }

/* Centered against the pair of lines, not sat on the first one's baseline.

   Baseline alignment is right when a label and its answer are both one line,
   and it was, and then the answer grew a second line under it and the label
   stayed pinned to the top of the two. A lamp's title has always been centered
   against its pair, so the pumps were the odd rows out on a board whose whole
   point is that every row is the same shape. */
.fact {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--border);
  font-size: 1.05rem;
}

.fact dt { color: var(--text-muted); }

/* Two lines to the right, exactly as a lamp row has: the answer, and what it
   has been doing lately under it in the muted weight.
   
   The pump was one line with the second half tucked in beside it, which made
   four sections down a page read as two typographies. A dt on the left and a
   pair of lines on the right is now the only row shape this board has. */
.fact dd {
  margin: 0;
}

/* Nothing to report, in every field that can have nothing to report. Same
   type as the reading it stands in for, muted and a little dimmer, so a
   section with nothing behind it reads differently from a working one without
   reading in a different font from the line above it.

   It used to drop a weight as well, which is why an n/a sat in a column of
   answers looking like it belonged to some other page. */
.fact .none,
.fact .answer.none,
.fact .aside.none,
.lamp-last.none,
.lamp-count.none,
.lamp-count .none {
  color: var(--text-muted);
  opacity: 0.75;
}

/* The one number that changes by the second. Monospaced so it does not shuffle
   the line about as it changes, and no larger than anything else: a reading
   that is zero almost all the time has not earned the biggest type on the
   page, and what a pump has been doing over weeks has more to say than what it
   is doing this second. */
.amps {
  font-family: var(--mono);
  font-weight: 700;
}

/* With no reading behind it, it stops being a number and stops being set like
   one: the body face at the weight every other answer on the card uses, so a
   fresh install reads as one column of n/a rather than a monospaced dash on
   top of three of them. */
.amps.none {
  font-family: var(--font);
  font-weight: 600;
}

.unit { color: var(--text-muted); margin-left: 0.15rem; }

/* The panel's own button, brought up to the top of the board.

   The lamp is the point. It reads the same contact the alarm row does and
   wears the same three states the one on the door wears: green while all is
   well, red and steady when somebody has silenced an alarm that is still up,
   and red and blinking when nobody has. Blinking is not decoration here. On
   this panel a pulsing alarm is one nobody has acknowledged, so a blinking
   lamp on a phone means nobody is standing in front of the real one. */
.panel-card { --icon: var(--ok); }

.panel-lamp {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 2.3rem;
  height: 2.3rem;
  padding: 0.3rem;
  border-radius: 50%;
  color: var(--icon);
  background: color-mix(in srgb, var(--icon) 13%, transparent);
}

.panel-lamp svg { width: 100%; height: 100%; }

.panel-card.alarm { --icon: var(--crit); }

.panel-card.alarm .panel-lamp { animation: none; }

/* Unacknowledged, and flashing the way the beacon in the basement is. The
   animation runs on a timer of its own rather than following the contact,
   because the contact arrives over a socket and a dropped frame would show as
   a lamp that stopped rather than an alarm that did. */
.panel-card.pulsing .panel-lamp {
  animation: panel-flash 1s steps(1, end) infinite;
}

@keyframes panel-flash {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.22; }
}

/* What the lamp means, in a word, for anybody who is not going to learn a
   color. Beside the heading rather than under the lamp: the heading is where
   the eye already is. */
.panel-state {
  margin-left: 0.5rem;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text-muted);
  /* Allowed to be cut short rather than to push the card wide. What it says
     is a convenience beside the lamp, and the lamp is the thing that carries
     across a room; losing the tail of a sentence costs less than a page that
     scrolls sideways on a phone at the moment somebody is reading it because
     an alarm went off. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.panel-card.alarm .panel-state,
.panel-card.pulsing .panel-state { color: var(--crit); }

/* A running pump, said three times over: the section is outlined and tinted,
   its icon goes green, and the amps do too. That is not repetition for its own
   sake. The tint is what carries across a room, the icon is what a glance
   lands on, and the amps are the one that says how hard it is working.

   There is no separate lamp beside the name any more. It was a 13 pixel dot
   saying what the whole section now says. */
.pump-card.running {
  --icon: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 45%, transparent);
  background: color-mix(in srgb, var(--ok) 8%, transparent);
}

.pump-card.running .amps { color: var(--ok); }

/* A pump that is out, said the same three ways for the same reason. Running
   was the only state on this card that carried across a room, and being out of
   service is the other one worth seeing from the door. It was a word in a pill
   beside the name, which is something you find by reading the card you were
   hoping would tell you.

   Listed after running rather than before it so that red wins if both are
   somehow set at once. That should not happen, because a tripped pump is not
   turning, but the frame where a contact has changed and its partner has not
   is the wrong place to be showing green. */
.pump-card.faulted {
  --icon: var(--crit);
  border-color: color-mix(in srgb, var(--crit) 45%, transparent);
  background: color-mix(in srgb, var(--crit) 8%, transparent);
}

.pump-card.faulted .amps { color: var(--crit); }

/* Alerts and floats. Two sections of the same shape, four contacts and two, so
   the six rows read the same way one under the other.

   The overloads had a section of their own, on the argument that they are a
   different kind of bad news: an alert means read the panel, an overload means
   a pump is off and staying off. That is true and it did not earn a heading.
   Somebody who came to this page because a phone went off is looking for the
   one thing that is lit, and a third heading is one more thing between them
   and it. Under one heading they are four rows of the same shape, counted over
   the same month, and what each of them means is in the note behind the i. */
.lamp-pair {
  display: grid;
  margin-top: 0.35rem;
}

/* What is lit, what it is called, and what it has been doing, across one row.
   It was a bulb with two lines of small print stacked under it in a third of a
   phone; there is room for it to be a line now. */
.lamp {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--border);
  min-width: 0;
}

/* The name of the contact, which is the label on this row exactly as "Load
   now" is the label on a pump's. So it is set as one: same size, same weight,
   same muted color as a fact's term. The contact sections were a size and a
   weight of their own before, and a page of boxes in two typographies reads as
   two pages. */
.lamp-title {
  flex: 1;
  min-width: 0;
  font-size: 1.05rem;
  color: var(--text-muted);
}

/* When it last went and how often lately, at the end of the row: the answer,
   in the type a pump's answers are in. They are what the lamp is missing
   rather than a footnote to it, because a lamp that is off looks exactly the
   same whether it went twenty times today or has never gone at all.

   Two lines rather than one, and both of them in the type the rest of the page
   answers in. The count was two thirds the size of the line above it, which
   made the pair read as an answer with a footnote under it; they are two
   answers to two questions, when it last went and how often it has been going,
   and neither is a note on the other. The count is muted and the last time is
   not, which is the same difference a label and its number have on a pump, and
   it is enough. */
.lamp-when,
.fact dd {
  display: grid;
  flex: none;
  text-align: right;
  line-height: 1.3;
}

.lamp-last,
.fact .answer {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.lamp-count,
.fact .aside {
  font-size: 1.05rem;
  font-weight: 400;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.bulb {
  flex: none;
  width: 1.35rem;
  height: 1.35rem;
  border-radius: 50%;
  border: 2px solid var(--border);
  /* Unlit: the color of a bulb with nothing behind it, not the color of the
     alarm it would be if it were on. */
  background: var(--surface-2);
  transition: background 120ms linear, box-shadow 120ms linear;
}

.lamp.unset .bulb { opacity: 0.45; }
.lamp.unset .lamp-title { opacity: 0.75; }

/* Two colors on the lamps and they are the two kinds of news. Red is an alarm:
   something is wrong and somebody has to do something. Green is the equipment
   working, which is what a float closing is, because a pit that fills and gets
   pumped out is a pit doing its job.

   The floats were amber, on the reasoning that water rising is a warning. It is
   not: it is Tuesday. Amber for the ordinary thing left this page with no color
   that meant fine, and three sections of warm lamps where only one of them was
   ever bad news. */
.lamp-red.on .bulb {
  background: var(--crit);
  border-color: var(--crit);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--crit) 22%, transparent),
              0 0 16px color-mix(in srgb, var(--crit) 60%, transparent);
}

.lamp-green.on .bulb {
  background: var(--ok);
  border-color: var(--ok);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent),
              0 0 16px color-mix(in srgb, var(--ok) 55%, transparent);
}

/* The narrow screen gives up padding and nothing else.
 *
 * It used to give up type: headings, labels, counts and bulbs all came down a
 * size here, on the screen where they were already smallest. That is exactly
 * backwards. The phone in a basement is the hard case and it gets the same
 * type as the desktop; what it gets less of is the room around it. */
@media (max-width: 700px) {
  main { padding: 1.25rem 0.85rem; }

  .board { gap: 0.7rem; }
  .board-card { padding: 0.85rem 0.9rem 0.95rem; }
  .card-top { gap: 0.5rem; }

  /* The building picker, narrowed to what fits.

     It costs the header a second row on a phone, which the comment above
     .brand-name says to avoid. That trade is deliberate and it is worth being
     honest about who pays it: the picker is drawn for PitWatch's owner on
     every page whether there is one building or ten, so on a phone the owner's
     header is 75px where everybody else's is 48px. Nobody else ever sees it,
     which is most of the people reading this page in a basement.

     8rem measured rather than guessed: at 390px, 9rem and up push the row
     past the viewport and the page scrolls sideways. A truncated address is
     still the address, and the whole one is there when the list opens. */
  .site-switch select { max-width: 8rem; }
}

/* Two faces of one thing ---------------------------------------------------
 *
 * Alerts are a history and a set of rules, and they share a header icon rather
 * than taking two. Seven icons already compete for a phone's top row, and an
 * eighth for a page somebody opens twice a year would have cost the row for
 * nothing.
 */
.subtabs {
  display: flex;
  gap: 0.25rem;
  margin-bottom: var(--gap);
  border-bottom: 1px solid var(--border);
}

.subtabs a {
  padding: 0.55rem 1rem;
  border-bottom: 2px solid transparent;
  color: var(--text-muted);
  font-weight: 600;
  text-decoration: none;
}

.subtabs a.on {
  color: var(--text);
  border-bottom-color: var(--accent);
}

/* One alert per block, not per table row.
 *
 * It was a three column table and the third column -- how long it lasted --
 * was pushed off the right of a phone. A table of prose cannot fit 390 pixels
 * without either scrolling sideways or clipping, and the same argument settled
 * the dashboard: the narrow screen is the one this gets read on.
 *
 * So the times sit on their own line above the text, which wraps. The severity
 * is a bar down the left rather than a word: a column of the word "critical"
 * is read once and skipped, where a color at the edge is still working on the
 * twentieth row. */
.alert-list { list-style: none; margin: 0; padding: 0; }

.alert {
  padding: 0.7rem 0 0.7rem 0.85rem;
  border-top: 1px solid var(--border);
  border-left: 3px solid transparent;
}

.alert:first-child { border-top: none; }

.alert-head {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 0.15rem;
  color: var(--text-muted);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

.alert-title { margin: 0; font-weight: 650; }
.alert-detail { margin: 0.1rem 0 0; color: var(--text-muted); }
.alert-for.bad { color: var(--crit); font-weight: 700; }

.alert.sev-critical { border-left-color: var(--crit); }
.alert.sev-warning { border-left-color: var(--warn); }
.alert.sev-info { border-left-color: var(--water); }

.all-clear { color: var(--ok); font-weight: 600; margin: 0; }

/* The settings page --------------------------------------------------------- */

/* One section open at a time, done by the browser rather than by script: a
   name on <details> makes a group exclusive, so opening one closes the rest.
   Where that is not supported they simply all open independently, which is the
   page as it was. */
/* The line between "this building" and "PitWatch". Quiet on an installation
   with one site, where it is a label on a page nobody compares to anything,
   and load bearing on one with several: the sections below it are shared, and
   changing one changes every building at once. */
/* A value the database owns and nobody may edit. Greyed and in the monospace
   face the amp readings use, so it does not read as a box somebody forgot to
   fill in: a readonly input styled like an editable one is an invitation to
   type into it and wonder why Save did nothing. */
/* `input.readonly-value` and not `.readonly-value`: the base rule above is
   `input[type="text"]`, which is an element plus an attribute and outranks a
   bare class, so the plain version silently lost and the field rendered
   exactly like an editable one. */
input.readonly-value {
  background: var(--surface-2);
  border-style: dashed;
  color: var(--text-muted);
  font-family: var(--mono);
  cursor: default;
}

.settings-group {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 1.75rem 0 0.75rem;
  font-size: 1.05rem;
}

.settings-group:first-child { margin-top: 0; }

.settings-group-note {
  font-size: 0.85rem;
  font-weight: 400;
  color: var(--muted);
}

.settings-section { padding: 0; }

.settings-section summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 1rem 1.5rem;
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius);
}

.settings-section summary::-webkit-details-marker { display: none; }

/* The arrow, drawn rather than fetched, turning when the row opens. */
.settings-section summary::before {
  content: "";
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}

.settings-section[open] summary::before { transform: rotate(45deg); }

.settings-section summary h2 { margin: 0; font-size: 1.15rem; }

.settings-section summary:hover { background: var(--surface-2); }

.settings-section summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The contents, indented to the same edge the heading sits on, and standing
   off it. The heading is a heading: the first label sat one line under it and
   read as a caption to it rather than as the start of a form.

   Whatever the contents are, rather than a list of the tags they have been so
   far. It was `form` and `p`, and Diagnostics is neither: it is a div, so its
   tables ran to both edges of the card while every other section stood off
   them. A rule naming the shapes it has met is a rule that is wrong the next
   time somebody adds one. */
.settings-section > details > *:not(summary) {
  padding: 0.6rem 1.5rem 1.5rem;
}

/* A table that is a section: the same bordered box the other groups sit in, so
   a page of rounded cards does not have one group floating on bare lines. */
/* Diagnostics: what has arrived, per source. Read once while somebody is
   wiring a panel and rarely again, so it is a plain table rather than anything
   that needed a design. */
.diagnostics { font-size: 0.9rem; }
.diagnostics h3:first-of-type { margin-top: 0; }
table.diagnostic { min-width: 34rem; }
table.diagnostic code { font-size: 0.85em; }

/* A topic and a timestamp are each one thing. Broken over two lines they read
   as two, and the table is already inside a scroller for exactly this. */
table.diagnostic .nowrap { white-space: nowrap; }

/* Its own line under what was heard, rather than running on from it. */
table.diagnostic .note {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.85em;
  color: var(--text-muted);
}

/* Heard from, or not. Green and red rather than a lamp, because this is a
   column of sentences and a dot beside each one would be a second alphabet. */
.heard.yes { color: var(--ok); }
.heard.no { color: var(--crit); font-weight: 550; }

/* The summary above the tables: what to go and look at, or that there is
   nothing to look at. */
.watchouts { margin: 0 0 1.25rem; padding-left: 1.1rem; }
.watchouts li { margin-bottom: 0.3rem; }
.watchouts li.watchout { color: var(--crit); }
.watchouts li.allgood { color: var(--text-muted); }

.table-card { padding: 1rem 1.25rem; }
.table-card .table-scroll { margin-bottom: 0.5rem; }
.table-card .hint:last-child { margin-bottom: 0; }

/* One clamp. The heading sits at the top of its own padded card, so the
   default h4 margin above it is a gap nobody asked for. */
.source-card { padding: 0.9rem 1.25rem 1rem; }

/* A fold inside a form, for something most people never open.

   The five under MQTT are one exclusive group, so opening one shuts the rest
   and the section is never longer than the broker's own fields plus one open
   thing. They also have to read as parts of MQTT rather than as five more
   sections of the page, and the page's own sections are cards with a heading:
   so these are the opposite, a rail down the left and a row that highlights,
   which is what a list of parts looks like. */
.fold {
  margin: 0;
  border-left: 2px solid var(--border);
  /* On every one rather than between them. `.fold + .fold` left the first
     with no line above it, so the group started in mid air under the broker's
     card and the rail appeared to begin from nothing. */
  border-top: 1px solid var(--border);
}

/* And a line under the last, which nothing else was going to draw, so the
   group is closed at both ends. */
.fold:last-of-type {
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--gap);
}

.fold > summary {
  cursor: pointer;
  padding: 0.55rem 0.9rem;
  border-radius: 0 var(--radius) var(--radius) 0;
}

.fold > summary:hover { background: var(--surface-2); }
.fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Open, the rail becomes the accent: the one of the five being worked on. */
.fold[open] { border-left-color: var(--accent); }
.fold[open] > summary { background: var(--surface-2); }

/* The card inside a fold sits against the rail rather than away from it, and
   stands off the line below it rather than touching it. */
.fold > .source-card,
.fold > form,
.fold > div {
  margin-left: 0.9rem;
  margin-bottom: 0.9rem;
}

/* And off the row that opened it, by the same distance it stands off the one
   below. Without this the first card butts against the summary and reads as
   part of it rather than as the thing it revealed: opening Clamps put "Pump 1"
   hard against the word "Clamps" with about six pixels between them. Only the
   first, because everything after it already has the margin above from the
   one before. */
.fold > summary + * {
  margin-top: 0.9rem;
}

/* No wrapper around the group. The macro that emits four of these five is
   shared with the setup page, so a container opened inside it and closed
   outside would leave that page unbalanced. They are siblings in one form, so
   the rule above draws the separators between them anyway. */
.source-card > h4 { margin: 0 0 0.6rem; font-size: 1rem; }

/* The second group inside a clamp card. Smaller than the card's own heading,
   because it divides the card rather than naming it. */
.source-card > h5 { margin: 1.4rem 0 0; font-size: 0.9rem; font-weight: 650; }

/* Its own marker rather than the browser's, which carries an indent this does
   not want: the block under it is more of the same form, not a sub item of the
   label, so it lines up with the fields above. */
/* Rain, on the dashboard --------------------------------------------------- */

/* Water blue, not the alarm colors. What falls on the roof is not a fault, and
   the one confusion this card exists to prevent is between rain and what is
   standing in the pit. */
.rain-card .card-icon { color: var(--water); }

/* What the sky is doing right now, in a sentence rather than a lamp. There is
   no state here worth a colored bulb: rain is not a fault, it is the reason
   for one. */
.rain-state {
  margin: 0 0 0.75rem;
  font-size: 1rem;
  color: var(--text-muted);
}

/* Raining now gets the water color and the weight, so the card can be read
   from across a boiler room without reading the words. */
.rain-on { color: var(--water); font-weight: 600; }

.rain-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2.5rem;
  margin-bottom: 0.9rem;
}

.rain-figure { display: flex; flex-direction: column; gap: 0.1rem; }

.rain-value {
  font-size: 1.5rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.rain-label {
  font-size: 0.8rem;
  color: var(--text-muted);
}

.rain-strip svg { display: block; width: 100%; }

/* The bars, colored here rather than from script. A fill attribute would work
   under the content security policy, which forbids inline styles and not
   presentation attributes, but the palette belongs in one place and this is
   it. */
.rain-bar { fill: var(--water); }

/* What is coming, drawn lighter than what fell. A forecast and a measurement
   are different kinds of claim and should not look identical on a page
   somebody acts on. */
.rain-bar-ahead { fill: var(--water); opacity: 0.45; }

/* Now. Without it there is no telling which half of the strip already
   happened. */
.rain-split {
  stroke: var(--text-muted);
  stroke-width: 1;
  stroke-dasharray: 2 2;
}

.rain-floor { stroke: var(--border); stroke-width: 1; }

/* The scale and the age of the reading, at the two ends of one line. Both are
   small print and neither is worth a line of its own. */
.rain-foot {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.35rem 0 0;
  font-size: 0.78rem;
  color: var(--text-muted);
}

.rain-empty {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

/* The history page --------------------------------------------------------- */

/* A small quiet heading over a chart or a written summary. It names what is
   under it rather than announcing itself, which is what a heading over a
   drawing should do. The dashboard used to share this and does not any more:
   its four sections are read from a distance and their headings are the size
   of headings. */
/* The heading over a chart, a table or a list of alerts.

   Sentence case, like every other heading in the application. It was small
   uppercase with letters spread out, which reads as a label rather than a
   heading, and it meant the two pages built out of these shouted while the
   dashboard and the settings page did not. Uppercase at 0.72rem also has to be
   muted to be bearable, so the headings on the busiest page were the faintest
   text on it. */
.section-heading {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.6rem;
  font-size: 1.05rem;
  font-weight: 650;
  color: var(--text);
  min-width: 0;
}

.section-heading .info-mark { margin-left: 0; }

/* Three charts drawn as SVG by hand, at the width the box actually is. The
   same cap as the dashboard, so the two pages line up down one edge. */
.charts {
  max-width: 46rem;
  margin: 0 auto;
}

.chart-windows {
  display: flex;
  justify-content: flex-end;
  gap: 0.4rem;
  margin-bottom: 0.75rem;
}

/* A row of three, one of them on. Small, because choosing a window is not the
   job somebody came here to do. */
.chip {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-muted);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 550;
  cursor: pointer;
}

.chip:hover { color: var(--text); }

.chip-on {
  background: var(--accent);
  border-color: transparent;
  color: #ffffff;
}

.chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.chart-card { padding: 0.9rem 1rem; margin-bottom: 1rem; }

/* A sideways drag reads the chart and an up and down one still scrolls the
   page, which is what pan-y buys. Without it a finger on a chart either does
   nothing or traps the page. */
.chart svg { display: block; width: 100%; touch-action: pan-y; cursor: crosshair; }

/* The numbers under the cursor, above the chart rather than in a tooltip a
   thumb would be covering. The line is kept whether or not there is anything
   in it, so touching a chart does not push it down the page. */
.chart-readout {
  margin: 0 0 0.35rem;
  min-height: 1.15rem;
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: pre;
  overflow-x: auto;
}

/* The line under a chart that carries its key. */
.chart-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
}

/* Axis and row labels. In the stylesheet rather than on each element, because
   there are a hundred of them on a busy month. */
.chart-label {
  fill: var(--text-muted);
  font-family: var(--font);
  font-size: 10px;
}

.chart-empty {
  margin: 0.5rem 0 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* Which line is which pump. Named, because two colors are one color to a
   colorblind reader. */
.chart-key {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin-top: 0.6rem;
  font-size: 0.78rem;
  color: var(--text-muted);
}

.key-item { display: inline-flex; align-items: center; gap: 0.35rem; }

/* An SVG square rather than a colored span. The content security policy
   forbids inline styles, so a script coloring a span in is blocked and the dot
   comes out colorless; a fill attribute is not a style. */
.key-dot {
  width: 0.65rem;
  height: 0.65rem;
  flex: none;
  display: block;
}

/* The window in numbers, above the shapes. Somebody who reads nothing else on
   this page should still leave knowing whether it was a busy week. */
.figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0.9rem 1rem;
}

.figure { display: flex; flex-direction: column; gap: 0.1rem; }

.figure-value {
  font-size: 1.35rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.figure-label {
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* Dimmed while a new window is on its way, with the old one still readable
   underneath. A spinner over a chart that redraws in 200 ms is a flash. */
.charts.loading { opacity: 0.55; }

/* The chat ----------------------------------------------------------------
 *
 * Dark, whatever the rest of the application is doing, and deliberately not
 * PitWatch shaped. Every other page here is a card of numbers somebody scans.
 * This one is a conversation, it is read top to bottom like a conversation, and
 * borrowing the furniture of a chat people already use is worth more than
 * consistency with the dashboard.
 *
 * The question is a blue bubble and the answer is plain text on the ground, the
 * asymmetry doing the work that two colors of bubble would do worse: what you
 * said is a small thing you said, and what came back is the page.
 */
.chat {
  --chat-bg: #17181c;
  --chat-ink: #ececf1;
  --chat-dim: #9aa0aa;
  --chat-mine: #1f6feb;
  --chat-edge: #2c2f36;

  display: flex;
  flex-direction: column;
  /* The header and the footer, so the composer sits on the bottom of the
     window rather than below the fold. */
  height: calc(100vh - 8.5rem);
  min-height: 24rem;
  background: var(--chat-bg);
  color: var(--chat-ink);
  /* An edge, because without one the panel runs into whatever is behind it.
     In a dark browser the page ground is nearly this black, so the chat had no
     shape at all: it read as the window having gone dark rather than as a
     panel on a page. The border draws the box; the shadow lifts it off the
     ground in the light theme, where the contrast does the work instead. */
  border: 1px solid var(--chat-edge);
  border-radius: 0.9rem;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.18);
  overflow: hidden;
}

.chat .error {
  margin: 0.75rem 1rem 0;
}

/* The thread scrolls, not the page. A conversation somebody has kept going for
   a week is longer than a screen, and a composer below the fold is a composer
   nobody can reach. `contain: paint` for the same reason the wide tables have
   it: what overflows is this element's business. */
.chat-thread {
  flex: 1;
  overflow-y: auto;
  contain: paint;
  padding: 1.5rem 1rem;
  scrollbar-color: #40444d var(--chat-bg);
}

.chat-line {
  max-width: 48rem;
  margin: 0 auto 1.4rem;
  display: flex;
}

.chat-said {
  white-space: pre-wrap;
  line-height: 1.6;
  overflow-wrap: anywhere;
}

/* Mine: a bubble, on the right, and only as wide as it needs to be. */
.chat-user { justify-content: flex-end; }

.chat-user .chat-said {
  max-width: 85%;
  padding: 0.65rem 1rem;
  background: var(--chat-mine);
  color: #fff;
  border-radius: 1.15rem;
}

/* The answer: no bubble and no avatar. It is the page.

   It does carry one label now, the id of the model that wrote it, which is the
   whole point of being able to ask two of them the same question. That makes
   the line a column rather than a row: the answer, then the name under it. */
.chat-assistant {
  flex-direction: column;
  align-items: flex-start;
}

.chat-assistant .chat-said {
  width: 100%;
  color: var(--chat-ink);
}

.chat-by {
  margin: 0.3rem 0 0;
  font-size: 0.72rem;
  color: var(--chat-dim);
}

.chat-failed .chat-said {
  color: #ff9c8a;
  font-style: italic;
}

/* Three dots, because something has to say the question was heard. Animated
   opacity rather than movement: a bouncing row of dots beside a pump alarm
   reads as an emergency. */
.chat-thinking {
  display: inline-flex;
  gap: 0.3rem;
  padding: 0.35rem 0;
}

.chat-thinking i {
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: var(--chat-dim);
  animation: chat-pulse 1.4s ease-in-out infinite;
}

.chat-thinking i:nth-child(2) { animation-delay: 0.2s; }
.chat-thinking i:nth-child(3) { animation-delay: 0.4s; }

@keyframes chat-pulse {
  0%, 60%, 100% { opacity: 0.25; }
  30% { opacity: 1; }
}

/* An empty thread says what to ask rather than nothing. The four are buttons
   rather than text: the first question is the hard one to think of. */
.chat-empty {
  max-width: 48rem;
  margin: auto;
  padding: 2rem 0;
  text-align: center;
}

.chat-empty-title {
  margin: 0 0 1.25rem;
  font-size: 1.3rem;
  font-weight: 600;
  color: var(--chat-ink);
}

.chat-starters {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

.chat-starter {
  width: 100%;
  padding: 0.8rem 1rem;
  font: inherit;
  text-align: left;
  color: var(--chat-dim);
  background: transparent;
  border: 1px solid var(--chat-edge);
  border-radius: 0.9rem;
  cursor: pointer;
}

.chat-starter:hover {
  color: var(--chat-ink);
  border-color: #464b55;
}

/* The composer. Pinned to the bottom of the panel, the box and the send button
   sharing one rounded outline the way every chat box does. */
.chat-ask {
  flex: none;
  padding: 0.5rem 1rem 1rem;
  background: var(--chat-bg);
}

.chat-box {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  max-width: 48rem;
  margin: 0 auto;
  padding: 0.4rem 0.4rem 0.4rem 1rem;
  background: #24262c;
  border: 1px solid var(--chat-edge);
  border-radius: 1.5rem;
}

/* The ring goes on the box, not on the textarea inside it.

   Not decoration: the global `textarea:focus` rule further down this file
   outranks `.chat-box textarea` on equal specificity by coming later, so the
   inner outline was drawn as a blue rectangle inside a gray one. Removing it
   without putting the focus somewhere visible would be worse than the two
   rectangles, so it moves out to the edge where the eye already is. */
.chat-box:focus-within {
  border-color: var(--chat-mine);
  box-shadow: 0 0 0 1px var(--chat-mine);
}

.chat-box textarea:focus { outline: none; }

.chat-box textarea {
  flex: 1;
  border: 0;
  padding: 0.55rem 0;
  max-height: 200px;
  color: var(--chat-ink);
  background: transparent;
  font: inherit;
  line-height: 1.5;
  resize: none;
  outline: none;
}

.chat-box textarea::placeholder { color: #6d737e; }

.chat-send {
  flex: none;
  width: 2.1rem;
  height: 2.1rem;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: var(--chat-ink);
  color: #17181c;
  cursor: pointer;
}

.chat-send svg { width: 1.05rem; height: 1.05rem; }
.chat-send:disabled { opacity: 0.35; cursor: default; }

/* Under the box: the window, whether it can be used at all, and the one
   destructive thing, which lives down here because it is done twice a year. */
.chat-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* Wraps. A model id is whatever the gateway calls it, and the gateway is
     somebody else's: `qwen3.8-27b-ninfer` is an ordinary length for one. On a
     row that cannot wrap, a name like that either squashes the select down to
     an unreadable stub or pushes Clear conversation off the edge, and on a
     phone it did both. */
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  max-width: 48rem;
  margin: 0.5rem auto 0;
  font-size: 0.82rem;
  color: var(--chat-dim);
}

.chat-foot select {
  width: auto;
  /* Never squashed below something readable, and never wider than the row.
     A select shrinks to nothing by default in a flex line. */
  min-width: 0;
  max-width: 100%;
  padding: 0.25rem 0.5rem;
  font-size: 0.82rem;
  color: var(--chat-dim);
  /* Not transparent, and `color-scheme` as well. The list a select drops down
     is drawn by the browser rather than by this stylesheet, and a browser told
     nothing draws it white: on a black page that is a sheet of white paper
     falling out of the dark. `color-scheme` is the one property it listens to.
     The `option` rules underneath are for the browsers that would rather be
     told directly. */
  color-scheme: dark;
  background: var(--chat-bg);
  border: 1px solid var(--chat-edge);
  border-radius: var(--radius);
}

.chat-foot option {
  background: var(--chat-bg);
  color: var(--chat-ink);
}

.chat-note { margin-right: auto; color: #ffb4a2; }

.chat-clear {
  padding: 0.25rem 0.5rem;
  font: inherit;
  font-size: 0.82rem;
  color: var(--chat-dim);
  background: transparent;
  border: 1px solid var(--chat-edge);
  border-radius: var(--radius);
  cursor: pointer;
}

.chat-clear:hover { color: #ff9c8a; border-color: #5a3b38; }

@media (max-width: 30rem) {
  .chat { height: calc(100vh - 11rem); }
  .chat-thread { padding: 1rem 0.75rem; }
  .chat-ask { padding: 0.5rem 0.75rem 0.75rem; }

  /* The model gets the whole first line, because its name is the long one and
     it is the choice most worth being able to read. The window and Clear
     conversation share the line under it, which is where they fit. */
  .chat-foot { justify-content: space-between; }
  .chat-foot .chat-model { flex: 1 0 100%; }
  .chat-foot #window { flex: 0 1 auto; }
  .chat-clear { flex: 0 0 auto; }
}

/* Tokens, on the profile page. The new one is shown once and has to be
   copied whole, so it wraps anywhere rather than running off a phone. */
.token {
  display: block;
  padding: 0.5rem 0.65rem;
  overflow-wrap: anywhere;
  user-select: all;
}

.token-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
}

.token-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border, rgba(127, 127, 127, 0.25));
}

.token-list li.revoked { opacity: 0.55; }

/* Forms ------------------------------------------------------------------- */

.field { margin-bottom: 1rem; }

.field > label {
  display: block;
  font-weight: 550;
  margin-bottom: 0.3rem;
}

/* A checkbox label reads as one line, not as a heading over a control. */
.field > label > input[type="checkbox"] {
  margin-right: 0.4rem;
  vertical-align: -1px;
}

/* Explanatory text fills whatever it is inside. Capped at a measure, it sat as
   a narrow column against the left edge of a wide card, which reads as broken
   rather than as considered. Inside a grid cell the cell is the limit already. */
.hint {
  margin: 0.35rem 0 0;
  font-size: 0.85rem;
  font-weight: 400;
  color: var(--text-muted);
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  /* A row gap on top of the margin each field already carries. Without it, a
     grid that wraps to two rows stacks the second row straight onto the first
     and the two read as one column of unrelated boxes. */
  gap: 0.4rem 1.25rem;
}

/* A subheading divides a card into groups of fields. The space above it is
   what separates the group from the last field of the one before, so it is
   larger than the space below. */
.card h3 {
  margin: 2rem 0 0.5rem;
  font-size: 1rem;
  font-weight: 650;
}

.card h3:first-child { margin-top: 0; }

/* Explanation followed immediately by the thing it explains. Without this the
   first label sits on the last line of the paragraph and reads as part of it
   rather than as the start of the form. */
.hint + .grid,
.hint + .field,
.hint + .table-scroll,
.hint + fieldset.field,
p + .grid,
p + .table-scroll { margin-top: 1rem; }

/* A button that follows a paragraph. Everything else in a form carries its own
   bottom margin, so the gap above the Save button came from whatever happened
   to be last, and a section that ended in an explanation had the button
   sitting on the final line of it. */
.hint + .button,
p + .button { margin-top: 1.25rem; }

/* Two explanations in a row are two paragraphs, not one wrapped one. */
.hint + .hint { margin-top: 0.7rem; }

fieldset.field {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem 1rem 1rem;
}

fieldset.field legend {
  padding: 0 0.4rem;
  font-weight: 650;
}

fieldset.field label {
  display: block;
  font-weight: 550;
  margin: 0.75rem 0 0.3rem;
}

/* The editable list of signals. Wide enough that a name and its input are not
   fighting for the same inch, and it scrolls inside its wrapper below that
   rather than pushing the card sideways. */
table.signals { min-width: 30rem; }
table.signals input[type="text"] { min-width: 12rem; }
table.signals td, table.signals th { vertical-align: middle; }
table.signals td:last-child, table.signals th:last-child { width: 1%; white-space: nowrap; }

.narrow { max-width: 24rem; }

.error, .saved {
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  margin: 0 0 var(--gap);
  border: 1px solid;
}

.error {
  color: var(--crit);
  border-color: color-mix(in srgb, var(--crit) 40%, transparent);
  background: color-mix(in srgb, var(--crit) 10%, transparent);
}

.saved {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 40%, transparent);
  background: color-mix(in srgb, var(--ok) 10%, transparent);
}

/* Saving a settings section says so across the top of the window and then
   stops saying it, because the page you are looking at afterwards is the
   section you were already in rather than the top of the page.

   The fade is a CSS animation and not a timer in a script, so it works the
   same on a page with nothing else to run. Only the confirmation goes: an
   error stays in the flow of the page where it has to be read. */
.settings-saved {
  position: fixed;
  inset: 0 0 auto;
  z-index: 60;
  margin: 0;
  border-width: 0 0 1px;
  border-radius: 0;
  text-align: center;
  background: color-mix(in srgb, var(--ok) 18%, var(--surface));
  animation: settings-saved-goes 4s ease-in forwards;
}

@keyframes settings-saved-goes {
  0%, 70% { opacity: 1; }
  100% { opacity: 0; visibility: hidden; }
}

/* The icon sprite, defined once at the top of the page and referenced by name
   from everywhere. Positioned out of the layout at zero size, because an
   inline svg is still an inline box and `hidden` on one is not reliably
   honored: without this there is a blank strip above the header. */
.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Tables ------------------------------------------------------------------ */

/* Wide tables scroll inside their own box. The page body never scrolls
   sideways, which on a phone is the difference between usable and not. */
.table-scroll {
  overflow-x: auto;
  margin-bottom: 1rem;

  /* And the overflow stays inside this box.
     `overflow-x: auto` makes the table scroll within the div, which it did,
     and on its own that was not enough: the table's width still counted
     towards the page's own scrollable area, so the whole page slid sideways
     228px on a 390px phone and the header, which is only as wide as the
     viewport, left a blank strip behind it. Reported on the users page, where
     the accounts table carries a 30rem minimum.
     `contain: paint` makes this a containment context, so what overflows here
     is this element's business and not the document's. The table still scrolls
     inside, which is the part somebody needs. */
  contain: paint;
}

table {
  border-collapse: collapse;
  width: 100%;
  min-width: 32rem;
}

th, td {
  text-align: left;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

thead th {
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

td.center, th.center { text-align: center; }

/* Numbers line up under each other or they cannot be compared down a column,
   which is the only reason to put them in a column. */
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* A cell that would rather scroll than wrap. A timestamp broken across two
   lines makes every row in the table a different height. */
td.nowrap, th.nowrap { white-space: nowrap; }


table.channels th[scope="row"] { font-family: var(--mono); white-space: nowrap; }
table.channels td input[type="number"] { max-width: 8rem; }

/* Test results ------------------------------------------------------------ */

.test-result {
  margin-top: 0.75rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.test-result p { margin: 0 0 0.5rem; }
.test-result p:last-child { margin-bottom: 0; }

.test-result.good { border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.test-result.bad { border-color: color-mix(in srgb, var(--crit) 45%, transparent); }

.test-result .bad { color: var(--crit); }

table.probe { min-width: 0; }
table.probe td, table.probe th { border-bottom: none; padding: 0.2rem 0.6rem 0.2rem 0; }

.test-result .on { color: var(--ok); }
.test-result thead th { padding-bottom: 0.35rem; }

/* Dashboard --------------------------------------------------------------- */

/* The i beside a heading, and the note it opens.

   A native dialog, shown as a modal. It renders in the top layer, so it cannot
   push a card around or end up behind one whatever the stacking looks like,
   and it comes with a backdrop and an Escape key for nothing. */
.info-mark {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 1.3rem;
  height: 1.3rem;
  margin-left: 0.45rem;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: 0.72rem;
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  vertical-align: 0.1em;
  cursor: pointer;
}

.info-mark:hover { color: var(--text); border-color: var(--text-muted); }

.info-mark:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

dialog.note {
  width: min(34rem, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
  padding: 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.5);
  overflow-y: auto;
}

dialog.note::backdrop { background: rgba(0, 0, 0, 0.6); }

dialog.note .hint { margin: 0; }
dialog.note .hint p { margin: 0 0 0.7rem; }
dialog.note .hint p:last-child { margin-bottom: 0; }

.note-title { margin: 0 0 0.75rem; font-size: 1.05rem; }

.note-close {
  margin: 1rem 0 0;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The ordinary day, next to today's count. Quieter than the count, because it
   is the thing being compared against rather than the answer. */
/* The note that rides beside an answer: the typical load next to the live one,
   the ordinary day next to today's count.

   A size up from where it started. At 0.8rem it read as a footnote on the
   answer rather than a second answer, which is the same mistake the count
   under a lamp was making, and these two carry more of the card now that the
   pump is two rows. */
.beside {
  margin-left: 0.4rem;
  font-size: 0.9rem;
  font-weight: 400;
  color: var(--text-muted);
}

/* Alerts ------------------------------------------------------------------ */

/* One card per rule. A rule that is switched off is dimmed rather than hidden,
   because what is available and turned off is worth knowing and a page that
   hides it looks like a page that does not have it. */
.rule-off { opacity: 0.62; }

.rule-needs {
  flex: none;
  font-size: 0.75rem;
  color: var(--text-muted);
  white-space: nowrap;
}

textarea {
  width: 100%;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  line-height: 1.45;
  resize: vertical;
}

textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Users ------------------------------------------------------------------ */

/* Four columns and a set of icons. It was eight columns of words, which was
   wider than the page and put a horizontal scrollbar under a list of four
   people. */
table.users { min-width: 30rem; }

table.users th[scope="row"] { font-weight: 600; white-space: nowrap; padding-left: 0; }
table.users td { white-space: nowrap; }

/* As narrow as its heading. A column holding one checkbox should not take the
   room a column of words would. */
table.users .tight { width: 1%; text-align: center; }

/* The context cell on the model list: a Default tick above the number that
   tick works out, dimmed while it is the tick's to decide. */
table.users td.budget .check { display: block; margin-bottom: 0.3rem; }
table.users td.budget input[type="number"] { width: 7.5rem; }
table.users td.budget input[type="number"]:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
table.users .tight input[type="checkbox"] { margin: 0; }

/* A disabled account, dimmed. The row still reads; it just stops competing
   with the accounts that are actually being sent things. */
table.users tr.off th[scope="row"],
table.users tr.off td { color: var(--text-muted); }

/* The building this page is currently about. Two buttons are missing from its
   row, and without this the row reads as one somebody forgot to finish. */
table.users tr.here { background: color-mix(in srgb, var(--accent) 7%, transparent); }

/* Two buttons in one cell, each its own form, because forms do not nest and
   these post to different places. */
.inline-form { display: inline; }

table.users .row-actions {
  display: flex;
  gap: 0.15rem;
  flex-wrap: nowrap;
  justify-content: flex-end;
}

/* An icon on its own, with a target around it big enough for a thumb. */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.icon-button svg { width: 19px; height: 19px; }
.icon-button:hover { background: var(--surface-2); color: var(--text); }
.icon-button.danger:hover { color: var(--crit); }

.icon-button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The confirmation. Same panel as an information note, with buttons that say
   what they do instead of OK and Cancel. */
dialog.confirm { width: min(26rem, calc(100vw - 2rem)); }
dialog.confirm .row-actions { margin-top: 1.25rem; }

/* The heading and the one button that starts something new. */
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

.card-head h1, .card-head h2 { margin: 0; }

/* A notification box with nowhere to send. Dimmed rather than hidden, so it is
   clear the option exists and what it is waiting for. */
.checkboxes label.unavailable { color: var(--text-muted); cursor: not-allowed; }

/* Beside a pump's typical load, saying which way its draw is moving. Not
   painted red: a pump drawing more than it did is worth reading, not worth
   alarming about, and the alert rules are where an alarm belongs. */
.drift-up { color: var(--warn); font-weight: 550; }

/* A failed step or a reading that did not arrive, in the setup pages. */
.bad { color: var(--crit); }

/* Whether each device is talking -------------------------------------------
 *
 * Two small named indicators, right aligned above everything. Quiet when both
 * are green, which is almost always. Hollow rather than red for a device that
 * is not set up: running on the clamps alone is a normal way to run. */

.links {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 1rem;
  margin-bottom: 0.5rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}

.link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.link-off { opacity: 0.55; }

.link-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--text-muted);
  flex: none;
}

.link-ok { background: var(--ok); }

/* Hollow, so that not set up cannot be mistaken for either answer. */
.link-idle {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--text-muted);
}

.link-crit {
  background: var(--crit);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--crit) 25%, transparent);
}

/* Banner ------------------------------------------------------------------ */

.banner {
  padding: 0.9rem 1.25rem;
  border-radius: var(--radius);
  margin-bottom: var(--gap);
  font-weight: 650;
  border: 1px solid;
}

.banner-warn {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
}

.banner-crit {
  color: var(--crit);
  border-color: color-mix(in srgb, var(--crit) 50%, transparent);
  background: color-mix(in srgb, var(--crit) 12%, transparent);
}

/* Stale ------------------------------------------------------------------- */

/* The live feed has dropped. Everything on screen is still the last thing we
   knew, which is worth showing, but it must not look current. */
/* Losing the feed, said without moving anything.
 *
 * This used to insert a line of text above the board, which pushed every
 * number down the page at the exact moment somebody was reading one, and
 * pushed them back up when it reconnected. A dropped websocket on a phone
 * changing cells is a second of nothing; it should not rearrange the page
 * twice.
 *
 * So the board dims and a spinner sits over it. The dimming is the message:
 * these numbers are the last ones received. Nothing reflows, and when the
 * socket comes back the page is exactly where it was. */
body.stale main {
  opacity: 0.45;
  transition: opacity 200ms linear;
  pointer-events: none;
}

body.stale::after {
  content: "";
  position: fixed;
  top: 50%;
  left: 50%;
  width: 2.2rem;
  height: 2.2rem;
  margin: -1.1rem 0 0 -1.1rem;
  border: 3px solid color-mix(in srgb, var(--text-muted) 35%, transparent);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: stale-spin 900ms linear infinite;
  z-index: 50;
}

@keyframes stale-spin {
  to { transform: rotate(360deg); }
}

/* A spinner that cannot be seen still says "wait", and a reader who has asked
   for less movement should not be given a rotating one. */
@media (prefers-reduced-motion: reduce) {
  body.stale::after { animation: none; border-top-color: var(--text-muted); }
  body.stale main { transition: none; }
}

table.clamps, table.steps { min-width: 0; }
table.clamps th[scope="row"] { white-space: nowrap; padding-left: 0; }
table.clamps td { border-bottom: none; }
table.clamps select { min-width: 11rem; }
table.steps td, table.steps th { border-bottom: none; padding: 0.15rem 0.6rem 0.15rem 0; }
table.steps th { font-weight: 500; }

/* A test row is a label, an input and a button that belong together. */
/* A field that wants the whole grid rather than one column of it. The address
   box sits above the two coordinate boxes it fills in, and a street address
   squeezed into a third of the row is a box nobody can read what they typed
   in. */
.field-wide { grid-column: 1 / -1; }

/* The address and its Look up button on one line, the button keeping its
   width while the box takes what is left. It wraps on a narrow screen, which
   is the right thing: a button under the box is still a button. */
.lookup-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.lookup-row input { flex: 1 1 16rem; min-width: 0; }
.lookup-row .button { flex: none; }

.test-row {
  border-top: 1px solid var(--border);
  padding-top: 1rem;
  margin-top: 1rem;
}

.test-row input { max-width: 22rem; }
.test-row .button { margin-top: 0.5rem; }

/* Sign in and policy pages ------------------------------------------------ */

.login-layout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--gap);
  align-items: start;
}

/* The card is the measure. Both halves of this have been wrong once.
 *
 * Capping the lines instead of the card, which is what this used to do, keeps
 * the card as wide as the page while the text stops two thirds of the way
 * across it. On a laptop that is unremarkable. On a wide monitor it is a band
 * of empty card beside every paragraph, and it reads as a layout that has gone
 * wrong rather than as a margin.
 *
 * So the width lives on the card, and 80ch is the upper end of a comfortable
 * line rather than the middle of one: erring wide here is what keeps the card
 * from looking pinched on a laptop, which is the failure this replaced. */
.card.prose {
  max-width: calc(80ch + 3rem);
  margin-left: auto;
  margin-right: auto;
}

.prose table { max-width: 100%; }
.prose h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; }
.prose h2:first-of-type { margin-top: 1rem; }
.prose li { margin-bottom: 0.35rem; }
.prose a:not(.button) { color: var(--accent); }

/* The public pages: home, contact, the policies and the login form. Small
   additions rather than a second design, because a visitor who sees this and
   then signs in should not feel like they changed websites. */

.prose .lede {
  font-size: 1.05rem;
  color: var(--text);
}

.public-footer {
  align-items: baseline;
  row-gap: 0.75rem;
}

.footer-who {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  color: var(--text);
}

/* A rule rather than a separator character, so that a name wrapping onto its
   own line on a phone does not leave a dot stranded at the start of it. */
.footer-who span + span {
  border-left: 1px solid var(--border);
  padding-left: 0.6rem;
}

.footer-who a { color: inherit; }

/* The sample alerts on the terms and conditions. Set apart from the prose around
   them, because a reviewer is looking for the message text and should not have
   to work out which words are the sample and which are the explanation. */
.samples {
  list-style: none;
  padding: 0;
  margin: 1rem 0;
  display: grid;
  gap: 0.75rem;
}

.samples li {
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.sample-label {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 0.3rem;
}

.sample-text { display: block; }

@media (max-width: 30rem) {
  .footer-who span + span {
    border-left: 0;
    padding-left: 0;
  }
}

.break { word-break: break-all; }

.notice-inline {
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  color: var(--warn);
}

/* Who is signed in, in the header ----------------------------------------- */

.who {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin-left: 0.5rem;
  padding-left: 0.8rem;
  border-left: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.9rem;
}

.who a, .linkish {
  color: var(--accent);
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
  text-decoration: none;
}

form.inline { display: inline; }

/* People ------------------------------------------------------------------ */

.person {
  border-top: 1px solid var(--border);
  padding-top: 1rem;
  margin-top: 1rem;
}

.checkboxes {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
}

.checkboxes label { font-weight: 500; }

.row-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.button.danger {
  background: var(--surface-2);
  color: var(--crit);
  border-color: color-mix(in srgb, var(--crit) 40%, transparent);
}

/* Header icons -------------------------------------------------------------- */

.icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A 44px target. Anything smaller is a coin toss with a thumb. */
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius);
  color: var(--text-muted);
  cursor: pointer;
  list-style: none;
}

.icon-link svg { width: 22px; height: 22px; }
/* The page you are on is the brand color on a tint of it, and a hover is the
   same color without the tint, so the row answers "where am I" before it is
   read. Gray on gray said it too quietly to find on a tablet across a room. */
.icon-link:hover { background: var(--surface-2); color: var(--accent); }
.icon-link.active {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 13%, transparent);
}

/* Seven icons and a name across a phone. Everything comes down a size rather
   than anything going away: the name is the one place a stranger reading this
   finds out what it is, and it was being hidden to buy room the icons could
   give up instead. */
@media (max-width: 30rem) {
  .topbar { padding: 0.4rem 0.7rem; gap: 0.25rem; }
  .topbar nav { gap: 0; }

  /* On a phone the picker rides the brand's row, which is where the space is.
     Seven icons come to 241px and the picker to 153px: 394px of content in
     the 298px a 320px screen has, so they cannot share a line. Beside the
     brand there is room for the whole building name, and the header stays two
     rows rather than three. */
  .site-switch {
    order: 0;
    margin-left: auto;
    min-width: 0;
  }

  .site-switch select { max-width: 100%; }
  .icon-link { width: 2.15rem; height: 2.15rem; }
  .icon-link svg { width: 19px; height: 19px; }
  .brand { font-size: 0.85rem; gap: 0.35rem; }
  .brand-logo { width: 1.45rem; height: 1.45rem; }
  main { padding: 1rem 0.9rem; }
  .card { padding: 1rem 1.1rem; }
  .footer { padding: 0.75rem 0.9rem; font-size: 0.8rem; }

  /* Seven closed sections on a phone.
   *
   * Measured at 390 px: each one 94 px tall and the set of them 751 px, which
   * is an 844 px screen spent almost entirely on the lids of boxes. Most of it
   * was not the row at all: the summary is 45 px, and the rest was a whole
   * card's padding around it, from the .card rule three lines up. A section
   * has never wanted that padding, which is why it is zeroed where the section
   * is defined; this restates it because these two selectors weigh the same
   * and this one comes second.
   *
   * The rest is the row itself coming in a little and the space between them
   * halving. Nothing is hidden and nothing moves: the same seven rows, tapped
   * the same way.
   */
  .settings-section { padding: 0; margin-bottom: 0.5rem; }
  .settings-section summary { padding: 0.65rem 1rem; gap: 0.5rem; }
  .settings-section summary h2 { font-size: 1rem; }
  .settings-section > details > *:not(summary) { padding: 0.5rem 1rem 1.1rem; }
}

.login-single { max-width: 24rem; }

/* Taking the window away as a spreadsheet.

   Quieter than the charts above it, because it is not what the page is for. A
   secondary button and a line of explanation, at the end, where somebody who
   came to read the charts arrives after reading them. */
.export-card {
  margin-top: 1.25rem;
}

.export-title {
  margin: 0 0 0.35rem;
  font-size: 1.05rem;
  font-weight: 650;
}

/* The brochure ------------------------------------------------------------
 *
 * The home page a signed out visitor sees, and nothing else. Every rule here
 * is under `.brochure`, which only that page sets, so none of it can reach
 * the application or the policy pages: a visitor who reads this and then
 * signs in should not feel like they changed websites, and an edit here
 * should never be able to move a pump card.
 *
 * It is the same palette as everything else. The difference is width and
 * rhythm -- full bleed bands alternating against the page, rather than the
 * single centered column the policies want.
 */

body.brochure main {
  max-width: none;
  padding: 0;
}
.brochure h2 {
  font-size: 1.6rem;
  margin: 0 0 1.25rem;
  letter-spacing: -0.01em;
}
.brochure h3 {
  font-size: 1.02rem;
  margin: 1.4rem 0 0.4rem;
}
.brochure p { line-height: 1.6; }

/* Bands -------------------------------------------------------------------
 * Alternating grounds, so the eye has somewhere to rest between sections
 * without a rule across the page.
 */
.brochure .band { padding: 3.5rem 1.25rem; }
.brochure .band.alt { background: var(--surface); border-block: 1px solid var(--border); }
.brochure .band-inner { max-width: 1000px; margin: 0 auto; }

/* The hero ----------------------------------------------------------------
 * No photograph. The graphic is three real calls for water on one clamp, drawn
 * from the readings themselves, sitting behind the words as a band across the
 * bottom. It is the product describing itself: this is what the thing sees.
 *
 * Dark in both themes, because the trace is a light line and the words are
 * white over it. A hero that inverted would need two of everything.
 */
.brochure .hero {
  position: relative;
  display: grid;
  align-items: center;
  min-height: min(62vh, 520px);
  padding: 4rem 1.25rem 12rem;
  background:
    radial-gradient(120% 90% at 18% 0%, #1b2432 0%, rgba(17, 22, 28, 0) 60%),
    linear-gradient(180deg, #11161c 0%, #0c1015 100%);
  overflow: hidden;
  isolation: isolate;
}
.brochure .hero-trace {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: -1;
  width: 100%;
  height: clamp(140px, 24vh, 190px);
  opacity: 0.95;
}
.brochure .hero-grid line {
  stroke: rgba(255, 255, 255, 0.07);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.brochure .hero-caption {
  position: relative;
  max-width: 1000px;
  margin: 0 auto;
  padding-top: 0.75rem;
  width: 100%;
  font-size: 0.76rem;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.45);
}
.brochure .hero-inner { max-width: 1000px; margin: 0 auto; width: 100%; }
/* Fixed light values, not tokens. This text is over a dark photograph in both
 * themes, so a token that flips would turn it black on black at dusk. */
.brochure .hero h1 {
  margin: 0.6rem 0 0;
  font-size: clamp(1.9rem, 5.2vw, 3.1rem);
  line-height: 1.12;
  letter-spacing: -0.02em;
  color: #fff;
  text-wrap: balance;
}
.brochure .eyebrow {
  margin: 0;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: #9ec5ff;
}
.brochure .hero-lede {
  margin: 1rem 0 0;
  max-width: 54ch;
  font-size: clamp(1rem, 2.1vw, 1.14rem);
  color: rgba(255, 255, 255, 0.88);
}
.brochure .hero-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin: 1.8rem 0 0;
}
.brochure .hero-links .button {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.38);
  color: #fff;
}
.brochure .hero-links .button:hover { background: rgba(255, 255, 255, 0.2); }
.brochure .hero-links .button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.brochure .hero-links .button.primary:hover { filter: brightness(1.08); }

/* What it watches ---------------------------------------------------------- */
.brochure .split {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: 2.5rem;
  align-items: start;
}
.brochure .split-text h3:first-child { margin-top: 0; }
.brochure .split-text .note {
  margin-top: 1.25rem;
  padding-left: 0.9rem;
  border-left: 3px solid var(--accent);
  color: var(--text-muted);
}
/* The schematic: what is measured, what carries it, what reads it. Drawn with
 * the page's own tokens so it is legible in either theme without a second
 * copy. */
.brochure .split-media {
  display: grid;
  place-items: center;
}
.brochure .flow {
  width: 100%;
  max-width: 320px;
  height: auto;
}
.brochure .flow-node rect {
  fill: var(--surface-2);
  stroke: var(--border);
  stroke-width: 1;
}
.brochure .flow-node.accent rect {
  fill: color-mix(in srgb, var(--accent) 12%, var(--surface-2));
  stroke: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.brochure .flow-node text {
  fill: var(--text);
  font-size: 13px;
  font-weight: 600;
  text-anchor: middle;
}
.brochure .flow-node text.flow-sub {
  fill: var(--text-muted);
  font-size: 10.5px;
  font-weight: 400;
}
.brochure .flow-link path {
  fill: none;
  stroke: var(--border);
  stroke-width: 1.5;
  stroke-linecap: round;
}
.brochure .flow-end {
  fill: var(--text-muted);
  font-size: 11px;
  text-anchor: middle;
}
.brochure figure { margin: 0; }
.brochure figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-2);
}
.brochure figcaption {
  margin-top: 0.5rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}

/* The features ------------------------------------------------------------- */
.brochure .features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem 2rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.brochure .features li { margin: 0; }
.brochure .features svg {
  width: 26px;
  height: 26px;
  color: var(--accent);
}
.brochure .features h3 { margin: 0.5rem 0 0.35rem; }
.brochure .features p { margin: 0; color: var(--text-muted); }

/* The screenshots ---------------------------------------------------------
 * Tall and narrow because the pages are. Capped in height and cropped from
 * the top so three sit side by side without becoming a wall, which is the
 * reason each one also opens full length: the thumbnail is a crop, and the
 * part that got cropped is most of the page.
 */
.brochure .shots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 1.5rem;
  max-width: 820px;
}
.brochure .shot {
  position: relative;
  display: block;
  border-radius: var(--radius);
  cursor: zoom-in;
}
.brochure .shots figure img {
  max-height: 460px;
  object-fit: cover;
  object-position: top;
}
/* Always visible rather than on hover, because half the people reading this
 * are on a phone and a phone has no hover. */
.brochure .shot-zoom {
  position: absolute;
  right: 0.5rem;
  bottom: 0.5rem;
  padding: 0.2rem 0.5rem;
  border-radius: 999px;
  background: rgba(10, 13, 17, 0.72);
  color: #fff;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
}
.brochure .shot:hover .shot-zoom,
.brochure .shot:focus-visible .shot-zoom { background: var(--accent); }

/* Enlarged, with no script. The overlay is hidden until it is the fragment
 * in the address bar, which is what an ordinary link to `#shot-now` makes it.
 * It scrolls rather than fitting to the viewport, because these pages are
 * 3.5 times taller than they are wide and fitting one to the screen would
 * make it smaller than the thumbnail it came from.
 */
.brochure .lightbox { display: none; }
.brochure .lightbox:target {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 60;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: rgba(8, 10, 14, 0.88);
  backdrop-filter: blur(3px);
}
.brochure .lightbox-back {
  position: fixed;
  inset: 0;
  cursor: zoom-out;
}
.brochure .lightbox-body {
  position: relative;
  width: min(430px, calc(100vw - 2.5rem));
  margin: 1.25rem auto 2.5rem;
}
.brochure .lightbox-body img {
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.brochure .lightbox-close {
  position: fixed;
  top: 0.75rem;
  right: 0.9rem;
  width: 2.2rem;
  height: 2.2rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(10, 13, 17, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  font-size: 1.4rem;
  line-height: 1;
  text-decoration: none;
}
.brochure .lightbox-close:hover { background: var(--accent); }

@media (max-width: 820px) {
  .brochure .band { padding: 2.5rem 1.1rem; }
  .brochure .split { grid-template-columns: 1fr; gap: 1.75rem; }
  .brochure h2 { font-size: 1.35rem; }
  /* The trace is positioned against the bottom, so the padding here is what
   * keeps it clear of the button rather than decoration. */
  .brochure .hero { min-height: 0; padding: 2.5rem 1.1rem 9rem; }
  .brochure .hero-trace { height: clamp(96px, 16vh, 128px); }
  /* The enlarged view nearly fills a phone, so the close button would sit on
   * top of the screenshot's own header. Start the image below it. */
  .brochure .lightbox-body { margin-top: 3.5rem; }
}
