/* IBM Plex, the brand's UI face (1.105.0; design/brand/README.md): Sans
 * for the chrome, Mono for the data. Shipped weights are 400, 500 and 600,
 * so nothing here asks for 700 -- a browser would fake it. Quicksand, the
 * brand's own face, is only in the logo, which is outlines (brand.js). */
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-latin-400-normal.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-latin-500-normal.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-latin-600-normal.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2");
}

/* panel
 *
 * Sans for the chrome, monospace for the data. Hostnames, addresses, rule text
 * and counters have to align and be read character by character; labels and
 * prose do not, and setting them in monospace made the whole thing read like a
 * terminal transcript rather than something you operate.
 *
 * Colour carries meaning and nothing else. Pine is the panel; brick means an
 * action reaches the root process, and appears nowhere it does not. Pine
 * replaced cobalt once the mockup-matching phase of this redesign settled
 * and the user asked for something less generic than the blue every network
 * tool reaches for -- a confident single pick, not another round of swatches.
 */

:root {
  /* The page is not white, which is what gives a card something to sit on: a
   * white card on a white page needs a border to exist at all, and a border
   * around everything is what made the old panel feel boxed in. */
  /* Retuned at 0.94.0 to the palette of the design mockup the user
   * supplied: a cool grey page, white cards, and the system's own accent
   * blue rather than the pine of 0.81.0. The token *names* are unchanged
   * on purpose -- contrast-check.py reads --ink, --paper, --surface,
   * --marine and --privl by name and measures every pairing at 4.5:1, so
   * renaming them to the mockup's own (--bg, --tx, --card) would have
   * quietly disabled the one check that catches unreadable text. The
   * values moved; the guarantee did not. */
  --paper:   #F2F3F5;
  --surface: #FFFFFF;
  --sunk:    #F7F8F9;
  --ink:     #1D1D1F;   /* every colour here is measured against the surfaces */
  --muted:   #5F5F66;   /*  the mockup's #6e6e73 is 4.36:1 on --surface, under the floor */
  --faint:   #6B6B72;   /*  likewise: its #8e8e93 is 2.9:1 and would fail outright */
  --rule:    #E4E5E9;
  --marine:  #0A66C9;   /*  the mockup's blue, darkened from #0a84ff to clear 4.5:1 on its own wash too */
  --marine-wash: #E7F1FD;
  /* TRAFFIC DIRECTION (1.91.13): download blue, upload purple, everywhere a
   * rate is shown. --down/--up are for marks -- lines, fills, dots -- and
   * were chosen together with scripts from the dataviz method: in both
   * themes they pass the colour-blind separation check (worst ΔE 9.8 light,
   * 10.0 dark, deuteranopia/protanopia) and 3:1 against the card. Hue alone
   * could not do it -- blue and violet collapse for a deuteranope -- so the
   * purple is LIGHTER than the blue here and darker in the dark theme.
   * --up-text is the same purple at 4.5:1+ for figures, as --marine is for
   * download figures. --local was a purple too until 1.91.15 and is
   * neutral now, so purple means upload and nothing else. */
  --down:    #0A66C9;
  --up:      #B573E6;
  --up-text: #8D4BB7;   /*  5.5:1 on --surface */
  --privl:   #B03A30;   /*  root actions stay their own colour, and stay readable */
  --privl-wash: #FBECEA;
  /* Caution, which is not danger.
   *
   * .warn was built on --privl and so rendered at #B03A30 -- within a
   * hair of --danger's #B3261E. Two states meant to read differently
   * looked identical, which is worse than not distinguishing them at
   * all: somebody learns that the red box sometimes means nothing.
   *
   * Amber, darkened until it clears 4.5:1 on both its own wash and the
   * surface -- the mockup's lighter amber measured 2.07:1 in the dark
   * theme, which is the mistake noted above this block.
   */
  --caution: #8A5A00;
  --caution-wash: #FBF2E0;
  --teal:    #0E6F7E;   /*  a person's colour (1.125.0), 5.6:1 on its wash */
  --teal-wash: #E1F2F4;
  /* The kinds' hues (1.172.0, decision 307): a tile's and a section's icon
   * and label, as the app has them. Text colours, measured like the rest. */
  --k-purple: #7A3FC4;
  --k-pink:   #B4235F;
  --k-orange: #A84C07;
  --k-indigo: #4B4FC8;
  /* A card is a ring and a hint of lift rather than a drawn border. The
   * mockup's own shadow, which is the same idea: .5px ring plus a 1px
   * drop, no border anywhere. */
  /* An elevation scale, not one shadow.
   *
   * Every card carried the same --lift, so the hero and a minor stat
   * panel sat at the same height and the eye had nothing to rank them
   * by. Elevation is the cheapest hierarchy there is and it was spent on
   * nothing.
   *
   * The hairline ring is kept in each: it is what separates a card from
   * the page on a screen where the shadow is nearly invisible.
   */
  --lift: 0 1px 2px rgba(0,0,0,.045), 0 0 0 0.5px rgba(0,0,0,.06);
  --lift-flat: 0 0 0 0.5px rgba(0,0,0,.07);
  --lift-raised: 0 4px 16px -4px rgba(15,23,42,.10), 0 0 0 0.5px rgba(0,0,0,.06);
  /* A button face, a shade off the card it sits on, so it reads as a control
   * rather than as a line drawn on the background. */
  /* What is knocked out of the solid mark: the page behind it, so the shape
   * reads as a panel with content in it rather than as a filled block. */
  /* Direction, on the connections list: incoming, outgoing, and from the LAN.
   * Colour that carries a fact rather than decorating one. */
  --in:     #0F6E56;
  --in-wash: #E4F2EC;
  --out:    #1B5FA8;
  --out-wash: #E7EFF9;
  /* Local is NEUTRAL since 1.91.15: it went neither in nor out. It was a
   * purple, which upload now is. Every hue near the others is taken --
   * orange measured within ΔE 5-11 of --caution and --privl -- and a slate
   * was too close to --out; zinc clears all of them (ΔE >= 15 normal, >= 9
   * for colour-blind vision). Used only by the connections-list rules,
   * which no markup sets today. */
  --local:  #3F3F46;
  --local-wash: #EEEEF0;
  /* Apps' hue (1.116.0): --up-text's violet, with a wash of its own for
   * the icon tiles and pills on the Apps page and the Modules page. */
  --violet-wash: #F4ECFA;
  /* The logo (1.105.1): the outline and the word, and the knob, which is
   * the accent blue -- the operator's call. */
  --brand-ink:  var(--ink);
  --brand-knob: var(--marine);
  --btn-face: #EFECE6;
  --btn-face-hover: #E6E2DA;
  --btn-edge: #D9D5CC;
  /* Solid button faces. An accent that reads as text on a page is not the same
   * colour as one that carries white text on a button, and using one token for
   * both is what put white on #E5A955 at 2.07:1 in the dark theme. */
  --marine-solid: #0A66C9;   /*  5.6:1 under #fff */
  --privl-solid:  #A83326;   /*  darker than --privl, for white text on a fill */
  --danger:  #B3261E;
  --danger-wash: #FBEAE8;
  --tape-bg: #1B1B1E;
  --tape-fg: #C4C4CA;

  /* Shipped rather than borrowed from the system, so the panel reads the same
   * on a workstation as on a phone as on a console browser, and so an
   * appliance with no route to the internet still gets it. */
  --sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", "DejaVu Sans Mono", "Liberation Mono", Menlo, monospace;
  /* Large figures and the countdown. It was a display face for the word
   * "panel" too; the logo is a drawing now, and the brand keeps the UI in
   * Plex (1.105.0). */
  --display: var(--sans);
  --tape-h: 148px;
  /* A radius scale, not one radius.
   *
   * Everything used --radius: 10px -- the hero, a card, a button, a 20px
   * tag. Identical corners say identical kind of object, and a
   * page-dominating panel and an inline chip are not the same kind of
   * object. Curvature reads as softness, and softness should follow
   * size: a large surface with a small radius looks machined, a small
   * one with a large radius looks like a pill.
   *
   * --radius stays 10px so nothing unconverted changes.
   */
  --radius: 10px;
  --r-chip: 999px;   /* pills and tags: fully round, so they read as labels */
  --r-control: 8px;  /* buttons, inputs: crisp, hand-sized */
  --r-card: 14px;    /* cards: softer, because they are large and passive */
  --r-hero: 18px;    /* the one panel that dominates the page */
}

body.tape-shut { --tape-h: 30px; }

/* No tape at all, which is the normal case.
 *
 * The event stream became development-only at 1.30.1, but --tape-h went
 * on reserving 30px for it -- so every page carried a strip of nothing
 * at the bottom, and on a phone the tab bar floated above a gap. The
 * script adds this class when it decides not to build the tape, because
 * removing an element does not tell a stylesheet anything.
 */
body.no-tape { --tape-h: 0px; }

/* Dark is not light with the numbers flipped.
 *
 * Every one of these was measured against the card it sits on. The previous
 * --faint came out at 3.2:1, under the 4.5 that text needs to be read without
 * effort -- and --faint carries every caption, axis label and sub-line on the
 * page, so it was most of what there was to read. The page is also pushed
 * darker than the cards rather than nearly equal to them: on a dark ground a
 * card has to be lighter than what it sits on, or it stops being a card. */
@media (prefers-color-scheme: dark) {
  :root {
    /* Warm, and not nearly black.
     *
     * Two things were wrong. The greys disagreed -- backgrounds leaning blue,
     * text leaning green -- so they pulled against each other and read as
     * muddy however well the contrast measured. And the page was #0F0F10,
     * which is essentially black: every ratio was enormous and the whole thing
     * was harsh to sit in front of. A dark interface wants a dark grey, not an
     * absence of light.
     *
     * Everything leans the same way now, warm, and the page is light enough to
     * be a surface rather than a hole. */
    /* Retuned at 0.94.0 to the mockup's own dark values -- a neutral
     * near-black page with a distinctly lighter card, which is the same
     * principle the warm palette below was built on: on a dark ground a
     * card has to be lighter than what it sits on or it stops being a
     * card. The warmth is gone; the structure is not. */
    --paper:   #1A1A1C;
    /* The mockup's card is #2C2C2E. Nudged two steps darker: white text on
     * a filled button needs 4.5:1 on the fill *and* the fill needs 3:1 on
     * the card behind it, and against #2C2C2E those two pull hard enough
     * against each other that nothing recognisably blue or red clears both
     * -- a sweep of the whole colour space returned only muddy purples.
     * Two steps of card darkness buys the headroom back and is not a
     * visible departure from the mockup. */
    --surface: #26262A;
    --sunk:    #202024;
    --ink:     #F5F5F7;   /* 13.4:1 on a card */
    --muted:   #A1A1A6;   /*  6.2:1 */
    --faint:   #949499;   /*  5.2:1 -- the mockup's #8a8a8e is 4.4:1, just under */
    --rule:    #3A3A3D;
    --marine:  #4C9DF5;   /*  the mockup's blue, lightened for a dark card */
    --marine-wash: #1B2C42;
    /* See the light values. #4495EA rather than --marine's #4C9DF5 for the
     * marks: the chart method's lightness band for a dark surface ends at
     * L 0.67 and #4C9DF5 is 0.69, and in band the pair separates cleanly. */
    --down:    #4495EA;
    --up:      #9752B4;
    --up-text: #B786DB;   /*  5.4:1 on a card */
    /* A drop shadow on dark reads as a glow, so the lift is a light ring.
     *
     * The scale still exists here, expressed in ring brightness rather
     * than shadow depth: a raised surface catches more light at its edge.
     * A dark card given a light-theme shadow would look smudged, which is
     * what would have happened if only the light values were defined.
     */
    --lift: 0 0 0 0.5px rgba(255,255,255,.07);
    --lift-flat: 0 0 0 0.5px rgba(255,255,255,.045);
    --lift-raised:
      0 0 0 0.5px rgba(255,255,255,.12),
      0 8px 24px -8px rgba(0,0,0,.55);
    --in:     #58C79E;
    --in-wash: #16302A;
    --out:    #79B3E8;
    --out-wash: #16253A;
    --local:  #D4D4D8;
    --local-wash: #303034;
    --violet-wash: #2C2236;
    /* Softer than the text's near-white (1.105.1): at 22px the outline and
     * word glared on the dark header. 9.9:1 on --surface still. */
    --brand-ink:  #D1D1D6;
    --btn-face: #2C2A26;
    --btn-face-hover: #35322D;
    --btn-edge: #3D3A34;
    --privl:   #F0938A;   /*  the same brick as light mode, brightened for a dark card */
    /* Caution in the dark theme: light enough to clear 4.5:1 on a dark
     * card, and far enough from --privl that the two are not confusable
     * at a glance -- which is the whole reason this colour exists. */
    --caution: #E8B44A;
    --caution-wash: #3A2E12;
    --teal:    #5CC4D2;
    --teal-wash: #13313A;
    --k-purple: #B794F6;
    --k-pink:   #F27AAE;
    --k-orange: #F2A65A;
    --k-indigo: #9BA3FA;
    --privl-wash: #3A2422;
    /* These sit on a card, not on the page, and the card is now #2C2C2E --
     * considerably lighter than the old #201F1C, so the previous solids
     * fell under the 3:1 a filled button needs to be distinguishable from
     * what it sits on. Lightened to clear it. */
    --marine-solid: #2B76C4;   /*  4.7:1 under #fff, 3.2:1 on a card */
    --privl-solid:  #BE5049;   /*  4.7:1 under #fff, 3.2:1 on a card */
    --danger:  #F0938A;   /*  7.3:1 */
    --danger-wash: #3C211A;
    --tape-bg: #151518;
    --tape-fg: #B9B9BF;
  }
}

* { box-sizing: border-box; }

/* A step bigger on a phone (1.173.0, decision 314): everything, a tenth,
 * as the operator found the words a tad small there. The sizes are in
 * pixels throughout, so the page is zoomed rather than each one changed;
 * the layout still fits, at 355 of a 390-wide phone's points. */
@media (max-width: 640px) {
  html { zoom: 1.1; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  /* 14.5/1.45 and -.004em are the mockup's own body metrics. The typeface
   * stays Inter: it is self-hosted here (see the @font-face at the top),
   * so the panel renders identically on a machine with no network and no
   * system font of the mockup's choosing, which an appliance on a LAN
   * frequently is. Swapping to -apple-system would look right on the
   * designer's Mac and different everywhere else, and would leave two
   * font files shipped and unused. */
  font: 14.5px/1.45 var(--sans);
  letter-spacing: -0.004em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Every figure that can change under you: columns must not shuffle sideways
 * as a counter ticks over. */
.kv dd, td, .tile .value, .countdown, .num, .lines .n,
.tape .line, table.rules td { font-variant-numeric: tabular-nums; }

button, input, select, textarea { font: inherit; color: inherit; }
a { color: var(--marine); }

/* Anything read as a value rather than as language. */
.mono, .kv dd, td, th.right, .rule, .countdown, pre.conf, textarea.editor,
.tape, .signin input { font-family: var(--mono); }

/* --- primitives ------------------------------------------------------- */

.lbl {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 5px;
}

/* A button has to look like something you press.
 *
 * This was --surface with a hairline border, which is white on white with a
 * line round it: on a card it did not read as a control at all. It now has a
 * filled face a shade off the card it sits on, a border with some weight, and
 * a size worth aiming at.
 */
.btn {
  background: var(--btn-face);
  border: 1px solid var(--btn-edge);
  border-radius: var(--radius);
  padding: 8px 15px;
  min-height: 34px;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}
.btn:active { transform: translateY(0.5px); }
.btn:hover { background: var(--btn-face-hover); border-color: var(--marine); color: var(--marine); }

/* Working. The spinner sits before the label rather than replacing it, so the
 * button keeps its width and the row does not jump while something is running.
 * Disabled buttons are dimmed; a busy one is not, because it is doing what was
 * asked rather than refusing. */
.btn.busy { position: relative; padding-left: 30px; opacity: 1; cursor: progress; }
.btn.busy::before {
  content: ""; position: absolute; left: 11px; top: 50%; width: 12px; height: 12px;
  margin-top: -6px; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: panel-spin .7s linear infinite;
}
@keyframes panel-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn.busy::before { animation-duration: 2.4s; }
}
.btn:focus-visible { outline: 2px solid var(--marine); outline-offset: 1px; }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn.primary { background: var(--marine-solid); border-color: var(--marine-solid); color: #fff; }
.btn.primary:hover { opacity: .9; color: #fff; }
/* Quiet: a control that is present without asking for attention -- Cancel,
 * Configure, Change settings, Show all.
 *
 * It used to turn red on hover, which was written when the only quiet buttons
 * were Cancel and was wrong the moment there were thirty-seven of them:
 * hovering "Configure" told the reader it was about to destroy something. Red
 * on hover now belongs to .danger, which is the class that means it.
 */
/* Since 1.117.0 a quiet button has an outline: with none (it was
 * `border-color: transparent; color: var(--muted)`), Eject, Format...,
 * Disconnect and Stop read as labels (the operator, 1.116.3). Three kinds
 * of button, and only three: filled for the main thing, an outline for the
 * rest, red for what destroys. Quiet is an outline with no face, so it
 * still sits back beside a filled one. */
.btn.quiet { background: none; border-color: var(--btn-edge); color: var(--ink); }
.btn.quiet:hover { color: var(--marine); border-color: var(--marine); }

/* Danger: a control that destroys something.
 *
 * The comment above .quiet has referred to this class since the hover
 * colour was moved off .quiet -- and nothing defined it, so every button
 * asking for it got the plain style. Named in prose and absent in fact,
 * which is the same fault .ok had.
 *
 * Not red until hovered: a row of red buttons is a page that looks
 * alarming at rest, and the colour means more when it arrives as the
 * pointer does.
 */
.btn.danger { background: none; border-color: var(--rule); color: var(--danger); }

/* A filled destructive button, for the confirming action of a
 * confirmation drawer.
 *
 * The outline above is right in a form, where Delete sits beside fields
 * and must not shout. It is wrong on a drawer that exists ONLY to ask
 * "remove this?" -- there the destructive action is the primary one, and
 * leaving it as an outline beside a quiet Cancel gives a dialog with no
 * primary action at all.
 *
 * Red rather than the panel's blue: `root` would make the button that
 * destroys something look like the button that saves one. */
.btn.danger.fill {
  background: var(--privl-solid);
  border-color: var(--privl-solid);
  color: #fff;
}
.btn.danger.fill:hover { background: var(--privl-solid); opacity: .9; color: #fff; }
.btn.danger:hover {
  background: var(--danger-wash);
  border-color: var(--danger);
  color: var(--danger);
}

/* Destructive, and only that. The one thing on a page that cannot be undone by
 * doing it again. */
.btn.quiet.danger:hover { color: var(--danger); border-color: var(--danger); }

/* The only place brick appears: this leaves the unprivileged tier. */
/* The primary action is BLUE, not red.
 *
 * `root` means "this reaches the root process", and it was coloured with
 * --privl to match the privilege convention used elsewhere -- the pending
 * band, the confirm/rollback state. Consistent, and wrong here.
 *
 * Colour on a button encodes CONSEQUENCE, not mechanism. Nobody reads a
 * red fill as "privileged"; they read it as "careful". So the VPN peer
 * drawer offered "Save the name" as a red fill -- the loudest thing on
 * the panel -- for a rename that changes a label, while "Remove this
 * device", which cannot be undone because the panel does not keep the
 * key, sat quietly beside it as an outline.
 *
 * The irreversible action was the quieter one. Red belongs to
 * destruction, and now only to it.
 *
 * Privilege is still worth saying, and the drawers already say it in
 * words -- "applying this restarts the tunnel" -- where a colour could
 * only hint.
 */
.btn.root { background: var(--marine-solid); border-color: var(--marine-solid); color: #fff; }
.btn.root:hover { opacity: .9; color: #fff; }

.tag {
  display: inline-block;
  font: 11px/1.6 var(--sans);
  border-radius: 20px;
  padding: 1px 9px;
  background: var(--sunk);
  color: var(--muted);
}
/* A label somebody wrote down, as against one this panel worked out. They were
 * the same grey, so a description and an inferred role read as equally
 * authoritative -- and on an interface called "LAN interface" with an inferred
 * role of LAN, as the same fact stated twice. */
.tag.said { background: var(--marine-wash); color: var(--marine); font-weight: 500; }
.tag.guess { background: transparent; color: var(--faint); border: 0.5px solid var(--rule); }
.tag.root { background: var(--privl-wash); color: var(--privl); }
.tag.ok   { background: var(--marine-wash); color: var(--marine); }
.tag.off  { background: var(--sunk); color: var(--muted); }
.tag.bad  { background: var(--danger-wash); color: var(--danger); }

/* textarea belongs here and was missing.
 *
 * The focus rule immediately below already names it, so the intent was
 * always to treat these alike -- a textarea just had no use until the
 * DHCP settings drawer needed one, and it would have rendered with the
 * browser's default border against the panel's own inputs. */
input[type=text], input[type=password], select, textarea {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 8px 10px;
  width: 100%;
  font-size: 13px;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--marine); }

/* --- sign in ---------------------------------------------------------- */

.signin { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.signin form {
  width: 100%;
  max-width: 340px;
  border-radius: 16px;
  background: var(--surface);
  box-shadow: 0 1px 3px rgba(20,20,25,.07), 0 8px 28px rgba(20,20,25,.06), 0 0 0 0.5px rgba(20,20,25,.07);
  padding: 32px 30px 30px;
}

/* The mark and the name together, which is the only place the panel introduces
 * itself. There was a line under it saying what OpenBSD is for; anybody at this
 * screen already knows what they are signing in to. */
.signin-brand {
  display: flex; align-items: center; gap: 11px;
  margin: 0 0 26px;
}
/* The logo at 40px, the regular cut (design/brand: 28px and up). It takes
 * the text colour, as the brand asks: no colour of its own. */
.signin h1 { margin: 0; line-height: 0; }
.signin .brand-logo { height: 40px; width: auto; color: var(--brand-ink); }
.signin .field { margin-bottom: 16px; }
.signin .btn { width: 100%; margin-top: 8px; }
/* The second step (1.152.2): the code alone, said as a step, big enough
 * to read back against the phone. */
.signin-step { margin: 0 0 12px; color: var(--muted); font-size: 14px; line-height: 1.45; font-family: var(--sans); }
.signin .signin-code { font-size: 22px; letter-spacing: 0.3em; text-align: center; padding: 10px; }
.signin .signin-back { margin-top: 6px; border-color: transparent; background: none; box-shadow: none; color: var(--muted); font-weight: 500; }
.signin .signin-back:hover { color: var(--ink); border-color: transparent; }
/* Autofill in the panel's own colours: the browser's own (an olive yellow
 * in a dark theme) painted the two fields the operator had just typed. */
input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--ink);
  box-shadow: 0 0 0 1000px var(--surface) inset;
  transition: background-color 600000s 0s;
}

.err {
  border-left: 3px solid var(--danger);
  background: var(--danger-wash);
  color: var(--danger);
  padding: 9px 12px;
  border-radius: 0 var(--radius) var(--radius) 0;
  margin-bottom: 14px;
  font-size: 13px;
}
.err .hint { color: var(--muted); display: block; margin-top: 3px; }

/* A caution that is not a failure.
 *
 * Between .err and .ok-note, and needed because the interface editor has
 * three answers rather than two: this would cut you off, this is fine,
 * and -- the common one -- the panel cannot tell. That third is not a
 * failure and must not look like one, or somebody learns to dismiss the
 * red box that sometimes means something.
 */
.warn {
  border-left: 3px solid var(--caution);
  background: var(--caution-wash);
  color: var(--caution);
  padding: 9px 12px;
  border-radius: 0 var(--radius) var(--radius) 0;
  margin-bottom: 14px;
  font-size: 13px;
}
.warn .hint { color: var(--muted); display: block; margin-top: 3px; }

/* The hint on its own line, as it already is inside .err and .warn.
 *
 * Without it the two spans are inline neighbours with no whitespace between
 * them, and the card read
 *
 *     Version 1.72.6 is ready to install.The whole panel restarts -- ...
 *
 * Seen in a screenshot on a real machine. `.err .hint` and `.warn .hint`
 * have carried this rule for a long time; `.ok-note` was built later and
 * did not get it, so the one note that pairs a headline with a hint was the
 * one where they ran together.
 *
 * Both classes are set in app.js on the staged-release note -- `ok-note` on
 * the div, `hint` on the inner span -- so this selector matches markup that
 * exists. That is checked rather than assumed; there is no browser here.
 */
.ok-note .hint { display: block; margin-top: 3px; }

.ok-note {
  border-left: 3px solid var(--marine);
  background: var(--marine-wash);
  color: var(--marine);
  padding: 9px 12px;
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: 13px;
}

/* --- frame ------------------------------------------------------------ */

.top {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 20px;
  height: 46px;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}
/* The logo, small cut, at 22px (design/brand: under 28px). */
.top .brand-logo { height: 22px; width: auto; color: var(--brand-ink); flex: none; margin-right: 4px; }
/* The knob in the accent blue: the switch is on. A class rather than a
 * fill attribute, which cannot read a custom property. */
.brand-logo .knob { fill: var(--brand-knob); }
/* The version is a footnote to the name, not a second word in it. */
.top .ver {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--faint);
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: 20px;
  padding: 1px 7px;
  align-self: center;
}
/* A link to About since 1.93.1; says so on hover, and keeps its look. */
.top a.ver { text-decoration: none; }
.top a.ver:hover, .top a.ver:focus-visible { color: var(--ink); border-color: var(--muted); }
.top .spacer { flex: 1; }
.top .who { color: var(--muted); font-size: 13px; background: none; border: 0; padding: 4px 6px; border-radius: 6px; cursor: pointer; font-family: inherit; }
.top .who:hover { color: var(--ink); background: var(--sunk); }
/* The search in the top bar, for a phone, where the rail is not (1.154.0). */
.top .findtop { display: none; }
/* The second factor (1.152.0): its heading, the QR code on white, the key. */
.tfabox { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--rule); }
.tfahead { margin: 0 0 6px; font-size: 13px; font-weight: 600; }
.tfaqr { margin: 10px 0; }
.tfakey { overflow-wrap: anywhere; }
.sheet-you { width: 100%; background: none; border: 0; font: inherit; color: inherit; text-align: left; cursor: pointer; }

/* --- pending band --------------------------------------------------------
 *
 * A commit-confirm window used to be visible only on the page that started
 * it: leave the firewall page mid-countdown and there was nothing anywhere
 * else on screen to say a rollback was coming, or to confirm or undo it
 * from -- and if the change in progress was the one that cut off access,
 * that also meant no way back to the one page that could still fix it.
 *
 * This sits under the header on every page instead: pinned, not a modal, so
 * whichever page is open stays readable while the clock runs. --privl-solid
 * rather than --privl itself, for the reason the comment by its definition
 * gives -- a colour that reads as text on a page is not the one that carries
 * white text on a fill, and reusing it silently is how white on --privl
 * reached a browser at 2.07:1 in the dark theme before contrast-check.py
 * existed to say so.
 */
.pending-band:empty { display: none; }
.pending-band-item {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 20px;
  background: var(--privl-solid);
  color: #fff;
}
.pending-band-item + .pending-band-item { border-top: 1px solid rgba(255,255,255,.25); }
.pending-band .count {
  font-family: var(--display);
  font-weight: 600;
  font-size: 26px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  flex: none;
  min-width: 2.4ch;
  text-align: right;
}
.pending-band .what { min-width: 0; }
.pending-band .headline { font-weight: 600; font-size: 13px; }
.pending-band .sub { font-size: 12px; opacity: .88; margin-top: 1px; }
.pending-band .actions { margin-left: auto; display: flex; gap: 8px; flex: none; }
/* Both buttons keep the band's own colour rather than borrowing --marine or
 * --danger: this band is already the "reaches root" signal, and mixing in
 * another colour on top of it would read as a second, unrelated warning. */
.pending-band .btn { border-color: rgba(255,255,255,.6); color: #fff; }
.pending-band .btn:hover { background: rgba(255,255,255,.15); border-color: rgba(255,255,255,.6); color: #fff; }
.pending-band .btn.keep { background: #fff; color: var(--privl-solid); border-color: #fff; }
.pending-band .btn.keep:hover { background: rgba(255,255,255,.85); color: var(--privl-solid); }

.body { display: flex; min-height: calc(100vh - 46px - var(--tape-h)); padding-bottom: var(--tape-h); }

/* --- drawer ---------------------------------------------------------------
 *
 * One editing pattern instead of each module inventing its own. Every editor
 * used to grow inline where you clicked -- a card expanding under the row --
 * which meant no two modules opened, closed or laid out an edit the same way,
 * and on a phone an inline card pushed everything below it down the page
 * before you had typed anything.
 *
 * The drawer is the same element at every width: it always slides in from the
 * right over a scrim, never from the bottom, so a desktop and a phone are
 * teaching the same gesture rather than two. What changes at the narrow
 * breakpoint is only its width -- a partial panel has nowhere to go on a
 * phone-sized screen, so it takes the whole one, still arriving from the
 * right rather than switching to a sheet from below.
 *
 * A module reaches this through ctx.drawer.open(title, build) rather than
 * building its own overlay, the same bargain as ctx.button and ctx.details:
 * one implementation of the mechanics so a module's code is only ever the
 * part that is actually about that module.
 */
.drawer-scrim {
  position: fixed; inset: 0; z-index: 55;
  background: rgba(10,10,12,.4);
  opacity: 0; pointer-events: none;
  transition: opacity .16s;
}
body.drawer-open .drawer-scrim { opacity: 1; pointer-events: auto; }
.drawer-panel {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 60;
  width: min(440px, 100%);
  background: var(--surface);
  display: flex; flex-direction: column;
  transform: translateX(100%);
  transition: transform .18s ease-out;
}
/* The shadow only while open: shut, it showed as a grey edge down the
 * right of every page (found at 1.173.0). */
body.drawer-open .drawer-panel { transform: translateX(0); box-shadow: -2px 0 24px rgba(10,10,12,.22); }
.drawer-head {
  display: flex; align-items: center; gap: 10px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--rule);
  flex: none;
}
.drawer-head h3 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.drawer-close {
  margin-left: auto; flex: none;
  width: 30px; height: 30px; padding: 0;
  border-radius: var(--radius);
  background: none; border: 1px solid transparent;
  color: var(--muted); cursor: pointer;
  font-size: 18px; line-height: 1;
}
.drawer-close:hover { background: var(--paper); color: var(--ink); }
.drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: 18px; }

@media (prefers-reduced-motion: reduce) {
  .drawer-scrim, .drawer-panel { transition: none; }
}

.rail { width: 200px; flex: none; background: var(--surface); border-right: 0.5px solid var(--rule); padding: 18px 12px; }
.rail .group {
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--faint);
  padding: 16px 10px 5px;
}
.rail .group:first-child { padding-top: 4px; }
.rail a {
  /* Flex, for the icon the rail gained at 1.34.2. `display: block` put
   * the svg on its own line above the label. */
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 7px 10px;
  margin-bottom: 2px;
  border-radius: var(--radius);
  color: var(--muted);
  text-decoration: none;
  font-size: 14px;
}
.rail a:hover { background: var(--paper); color: var(--ink); }
.rail a.on { background: var(--marine-wash); color: var(--marine); font-weight: 600; }
.rail .empty { padding: 4px 10px; color: var(--faint); font-size: 12px; }

/* --- the menu of four (1.154.0; decisions 252-262) ------------------------
 *
 * The rail holds the search and Home, Network, Activity, Settings. A page
 * under Settings has Settings' own list beside it, the subrail, on a wide
 * screen; on a narrower one that list is a page, and the page under it
 * leads with the way back to it (.backto). */
.rail .findbtn {
  display: flex; align-items: center; gap: 9px;
  width: 100%; height: 36px; padding: 0 10px; margin-bottom: 14px;
  border: 1px solid var(--rule); border-radius: var(--r-control);
  background: var(--paper); color: var(--faint);
  font: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
.rail .findbtn svg { width: 16px; height: 16px; flex: none; }
.rail .findbtn .navlabel { flex: 1; }
.rail .findbtn kbd { font-family: var(--mono); font-size: 11px; border: 1px solid var(--rule); border-radius: 4px; padding: 0 5px; }
.rail .findbtn:hover { color: var(--ink); border-color: var(--muted); }

.subrail { width: 184px; flex: none; background: var(--sunk); border-right: 0.5px solid var(--rule); padding: 18px 10px; }
.subrail .subhead { display: block; padding: 4px 10px 6px; font-weight: 600; font-size: 14px; color: var(--ink); text-decoration: none; }
.subrail .group { font-size: 11px; letter-spacing: .05em; text-transform: uppercase; color: var(--faint); padding: 14px 10px 4px; }
.subrail .item {
  display: block; padding: 6px 10px; margin-bottom: 1px;
  border-radius: var(--r-control); color: var(--muted); text-decoration: none; font-size: 13.5px;
}
.subrail .item:hover { background: var(--paper); color: var(--ink); }
.subrail .item.on { background: var(--marine-wash); color: var(--marine); font-weight: 600; }
.subrail .item.dim { opacity: .55; }
.backto { display: none; }

/* Settings, as a page: its groups, each a card of rows that open. */
.slgroups { display: flex; flex-direction: column; gap: 22px; max-width: 720px; }
.slhead { margin: 0 0 7px 4px; font-size: 11px; font-weight: 500; letter-spacing: .05em; text-transform: uppercase; color: var(--faint); }
.sllist { background: var(--surface); border-radius: var(--r-card); box-shadow: var(--lift); overflow: hidden; }
.slrow {
  display: flex; align-items: center; gap: 12px;
  width: 100%; min-height: 52px; padding: 10px 16px; box-sizing: border-box;
  border: 0; border-bottom: 0.5px solid var(--rule); background: none;
  color: var(--ink); font: inherit; text-align: left; text-decoration: none; cursor: pointer;
}
.slrow:last-child { border-bottom: 0; }
.slrow:hover { background: var(--sunk); }
.slrow .grow { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.slrow .slname { font-size: 14.5px; }
.slrow .slhint { font-size: 12.5px; color: var(--muted); }
.slrow .chev { flex: none; color: var(--faint); font-size: 18px; }
.slrow.dim .slname { color: var(--muted); }

/* The search: a box over the page, the matches under it. */
.findscrim {
  position: fixed; inset: 0; z-index: 70;
  display: flex; justify-content: center; align-items: flex-start;
  padding: 12vh 16px 16px; background: rgba(10,10,12,.45);
}
.findpanel { width: 100%; max-width: 560px; background: var(--surface); border-radius: var(--r-card); box-shadow: var(--lift-raised); overflow: hidden; }
.findhead { display: flex; align-items: center; gap: 10px; height: 54px; padding: 0 12px 0 18px; border-bottom: 0.5px solid var(--rule); color: var(--faint); }
.findhead svg { width: 18px; height: 18px; flex: none; }
.findinput { flex: 1; min-width: 0; border: 0; background: none; color: var(--ink); font: inherit; font-size: 16px; outline: none; }
.findinput::-webkit-search-cancel-button { display: none; }
.findesc { flex: none; min-height: 28px; padding: 2px 8px; border: 1px solid var(--rule); border-radius: 4px; background: none; color: var(--faint); font-family: var(--mono); font-size: 11px; cursor: pointer; }
.findlist { max-height: 50vh; overflow-y: auto; padding: 6px; }
.findlist:empty { display: none; }
.findrow { display: flex; align-items: center; gap: 12px; padding: 9px 12px; border-radius: var(--r-control); color: var(--ink); text-decoration: none; }
.findrow:hover, .findrow.on { background: var(--marine-wash); }
.findrow .grow { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.findrow .findhint { font-size: 12.5px; color: var(--muted); }
.findrow .findwhere { flex: none; font-size: 12.5px; color: var(--muted); }
.findfoot { margin: 0; padding: 10px 18px; border-top: 0.5px solid var(--rule); color: var(--faint); font-size: 12.5px; }

.main { flex: 1; padding: 26px 30px; min-width: 0; }
.main h2 { margin: 0 0 3px; font-size: 19px; font-weight: 500; letter-spacing: -0.015em; }
.main .lede { color: var(--muted); font-size: 13px; margin: 0 0 20px; }
.main h2 .tag { margin-left: 10px; vertical-align: 3px; }

/* --- tiles, cards and tables ------------------------------------------ */

/* The figures a page leads with. Same card as everything else -- a ring and a
 * hint of lift rather than a drawn border -- so a page of stacked groups reads
 * as one thing whichever module built it. */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px;
  margin-bottom: 20px;
}
.tile { background: var(--surface); border-radius: 14px; padding: 16px 18px; border: 1px solid var(--rule); }
.tile .name { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.tile .value { font-family: var(--mono); font-size: 27px; font-weight: 500; line-height: 1.1; }
.tile .value .unit { font-size: 14px; color: var(--muted); }
.tile .sub { font-family: var(--mono); font-size: 11px; color: var(--faint); margin-top: 5px; }

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 14px;
  align-items: start;
}
.card {
  background: var(--surface);
  border-radius: var(--r-card);
  padding: 16px 18px;
  box-shadow: var(--lift);
}

/* A card heading, in sentence case.
 *
 * It was 11.5px, 600 weight, .07em tracking, ALL CAPS -- the eyebrow
 * label that appears above every heading on every generated page, and a
 * shape that has to be decoded before it is read. Capitals remove the
 * ascender and descender pattern the eye uses to recognise a word, which
 * is why they are slower to read at small sizes, which is exactly where
 * they get used.
 *
 * Sentence case at a normal size, in the muted ink rather than the
 * faintest: the heading is the label for everything beneath it and it
 * had been dimmed almost out of the page to compensate for shouting.
 */
.card h3 {
  margin: 0 0 12px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--muted);
}

/* A card whose header is a bar with a rule under it, for the ones carrying a
 * table or a chart rather than a short list. */
/* `overflow: hidden` CLIPPED the wide table instead of letting it
 * scroll -- the columns were cut off at the card's edge with no way to
 * reach them, which is exactly what the user kept reporting.
 *
 * It is there to keep the children inside the rounded corners. `clip`
 * does that without establishing a scroll container, but the child that
 * needs to scroll must not be clipped by its parent at all -- so the
 * radius is kept and the overflow is left to the row that scrolls. */
.panelcard {
  background: var(--surface);
  border-radius: 14px;
  margin-bottom: 16px;
  box-shadow: var(--lift);
  /* Not `hidden`: see above. The head and the rows are square-cornered
   * children of a rounded box, so they are clipped individually. */
  overflow: visible;
}
.panelcard > .head:first-child { border-radius: 14px 14px 0 0; }
.panelcard > :last-child { border-radius: 0 0 14px 14px; }
.panelcard > .head {
  display: flex; align-items: center; gap: 10px;
  padding: 13px 18px; border-bottom: 0.5px solid var(--rule);
  font-size: 13px; font-weight: 600;
}
.panelcard > .head .spacer { flex: 1; }
.panelcard > .head .dim { font-weight: 400; }
.panelcard > .inner { padding: 16px 18px; }
.card h3 .tag { margin-left: 8px; }
.card.privileged { box-shadow: 0 1px 2px rgba(20,20,25,.06), 0 0 0 1px var(--privl); }
/* The pending commit is the one place a whole card earns the wash: the
 * firewall is live and provisional while it is on screen. */
.card.pending { background: var(--privl-wash); border-color: var(--privl); }

.kv { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin: 0; font-size: 13px; }
.kv dt { color: var(--muted); font-family: var(--sans); }
.kv dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }

table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
  text-align: left;
  font: 12px/1.6 var(--sans);
  font-weight: 500;
  color: var(--muted);
  border-bottom: 1px solid var(--rule);
  padding: 6px 12px 6px 0;
}
td { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--sunk); }
td.right, th.right { text-align: right; font-variant-numeric: tabular-nums; }
/* Flush with the card's edge only when it IS the edge.
 *
 * This set padding-right: 0 on every right-aligned cell, to line the last
 * column up with the card. But a cell has no left padding, so a
 * right-aligned column followed by a left-aligned one had nothing between
 * them: the DHCP pool table read "In useGateway told to clients" in its
 * header and "0192.168.50.1" in its row -- the count of addresses in use
 * run into the gateway's address (1.91.3, from a screenshot). */
td.right:last-child, th.right:last-child { padding-right: 0; }
/* The `hidden` attribute must win.
 *
 * The browser's own `[hidden] { display: none }` is a type-less rule with
 * the lowest specificity there is, so ANY class rule setting `display`
 * beats it -- and this panel hides sixty-odd elements by assigning
 * `.hidden = true` and styles most of them by class. Six were found never
 * hiding at all: the order bar (a permanently visible empty strip under
 * the rules card head), both `.err` banners, the audit disclosure, a meter
 * and the TOTP field.
 *
 * The `.err` pair is the one that did damage. Those banners are hidden on
 * a good poll and shown on a bad one, so an error raised once stayed on
 * screen for ever -- which is very likely why a resolver message persisted
 * after the underlying fault had passed, and sent two people looking for a
 * fault that was no longer there.
 *
 * `!important` because the whole point is to outrank whatever a class
 * says, and an element told to be hidden is not a styling question.
 */
[hidden] { display: none !important; }

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

.form-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 18px; }
.form-row .field { flex: 1 1 160px; }

h3.section { margin: 28px 0 10px; font-size: 14px; color: var(--ink); font-weight: 600; }

/* --- the tape --------------------------------------------------------- */

.tape {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  height: var(--tape-h);
  background: var(--tape-bg);
  color: var(--tape-fg);
  display: flex;
  flex-direction: column;
}
.tape .head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 16px;
  font: 11px/1.6 var(--sans);
  color: var(--faint);
  border-bottom: 1px solid rgba(255,255,255,.07);
  cursor: pointer;
  user-select: none;
}
.tape .head:hover { color: var(--tape-fg); }
.tape .head .spacer { flex: 1; }
body.tape-shut .tape .tapelines { display: none; }
.tape .tapelines { flex: 1; overflow-y: auto; padding: 6px 16px 10px; font-size: 11.5px; line-height: 1.7; }
.tape .line { display: flex; gap: 12px; white-space: pre; animation: rise .12s ease-out; }
.tape .line .t { color: var(--faint); }
.tape .line .c { color: var(--marine); min-width: 130px; }
.tape .line .d { overflow: hidden; text-overflow: ellipsis; }
.tape .line.audit .c { color: var(--privl); }
.tape .line.gap { color: var(--privl); }

@keyframes rise { from { opacity: 0; transform: translateY(2px); } }
@media (prefers-reduced-motion: reduce) { .tape .line { animation: none; } }

.pip { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); flex: none; }
.pip.live { background: var(--marine); }
.pip.retry { background: var(--privl); animation: blink 1s steps(2) infinite; }
@media (prefers-reduced-motion: reduce) { .pip.retry { animation: none; } }
@keyframes blink { 50% { opacity: .25; } }

/* The layout had one breakpoint and did not survive a narrow window: the rail
 * kept its width, tables pushed the page sideways, and the tape covered more
 * of a short screen than it was worth. */
@media (max-width: 900px) {
  .rail { width: 156px; }
  /* No room for a second list beside the page: Settings is the page, and
   * a page in it leads with the way back. */
  .subrail { display: none; }
  .backto { display: inline-flex; align-items: center; min-height: 32px; margin: -6px 0 10px; color: var(--marine); text-decoration: none; font-size: 14px; }
  .main { padding: 20px 18px; }
  /* The figures a page leads with. Same card as everything else -- a ring and a
 * hint of lift rather than a drawn border -- so a page of stacked groups reads
 * as one thing whichever module built it. */

.tiles { grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 10px; }
}

@media (max-width: 680px) {
  .body { display: block; padding-bottom: var(--tape-h); }

  /* A drawer, not a wrapping row of pills.
   *
   * Pills were fine with four entries and wrapped to two lines with seven. A
   * router will have ten, and the row would have taken a third of the screen
   * before anything was read. */
  /* The rail narrows here; it disappears where the tab bar arrives, which is
   * further down the sheet. Hiding it at this width would leave nothing to
   * navigate with between the two breakpoints. */
  .rail { width: 168px; padding: 14px 8px; }

  .main { padding: 16px 14px; }
  .top { padding: 0 14px; gap: 10px; }
  .top .who { display: none; }

  /* A table with six columns does not fit; let it scroll rather than
   * stretching the page and putting the rail off screen. */
  .scrolls { overflow-x: auto; -webkit-overflow-scrolling: touch; }

  /* A table that does not fit scrolls INSIDE its card.
   *
   * `.scrolls` has existed for this since the beginning and one table
   * uses it; the other nine push the whole page sideways instead, so the
   * body scrolls, the layout breaks, and the table still cannot be read.
   *
   * Done in CSS rather than by wrapping each table, because the card
   * body is already the right container -- it is the element with the
   * card's width -- and wrapping ten call sites by hand is ten chances
   * to get a closing bracket wrong. (I tried; I did.)
   *
   * `overflow-x: auto` on a block forces `overflow-y` to `auto` as well,
   * which would clip anything overflowing upwards. Nothing in a card
   * body does: the drawers are elsewhere in the document and the tape
   * is fixed. */
  /* `.panelcard`, WHICH IS WHAT THESE CARDS ACTUALLY ARE.
   *
   * This said `.card .inner` for two releases and matched nothing: the
   * firewall, network and DHCP cards are all `.panelcard`, a separate
   * class defined thirty lines below `.card`. Both fixes I shipped were
   * inert, and I could not tell because there is no browser here to
   * render them -- I was reading rules and calling it testing.
   *
   * Written for both classes now, because `.card` does hold a table on
   * the About page. */
  .card .inner:has(> table),
  .panelcard .inner:has(> table) {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  .card .inner:has(> table) > table,
  .panelcard .inner:has(> table) > table { min-width: 34rem; }
  /* One line per row, not wrapped to 30 characters.
   *
   * `pre-wrap` at `max-width: 30ch` made every rule cell wrap to five or
   * six lines, so each row was several centimetres tall and four rules
   * filled the screen -- with the columns still cut off at the right,
   * because wrapping one cell does not make the others fit.
   *
   * Inside a container that scrolls, wrapping is the wrong trade
   * entirely: it buys nothing and costs the height. The cell stays on
   * one line and the table is read by scrolling, which is what the
   * scrolling is for. */
  .card .inner table.rules td.rule,
  .panelcard .inner table.rules td.rule {
    white-space: nowrap;
    max-width: none;
    word-break: normal;
  }
  .cards { grid-template-columns: 1fr; }
  .form-row { gap: 8px; }
  .form-row .field { flex: 1 1 100%; }
  :root { --tape-h: 108px; }
  body.tape-shut { --tape-h: 30px; }

/* No tape at all, which is the normal case.
 *
 * The event stream became development-only at 1.30.1, but --tape-h went
 * on reserving 30px for it -- so every page carried a strip of nothing
 * at the bottom, and on a phone the tab bar floated above a gap. The
 * script adds this class when it decides not to build the tape, because
 * removing an element does not tell a stylesheet anything.
 */
body.no-tape { --tape-h: 0px; }

  /* Same gesture, no room to spare it a margin. Still arrives from the right
   * rather than switching to a sheet from below -- one pattern taught once,
   * not a desktop one and a phone one. */
  .drawer-panel { width: 100%; }

  /* Same band, thumb-sized. The actions drop to their own row and go full
   * width rather than shrinking to fit beside the countdown -- Keep and Roll
   * back are the two things this screen is for, so they get the width a
   * thumb needs rather than whatever is left over. */
  .pending-band-item { flex-wrap: wrap; padding: 10px 14px; gap: 10px 14px; }
  .pending-band .actions { margin-left: 0; width: 100%; }
  .pending-band .actions .btn { flex: 1; }
}

@media (max-width: 460px) {
  .tiles { grid-template-columns: 1fr; }
  .main h2 { font-size: 18px; }
}

/* --- charts and meters ------------------------------------------------ */

.chart { background: var(--surface); border: 1px solid var(--rule); border-radius: 14px; padding: 14px 16px; }
.chart .head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.chart .head .title { font-size: 13px; font-weight: 500; }
.chart .head .now { font-family: var(--mono); font-size: 13px; color: var(--muted); }
.chart .head .spacer { flex: 1; }

.spark-line { fill: none; stroke: var(--marine); stroke-width: 2; vector-effect: non-scaling-stroke; }
.spark-fill { fill: var(--marine); opacity: .12; stroke: none; }
.spark-empty { fill: var(--faint); font: 11px var(--mono); }
/* The throughput chart's own labels: which half is which, and the value
 * both halves are scaled against. --muted rather than --faint because this
 * is text somebody reads, and faint does not clear 4.5:1 on the card
 * surface -- contrast-check settles that rather than taste. */
/* .traffic-axis removed: the plot's labels are HTML now (.traffic-lbl),
 * because text inside the stretched SVG was being distorted. */


.meter { display: block; height: 8px; background: var(--sunk); border-radius: 4px; overflow: hidden; margin: 4px 0 10px; }
.meter-fill { display: block; height: 100%; background: var(--marine); transition: width .3s; }
.meter-fill.hot { background: var(--privl); }

.card .model { margin-top: 10px; font-size: 12px; }
td.warn { color: var(--privl); }

/* --- rules ------------------------------------------------------------ */

table.rules td.rule { white-space: pre-wrap; word-break: break-word; max-width: 46ch; font-size: 12.5px; }

pre.conf {
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 14px 16px;
  overflow-x: auto;
  font-size: 12.5px;
  line-height: 1.6;
  margin: 0;
}

textarea.editor {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 14px 16px;
  font-size: 12.5px;
  line-height: 1.6;
  resize: vertical;
}

/* The countdown is the most consequential thing the panel shows. */
.card.pending { margin-bottom: 22px; }
.card.pending p { margin: 0 0 10px; font-size: 13px; }
.countdown { font-size: 28px; font-weight: 500; color: var(--privl); font-variant-numeric: tabular-nums; }

/* --- the ruleset, line by line ---------------------------------------- */

.lines {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sunk);
  padding: 8px 0;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.7;
  overflow-x: auto;
}
.lines .line { display: flex; align-items: baseline; gap: 10px; padding: 0 14px; }
.lines .line:hover { background: var(--surface); }
.lines .n { color: var(--faint); min-width: 22px; text-align: right; user-select: none; }
.lines .toggle { min-width: 34px; flex: none; }
.lines button.toggle {
  font-family: var(--mono);
  font-size: 11px;
  padding: 0 6px;
  border-radius: 4px;
  border: 1px solid transparent;
  color: var(--marine);
  background: none;
  line-height: 1.5;
}
.lines button.toggle:hover { border-color: var(--rule); }
.lines .line.off button.toggle { color: var(--privl); }
.lines .line.off .src { color: var(--faint); text-decoration: line-through; }
.lines .src { white-space: pre; }
.form-row .spacer { flex: 1; }


/* --- rail groups ------------------------------------------------------- */

/* The rail's one group divider keeps its capitals, deliberately.
 *
 * This is not a heading above content -- it is a single word separating
 * the pages that configure the network from the pages that administer
 * the panel, and it must not be mistaken for a nav entry. Capitals at a
 * small size are exactly wrong for reading and exactly right for a thing
 * that should register as a boundary and never be read twice.
 *
 * One instance on the page. That is the difference between a choice and
 * a habit.
 */
.rail .group {
  font-size: 11px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 20px 10px 6px;
  border-top: 1px solid var(--rule);
  margin-top: 12px;
}
.rail .group:first-child { padding-top: 4px; border-top: none; margin-top: 0; }


/* --- home, calmer (1.155.0; decisions 252-262) -------------------------
 *
 * A sentence, four tiles, what is worth a look and today. Quiet: grey and
 * blue for what is normal, amber only for what wants a look. */
.hn { display: flex; flex-direction: column; gap: 30px; max-width: 980px; }
.hnhead { display: grid; grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; column-gap: 12px; }
.hnhead > * { grid-column: 1; }
/* The ? (1.171.0, decision 302): on the heading's row, at its end. */
.hnhead > .helpq { grid-column: 2; align-self: start; }
.helpq { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; margin-top: -1px; padding: 0; border: 0; border-radius: var(--r-chip); background: none; color: var(--muted); cursor: pointer; }
.helpq:hover { background: var(--sunk); color: var(--ink); }
.helpq:focus-visible { outline: 2px solid var(--marine); outline-offset: 1px; }
.helpbody { display: flex; flex-direction: column; gap: 10px; font-size: 15px; line-height: 1.55; }
.helptext { margin: 0; }
.helplist { margin: 0; padding-left: 20px; }
.helplist li { margin: 3px 0; }
.helphead { margin: 8px 0 0; font-size: 13px; font-weight: 600; color: var(--muted); }
.helplinks { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 2px; }
.helplinks a { display: block; padding: 6px 0; color: var(--marine); text-decoration: none; }
.helplinks a:hover { text-decoration: underline; }
.helpall { margin: 8px 0 0; padding-top: 12px; border-top: 1px solid var(--rule); font-weight: 500; }
.helpall a { color: var(--marine); text-decoration: none; }
.nwhead > h2 { margin: 0; }
/* A module's state as a pill (1.171.0, decision 303). */
.slrow.morow { align-items: center; }
.morow .mostate { margin-left: 8px; }
/* The older pages' heading and lede, with the ? on the heading's row (1.171.0). */
.hnhead.oldhead { row-gap: 0; }
.oldhead > .helpq { margin-top: -7px; }
.page-head .line > .helpq { margin-left: auto; align-self: center; }
.hnwhen { color: var(--faint); font-size: 13px; }
.main .hntitle { margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.015em; line-height: 1.25; }
.hnline { margin: 0; color: var(--muted); font-size: 15px; line-height: 1.55; max-width: 620px; }
.hnline:empty { display: none; }
.hntiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.hntile {
  display: flex; flex-direction: column; gap: 8px; padding: 16px 18px;
  background: var(--surface); border-radius: var(--r-card); box-shadow: var(--lift);
  color: var(--ink); text-decoration: none; min-width: 0;
}
.hntile:hover { box-shadow: var(--lift-raised); }
.hntile.amber { box-shadow: inset 0 0 0 1px var(--caution); }
.hnlbl { display: flex; align-items: center; gap: 7px; color: var(--k, var(--muted)); font-size: 14px; font-weight: 600; }
.hnlbl > svg { flex: none; }
.hnbig { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.hnnum { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.hnunit { color: var(--muted); font-size: 14px; }
.hncap { color: var(--faint); font-size: 12.5px; overflow-wrap: anywhere; }
.hntile.amber .hncap { color: var(--caution); }
.hnhead3 { margin: 0; font-size: 15px; font-weight: 600; }
/* A section's heading with its icon, both in its hue (1.172.0, decision 307). */
.hnhead3.khead { display: flex; align-items: center; gap: 8px; color: var(--k, var(--ink)); }
.hnhead3.khead > svg { flex: none; }
/* The traffic on Home (1.173.0, decisions 312-313): the heading with the
 * port and the spans beside it, a white card of the two figures and the
 * chart, dots and a small label on hover. */
.hntrafficbox[hidden] { display: none; }
.hntraffic { display: flex; flex-direction: column; gap: 10px; }
.hntrhead { display: flex; align-items: center; gap: 8px 12px; flex-wrap: wrap; }
.hntrhead .khead { margin-right: auto; }
.hntrhead .ifacepick { font: inherit; font-size: 13px; color: var(--muted); background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-control); padding: 3px 8px; }
.hntrhead .spanpicks { margin: 0; display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--r-chip); background: var(--sunk); }
.hntrhead .spanpick { border: 0; background: none; font: inherit; font-size: 12.5px; color: var(--muted); padding: 3px 10px; border-radius: var(--r-chip); cursor: pointer; }
.hntrhead .spanpick.on { background: var(--surface); color: var(--ink); font-weight: 500; box-shadow: var(--lift); }
.hntrcard { display: flex; flex-direction: column; gap: 12px; padding: 16px 18px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--lift); min-width: 0; }
.hntrnums { display: flex; gap: 12px 36px; flex-wrap: wrap; align-items: flex-start; }
.hntrnum .l { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--muted); }
.hntrnum .dot { width: 9px; height: 9px; border-radius: 50%; }
.hntrnum .dot.down { background: var(--down); }
.hntrnum .dot.up { background: var(--up); }
.hntrnum .v { font-size: 26px; font-weight: 600; font-variant-numeric: tabular-nums; margin-top: 2px; }
.hntrnum .cap { font-size: 12.5px; color: var(--faint); }
.hntrnums .rt { margin-left: auto; font-size: 12.5px; color: var(--faint); }
.hntrcard .dash-chart { margin: 0; padding: 0; border: 0; box-shadow: none; background: none; }
.dashdots > .head { display: none; }
.tplot { position: relative; }
.tdot { position: absolute; width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%; border: 2px solid var(--surface); pointer-events: none; }
.tdot.down { background: var(--down); }
.tdot.up { background: var(--up); }
.tdotlabel { position: absolute; transform: translate(12px, -110%); display: flex; flex-direction: column; gap: 1px; padding: 6px 9px; border-radius: 8px; background: var(--surface); box-shadow: var(--lift-raised); font-size: 12.5px; white-space: nowrap; pointer-events: none; }
.tdotlabel.left { transform: translate(calc(-100% - 12px), -110%); }
.tdotwhen { color: var(--muted); font-family: var(--mono); font-size: 12px; }
.tdotv { font-variant-numeric: tabular-nums; font-weight: 500; }
.tdotv.down { color: var(--marine); }
.tdotv.up { color: var(--up-text); }
.tdot[hidden], .tdotlabel[hidden] { display: none; }
/* On a phone the two figures side by side, as on the card in the app. */
@media (max-width: 640px) {
  .hntrnums { display: grid; grid-template-columns: 1fr 1fr; column-gap: 16px; }
  .hntrnums .rt { grid-column: 1 / -1; margin-left: 0; }
  .hntrnum .v { font-size: 22px; }
}
/* The hues: a tile's, a heading's, a menu entry's icon. */
.k-blue   { --k: var(--marine); }
.k-green  { --k: var(--in); }
.k-teal   { --k: var(--teal); }
.k-violet { --k: var(--up-text); }
.k-purple { --k: var(--k-purple); }
.k-pink   { --k: var(--k-pink); }
.k-orange { --k: var(--k-orange); }
.k-indigo { --k: var(--k-indigo); }
.k-grey   { --k: var(--muted); }
.k-amber  { --k: var(--caution); }
/* A row's icon in its hue (1.172.0): the household's quiet rows. */
.slicon { flex: none; display: inline-flex; color: var(--k, var(--muted)); }
.slrow > .slicon { margin-right: 2px; }
.hhqrow > .slicon { align-self: center; }
.hnlooks { display: flex; flex-direction: column; gap: 10px; }
.hnlook {
  display: flex; align-items: center; gap: 14px; padding: 13px 18px;
  border-radius: var(--r-card); box-shadow: inset 0 0 0 1px var(--caution);
  color: var(--ink); text-decoration: none;
}
.hnlook:hover { background: var(--caution-wash); }
.hndot { flex: none; width: 8px; height: 8px; border-radius: 4px; background: var(--caution); }
.hnlook .grow { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.hnwhere { color: var(--muted); font-size: 13px; }
.hnlook .chev { flex: none; color: var(--faint); font-size: 18px; }
.hntoday { display: flex; flex-direction: column; }
.hntodayhead { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 6px; }
.hntodayhead a { font-size: 13px; color: var(--marine); text-decoration: none; }
.hnmoment { display: flex; gap: 14px; padding: 9px 0; border-bottom: 0.5px solid var(--rule); color: var(--ink); text-decoration: none; }
.hnmoment:last-child { border-bottom: 0; }
.hnmoment .grow { flex: 1; min-width: 0; }
.hnmoment.lvlwarn .hntime, .hnmoment.lvlbad .hntime { color: var(--caution); }
.hntime { flex: none; width: 48px; color: var(--faint); font-family: var(--mono); font-size: 13px; font-variant-numeric: tabular-nums; }
.hnquiet { margin: 0; padding: 9px 0; color: var(--muted); }
.hnold { margin: 8px 0 0; font-size: 13px; }
.hnold a { color: var(--muted); }
/* Two tiles a row where four would be too narrow to read. After the
 * rule it narrows, which it would not beat if it came first. */
@media (max-width: 900px) {
  .hntiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .main .hntitle { font-size: 24px; }
}

/* --- page tabs (1.156.0) ------------------------------------------------
 *
 * A page's parts, one at a time, instead of every card stacked (decision
 * 254): a row of words, the one showing underlined in blue. */
.ptabs { display: flex; gap: 26px; border-bottom: 1px solid var(--rule); margin: 6px 0 20px; overflow-x: auto; scrollbar-width: none; }
.ptab {
  flex: none; padding: 0 0 11px; margin-bottom: -1px; border: 0; border-bottom: 2px solid transparent;
  background: none; color: var(--muted); font: inherit; font-size: 14px; cursor: pointer; white-space: nowrap;
}
/* More tabs past an edge: that edge fades (1.175.0, decision 327). */
.ptabs[data-more="right"] { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent); }
.ptabs[data-more="left"] { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 36px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 36px), transparent); }
.ptabs[data-more="left right"] { -webkit-mask-image: linear-gradient(to right, transparent, #000 36px, #000 calc(100% - 36px), transparent); mask-image: linear-gradient(to right, transparent, #000 36px, #000 calc(100% - 36px), transparent); }
.ptab:hover { color: var(--ink); }
.ptab[aria-selected="true"] { color: var(--ink); font-weight: 500; border-bottom-color: var(--marine); }
.ptabpanel { display: flex; flex-direction: column; gap: 16px; }
.netswitch { background: none; border: 0; padding: 0; font: inherit; font-size: 13px; color: var(--muted); text-decoration: underline; cursor: pointer; }

/* --- the DNS page (1.158.0) -----------------------------------------------
 *
 * The canvas's "DNS": a sentence, tabs, three tiles, what was blocked most
 * and who asks most, and Settings at a glance; a dashed box, which can be
 * closed, when lookups are not logged. */
.dnpage { gap: 0; }
.dnpage > .hnhead { margin-bottom: 18px; }
.hnline .hnstrong { color: var(--ink); }
.dnpanel { gap: 26px; }
.dntiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.dnsec { display: flex; flex-direction: column; gap: 6px; }
.dnlist { display: flex; flex-direction: column; }
.dnrow, .dnrowx {
  display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) 72px; gap: 16px; align-items: center;
  width: 100%; padding: 11px 4px; box-sizing: border-box; border: 0; border-bottom: 0.5px solid var(--rule);
  background: none; color: var(--ink); font: inherit; text-align: left;
}
.dnrow { cursor: pointer; }
.dnrow:hover { background: var(--sunk); }
.dnrow:last-child, .dnrowx:last-child { border-bottom: 0; }
.dnrowx.amber .dnmono { color: var(--caution); }
.dnname, .dnmono { font-family: var(--mono); font-size: 13px; overflow-wrap: anywhere; }
.dnwho { color: var(--muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dnn { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
.dnbarrow { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) 72px; gap: 16px; align-items: center; padding: 9px 4px; }
.dnbarname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dnbar { height: 6px; border-radius: 3px; background: var(--sunk); overflow: hidden; display: flex; }
.dnbarfill { display: block; background: var(--marine); border-radius: 3px; }
.dnall { background: none; border: 0; padding: 0; font: inherit; font-size: 13px; color: var(--marine); cursor: pointer; }
.dnall:hover { text-decoration: underline; }
.dnbox { display: flex; flex-direction: column; gap: 12px; padding: 18px 20px; border: 1px dashed var(--faint); border-radius: var(--r-card); }
.dnboxhead { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.dnclose { flex: none; min-width: 32px; min-height: 32px; margin: -6px -8px 0 0; border: 0; background: none; color: var(--muted); font-size: 20px; line-height: 1; cursor: pointer; border-radius: 8px; }
.dnclose:hover { background: var(--sunk); color: var(--ink); }
.dnboxp { margin: 0; color: var(--muted); line-height: 1.55; max-width: 640px; }
.dnchoices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; max-width: 640px; }
.dnchoice {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; padding: 12px 16px;
  border: 1px solid var(--rule); border-radius: var(--r-control); background: none; color: var(--ink);
  font: inherit; text-align: left; cursor: pointer;
}
.dnchoice.primary { background: var(--surface); box-shadow: var(--lift); }
.dnchoice:hover { border-color: var(--marine); }
.dnchoice:disabled { opacity: .6; cursor: default; }
.dnchoicet { font-weight: 500; }
.dnchoicew { color: var(--muted); font-size: 13px; line-height: 1.45; }
.dnglancek { flex: 0 0 170px; color: var(--muted); }
.dnlede { margin: 0; color: var(--muted); line-height: 1.55; max-width: 680px; }
.dnnote { margin: 0; font-size: 13px; }
/* Narrower: two tiles a row, the far column gone, one choice a row. After
 * the rules above, which it would not beat if it came first. */
@media (max-width: 900px) {
  .dntiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dnchoices { grid-template-columns: minmax(0, 1fr); }
  .dnrow, .dnrowx { grid-template-columns: minmax(0, 1fr) auto; }
  .dnrow .dnwho, .dnrowx .dim { display: none; }
  .dnbarrow { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; }
  .dnglancek { flex-basis: 120px; }
}

/* --- the Firewall page, new look (1.162.0) -------------------------------
 *
 * A sentence, tabs (Rules, Port forwards, Settings), each part of the
 * firewall a heading over rows of words; "Put in order" puts arrows on the
 * rows and a bar above them. */
.fwpage { gap: 0; }
.fwpage > .hnhead { margin-bottom: 18px; }
.fwgroups { display: flex; flex-direction: column; gap: 22px; }
.fwgrouphead { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; }
.fwgroupsub { font-size: 13px; }
.slrow.fwrowtext { cursor: default; }
.slrow.fwrowtext:hover { background: none; }
.slrow.fwordering { cursor: default; }
.slrow.fwordering:hover { background: none; }
.fwarrows { flex: none; display: flex; gap: 6px; }
.fwarrows .btn { min-width: 40px; min-height: 36px; padding: 0 10px; }
.fwarrows .btn:disabled { opacity: .35; }
.fworderbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  padding: 12px 16px; border-radius: var(--r-card); background: var(--sunk);
}
.fworderbar .grow { flex: 1 1 220px; min-width: 0; line-height: 1.45; }
.fworderbar .grow.err { background: none; padding: 0; border: 0; margin: 0; border-radius: 0; font-size: inherit; color: var(--danger); }
.fwsaid { margin: 4px 0 0; }
.fwsaid:empty { display: none; }
.fwnewactions { display: flex; justify-content: flex-end; gap: 10px; margin: 0; }

/* --- the Wi-Fi page, new look (1.163.0) ----------------------------------
 *
 * One page: a sentence, the card to join it (the password behind Show, the
 * code a phone's camera reads), the devices on it, its settings as rows. */
.wfpage { gap: 24px; }
.wfjoin {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px 28px;
  padding: 18px 20px; background: var(--surface); border-radius: var(--r-card); box-shadow: var(--lift);
}
.wfjoinwords { display: flex; flex-direction: column; gap: 12px; min-width: 0; flex: 1 1 240px; }
.wfjoinwords .hnhead3 { font-size: 17px; overflow-wrap: anywhere; }
.wfjoinpass { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.wfjoincode .wifijoin { margin: 0; }
.wfjoinnotes:empty, .wfproblems:empty { display: none; }
.wfjoinnotes .wifinote { margin: 0 0 6px; }
.slrow.wfstill { cursor: default; }
.slrow.wfstill:hover { background: none; }
.wfsig { flex: none; font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.wfsig.amber { color: var(--caution); }

/* --- the VPN page, new look (1.164.0) -------------------------------------
 *
 * One page: a sentence, the devices (or the other end) as rows, what goes
 * over the tunnel, the settings; no card of its own for keeping a change. */
.vppage { gap: 24px; }
.vpnone { display: flex; flex-direction: column; gap: 14px; }
.vphead { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.vpamount { flex: none; font-size: 12.5px; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.slrow.vpstill { cursor: default; }
.slrow.vpstill:hover { background: none; }
.vpval { overflow-wrap: anywhere; }
.slhint.amber { color: var(--caution); }
.vpfoot { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }

/* --- the NAS page, new look (1.168.0) ------------------------------------
 *
 * One page: a sentence, Shares with who is using them, the disks as rows,
 * what is plugged in and not used; a disk's parts in its drawer. */
.nspage { gap: 24px; }
.nsside { flex: none; font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.slrow.nsstill { cursor: default; }
.slrow.nsstill:hover { background: none; }
.nssub { margin: 10px 0 0; font-size: 13px; font-weight: 500; color: var(--muted); }
.nsdrawer { display: flex; flex-direction: column; gap: 12px; }
.nsstate { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.nsstate .spacer { flex: 1; }
.nsdrawer .cardacts { display: flex; flex-wrap: wrap; gap: 8px; }
.nsdrawer p { margin: 0; }

/* --- the Self-hosted page, new look (1.169.0) ----------------------------
 *
 * The NAS page's pieces, and a row with Open on it: a div that acts as a
 * button, since a link may not sit in one. */
.appage { gap: 24px; }
.slrow.aprow > .btn { flex: none; }
.slrow.aprow:focus-visible { outline: 2px solid var(--marine); outline-offset: -2px; }
.apcatalogue .appfilter { display: flex; gap: 8px; flex-wrap: wrap; }
.apcatalogue .appfilter input { flex: 1; min-width: 180px; }
.applaying .rows { display: flex; flex-direction: column; }

/* --- Accounts and Modules, new look (1.170.0) ----------------------------
 *
 * The sections of rows the other new pages use; a person's initial
 * beside their name. */
.acnow, .mogroups { display: flex; flex-direction: column; gap: 24px; margin: 16px 0 8px; }
.acnow ~ .acpeople { margin-top: 24px; }
.acpeople .slrow .hhavatar { flex: none; }
.modrawer .detail { display: flex; flex-direction: column; gap: 8px; }

/* --- the Status page, new look (1.165.0) ---------------------------------
 *
 * One page in sections: a sentence, what needs a look, the storage, memory
 * and the processor, what is using it, the checks, the containers, the
 * machine. Bars only where a figure fills up. */
.stpage { gap: 24px; }
.stproblem:empty { display: none; }
.slrow.ststill { cursor: default; }
.slrow.ststill:hover { background: none; }
.stmeter { flex: 0 0 120px; display: block; }
.stmeter .meterfill { display: block; }
.slrow.amber .slname, .slname.amber { color: var(--caution); }
.stlook.bad .hndot { background: var(--danger); }
.starea { margin: 10px 0 2px; color: var(--muted); font-size: 13px; }
.starea:first-of-type { margin-top: 0; }
.stproc { font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
/* On a phone the figures say it; the bars would squeeze them to three lines. */
@media (max-width: 640px) {
  .stmeter { display: none; }
}

/* --- the household page, new look (1.161.0) -------------------------------
 *
 * The canvas's "Her account": a sentence, a card for each person with one
 * big button, Everyone, The house, then quiet rows. */
.hhnew { gap: 22px; max-width: 760px; }
.hhcards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.hhcard {
  display: flex; flex-direction: column; gap: 10px; padding: 14px 16px; min-width: 0;
  background: var(--surface); border-radius: var(--r-card); box-shadow: var(--lift);
}
.hhcardrow { display: flex; align-items: center; gap: 12px; }
.hhcardtop {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 12px; padding: 0;
  border: 0; background: none; color: var(--ink); font: inherit; text-align: left;
}
.hhcardtop.tap { cursor: pointer; }
.hhcardwho { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.hhcardname { font-size: 17px; font-weight: 600; overflow-wrap: anywhere; }
.hhcardstate { color: var(--muted); font-size: 13px; }
.hhcardstate.amber { color: var(--caution); }
.hhcardline { color: var(--faint); font-size: 12.5px; overflow-wrap: anywhere; }
.hhbig { flex: none; min-height: 44px; padding: 10px 16px; }
.hhquiet { display: flex; flex-direction: column; gap: 4px; }
.hhqrow { display: flex; align-items: center; gap: 12px; padding: 10px 4px; border-bottom: 0.5px solid var(--rule); flex-wrap: wrap; }
.hhqrow .grow { flex: 1 1 200px; color: var(--muted); }
.hhquiet .sllist { margin-bottom: 10px; }
/* One card a row on a phone. After the rule, which it would not beat first. */
@media (max-width: 900px) {
  .hhcards { grid-template-columns: minmax(0, 1fr); }
}

/* --- Maintenance (1.160.0) ------------------------------------------------
 *
 * The canvas's "Maintenance": rows that read as words, in four groups,
 * each opening a drawer; amber, with a dot, only for what wants a look. */
.mtpage { gap: 0; }
.mtpage > .hnhead { margin-bottom: 22px; }
.mtsections { display: flex; flex-direction: column; gap: 26px; }
.mtrow .slname { display: flex; align-items: center; gap: 8px; }
.mtrow.amber .slhint { color: var(--caution); }
.mtcard .panelcard { box-shadow: none; background: none; margin: 0; }
.mtcard .panelcard > .head { display: none; }
.mtcard .panelcard > .inner { padding: 0; }
.mtkept { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 0.5px solid var(--rule); }
.mtkept .grow { flex: 1; min-width: 0; }
.mtchoice { display: flex; flex-direction: column; gap: 8px; padding: 0 0 18px; margin-bottom: 18px; border-bottom: 1px solid var(--rule); }

/* --- the DHCP page (1.159.0) ---------------------------------------------
 *
 * The canvas's "DHCP": a list that reads as words, a search and three
 * chips; a device new today washed blue. */
.dhchips { display: flex; gap: 6px; flex-wrap: wrap; }
.dhchip {
  min-height: 34px; padding: 6px 14px; border: 0; border-radius: 999px; background: var(--sunk);
  color: var(--muted); font: inherit; font-size: 13px; cursor: pointer;
}
.dhchip.on { background: var(--ink); color: var(--surface); font-weight: 500; }
.dhlist { display: flex; flex-direction: column; }
.dhhead, .dhrow { display: flex; align-items: center; gap: 16px; padding: 12px 12px; box-sizing: border-box; }
.dhhead { padding-top: 0; padding-bottom: 8px; color: var(--faint); font-size: 12px; border-bottom: 1px solid var(--rule); }
.dhrow {
  width: 100%; border: 0; border-bottom: 0.5px solid var(--rule); background: none;
  color: var(--ink); font: inherit; text-align: left; cursor: pointer;
}
.dhrow:hover { background: var(--sunk); }
.dhrow.new { background: var(--marine-wash, var(--sunk)); }
.dhhead .grow, .dhrow .grow { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.dhname { display: flex; align-items: center; gap: 8px; min-width: 0; overflow-wrap: anywhere; }
.dhbadge { flex: none; padding: 2px 8px; border-radius: 999px; background: var(--marine); color: var(--surface); font-size: 11.5px; }
.dhsub { color: var(--muted); font-size: 12.5px; overflow-wrap: anywhere; }
.dhaddr { flex: 0 0 120px; font-family: var(--mono); font-size: 13px; }
.dhkeeps { flex: 0 0 210px; color: var(--muted); }
.dhkeeps.pinned { color: var(--ink); }
.dhrow .chev, .dhhead .chev { flex: 0 0 12px; color: var(--faint); font-size: 18px; }
.dhmore { margin: 0; padding: 12px; color: var(--muted); font-size: 13px; }
/* Narrower: the address and how long it keeps it go under the name. */
@media (max-width: 900px) {
  .dhhead { display: none; }
  .dhrow { flex-wrap: wrap; row-gap: 4px; column-gap: 12px; }
  .dhrow .grow { flex-basis: 100%; }
  .dhrow .chev { display: none; }
  .dhaddr, .dhkeeps { flex: 0 1 auto; font-size: 12.5px; }
}

/* --- what is happening: the connections (1.156.0) ----------------------- */
.cnview { display: flex; flex-direction: column; gap: 22px; max-width: 980px; }
.cnline { max-width: 680px; }
.cnbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.cnsearch {
  flex: 1 1 280px; min-width: 0; height: 40px; padding: 0 14px; box-sizing: border-box;
  border: 1px solid var(--rule); border-radius: var(--r-control); background: var(--surface); color: var(--ink); font: inherit;
}
.cngroups { display: flex; flex-direction: column; gap: 22px; }
.cngroup { display: flex; flex-direction: column; gap: 6px; }
.cnhead { margin: 0 4px 2px; font-size: 15px; font-weight: 600; }
.cngroup.amber .cnhead { color: var(--caution); }
.cnrow {
  display: grid; grid-template-columns: minmax(0, 150px) minmax(0, 1fr) auto; gap: 14px; align-items: center;
  width: 100%; min-height: 52px; padding: 10px 16px; box-sizing: border-box;
  border: 0; border-radius: var(--r-control); background: var(--surface); box-shadow: var(--lift);
  color: var(--ink); font: inherit; text-align: left; cursor: pointer;
}
.cnrow:hover { box-shadow: var(--lift-raised); }
.cnrow.amber { box-shadow: inset 0 0 0 1px var(--caution); background: none; }
.cnwho { color: var(--muted); font-size: 13px; overflow-wrap: anywhere; }
.cnrow .grow { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cndoing { font-weight: 500; }
.cnrow.amber .cndoing { font-weight: 400; }
.cnsites { color: var(--muted); font-size: 12.5px; overflow-wrap: anywhere; }
.cnrow.amber .cnsites { font-family: var(--mono); font-size: 12px; }
.cnrate { color: var(--marine); font-family: var(--mono); font-size: 12.5px; white-space: nowrap; }
.cnempty { margin: 0; }
.cnnote { margin: 0; font-size: 12.5px; }
.cntable { display: flex; flex-direction: column; }
.cntr {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1.2fr) 110px 70px; gap: 12px; align-items: center;
  width: 100%; padding: 10px 12px; box-sizing: border-box; border: 0; border-bottom: 0.5px solid var(--rule);
  background: none; color: var(--ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
.cntr:hover { background: var(--sunk); }
.cnth { color: var(--faint); font-size: 12px; cursor: default; }
.cnth:hover { background: none; }
.cntr.amber .cnprob { color: var(--caution); }
.cnto, .cnfrom { overflow-wrap: anywhere; }
.cnto { font-family: var(--mono); font-size: 12.5px; }
.cnnow { font-family: var(--mono); font-size: 12px; color: var(--marine); }
.cnsince { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.cnkicker { margin: 0 0 8px; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.cnkicker.amber { color: var(--caution); }
.cnguess { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: var(--r-control); background: var(--sunk); line-height: 1.5; margin-bottom: 16px; }
.cnfacts { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 10px 14px; margin: 0 0 18px; font-size: 13.5px; }
.cnfacts dt { color: var(--muted); }
.cnfacts dd { margin: 0; overflow-wrap: anywhere; }
.cnname, .cnblock { display: flex; flex-direction: column; gap: 8px; padding-top: 16px; border-top: 1px solid var(--rule); }
.cnnamehead { font-weight: 500; }
.cnnamerow { display: flex; gap: 8px; flex-wrap: wrap; }
.cnnamein { flex: 1 1 180px; min-width: 0; height: 38px; padding: 0 12px; box-sizing: border-box; border: 1px solid var(--rule); border-radius: var(--r-control); background: var(--surface); color: var(--ink); font: inherit; }
/* The Firewall page's "Blocked for one device" (1.157.0). */
.fwblocks { display: flex; flex-direction: column; }
.fwblockrow { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 0.5px solid var(--rule); }
.fwblockrow:last-child { border-bottom: 0; }
.fwblockrow .grow { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; overflow-wrap: anywhere; }
.fwblockrow .btn { flex: none; }
.cnlist { display: flex; flex-direction: column; }
.cnlistrow { display: flex; gap: 12px; padding: 10px 2px; border: 0; border-bottom: 0.5px solid var(--rule); background: none; color: var(--ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
.cnlistrow .grow { flex: 1; min-width: 0; font-family: var(--mono); font-size: 12.5px; overflow-wrap: anywhere; }
@media (max-width: 700px) {
  .cnrow { grid-template-columns: minmax(0, 1fr) auto; }
  .cnwho { grid-column: 1 / -1; }
  .cntr { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 90px; }
  .cnth span:nth-child(3), .cnth span:nth-child(5), .cnprob, .cnsince { display: none; }
}

/* --- home -------------------------------------------------------------- */


.change {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  padding: 11px 18px;
  border-bottom: 1px solid var(--rule);
  font-size: 13px;
}
.panelcard .change:last-child { border-bottom: none; }
.change .when { font-family: var(--mono); font-size: 12px; color: var(--faint); width: 72px; flex: none; }
/* The Activity card (1.100.0): a warning or a problem marked at the edge,
 * and a line that arrives while somebody watches, seen arriving. */
.change.lvlwarn { box-shadow: inset 3px 0 0 var(--caution); }
.change.lvlbad  { box-shadow: inset 3px 0 0 var(--danger); }
.change.lvlbad .what > div:first-child { color: var(--danger); }
.change.fresh { animation: changein 600ms ease-out; }
@keyframes changein {
  from { opacity: 0; transform: translateY(-4px); background: var(--rule); }
  to   { opacity: 1; transform: none; background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .change.fresh { animation: none; }
}
.change .what { flex: 1; min-width: 0; }
.change .what .dim { font-size: 12px; margin-top: 1px; }
/* On Home, one line a row (1.108.1, the operator's choice): what happened,
 * and after it, dimmed, who or where -- cut short with an ellipsis, whole
 * on hover and on the Activity page. Not a row with buttons (Now playing),
 * which keeps its lines. */
.homecard .change:not(.actrow) { align-items: baseline; padding-top: 9px; padding-bottom: 9px; }
.homecard .change:not(.actrow) .what { display: flex; align-items: baseline; gap: 10px; }
.homecard .change:not(.actrow) .what > div {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 0;
}
.homecard .change:not(.actrow) .what > div:first-child { flex: 0 1 auto; }
.homecard .change:not(.actrow) .what > .dim { flex: 1 1 0; min-width: 4em; }
/* A row with buttons (1.108.0: Now playing): the name on one line, the
 * tags and buttons beside it, wrapping under it on a phone. */
.change.actrow { align-items: center; flex-wrap: wrap; row-gap: 6px; }
.change.actrow .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.change.actrow .tags { display: flex; gap: 6px; flex: none; }
.change.actrow .acts { display: flex; gap: 4px; flex: none; margin-left: auto; }
.change.actrow .acts .btn { padding: 3px 9px; font-size: 12px; }
.change.actrow .said { font-size: 12px; margin-top: 3px; color: var(--danger); }

.ident h2 { margin: 0 0 3px; font-size: 19px; font-weight: 500; letter-spacing: -0.015em; }
.ident .lede { color: var(--muted); font-size: 13px; margin: 0 0 18px; }

/* --- disclosures ------------------------------------------------------- */

/* A disclosure is a card like everything else, rather than a rule across the
 * page. Doing it here reaches every module: pf, unbound and sys all build one
 * through ctx.details() and none of them had to be edited. */
details.advanced {
  margin: 0 0 14px;
  background: var(--surface);
  border-radius: 14px;
  box-shadow: var(--lift);
  padding: 13px 18px;
}
details.advanced > summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--muted);
  list-style: none;
  user-select: none;
  padding: 2px 0;
}
details.advanced > summary::-webkit-details-marker { display: none; }
details.advanced > summary::before { content: '\25B8'; margin-right: 8px; font-size: 11px; }
details.advanced[open] > summary::before { content: '\25BE'; }
details.advanced > summary:hover { color: var(--ink); }
.advanced-body { padding-top: 14px; }
/* Inside a card that already has one, it is just the disclosure. */
.card .advanced, .panelcard .advanced { box-shadow: none; padding: 0; margin: 0; }

/* --- findings ---------------------------------------------------------- */

.finding {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 12px;
  border-left: 3px solid var(--rule);
  margin-bottom: 6px;
  font-size: 13px;
}
.finding.good { border-left-color: var(--marine); }
.finding.bad { border-left-color: var(--danger); color: var(--danger); }
.finding .hint { color: var(--muted); font-size: 12px; }

/* --- rules as rules ---------------------------------------------------- */


.check { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--muted); }
.check input { width: auto; }

h3.section .tag { margin-left: 10px; vertical-align: 1px; font-weight: 400; }

/* Something another module declared that the firewall does not admit. */
.suggest { border-top: 1px solid var(--rule); background: var(--sunk); }
.suggest .form-row { margin-bottom: 8px; }
.suggest .form-row:last-child { margin-bottom: 0; }


/* ======================================================================
 * The shared page grammar
 *
 * Three components, used the same way on every page: a heading that says what
 * state something is in, a strip of live figures, and rows. Nothing nests.
 *
 * What this replaces was four containers deep before any content -- page, card,
 * inner card, a dim floating label, then a row -- which is why the interface
 * read as cluttered even when every colour measured correctly.
 * ====================================================================== */

.page-head { margin: 0 0 26px; }
.page-head .line { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.page-head h2 {
  margin: 0;
  font-size: 22px;
  font-weight: 500;
  letter-spacing: -0.012em;
}
.page-head .lede { margin: 2px 0 0; font-size: 13px; color: var(--muted); }

/* The status word carries the alarm: it is where a real problem surfaces, so a
 * card can say "3 services failed" in place of "nothing failed" without
 * changing shape. */
/* One shape for stating a state, whatever the state is.
 *
 * A coloured word and a coloured word on a tint read as two different kinds of
 * thing, and the panel used both. A pill says "here is a state" the same way
 * for good and bad, so the colour carries which one rather than how important
 * it is. */
.state {
  font-size: 12.5px; font-weight: 500;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: 999px;
  background: var(--sunk); color: var(--muted);
}
.state::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
}
/* The bar under a vital. Track and fill, thin enough to read as a gauge rather
 * than as a chart.
 *
 * The width is fixed rather than the cell's, because the strip is a flex row
 * whose cells shrink to their content: the track under "75.7%" was wider than
 * the one under "2.0%", so two bars of the same length meant different numbers.
 * A gauge with no common scale is worse than no gauge, since it still looks
 * like one. */
/* A thinner .meter for the vitals strip, and a fixed width.
 *
 * The width is fixed rather than the cell's, because the strip is a flex row
 * whose cells shrink to their content: the track under "75.7%" was wider than
 * the one under "2.0%", so two bars of the same length meant different
 * numbers. A gauge with no common scale is worse than no gauge, since it still
 * looks like one. */
/* Full width of the card, not a fixed track.
 *
 * The fixed width was right when these sat side by side in a flex row, where
 * cells shrink to their content and a track sized to the cell made two bars of
 * the same length mean different numbers. Stacked in a card they share the
 * card's width already, so the common scale comes for free and a wider bar
 * reads better -- 3% of 84px was a dot. */
.card.feature .metrics .meter.thin { width: 100%; height: 3px; border-radius: 2px; margin: 0 0 2px; }
/* A fill this small is a smear rather than a bar; give it a floor so a low
 * reading still shows something was measured. */
.card.feature .metrics .meter-fill { min-width: 2px; }

.state.good { background: var(--marine-wash); color: var(--marine); }
.state.warn { background: var(--privl-wash);  color: var(--privl); }
.state.bad  { background: var(--danger-wash); color: var(--danger); }
.state.idle { background: var(--sunk);        color: var(--faint); }

/* --- live figures ------------------------------------------------------ */

.metrics {
  display: flex;
  flex-wrap: wrap;
  gap: 16px 44px;
  margin: 0;
  padding: 0;
}
/* A label above a value, in sentence case -- the same fault as .card h3.
 *
 * At 11px, capitals are slower to read than the word they replace: the
 * ascender and descender pattern the eye recognises a word by is gone.
 * The size already says this is subordinate to the figure beneath it. */
.metrics .k {
  font-size: 11.5px;
  letter-spacing: 0;
  color: var(--muted);
}
.metrics .v {
  font-size: 22px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  line-height: 1.25;
}
.metrics .v .u { font-size: 13px; font-weight: 400; color: var(--muted); }
.metrics .spark { flex: 1; min-width: 120px; align-self: flex-end; padding-bottom: 4px; }

/* --- rows ------------------------------------------------------------- */

/* A list of rows is a card, the same as the interface list on the network page:
 * rows ruled between rather than a bordered box, so every page that shows a
 * list looks like the same product. Defined once -- a second .rows rule further
 * down would win on source order alone, which is not a thing to rely on. */
.rows {
  background: var(--surface);
  border-radius: 14px;
  box-shadow: var(--lift);
  padding: 2px 18px;
  margin-bottom: 20px;
}
.row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 0;
  border-bottom: 0.5px solid var(--rule);
}
.rows .row:last-child { border-bottom: 0; }
/* A row carrying something under it: the top line keeps its shape and the
 * detail takes a second line of the same row.
 *
 * Defined here, once, beside .row -- a second .row rule further down would win
 * on source order alone, which is how the front page turned Firewall and DNS
 * into a column of stacked labels. */
.row.wrapped { flex-wrap: wrap; }
.row .detail { flex-basis: 100%; }
.row .detail .rows { box-shadow: none; margin-bottom: 0; padding: 0 14px; }
.row .grow { flex: 1; min-width: 0; }
.row .label { font-size: 15px; color: var(--ink); }
.row .value { font-size: 13px; color: var(--muted); }
/* Something to attend to, and something quieter than the text around it.
 *
 * Not scoped to .row: these started as the network page's note about the
 * running kernel disagreeing with the boot-time file, and the same two weights
 * are wanted anywhere -- the Maintenance page uses warn for what a restart
 * costs and dim for why something is not available yet. */
.value.warn { color: var(--privl); }
/* A pairing code (1.101.0), read off one screen and typed on another. */
.paircode {
  font-family: var(--mono);
  font-size: 28px;
  letter-spacing: 0.08em;
  color: var(--ink);
  margin: 6px 0 4px;
}
/* The code, click to copy (1.111.0). user-select: all -- one click takes
 * the whole code even where the clipboard is refused. */
.paircodeline { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.paircode.copyable {
  cursor: pointer; user-select: all; -webkit-user-select: all;
  border-radius: var(--radius); padding: 0 4px; margin-left: -4px;
}
.paircode.copyable:hover { background: var(--sunk); box-shadow: 0 0 0 1px var(--rule); }
.paircode.copyable:focus-visible { outline: 2px solid var(--marine); outline-offset: 2px; }
.paircodeline .copied { color: var(--faint); font-size: 13px; }
/* Managing another panel (1.114.0): the switcher beside the logo, and
 * the band that says, on every page, whose pages these are. */
.top .switcher { margin-left: 12px; max-width: 16em; }
.peer-band {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 8px 16px; background: var(--caution-wash); color: var(--caution);
  border-bottom: 1px solid var(--rule); font-weight: 500;
}
.peer-band .btn { margin-left: auto; }
/* Running from the USB stick (1.139.0): the same place and look as the
 * band above, on every page, with the way to the install page. */
.live-band {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 8px 16px; background: var(--caution-wash); color: var(--caution);
  border-bottom: 1px solid var(--rule); font-weight: 500;
}
.live-band[hidden] { display: none; }
/* A restart followed (1.142.0): the same place, marine while it waits,
 * green once it is back, amber when it is late. */
.restart-band {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 16px; background: var(--marine-wash); color: var(--marine);
  border-bottom: 1px solid var(--rule); font-weight: 500;
}
.restart-band[hidden] { display: none; }
.restart-band.late { background: var(--caution-wash); color: var(--caution); }
.restart-band.back { background: var(--in-wash); color: var(--in); }
.restart-band .rbicon { font-weight: 600; width: 16px; text-align: center; flex: none; }
.restart-band .spin { flex: none; }
.live-band .btn { margin-left: auto; text-decoration: none; }
/* The install page: the disks to choose from, then what installing does,
 * the model typed, and the job. */
.installpage { max-width: 680px; }
.installdisks { padding: 4px 0; }
.installdisk { display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left; background: none;
  border: 0; border-top: 0.5px solid var(--rule); padding: 12px 16px 12px 44px; font: inherit; color: inherit;
  cursor: pointer; position: relative; }
.installdisk:first-child { border-top: 0; }
.installdisk::before { content: ''; position: absolute; left: 16px; top: 15px; width: 16px; height: 16px;
  border-radius: 50%; box-shadow: inset 0 0 0 1.5px var(--faint); }
.installdisk.on::before { box-shadow: inset 0 0 0 5px var(--marine); }
.installdisk:hover:not(.off) { background: var(--sunk); }
.installdisk:focus-visible { outline: 2px solid var(--marine); outline-offset: -2px; }
.installdisk.off { cursor: not-allowed; opacity: .6; }
.installdiskname { font-weight: 600; }
.installdisksub { font-size: 13px; color: var(--muted); }
.installdiskwhy { font-size: 13px; color: var(--caution); font-weight: 600; }
.installcard { padding: 16px 18px; }
.installwill { margin: 6px 0 14px; padding-left: 20px; line-height: 1.6; }
.installtype { width: 100%; max-width: 360px; margin: 4px 0 12px; }
.installnow { display: flex; align-items: center; gap: 10px; font-family: var(--mono); font-size: 13px; }
.spin { width: 14px; height: 14px; border-radius: 50%; flex: none;
  border: 2px solid var(--rule); border-top-color: var(--marine); animation: panel-spin 0.9s linear infinite; }
@media (prefers-reduced-motion: reduce) { .spin { animation-duration: 2.4s; } }

/* Sharing accounts (1.113.0): each account both panels have, and differ
 * on, with the two versions to choose between. */
.shareclash { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; padding: 8px 10px;
  border: 1px solid var(--rule); border-radius: var(--radius); }
.shareclash .check { align-items: flex-start; }
.value.dim { color: var(--faint); }
.row .num {
  font-family: var(--mono);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.row:hover { background: var(--sunk); }
.row.off .label { color: var(--faint); text-decoration: line-through; }

/* --- module cards ----------------------------------------------------- */


/* --- the dashboard ------------------------------------------------------
 *
 * Rebuilt at 0.92.0 from a design mockup. Cards on a tinted page rather
 * than the coloured hero band of 0.81.0 -- the band was three attempts at
 * making one block of solid colour work (0.75.0, 0.79.0, 0.81.0) and it
 * never did. A card carries its own weight without needing the page to
 * shout underneath it.
 */
.dash2 { max-width: 1060px; margin: 0 auto; }
/* Overrides the global .card's own padding: these cards have a header rule
 * and their own per-section padding, so a wrapper pad would double it. */
.dash2 .card { border-radius: 12px; padding: 0; box-shadow: var(--lift); overflow: hidden; }
/* Every block on the dashboard is 14px below the last, whatever kind it
 * is. This listed .card and .grid2 only, so a grid3 row sat flush against
 * the grid2 above it -- the uneven spacing between the lower cards. */
/* Every top-level block on the dashboard is 14px below the one before it.
 *
 * `> * + *`, not a list of class combinations. Two goes at this got it
 * wrong in opposite directions, both because the rule was trying to
 * enumerate pairs:
 *
 *   `.dash2 .card + .card` was a descendant selector, so it also matched
 *   the second card *inside* a grid row -- Connections got 14px because
 *   Clients preceded it, System because DNS did. The grid gap already
 *   separates those, so it was pure vertical offset, and two cards in one
 *   row began and ended at different heights.
 *
 *   Scoping it to `.dash2 > .card + .card` fixed that and broke Recent
 *   changes: it is a top-level card, but the block before it is a grid
 *   div, so `.card + .card` does not match and it lost its margin
 *   entirely.
 *
 * The rule was never about what the blocks are. It is "a gap between
 * consecutive blocks", and the selector that says that has no class names
 * in it. Every combination is covered, including the ones nobody has
 * written yet.
 */
.dash2 > * + * { margin-top: 14px; }
.dash2 .cardhead {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 16px; border-bottom: 1px solid var(--rule);
}
.dash2 .cardhead h2 { margin: 0; font-size: 13.5px; font-weight: 600; }
.dash2 .cardhead .rt, .dash2 .cardhead .pill { margin-left: auto; font-size: 12.5px; color: var(--faint); }

/* No flex on the card, which is what the mockup does too.
 *
 * These held `display: flex` with the figure block set to `flex: 1` and
 * the split pushed down with `margin-top: auto`, to stop equal-height
 * cards leaving dead space inside themselves. That was solving a problem
 * that only existed because `.dash2 .card + .card` was adding 14px to the
 * second card in every row -- so the cards were both offset and unevenly
 * filled, and stretching the content was treating the symptom.
 *
 * With the margin scoped to top-level blocks the cards line up, and
 * content sitting at the top of an equal-height card is what the mockup
 * looks like. Removed rather than kept: two mechanisms for one layout is
 * how the next person gets confused about which is load-bearing.
 */
/* Three across, but auto-fit so they wrap rather than squeeze: three
 * cards of statistics at 228px each are readable, and narrower than that
 * they are not. */
/* No align-items, deliberately -- unlike the .grid2 it replaced.
 *
 * grid2 holds cards of unrelated shapes (a donut, a big number) where
 * stretching the short one leaves it with dead space at the bottom, so
 * `start` is right there. grid3 holds three cards of the same shape -- a
 * figure, a caption, two sub-figures -- and a row of those reads as a row
 * only if they line up. With `start`, copied from grid2 without thinking
 * about it, they were ragged: the System card ran taller than DNS beside
 * it and the bottom edges did not meet.
 *
 * Default stretch makes every card in the row the height of the tallest,
 * which is what the mockup does and why its bottom edge is straight.
 */
.grid3 {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(228px, 1fr));
}

/* The figure block fills the card so the split sits at the bottom rather
 * than floating under the caption. Without this, equal-height cards just
 * move the dead space from outside the card to inside it. */


/* The time axis under the traffic chart. Without it the trace is a shape
 * with no indication of how much time it covers, which is most of what
 * makes the mockup's chart readable: "the last 60 seconds" is the whole
 * frame of reference. */
.xax {
  font-family: var(--mono); font-size: 11px; color: var(--faint);
  padding: 0 16px 13px;
}
/* The five marks, each at its time (1.91.14): the ends flush with the
 * plot's edges, the middle three centred on 25/50/75%. See timeAxis(). */
.xax-marks { position: relative; height: 1.4em; }
.xax-mark { position: absolute; top: 0; white-space: nowrap; transform: translateX(-50%); }
.xax-mark.m0 { left: 0; transform: none; }
.xax-mark.m1 { left: 25%; }
.xax-mark.m2 { left: 50%; }
.xax-mark.m3 { left: 75%; }
.xax-mark.m4 { right: 0; transform: none; }
.xax .axnote { display: block; text-align: right; }

/* A sparkline inside a card, for the connections trend. Small on purpose:
 * it is context for the number above it, not a chart in its own right. */
.spark { display: block; width: 100%; height: 34px; margin-top: 12px; }

/* The health hero. A badge, a sentence, a supporting line -- it reads what
 * the cards below it found rather than fetching anything of its own, so it
 * cannot contradict them. */
.hero2 { display: flex; align-items: center; gap: 14px; padding: 16px; }
.hero2 .grow { flex: 1; min-width: 0; }
.hero-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.hero-sub { margin: 2px 0 0; font-size: 13.5px; color: var(--muted); }
/* What needs attention, opened (1.144.1): the status line's words are a
 * button while there is a problem; the drawer says each. */
.herocard .grow.tap { cursor: pointer; border-radius: 10px; margin: -6px -8px; padding: 6px 8px; }
.herocard .grow.tap:hover { background: var(--sunk); }
.herocard .grow.tap:focus-visible { outline: 2px solid var(--marine); outline-offset: 2px; }
.herocard .grow.tap .hero-title::after { content: ' \203A'; color: var(--faint); font-weight: 400; }
.probsec { padding: 0 0 18px; }
.probsec + .probsec { border-top: 1px solid var(--rule); padding-top: 16px; }
.probhead { margin: 0 0 4px; font-size: 15px; font-weight: 600; }
.probmsg { margin: 0 0 8px; font-size: 15px; }
.probrow { display: flex; gap: 10px; padding: 8px 12px; border-radius: 10px; background: var(--caution-wash); margin: 6px 0; }
.probrow.bad { background: var(--danger-wash); }
.probmark { flex: none; font-weight: 600; color: var(--caution); }
.probrow.bad .probmark { color: var(--danger); }
.probwords { display: flex; flex-direction: column; min-width: 0; }
.probname { font-weight: 600; font-size: 14px; }
.probdetail { font-size: 13px; overflow-wrap: anywhere; }
.problink { display: inline-block; margin-top: 8px; text-decoration: none; }
.hero-badge {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  display: grid; place-items: center; font-size: 17px; font-weight: 600; color: #fff;
  background: var(--faint);
}
.hero-badge.good { background: var(--marine-solid); }
.hero-badge.bad { background: var(--privl-solid); }
.hero-badge.idle { background: var(--faint); }

/* Traffic. Two figures side by side, then the chart. */
.tnums { display: flex; gap: 34px; padding: 16px 16px 4px; }
.tnum .l { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--muted); }
.tnum .v { font-size: 30px; font-weight: 600; letter-spacing: -.02em; margin-top: 2px; font-variant-numeric: tabular-nums; }
.dot { width: 9px; height: 9px; border-radius: 50%; flex: none; display: inline-block; }
.dot.down { background: var(--down); }
.dot.up { background: var(--up); }
.chartbox { padding: 0 8px 8px; }

/* Clients. The ring is a conic gradient driven by one custom property, so
 * there is no circumference arithmetic and nothing to recompute if the
 * size changes -- the failure mode of the stroke-dashoffset version this
 * replaces was silent and wrong rather than visibly broken. */
.donutwrap { display: flex; align-items: center; gap: 22px; padding: 18px 16px; }
.donut { position: relative; width: 108px; height: 108px; flex: none; }
/* --pct is registered so it can be animated.
 *
 * An unregistered custom property is a string as far as CSS is
 * concerned, and a string interpolates discretely -- it jumps from one
 * value to the next with nothing in between. That is why the ring
 * snapped: the transition was there and had nothing to interpolate.
 *
 * Registering it as a number gives the engine something to move through,
 * and the conic gradient follows because it is computed from it.
 */
@property --pct {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

.donut-ring {
  position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(var(--marine) calc(var(--pct, 0) * 1%), var(--sunk) 0);
  /* Long enough to read as the ring sweeping to a new value, short
   * enough to have finished before the next poll. Eased out, because a
   * proportion arriving and settling is what happened -- unlike the
   * traffic trace, where easing would invent a shape the data does not
   * have. */
  transition: --pct 520ms cubic-bezier(.22, 1, .36, 1);
  -webkit-mask: radial-gradient(circle, transparent 58%, #000 59%);
  mask: radial-gradient(circle, transparent 58%, #000 59%);
  transition: background .4s;
}
.donut .mid {
  position: absolute; inset: 0; display: grid; place-items: center; text-align: center; line-height: 1.15;
}
.donut .mid b { display: block; font-size: 27px; font-weight: 600; letter-spacing: -.02em; }
.donut .mid span { display: block; font-size: 11.5px; color: var(--muted); }
.keys { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.key { display: flex; align-items: center; gap: 9px; font-size: 13.5px; }
.key .n { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; }
.sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.sw.on { background: var(--marine); }
.sw.off { background: var(--sunk); }

/* System, and any card wanting one big figure over a two-up detail row. */
.mstat { padding: 15px 16px 16px; }
.mstat .v { font-size: 30px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.mstat .v u { font-size: 15px; font-weight: 500; text-decoration: none; color: var(--muted); margin-left: 3px; }
.mstat .cap { font-size: 12.5px; color: var(--muted); margin-top: 1px; }
/* Two figures divided by rules, not spaced by a gap.
 *
 * This was `gap: 26px` with no borders, which left both figures bunched
 * against the left edge of the card with empty space to their right --
 * the mockup gives each cell half the width and separates them with a
 * hairline, above and between. The difference is structural rather than
 * cosmetic: with flex: 1 the second figure sits at the card's midpoint,
 * which is what makes two of these read as a pair rather than as a list
 * that stopped early.
 */
.split {
  display: flex; gap: 0; margin-top: 13px;
  border-top: 0.5px solid var(--rule);
}
.split > div { flex: 1; padding: 11px 0 0; }
.split > div + div { border-left: 0.5px solid var(--rule); padding-left: 14px; }
.split .l { font-size: 12.5px; color: var(--muted); }
.split .n {
  font-size: 17px; font-weight: 500; margin-top: 1px;
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}

@media (max-width: 760px) {
  .tnums { gap: 22px; }
  .donutwrap { gap: 16px; }
}

.dash-chart { margin-bottom: 14px; padding: 16px 18px; }
.dash-chart .head { gap: 16px; }
.dash-chart .head .title { font-size: 13px; font-weight: 600; }
.dash-chart .chart-now {
  font-size: 12.5px; color: var(--muted); display: flex; align-items: center; gap: 6px;
  font-variant-numeric: tabular-nums;
}
.dash-chart .chart-now::before { content: ''; width: 8px; height: 8px; border-radius: 50%; flex: none; }
.dash-chart .chart-now.down::before { background: var(--down); }
.dash-chart .chart-now.up::before { background: var(--up); }
/* The traffic chart (1.91.13): both directions a stepped line over a flat
 * wash of the same colour, as the operator's UniFi reference draws them.
 * The washes are faint enough to read through each other where they
 * overlap; the lines carry the shape. vector-effect keeps the stroke its
 * real width in a plot that is stretched horizontally. */
.traffic-line { stroke-width: 1.5; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.traffic-line.down { stroke: var(--down); }
.traffic-line.up { stroke: var(--up); }
.traffic-fill.down { fill: var(--down); fill-opacity: .16; }
.traffic-fill.up { fill: var(--up); fill-opacity: .16; }


.card.feature { padding: 16px 18px 14px; }
/* Sentence case, matching .card h3 -- see the note there. */
.card.feature > h2 {
  font-size: 13px; font-weight: 600; letter-spacing: -.005em;
  color: var(--muted); margin: 0 0 12px;
}

/* A key and a value on one line, ruled between. */
/* A key and a value on one line, ruled between -- and the value takes its
 * own line when it cannot share.
 *
 * A WireGuard public key is 44 unbreakable characters. With the label and
 * value fighting over one flex line, the LABEL lost: "Its public key"
 * wrapped to three lines beside a key that still overflowed. The short
 * thing wrapped so the long thing could fail anyway.
 *
 * So the label never wraps, and the row does: a value that does not fit
 * drops below its label instead of squeezing it. That is also what makes
 * these rows work on a phone, where nothing fits beside anything.
 */
.addr {
  display: flex; justify-content: space-between; align-items: baseline;
  flex-wrap: wrap; gap: 4px 12px; padding: 7px 0;
}
.addr + .addr { border-top: 0.5px solid var(--rule); }
.addr .k { font-size: 13.5px; color: var(--muted); flex: 0 0 auto; white-space: nowrap; }
.addr .v {
  font-size: 15px; font-weight: 500; font-family: var(--mono);
  min-width: 0;
  /* `anywhere` rather than `break-word`: a base64 key has no word
   * boundaries to break at, and break-word would leave it overflowing. */
  overflow-wrap: anywhere;
}
.addr .dim { font-size: 13.5px; color: var(--faint); }

/* The one-word answer a card leads with, before the addresses that back it
 * up -- Internet's Online/Offline, and anywhere else a card has one fact
 * more important than the rest of what it knows. */
.headline { font-size: 26px; font-weight: 600; letter-spacing: -.01em; line-height: 1.15; }
.headline + .sub { margin-top: 1px; margin-bottom: 8px; font-size: 13px; }


/* The Machine card's gauges: a line, then a bar under it.
 *
 * Scoped to the card. ".metrics" is the strip every page leads with -- pf and
 * unbound both use it -- so a bare rule here turned those into a vertical list
 * of stacked labels and figures. A second rule for a shared class, winning on
 * source order, twice in one stylesheet now. */
.card.feature .metrics { display: block; }
.gauge + .gauge { margin-top: 4px; }

/* A card carrying only a disclosure, so it does not shout for attention. */
.card.quiet .advanced { border: 0; }

/* --- connections ------------------------------------------------------- */

/* Direction, as colour.
 *
 * In and out are the fact somebody scans this list for, and reading it off two
 * grey words is slower than reading it off a colour. Local is its own thing
 * again: traffic that never left the building is not the same as traffic from
 * the internet, and both were "in" before. */
.filters { display: flex; gap: 8px; align-items: center; padding: 12px 18px;
           border-bottom: 0.5px solid var(--rule); flex-wrap: wrap; }
.filters .spacer { flex: 1; }
/* Inline with the chips rather than wrapping onto a line of its own: it is a
   narrowing of the same list, not a separate thing. */
.filters input[type=text] { flex: 0 1 240px; min-width: 160px; }
.chip {
  font: inherit; font-size: 12.5px; padding: 4px 11px; border-radius: 999px;
  background: var(--sunk); color: var(--muted);
  border: 1px solid var(--rule); cursor: pointer;
}
.chip.on { background: var(--marine-wash); color: var(--marine); border-color: transparent; font-weight: 500; }
.chip.in { color: var(--in); }
.chip.out { color: var(--out); }
.chip.local { color: var(--local); }

.conn { display: flex; align-items: center; gap: 14px; padding: 11px 18px; }
.conn + .conn { border-top: 0.5px solid var(--rule); }
.conn .who { flex: 1; min-width: 0; }
.conn .peer { font-family: var(--mono); font-size: 13.5px; font-weight: 500; }
.conn .peer.in { color: var(--in); }
.conn .peer.out { color: var(--out); }
.conn .peer.local { color: var(--local); }
.conn .svc { display: inline-flex; align-items: baseline; gap: 7px; min-width: 130px; }
.conn .svc .n { font-weight: 600; font-size: 13.5px; }
.conn .svc .p { font-family: var(--mono); font-size: 12.5px; color: var(--faint); }
.conn .dir {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 500; padding: 3px 10px; border-radius: 999px;
  white-space: nowrap; min-width: 104px;
}
.conn .dir::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.conn .dir.in { background: var(--in-wash); color: var(--in); }
.conn .dir.out { background: var(--out-wash); color: var(--out); }
.conn .dir.local { background: var(--local-wash); color: var(--local); }
.conn .num { font-family: var(--mono); font-size: 12.5px; color: var(--muted); white-space: nowrap; }
/* Download and upload in their own colours (1.91.15): these were --in and
 * --privl, a green and the root-action brick, from before the traffic
 * colours existed. */
.conn .num .d { color: var(--marine); }
.conn .num .u { color: var(--up-text); }
/* Fixed widths on the right so the columns line up down the list. A row whose
   own connection carries a marker instead of a button was ragged against the
   ones that carry a button. */
.conn .iface {
  font-family: var(--mono); font-size: 12.5px; font-weight: 500;
  padding: 2px 8px; border-radius: 6px; background: var(--sunk); color: var(--muted);
  min-width: 46px; text-align: center;
}
.conn .iface.none { background: none; }
.conn .act { min-width: 82px; display: flex; justify-content: flex-end; }
.conn .num { min-width: 138px; text-align: right; }
.conn .btn:hover { border-color: var(--danger); color: var(--danger); }
.conn .btn.blocking { background: var(--danger-wash); border-color: transparent; color: var(--danger); }


/* A titled section: the label sits outside the card, so the card stays quiet
 * and several can stack without the page becoming a wall of boxes.
 *
 * Named "sect" rather than "group" because the rail already has a .group for
 * its own headings, and a generic name in a shared stylesheet is a collision
 * waiting to happen -- this one silently gave the rail's PANEL label a 22px
 * margin. */
.sect { margin-bottom: 22px; }
.sectlabel {
  font-size: 12px; color: var(--faint); font-weight: 500;
  letter-spacing: .02em; margin: 0 0 7px 2px;
}


/* Below this the drawing stops being legible, so the same data is shown as
 * ranked bars. Length replaces thickness, which is the only thing the drawing
 * used width to say. */
.bars .bar { display: flex; align-items: center; gap: 10px; padding: 7px 0; }
.bars .bar .n { font-size: 13px; color: var(--ink); width: 74px; flex: none; }
.bars .bar .track {
  flex: 1;
  height: 5px;
  border-radius: 3px;
  background: var(--sunk);
  overflow: hidden;
}
.bars .bar .fill { height: 100%; background: var(--marine); }

/* A card that is visibly waiting, rather than one that is visibly broken. */
.skeleton { display: flex; flex-direction: column; gap: 9px; }
/* The row version: a name and one line, matching the shape it becomes, so the
 * page does not jump when the module answers. */
.skeleton.row { gap: 6px; padding: 3px 0; }
.skeleton.row .sk.name { width: 90px; height: 14px; }
.skeleton.row .sk.line { width: 190px; height: 12px; }
.skeleton .sk {
  background: var(--sunk);
  border-radius: 4px;
  animation: sk 1.4s ease-in-out infinite;
}
.skeleton .sk.name     { width: 42%; height: 15px; }
.skeleton .sk.headline { width: 28%; height: 30px; margin-top: 3px; }
.skeleton .sk.line     { width: 62%; height: 13px; }
.skeleton .sk.short    { width: 48%; height: 13px; }
@keyframes sk { 0%, 100% { opacity: .55; } 50% { opacity: .95; } }
/* Home before its figures (1.175.0): the sentence, the tiles, the traffic
 * and today as shapes, each where it will be. */
.skeleton .sk { display: block; }
.hntitle .skeleton .sk.hnskt { width: min(320px, 70%); height: 28px; margin: 4px 0; }
.hnline .skeleton .sk.line { width: min(460px, 85%); }
.hntile.hnskel { pointer-events: none; }
.hntile.hnskel .sk.headline { width: 45%; }
.hntrcard.hnskel .hnskchart { height: 96px; border-radius: 6px; background: var(--sunk); opacity: .7; }
.hntoday > .skeleton { padding-top: 6px; }

/* The drawer button, hidden where the rail is always visible.
 *
 * The rule that reveals it has to come after this one: at equal specificity a
 * media query does not beat a later plain rule, which is how it ended up hidden
 * at every width. */
.top .menu {
  display: none;
  font-size: 19px;
  line-height: 1;
  padding: 1px 9px 3px;
  border-color: transparent;
}
@media (max-width: 680px) {
  .top .menu { display: inline-flex; }
}
@media (prefers-reduced-motion: reduce) {
  .rail { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton .sk { animation: none; opacity: .7; }
}

/* A heading for a section within a page, one weight below the page title. */
/* The heading above a list. Styled to match the front page's section labels,
 * so Firewall and DNS inherit the same treatment without either module being
 * edited: they were already building this shape, it just did not look like the
 * shape used everywhere else. */
/* Room above, so a section is separated from whatever ended above it.
 *
 * With no top margin the Rules label sat tight against the bottom of the
 * connections card and the two read as one block. The first section on a page
 * does not need it, since the page head has already provided the space. */
.sectionhead {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 26px 0 7px 2px;
}
.sectionhead .title { font-size: 12px; font-weight: 500; color: var(--faint); letter-spacing: .02em; }
.sectionhead .note { font-size: 12px; color: var(--faint); }
.sectionhead .spacer { flex: 1; }
/* The exception is the first section on a page, where the page head has
 * already provided the space -- not the first child of any container.
 *
 * Each section here lives in its own box, so ":first-child" matched every one
 * of them and cancelled the margin everywhere. The Rules heading is the first
 * child of the card it is drawn into, which is why the gap above it never
 * appeared. */
.main > .sectionhead:first-child,
.main > div:first-child > .sectionhead:first-child { margin-top: 0; }


/* Reordering stays visible: on a firewall the order decides everything, and
 * putting it behind a menu would hide the most consequential control. */

.row .sentence { font-family: var(--mono); font-size: 12.5px; color: var(--ink); }
.row.off .sentence { color: var(--faint); text-decoration: line-through; }
.row.editing { display: block; background: var(--sunk); padding: 6px 14px 14px; }
.row .btn.quiet { padding: 3px 8px; font-size: 12px; }

/* A setting that opens where it is.
 *
 * These are touched once a year: what matters is seeing every current value at
 * a glance and reaching one without losing sight of the others, which is what a
 * separate page for each would cost.
 */
/* The log body.
 *
 * Monospace and scrollable, with the end in view: somebody opening a log wants
 * what just happened, not the first line ever written. Bounded in height so the
 * page does not grow to the length of the file. */
/* An install or removal's steps on the Modules page (1.94.0): one line
 * each, a mark for its state, and what it is doing now beside it. */
.jobsteps { list-style: none; margin: 8px 0 4px; padding: 0; }
.jobstep { display: flex; gap: 8px; align-items: baseline; padding: 2px 0; color: var(--muted); }
.jobstep .mark { width: 1em; text-align: center; font-family: var(--mono); }
.jobstep.done { color: var(--ink); }
.jobstep.done .mark { color: var(--marine); }
.jobstep.running { color: var(--ink); font-weight: 500; }
.jobstep.failed, .jobstep.failed .mark { color: var(--danger); }
.jobstep .jobdetail { color: var(--muted); font-weight: 400; font-size: 12.5px; }
.hiddenfile { display: none; }

/* The line between the log picker and the log, in the card's gutter. */
.lognote { padding: 10px 18px 0; }
.lognote:empty { display: none; }
.logbody {
  margin: 0;
  padding: 14px 18px;
  max-height: 60vh;
  overflow: auto;
  font-size: 12.5px;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.filters .chip.dim { opacity: .45; cursor: default; }

/* A page's own tabs (1.93.1): Activity's Logs and Audit. Chips, as the
 * log picker below already draws, so a page does not introduce a third
 * kind of tab beside the phone's tab bar. */
.pagetabs { display: flex; gap: 8px; margin: 2px 0 14px; }
.pagetabs a.chip { text-decoration: none; }

/* Accounts are cards too, and for once the editor is not a separate element:
 * the row and the body it opens are children of one wrapper, so the card can be
 * drawn round both without joining two siblings by their corners. */
/* `.acctlist` is gone: the accounts page uses `.panelcard` with `.blrow`
 * rows, like every other list in the panel.
 *
 * These rules made each account its own rounded card with its own
 * shadow, which is why a page with one account showed a single grey slab
 * floating under a blue button. The class they were written for was a
 * different shape of list that only this page used.
 */
/* The `.label`/`.editor` rules went with it: the row uses `.bltitle` and
 * `.bldetail` now, and the editor is in a drawer. */
/* Deleting an account cannot be undone, and neither can removing a tunnel: the
 * panel writes a private key and never reads it back, so once the rollback
 * window closes nothing anywhere has a copy. Both use .btn.quiet.danger,
 * defined once beside .btn.quiet rather than again here -- a second rule for
 * the same selector wins on source order, which is not a thing to rely on. */


/* A row you can open reads as one, before and while it is open.
 *
 * There was no hover at all and no mark on the open row beyond its label
 * turning teal, so a row somebody was pointing at and a row they had opened
 * looked the same as the two either side. */
.row.setting { cursor: pointer; user-select: none; border-radius: 8px; }
.row.setting:hover { background: var(--sunk); }
.row.setting:hover .chev { color: var(--muted); }
.row.setting:focus-visible { outline: 2px solid var(--marine); outline-offset: -2px; }
.row.setting .chev {
  font-family: var(--mono);
  font-size: 15px;
  color: var(--faint);
  flex: none;
  width: 14px;
  text-align: center;
}
/* Open: the body below is tinted, so the row joins it rather than floating
 * above it, and the chevron has already turned to point down. */
/* The open row's label and chevron take the accent. */
.row.setting.open { border-bottom-color: transparent; }
.row.setting.open .label { color: var(--marine); font-weight: 600; }
.row.setting.open .chev { color: var(--marine); }

/* --- one audit record ---------------------------------------------------- */

.row.auditrow { cursor: pointer; }
.row.auditrow .when {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--faint);
  width: 46px;
  flex: none;
}
.auditmore {
  padding: 10px 0 14px 46px;
  border-bottom: 1px solid var(--rule);
  background: var(--sunk);
  margin: 0 -14px;
  padding-left: 60px;
  padding-right: 14px;
}
.auditmore .pair { display: flex; gap: 14px; padding: 3px 0; font-size: 12.5px; }
.auditmore .pair .k { color: var(--faint); width: 74px; flex: none; }
.auditmore .pair .v { color: var(--ink); font-family: var(--mono); word-break: break-all; }

/* --- the tab bar ------------------------------------------------------- */

/* Hidden until the window is narrow; the rail is the navigation on a desktop
 * and two of them at once would be two answers to the same question. */
.tabs { display: none; }
.sheet-scrim, .sheet-panel { display: none; }

/* --- the narrow-window layout, last in the sheet on purpose.
 *
 * Everything here overrides a rule declared plainly above it, and a media
 * query does not win on being a media query -- it wins on specificity, then on
 * source order. Placed earlier, ".conn .dir { display: none }" lost to the
 * plain ".conn .dir { display: inline-flex }" further down and the direction
 * pill stayed visible on a phone while the row had been told not to wrap.
 * css-order-check.py now refuses that in both directions. */
@media (max-width: 560px) {
  .metrics { gap: 12px 26px; }
  .metrics .spark { display: none; }


  /* Wrapping is for what does not fit, not for everything.
   *
   * "flex: 1 1 100%" on the label pushed every other thing in the row onto a
   * line of its own, so a DNS setting became a title, a value, and a chevron
   * floating below them with a gap above it. These rows are a label and one or
   * two small things beside it, and those always fit: the label shrinks and
   * its text wraps inside its own box. */
  .rows { padding: 2px 14px; }
  .rows .row { flex-wrap: nowrap; column-gap: 10px; }
  .rows .row .grow { flex: 1 1 auto; min-width: 0; }
  .rows .row .label,
  .rows .row .value { overflow-wrap: anywhere; }
  .rows .row .num { font-size: 12px; }
  .rows .row .chev,
  .rows .row .btn,
  .rows .row .state { flex: none; }

  /* A module's row is the other exception (1.108.1, seen on the operator's
   * phone): a name and a sentence, a button, a status and a paragraph of
   * facts. Held on one line, the facts became a column one word wide. So
   * it wraps: the name and sentence across the top, the button and status
   * under them, the facts under those. ".rows .row.wrapped" is three
   * classes, to outrank the nowrap above. */
  .rows .row.wrapped { flex-wrap: wrap; row-gap: 10px; }
  .rows .row.wrapped > .grow { flex: 1 1 100%; }
  .rows .row.wrapped > .detail { flex: 1 1 100%; }


  /* One line, by taking out what the row does not need at this width.
   *
   * The address is the thing you cannot lose, so it gets the room: the
   * direction word goes, because the colour of the address already says it,
   * and the port goes, because the service name has just said the same thing.
   * What is left -- address, service, traffic, action -- fits.
   *
   * Hidden by name rather than through a helper class. A ".at-width" helper
   * was declared plainly further down the sheet, so its "display: inline-flex"
   * won on source order and nothing was hidden at all -- while the row was
   * told not to wrap. The address was squeezed to nothing and the page scrolled
   * sideways. */
  .conn .dir { display: none; }
  .conn .svc .p { display: none; }

  .conn { flex-wrap: nowrap; column-gap: 10px; padding: 12px 15px; overflow: hidden; }
  /* The address shrinks and ellipsises rather than pushing the row wider than
   * the card. Without the clip a row that did not fit made the whole page
   * scroll sideways, which is a worse failure than a truncated address. */
  .conn .who { flex: 1 1 auto; min-width: 0; overflow: hidden; }
  .conn .peer { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .conn .svc { flex: none; min-width: 0; }
  .conn .num { flex: none; min-width: 0; text-align: right; }
  .conn .iface.none { display: none; }
  .conn .act { flex: none; min-width: 0; }
  .conn .act .btn { padding: 5px 11px; min-height: 28px; font-size: 12.5px; }

  /* The bar sits above the event tape, which is fixed to the bottom, and the
   * page is padded so its last row is not hidden behind either. */
  .tabs {
    display: flex;
    position: fixed;
    left: 0; right: 0; bottom: var(--tape-h);
    z-index: 40;
    background: var(--surface);
    border-top: 0.5px solid var(--rule);
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
  }
  .tabs .tab {
    flex: 1;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    padding: 6px 2px;
    /* 44px is the smallest thing a finger hits reliably. */
    min-height: 44px;
    border: 0; background: none; font: inherit;
    color: var(--muted); text-decoration: none; cursor: pointer;
  }
  .tabs .tab .lbl { font-size: 11px; letter-spacing: .01em; }
  /* The open page stands out (1.174.0, decision 325): its icon on a pill
   * of its own colour, its word in that colour and bold; the others grey.
   * Every icon in its colour, as at 1.172.0, left nothing to tell them apart. */
  .tabs .tab svg { box-sizing: content-box; padding: 3px 16px; border-radius: 999px; }
  .tabs .tab.on { color: var(--k, var(--marine)); }
  .tabs .tab.on svg { background: color-mix(in srgb, var(--k, var(--marine)) 16%, transparent); }
  .tabs .tab.on .lbl { color: inherit; font-weight: 600; }
  .tabs .tab.dim { opacity: .45; }

  /* What did not fit, in a sheet from the bottom. */
  .sheet-scrim {
    display: block;
    position: fixed; inset: 0; z-index: 45;
    background: rgba(10,10,12,.4);
    opacity: 0; pointer-events: none;
    transition: opacity .16s;
  }
  body.sheet .sheet-scrim { opacity: 1; pointer-events: auto; }
  .sheet-panel {
    display: block;
    position: fixed; left: 0; right: 0; z-index: 50;
    bottom: 0;
    background: var(--surface);
    border-radius: 16px 16px 0 0;
    box-shadow: 0 -2px 24px rgba(10,10,12,.28);
    padding: 8px 14px calc(18px + env(safe-area-inset-bottom, 0px));
    transform: translateY(100%);
    transition: transform .18s ease-out;
  }
  body.sheet .sheet-panel { transform: translateY(0); }
  .sheet-grip {
    width: 36px; height: 4px; border-radius: 2px;
    background: var(--rule); margin: 4px auto 10px;
  }
  .sheet-item {
    display: flex; align-items: center; min-height: 48px;
    padding: 0 4px; color: var(--ink); text-decoration: none;
    border-bottom: 0.5px solid var(--rule);
  }
  .sheet-item:last-child { border-bottom: 0; }
  .sheet-item .lbl { flex: 1; font-size: 16px; }
  .sheet-item .chev { color: var(--faint); font-size: 18px; }
  .sheet-item.on .lbl { color: var(--marine); font-weight: 600; }
  .sheet-item.dim { opacity: .5; }

  /* The search in the top bar (1.154.0), not while the old menu is used. */
  .top .findtop { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; margin-right: -6px; border: 0; border-radius: var(--r-control); background: none; color: var(--muted); cursor: pointer; }
  .top .findtop[hidden] { display: none; }

  /* The rail goes where the tab bar arrives, not before: hidden at a wider
   * breakpoint than the tabs appear at, there was a band of widths with no
   * navigation at all. */
  .rail { display: none; }

  /* Room for the bar, so the last row of a page is reachable. */
  .main { padding-bottom: 74px; }

  @media (prefers-reduced-motion: reduce) {
    .sheet-scrim, .sheet-panel { transition: none; }
  }
}

/* An open tab running code older than the server it is talking to.
 *
 * Bottom of the window rather than the top: the top is where the page's own
 * heading lives, and a bar that pushes it down moves everything somebody was
 * looking at. Above the event stream strip, and out of the way of it. */
.upgraded {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: 44px;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: min(680px, calc(100vw - 40px));
  padding: 12px 16px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 14px;
  box-shadow: var(--lift);
  font-size: 13px;
  color: var(--ink);
}

/* --- the firewall's rule editor ------------------------------------------
 *
 * A form in the drawer. Deliberately plain: every control is the browser's
 * own, styled by the input/select rules near the top of this file, because
 * a custom select on a page where a wrong choice can take the machine off
 * the network is not a trade worth making.
 */
.fwfield { margin-bottom: 16px; }
.fwlabel {
  display: block; font-size: 12.5px; font-weight: 600;
  margin-bottom: 5px; letter-spacing: -0.003em;
}
.fwfield input[type=text], .fwfield select { width: 100%; }
/* The free-text escape hatch sits under its own select. */
.fwfield input[type=text]:not(:first-child) { margin-top: 8px; }
.fwhint { margin: 5px 0 0; font-size: 12.5px; color: var(--muted); }
/* A field that is its own <label> (1.108.0, Media): the text, the control
 * and the hint, one under the other; a password as wide as text. */
label.fwfield { display: block; }
label.fwfield .fwhint { display: block; }
.fwfield input[type=password] { width: 100%; }
/* The Apps page (1.109.0): the catalogue's search and kind, side by side
 * until the screen is too narrow; what an app gets, as a short list. */
.appfilter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
.appfilter input[type=search] {
  flex: 1 1 220px; min-width: 0;
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 8px 10px; font-size: 13px;
}
.appfilter select { flex: 0 0 auto; width: auto; }
.appgets { margin: 6px 0 10px; padding-left: 18px; font-size: 13px; color: var(--muted); }
.appgets li { margin: 2px 0; }
.ctrow.fwrow:hover { background: var(--sunk); }
/* A sentence to act on that is not a problem: an app waiting to be set up. */
.callout {
  padding: 10px 12px; font-size: 13px;
  border: 1px solid var(--rule); border-left: 3px solid var(--marine); border-radius: 6px;
}
/* A hint that has stopped being merely informative.
 *
 * `warn-text` for a choice that is allowed and worth reconsidering -- the
 * short backup passphrase, where the panel cannot know whether the file
 * will leave a trusted place and so says what is at stake rather than
 * refusing. `err-text` for something that did not work.
 *
 * Both are colour on top of an already-legible hint rather than colour
 * carrying the meaning on its own: the sentences say which they are, so a
 * reader who cannot distinguish them loses nothing.
 */
.fwhint.warn-text { color: var(--caution); }
.fwhint.err-text { color: var(--danger); }

/* A clickable rule row. The whole row is the target -- see the note in the
 * module -- so it needs to look like one and to be reachable by keyboard. */
.fwrow { cursor: pointer; }
.fwrow:hover > td { background: var(--sunk); }
.fwrow:focus-visible {
  outline: 2px solid var(--marine);
  outline-offset: -2px;
}

.fwactions {
  display: flex; gap: 10px; flex-wrap: wrap;
  margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--rule);
}

/* The chain section headings in the rules table.
 *
 * The table is grouped by chain because cross-chain position is not a
 * fact about the firewall -- see the note in fw-linux/ui/index.js. Each
 * heading carries the chain's own default policy, because the single
 * "Default policy" figure above the table describes the input chain and
 * says nothing about forward, which has its own.
 *
 * A tbody rather than a row inside the managed one: ctx.list removes any
 * node in its container it did not key.
 *
 * NOT VERIFIED VISUALLY. No browser in the container this was written in.
 */
tbody.chainhead td {
  padding: 18px 0 6px;
  border-bottom: 1px solid var(--rule);
}
tbody.chainhead:first-child td { padding-top: 4px; }
tbody.chainhead .chainname {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
tbody.chainhead .chainpolicy {
  font-size: 12.5px;
  color: var(--muted);
  margin-left: 10px;
}

/* The staged-order bar, above the rules table.
 *
 * Inside the rules card rather than in the shell's band at the top of the
 * page: it is about these rows, and somebody reordering them is looking at
 * them. The shell's band is for a change already committed and counting
 * down, which is a different thing to say.
 *
 * NOT VERIFIED VISUALLY. There is no browser in the container this was
 * written in.
 */
.orderbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 16px;
  border-bottom: 1px solid var(--rule);
  background: var(--sunk);
}
.orderbar .ordernote { font-size: 13px; }
.orderbar .orderfail { font-size: 13px; color: var(--danger); }
.orderbar .hint { font-size: 12.5px; color: var(--muted); }

/* The move-up / move-down cell on a rule row.
 *
 * nowrap because the two arrows are one control, and a column narrow
 * enough to break them onto separate lines would make the row twice as
 * tall for no gain -- the same trade 1.55.6 made for td.rule, inside a
 * container that scrolls sideways rather than wrapping.
 *
 * NOT VERIFIED VISUALLY. There is no browser in the container this was
 * written in, so the arrows' size, spacing and alignment against the Hits
 * column beside them have been reasoned about and not seen. The class
 * exists rather than being invented at the call site so that whoever does
 * look at it has one place to adjust.
 */
td.fwmove {
  white-space: nowrap;
  padding-left: 6px;
}
td.fwmove .btn {
  /* Square, and the same 34px the base .btn already reserves in height.
   *
   * The arrows read as small and they were -- but not in the way it
   * looks. `.btn` sets `min-height: 34px`, which this never overrode, so
   * they were already 34px TALL; the padding of `2px 7px` round a 13px
   * glyph left them about 24px WIDE. A tall thin sliver with a faint
   * arrow in it, which is a poor target and looks smaller than it is.
   *
   * `min-width: 34px` makes them square against the height that was
   * already there, so rows do not grow: the cell was reserving this much
   * vertical space regardless.
   *
   * 16px glyph because ↑ and ↓ are light characters -- at 13px they read
   * as punctuation rather than as controls.
   */
  min-width: 34px;
  padding: 2px 8px;
  font-size: 16px;
  line-height: 1.2;
}
/* A gap between them. They are adjacent buttons meaning opposite things,
 * and two touching targets invite pressing the wrong one -- on a table
 * where the wrong one moves a firewall rule the wrong way. Harmless if
 * pressed by mistake, since nothing commits until Apply, but a move that
 * has to be undone is still a move nobody wanted. */
td.fwmove .btn + .btn { margin-left: 6px; }

/* On a touch screen the arrows are the whole control -- drag was removed
 * at 1.59.0 -- and 34px is still short of the ~44px a finger needs. A miss
 * is not a no-op either: the row opens the rule drawer, so a mistimed tap
 * on a reordering control edits the rule instead.
 *
 * `pointer: coarse` rather than a width breakpoint -- the question is
 * what is doing the pointing, not how wide the window is, and a touch
 * laptop at 1400px has the same problem.
 */
@media (pointer: coarse) {
  td.fwmove .btn {
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    font-size: 17px;
  }
}

/* The Order column is the last of ten in a table that scrolls sideways on
 * a narrow screen, so reaching the arrows means scrolling past every
 * other column -- and once there, the rule's action and chain have
 * scrolled off the left, which is precisely what somebody deciding where
 * a rule belongs needs to see.
 *
 * Pinned to the right edge instead, so it stays put while the rest
 * scrolls under it. An opaque background is required, not decorative:
 * without one the cells underneath show through as they pass.
 *
 * NOT VERIFIED VISUALLY, and this one has a known gap I would rather
 * write down than leave to be discovered. `tr:hover td` repaints the row
 * to --sunk; this cell carries --bg, so on a pointer device the pinned
 * column will not follow the hover tint. Hover is the one state a touch
 * screen does not have, which is the screen this rule is for, but on a
 * desktop it will look slightly wrong until somebody sees it.
 */
table.rules td.fwmove,
table.rules th.fwmove {
  position: sticky;
  right: 0;
  background: var(--bg);
}

/* --- a switch ------------------------------------------------------------
 *
 * A real checkbox, restyled. Not a div with a click handler: this is the
 * control for taking a whole network off the internet, and it should be
 * reachable by keyboard and announced as what it is.
 */
.togglerow { display: flex; align-items: flex-start; gap: 16px; padding: 16px; }
.togglerow .grow { flex: 1; min-width: 0; }
.togglelabel { font-size: 14.5px; font-weight: 600; margin-bottom: 2px; }
.togglerow .dim { margin: 0; font-size: 13px; }

.switch {
  appearance: none; -webkit-appearance: none;
  flex: none; margin: 2px 0 0;
  width: 44px; height: 26px; border-radius: 13px;
  background: var(--sunk); border: 1px solid var(--rule);
  position: relative; cursor: pointer;
  transition: background .15s, border-color .15s;
}
.switch::after {
  content: ''; position: absolute; top: 2px; left: 2px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--surface); box-shadow: 0 1px 2px rgba(0,0,0,.2);
  transition: transform .15s;
}
.switch:checked { background: var(--marine-solid); border-color: var(--marine-solid); }
.switch:checked::after { transform: translateX(18px); }
.switch:focus-visible { outline: 2px solid var(--marine); outline-offset: 2px; }

/* --- the firewall's stats strip -----------------------------------------
 *
 * The mockup's row of figures above the rules. A grid rather than flex so
 * the columns are equal whatever the numbers are: a strip whose cells
 * resize as a counter ticks is a strip that twitches.
 */
.statstrip {
  display: grid; grid-template-columns: repeat(3, 1fr);
  background: var(--surface); border-radius: 14px;
  box-shadow: var(--lift); margin-bottom: 16px; overflow: hidden;
}
.stat { padding: 14px 16px; }
.stat + .stat { border-left: 1px solid var(--rule); }
.statlabel { font-size: 12.5px; color: var(--muted); }
.statval {
  font-size: 26px; font-weight: 600; letter-spacing: -.02em;
  margin-top: 2px; font-variant-numeric: tabular-nums;
}
.statsub { font-size: 12.5px; color: var(--faint); font-variant-numeric: tabular-nums; }

@media (max-width: 760px) {
  /* Three columns of big numbers do not fit a phone; one column each. */
  .statstrip { grid-template-columns: 1fr; }
  .stat + .stat { border-left: 0; border-top: 1px solid var(--rule); }
}

/* The peak figure under each traffic number. Small and quiet: it is
 * context for the big number above it, not a second reading. */
.tnum .cap { font-size: 12px; color: var(--faint); margin-top: 1px; font-variant-numeric: tabular-nums; }

/* The third key in the Clients card has no swatch: online and offline are
 * slices of the ring, and the week's total is what they are slices of --
 * a colour would imply a third slice. The element stays for alignment. */
.keys .sw.none { background: transparent; }
.keys .sub { color: var(--muted); }

/* The two figures under the ring sit inside the card's own padding, which
 * donutwrap has and this does not. */
.clientsplit { margin: 0 18px 16px; }

/* The sparkline in the Connections card. Marine, like the download trace,
 * because it is the same kind of thing -- a quantity over time -- and a
 * second colour would imply a second meaning. */
.spark-line { stroke: var(--marine); stroke-width: 1.5; }
.spark-fill { fill: var(--marine-wash); }

/* The meter and the sparkline in the Connections card are one reading
 * shown two ways -- share of the limit, then direction -- so they sit
 * together. The meter's own 10px bottom margin plus the sparkline's 12px
 * top put 22px between them, which read as two unrelated things.
 *
 * Targeted through .sparkwrap rather than `.meter + .spark`: the svg is
 * inside a wrapper div, so the adjacent-sibling selector matched nothing
 * at all. Written that way first, and it would have sat in the stylesheet
 * doing nothing -- a rule that cannot match is worse than an absent one,
 * because it looks like the question was settled. */
.sparkwrap .spark { margin-top: 0; }

/* A read-only settings list: a label and a value per line. Not a table --
 * six or seven facts read better as lines than as two columns and seven
 * rows. */
/* A label and a value, and the value wraps rather than overflowing.
 *
 * The same fault `.addr` had at 1.44.1, and worse: `.setlabel` took
 * `flex: 1`, so the LABEL absorbed all the free space while the value had
 * no `min-width: 0` and could not shrink -- a long one ran past the edge
 * of the drawer instead of wrapping inside it. A WireGuard key or a list
 * of allowed addresses does exactly that.
 *
 * The label takes what it needs and no more; the value takes the rest and
 * drops to its own line when there is not enough. Which is also what
 * makes these rows work on a phone.
 */
.setrow {
  display: flex; gap: 4px 16px; align-items: baseline;
  flex-wrap: wrap;
  padding: 7px 0; border-bottom: 0.5px solid var(--rule);
}
.setrow:last-child { border-bottom: 0; }
.setlabel { flex: 0 0 auto; font-size: 13px; color: var(--muted); white-space: nowrap; }
/* A button in a value cell needs air before it.
 *
 * `.setval` is right-aligned text with no layout of its own, so an inline
 * button sat flush against the value beside it -- "1.65.0" with a primary
 * button touching the final digit, which read as the number being clipped.
 */
.setval .btn { margin-left: 14px; }
.setval {
  flex: 1; min-width: 0;
  font-size: 13px; text-align: right;
  /* `anywhere`: a base64 key has no word boundaries to break at, and
   * break-word would leave it overflowing. */
  overflow-wrap: anywhere;
}

/* A textarea in a drawer form takes the shared input styling above; these
 * are the parts specific to it. Monospace because every line is an address
 * or a hardware address, read character by character. */
.fwfield textarea {
  resize: vertical; min-height: 76px;
  font-family: var(--mono); font-size: 12.5px; line-height: 1.5;
}

/* --- a container row (1.107.0) ------------------------------------------
 *
 * A row that wraps rather than a table: a phone has room for the name, not
 * for five columns and two buttons beside it. */
.ctrow {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  padding: 10px 0; border-bottom: 1px solid var(--rule);
}
.ctrow:last-child { border-bottom: none; }
.ctrow .who { flex: 1 1 220px; min-width: 0; }
.ctrow .who .name { font-family: var(--mono); font-size: 13.5px; }
.ctrow .who .dim { font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
.ctrow .use { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.ctrow .acts { margin-left: auto; display: flex; gap: 4px; }

/* --- a blocklist row -----------------------------------------------------
 *
 * A name, what it is doing, its address, a count, a switch and a remove.
 * Not a table: the "what it is doing" line runs to a sentence when a fetch
 * has failed, and a sentence in a table cell either wraps badly or forces
 * every other row taller.
 */
.blrow {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 16px; border-bottom: 0.5px solid var(--rule);
}
.blrow:last-child { border-bottom: 0; }
.blrow .grow { flex: 1; min-width: 0; }
.bltitle { font-size: 14px; font-weight: 600; }
.bldetail { margin: 2px 0 0; font-size: 12.5px; }
.bldetail .bad { color: var(--danger); }
/* The address is reference rather than reading matter: dimmed, and
 * truncated instead of wrapping, because a long URL would otherwise be the
 * tallest thing in the row. */
.blurl {
  margin: 3px 0 0; font-size: 11.5px; color: var(--faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.blcount { text-align: right; flex: none; min-width: 74px; }
.blcount .n { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
.blcount .l { font-size: 11.5px; color: var(--muted); }
.blsummary { font-variant-numeric: tabular-nums; }

/* The peer configuration, shown once when a device is added. Monospace and
 * tall enough to read whole: it is a file somebody is about to copy, and
 * scrolling to check they got all of it is a way to get less of it. */
.peerconf {
  width: 100%; font-family: var(--mono); font-size: 12px; line-height: 1.55;
  resize: vertical;
}
/* The tunnel's public key in the stats strip: long, and wrapping it into
 * two ragged lines is harder to compare against a device than letting it
 * break anywhere. */
.keyval code { font-size: 11px; word-break: break-all; line-height: 1.4; }

/* An input that is present and operable but not drawn.
 *
 * For the file picker: the native control cannot be restyled and says "No
 * file chosen" in the browser's own typeface, so a button opens it
 * instead. Not `display: none` -- a hidden input in some browsers cannot
 * be clicked programmatically, and this one has to be. Clipped instead,
 * which keeps it focusable and operable.
 */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* Somewhere to drop a file, and a place for the button that opens the
 * picker. Dashed rather than solid: a dashed border reads as "put
 * something here" where a solid one reads as a card. */
.dropzone {
  border: 1px dashed var(--rule); border-radius: var(--radius);
  padding: 18px 16px; text-align: center;
  display: flex; flex-direction: column; gap: 10px; align-items: center;
}
.dropzone .fwactions { justify-content: center; margin: 0; }
.droptitle { font-size: 13px; color: var(--muted); }
/* While a file is over it. The change has to be unmistakable -- a drag is
 * held, and somebody needs to know they are over the target before they
 * let go. */
.dropzone.over { border-color: var(--marine); background: var(--marine-wash); }

/* The traffic trace moves rather than jumps.
 *
 * paint() keeps the same <path> elements and rewrites their `d`, so the
 * browser can interpolate between the two shapes. Without that the chart
 * was rebuilt twice a second and every sample arrived as a step.
 *
 * 480ms against a 500ms tick, deliberately: a transition longer than the
 * interval never finishes before the next one starts, so the line
 * permanently trails the data and the newest reading is never actually
 * drawn. Just under leaves it continuously in motion and still honest
 * about where it has got to.
 *
 * `linear`, not an easing curve. Ease-out would make each sample arrive
 * fast and settle slowly, which reads as the traffic itself surging and
 * calming -- animation inventing a shape the data does not have.
 */
.dash-chart path {
  /* NO transition on `d`, and this is a correction rather than an
   * omission.
   *
   * It had `transition: d 480ms linear`, reasoned as making the trace
   * scroll smoothly. It does the opposite once the window is full.
   *
   * Transitioning `d` interpolates the path POINT BY POINT. While the
   * window is filling, points are added at the right and the existing
   * ones do not move, so it looks fine. Once full, every point shifts
   * left by one tick on each paint -- so point N animates toward where
   * point N+1 used to be, which is a different VALUE. The whole curve
   * morphs through shapes the data never had, twice a second, for ever.
   *
   * Two screenshots six seconds apart showed the same peaks at different
   * heights and widths. That is the wave, and it is the animation
   * inventing data.
   *
   * Without it the trace steps left twice a second, which is honest. The
   * x positions are already anchored to the wall clock (1.19.0), so each
   * step is exactly one tick and reads as scrolling rather than jumping.
   */
}

/* Except when the shape changes underneath. paint() rebuilds rather than
 * morphs when the number of paths differs -- a new run, or the window
 * filling -- and a fresh element has nothing to transition from, so this
 * costs nothing there. */
@media (prefers-reduced-motion: reduce) {
  /* Somebody who has asked for less movement is asking about exactly this
   * kind of thing: a line sliding continuously in the corner of their
   * eye. The chart still updates; it just arrives rather than travels. */
  .dash-chart path { transition: none; }
}

/* A card whose body opens an editor.
 *
 * The interfaces and firewall pages use clickable table rows; a settings
 * card is not a table -- it is six or seven facts as a description list --
 * so the clickable thing is the panel itself rather than each line. Same
 * affordances either way: a pointer, a surface that responds, and a focus
 * ring, because a card that only answers a mouse is one a keyboard cannot
 * reach.
 */
.clickcard { cursor: pointer; }
.clickcard:hover { background: var(--sunk); }

/* A dashboard card that opens a page.
 *
 * `.card` already has a surface and a shadow, so the plain hover wash is
 * nearly invisible on one -- it reads as nothing happening. A lifted
 * shadow says "this moves" where a background change says "this is
 * highlighted", and moving is what it does.
 *
 * The transition is short and on the shadow alone: the cards carry live
 * charts, and animating anything that affects layout would make the trace
 * jump every time a pointer crossed it.
 */
.card.clickcard { transition: box-shadow 120ms ease, transform 120ms ease; }
.card.clickcard:hover {
  background: var(--surface);
  box-shadow: var(--lift), 0 0 0 1px var(--rule);
  transform: translateY(-1px);
}
@media (prefers-reduced-motion: reduce) {
  /* The lift is decoration; the shadow still says it is interactive. */
  .card.clickcard { transition: none; }
  .card.clickcard:hover { transform: none; }
}
.clickcard:focus-visible {
  outline: 2px solid var(--marine);
  outline-offset: -2px;
}
/* The hint that it opens, which a row in a table does not need because a
 * table of rows reads as a list of things. A card does not. */
.clickcard .opens {
  color: var(--muted); font-size: 12px; margin-left: auto;
}

/* The mobile tab bar's overflow button.
 *
 * `.tab.more` has been asked for since the bar was built and never
 * defined, so it took the plain .tab style -- which mostly looked right,
 * which is why nobody noticed. It differs in one way that matters: it is
 * not a destination but a way to reach the rest, so the label sits closer
 * and the chevron carries the meaning.
 */
.tab.more .lbl { letter-spacing: 0; }

/* A row standing in while a table loads.
 *
 * The text inside carries .dim, so this was invisible as a fault. It
 * exists to stop the row being mistaken for data: no hover, and it never
 * responds to a click the way a real row does.
 */
.loading-row { pointer-events: none; }
.loading-row td { padding-top: 14px; padding-bottom: 14px; }

/* The System page's rows: a label, a bar, a figure.
 *
 * A grid rather than flex, so the bars line up across rows whatever the
 * labels say -- "Memory" and "/var/panel-data" in the same column would
 * otherwise start their bars at different places and the page would read
 * as ragged.
 */
.sysrow {
  display: grid;
  grid-template-columns: 9rem 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 0.5px solid var(--rule);
}
.sysrow:last-child { border-bottom: none; }
.syslabel { color: var(--muted); font-size: 13px; }
.sysval { font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }

.meter {
  height: 6px; border-radius: 3px;
  background: var(--sunk); overflow: hidden;
}
.meterfill {
  height: 100%; background: var(--marine);
  /* The width is set from script; the transition keeps a five-second
   * refresh from making the bars jump. */
  transition: width 400ms linear;
}
/* Above three quarters and above nine tenths. A disk at 91% is a
 * different thing from one at 60%, and the colour should say so before
 * the number is read. */
.meterfill.warm { background: var(--caution); }
.meterfill.hot { background: var(--danger); }

@media (max-width: 640px) {
  /* The bar goes under the label rather than squeezing to nothing
   * between a label and a figure that both need their width. */
  .sysrow { grid-template-columns: 1fr auto; }
  .sysbar { grid-column: 1 / -1; order: 3; }
}
/* The Machine card's rows (1.148.0): no bar, and what a disk or a port is
 * may run to a second line, right-aligned, rather than off the card. */
.syswide .sysbar { display: none; }
.syswide .sysval { white-space: normal; text-align: right; overflow-wrap: break-word; }
@media (min-width: 641px) {
  .sysrow.syswide { grid-template-columns: 9rem minmax(0, 1fr); }
}
@media (max-width: 640px) {
  .sysrow.syswide { grid-template-columns: auto minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .meterfill { transition: none; }
}

/* Calm numbers.
 *
 * This was a set of rolling digit columns. It looked worse, and the
 * reason is in web/rolling.js: rolling suits a number that changes
 * rarely and meaningfully, and a throughput figure changes twice a
 * second and is mostly noise. Animating each change turned jitter into
 * choreography.
 *
 * What is left is tabular figures, so the number does not shift sideways
 * as its digits change, and one brief fade when the UNIT changes --
 * Kb/s becoming Mb/s is a real event and it happens rarely.
 */
.calmnum {
  display: inline-flex;
  align-items: baseline;
  /* A 1 is narrower than an 8 in the proportional face, so a figure
   * updating twice a second twitches horizontally without this. */
  font-variant-numeric: tabular-nums;
}
.calmval.unitchange {
  animation: unitfade 320ms ease-out;
}
@keyframes unitfade {
  /* Down and back, not a flash: the figure is being read while this
   * happens, and something that brightens pulls the eye off whatever it
   * was looking at. */
  0% { opacity: .35; }
  100% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .calmval.unitchange { animation: none; }
}

/* The System page's rows: a label, a bar, a figure.
 *
 * A grid rather than flex, so the bars line up across rows whatever the
 * labels say -- "Memory" and "/var/panel-data" in the same column would
 * otherwise start their bars at different places and the page would read
 * as ragged.
 */
.sysrow {
  display: grid;
  grid-template-columns: 9rem 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 0.5px solid var(--rule);
}
.sysrow:last-child { border-bottom: none; }
.syslabel { color: var(--muted); font-size: 13px; }
.sysval { font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }

.meter {
  height: 6px; border-radius: 3px;
  background: var(--sunk); overflow: hidden;
}
.meterfill {
  height: 100%; background: var(--marine);
  /* The width is set from script; the transition keeps a five-second
   * refresh from making the bars jump. */
  transition: width 400ms linear;
}
/* Above three quarters and above nine tenths. A disk at 91% is a
 * different thing from one at 60%, and the colour should say so before
 * the number is read. */
.meterfill.warm { background: var(--caution); }
.meterfill.hot { background: var(--danger); }

@media (max-width: 640px) {
  /* The bar goes under the label rather than squeezing to nothing
   * between a label and a figure that both need their width. */
  .sysrow { grid-template-columns: 1fr auto; }
  .sysbar { grid-column: 1 / -1; order: 3; }
}
@media (prefers-reduced-motion: reduce) {
  .meterfill { transition: none; }
}


/* The release upload's progress.
 *
 * A 47MB upload over wifi takes long enough that a page showing nothing
 * looks broken -- and somebody reloads it half way, which is the failure
 * this is really preventing.
 */
.upmeter { margin: 14px 0 4px; }
.uptrack {
  height: 6px; border-radius: 3px;
  background: var(--sunk); overflow: hidden;
}
.upfill {
  height: 100%; width: 0;
  background: var(--marine);
  /* Linear, not eased: an eased progress bar appears to slow down near
   * every value it passes, which reads as the upload stalling. */
  transition: width 180ms linear;
}
.uprow {
  display: flex; align-items: baseline; gap: 10px;
  margin-top: 8px;
  font-size: 12px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.upsent { white-space: nowrap; }
.upspeed { white-space: nowrap; }

/* The bytes are gone and the broker is checking 44 checksums, which has
 * no progress to report. A stripe that moves says "still working" where a
 * full bar sitting still says "stuck". */
.uptrack.checking .upfill {
  background: repeating-linear-gradient(
    -45deg,
    var(--marine) 0 10px,
    var(--marine-wash) 10px 20px);
  animation: upstripe 700ms linear infinite;
}
@keyframes upstripe {
  from { background-position: 0 0; }
  to { background-position: 28px 0; }
}
@media (prefers-reduced-motion: reduce) {
  .upfill { transition: none; }
  .uptrack.checking .upfill { animation: none; }
}


/* The rail's icons.
 *
 * Smaller than the tab bar's 22px: a sidebar entry is read left to
 * right with its label, where a tab is a target on its own and needs the
 * size. At 22px here the icon competes with the word beside it.
 *
 * Dimmer than the label, and full strength on the current page -- the
 * icons are there to make the list scannable, not to be the list.
 */
.rail a svg {
  width: 17px; height: 17px;
  flex: 0 0 17px;
  /* A section's entries carry its hue (1.117.0), as its tiles and bands
   * do; the label stays grey, so the word is still what is read. */
  color: var(--k, var(--hue, currentColor));
  opacity: .62;
  transition: opacity 120ms ease;
}
/* The new menu's icons in their section's hue (1.172.0, decision 307):
 * stronger than the old menu's, as the app has them. */
.rail a[class*="k-"] svg { opacity: .9; }
.rail a:hover svg { opacity: .85; }
.rail a.on svg { opacity: 1; }
.navlabel { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

@media (prefers-reduced-motion: reduce) {
  .rail a svg { transition: none; }
}

/* Buttons, refined.
 *
 * The base was serviceable and flat: a hover that changed three colours
 * at once, a press that nudged half a pixel, and nothing between "I
 * clicked" and "something appeared". These are the three things that
 * make a control feel answered rather than merely functional.
 */

/* 1. The press.
 *
 * A half-pixel nudge is below the threshold of noticing. A small scale is
 * what a finger or a pointer expects -- the control yields. Fast down,
 * slower back: pressing should feel immediate and releasing should feel
 * like letting go.
 */
.btn {
  transition:
    background .14s ease,
    border-color .14s ease,
    color .14s ease,
    transform .18s cubic-bezier(.22, 1, .36, 1),
    box-shadow .18s ease;
}
.btn:active:not([disabled]) {
  transform: scale(.975);
  transition-duration: .06s;
}

/* 2. The primary button carries a little weight.
 *
 * A shadow in its own colour rather than a grey one: a blue button
 * floating on a grey shadow reads as a sticker on the page, where a
 * shadow tinted from the fill reads as the button being lit.
 */
.btn.root, .btn.primary {
  box-shadow: 0 1px 2px rgba(10, 102, 201, .18);
}
.btn.root:hover:not([disabled]), .btn.primary:hover:not([disabled]) {
  box-shadow: 0 2px 8px rgba(10, 102, 201, .28);
  transform: translateY(-1px);
}
.btn.root:active:not([disabled]), .btn.primary:active:not([disabled]) {
  box-shadow: 0 1px 2px rgba(10, 102, 201, .18);
  transform: scale(.975);
}

/* The danger button's shadow is its own colour too, for the same reason
 * -- and because a destructive control sharing the primary's blue glow
 * would be the one place the palette lies about what a thing does. */
.btn.danger:hover:not([disabled]) {
  box-shadow: 0 2px 8px rgba(179, 38, 30, .22);
  transform: translateY(-1px);
}

/* 3. The focus ring, which was a hard 2px outline.
 *
 * A ring offset from the edge with a soft halo behind it reads as the
 * control being selected rather than boxed. Kept obvious: this is the
 * only thing a keyboard user has to navigate by.
 */
.btn:focus-visible {
  outline: 2px solid var(--marine);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--marine-wash);
}

@media (prefers-reduced-motion: reduce) {
  /* The colour changes stay -- they are information. The movement goes. */
  .btn { transition: background .14s ease, border-color .14s ease, color .14s ease; }
  /* Every selector that sets a transform above, at the same specificity
   * or higher.
   *
   * `.btn:active` alone is outranked by `.btn.root:active`, so a reduced
   * -motion request would have suppressed the press on ordinary buttons
   * and not on the primary one -- caught by media-override-check, which
   * exists for exactly this.
   */
  .btn:active:not([disabled]),
  .btn.root:active:not([disabled]),
  .btn.primary:active:not([disabled]),
  .btn.root:hover:not([disabled]),
  .btn.primary:hover:not([disabled]),
  .btn.danger:hover:not([disabled]) { transform: none; }
}

/* --- hierarchy: radius and elevation carry it ------------------------- */

/* The hero, which is the one thing on the page that should be looked at
 * first. It had the same radius, shadow and padding as a minor stat
 * card, so the dashboard opened with five objects of equal weight.
 *
 * A larger radius and a real shadow, and nothing else: no gradient, no
 * border treatment, no accent bar. One element is allowed to be the
 * loudest and the way it gets there is by everything around it being
 * quieter, not by it acquiring decoration.
 */
.card.herocard {
  border-radius: var(--r-hero);
  box-shadow: var(--lift-raised);
  padding: 4px 6px;
}

/* Controls are hand-sized and crisp; a button with a card's curvature
 * reads as a soft target rather than a pressable one. */
.btn { border-radius: var(--r-control); }

/* Chips are fully round, so they read as labels attached to something
 * rather than as small cards of their own. */
.tag, .pill { border-radius: var(--r-chip); }

/* And the cards that only report sit flat: a hairline ring against the
 * page, no lift. Elevation should mean "act on me" or "read me first",
 * and a panel of eight equally raised surfaces means neither. */
.dash2 .grid3 > .card {
  box-shadow: var(--lift-flat);
}

@media (prefers-reduced-motion: reduce) {
  /* The ring's sweep is decoration; the proportion it lands on is the
   * information, and that arrives either way. */
  .donut-ring { transition: none; }
}

/* The status element, in its two states.
 *
 * `.calm` is the common one and almost always the truth, so it is a
 * ribbon: one line, a dot, no shadow, no badge. A problem removes the
 * class and it becomes the card it used to be -- which is the only time
 * it is worth the space at the top of the page.
 *
 * The transition is on the things that change size, so the page settles
 * rather than jumping when a fault appears.
 */
.card.herocard {
  transition: box-shadow 260ms ease, padding 260ms ease, background 260ms ease;
}
.card.herocard.calm {
  box-shadow: var(--lift-flat);
  padding: 0;
  background: none;
}
.card.herocard.calm .hero2 {
  padding: 10px 4px;
  gap: 10px;
}
/* The dot is part of the text, not a box beside it.
 *
 * Twice I made it a shrunken badge and twice it sat wrong -- too high,
 * then too low. The reason is structural rather than a matter of
 * pixels: the badge is a flex item of `.hero2`, which centres it on the
 * ROW, while the words live in their own flex aligned on the BASELINE.
 * Those two centres are not the same line, and nudging one against the
 * other is guesswork that changes with the font size.
 *
 * As a pseudo-element on the title with `vertical-align: middle`, the
 * dot is centred on the text's own middle by definition. There is
 * nothing left to align.
 */
.card.herocard.calm .hero-badge { display: none; }

.card.herocard.calm .hero-title::before {
  content: '';
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--in);
  vertical-align: middle;
  /* Lifted by half a pixel: `middle` centres on the x-height midpoint,
   * which sits very slightly below the optical centre of a line of text
   * with ascenders in it. Half a pixel is the difference and it is
   * visible at this size. */
  margin: -1px 10px 0 0;
}

.card.herocard.calm .hero-title {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--muted);
  display: inline;
}
/* The uptime sits on the same line as the phrase rather than under it.
 * Two lines to say nothing is wrong was the shape being complained
 * about. */
.card.herocard.calm .hero2 .grow {
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.card.herocard.calm .hero-sub {
  margin: 0;
  font-size: 13px;
  color: var(--faint);
}
.card.herocard.calm .cap { display: none; }

@media (prefers-reduced-motion: reduce) {
  .card.herocard { transition: none; }
}

/* The dashboard cards' meter.
 *
 * The System page's meter sits in a row beside a label; here it sits
 * under a headline figure, so it needs its own spacing and a little less
 * weight -- it is supporting the number rather than being the row.
 */
.cardmeter {
  margin: 10px 0 2px;
  height: 4px;
}

/* The hover cursor on the traffic chart.
 *
 * A hairline, not a highlight: the trace is the thing being read and a
 * band behind it would compete with the fill it sits on. No transition
 * -- it should be where the pointer is, and easing it makes it lag.
 */
.traffic-cursor {
  stroke: var(--muted);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  pointer-events: none;
}
.dash-chart .plotbody { cursor: crosshair; }

/* The mirrored chart's zero line.
 *
 * Faint: it is a reference the two directions grow from, not a gridline
 * -- this chart has no others and adding one here would imply a scale it
 * does not draw.
 */
.traffic-mid {
  stroke: var(--rule);
  stroke-width: 1;
}
.traffic-ceil {
  stroke: var(--rule);
  stroke-width: 1;
  stroke-dasharray: 3 4;
  vector-effect: non-scaling-stroke;
}

/* The chart's own spacing, on its head and body rather than on itself.
 *
 * `.dash-chart { padding: ... }` is specificity (0,1,0) and
 * `.dash2 .card { padding: 0 }` is (0,2,0), so every padding I set on
 * this element was discarded -- twice, in two different versions, while
 * I measured pixels and wondered why nothing moved. The title sat
 * against the card's own border and ended up LEFT of the "Traffic"
 * heading it belongs under, which is what gave it away.
 *
 * Put on the children instead, which nothing overrides. Matched to
 * `.cardhead` so the chart's title lines up with the card heading above
 * it rather than sitting proud of it.
 */
.dash-chart .head {
  padding: 14px 18px 0;
  margin-bottom: 10px;
}
/* The trace gets a little more room on the right, where "now" is: a line
 * touching the frame reads as clipped rather than current. */
.dash-chart .plotbody { padding: 0 20px 0 18px; }
.dash-chart .xax { padding: 0 20px 12px 18px; }

/* The chart's window selector.
 *
 * Three options compared against each other, so all three stay visible --
 * a dropdown would hide two behind a click and make the current one read
 * as a setting rather than a view.
 */
.spanpicks {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  margin-left: 14px;
  background: var(--sunk);
  border-radius: var(--r-chip);
}
.spanpick {
  border: 0;
  background: none;
  font: inherit;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  padding: 3px 10px;
  border-radius: var(--r-chip);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.spanpick:hover { color: var(--ink); }
.spanpick.on {
  background: var(--surface);
  color: var(--ink);
  font-weight: 600;
}
.spanpick:focus-visible {
  outline: 2px solid var(--marine);
  outline-offset: 1px;
}
/* What resolution the stored spans are drawn at, said once beside the
 * axis rather than in the legend: it explains the ticks, not the trace. */
.xax .axnote { color: var(--faint); font-style: normal; }

@media (prefers-reduced-motion: reduce) {
  .spanpick { transition: none; }
}

@media (max-width: 680px) {
  /* The chart's head wraps, and the selector takes its own line.
   *
   * It was one flex row -- title, spacer, two legend items, four span
   * buttons -- and on a phone the buttons ran off the right edge. The
   * last one was unreachable: there was no way to tap 30d at all.
   *
   * Wrapping rather than switching to a native select: four options that
   * are compared against each other stay better as a row somebody can
   * see, and a full-width row of four fits a phone comfortably. A select
   * would hide three of them behind a tap to save a line.
   */
  .dash-chart .head { flex-wrap: wrap; row-gap: 10px; }
  .dash-chart .head .spacer { display: none; }
  .dash-chart .spanpicks {
    margin-left: 0;
    width: 100%;
    justify-content: space-between;
  }
  .dash-chart .spanpick {
    flex: 1;
    /* 44px is the smallest thing a finger hits reliably -- the same
     * figure the tab bar uses. */
    min-height: 34px;
    padding: 6px 4px;
  }
  /* The legend sits under the title rather than competing with it for a
   * line that is now shared with nothing. */
  .dash-chart .chart-now { font-size: 12px; }
}

/* A form in a drawer: one field per line.
 *
 * `.form-row` lays fields out across the page, which is right above a
 * table and wrong in a 440px drawer, where four fields side by side are
 * four fields nobody can read the labels of.
 */
.form-col { display: flex; flex-direction: column; gap: 14px; }
/* A form that follows prose needs air before it.
 *
 * `.form-col` carries no margin and `.fwhint` no bottom one, so a field
 * label placed under an explanatory paragraph sat hard against it and read
 * as the last line of that paragraph rather than as the name of the input
 * below. Seen on the backup card.
 *
 * A sibling selector rather than a class on the one card: "a form after
 * text" is the general case, and anywhere else it happens wants the same
 * gap.
 */
p + .form-col { margin-top: 18px; }
.form-col .field { display: flex; flex-direction: column; gap: 6px; }
.form-col .field input,
.form-col .field select { width: 100%; }

/* Sections of a page that holds more than one module.
 *
 * Separated by space rather than a rule: each module already draws its
 * own cards, and a divider between two groups of cards is a line between
 * things that are already visibly apart.
 */
.modsection + .modsection { margin-top: 26px; }

/* A module's heading when it is a section rather than a page.
 *
 * Smaller than the page's h2 and set in the muted ink: it labels a group
 * of cards, and at page-title weight it would compete with the title
 * above it for what the page is about.
 */
.modhead {
  margin: 0 0 12px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--muted);
}

/* A page title with an action on the same line.
 *
 * Accounts had the title, then a second heading, then the action on a
 * third line where it read as a stray link. One row: what the page is,
 * and the one thing you might do to it.
 */
.pagehead {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  margin-bottom: 18px;
}
.pagehead .grow { flex: 1; min-width: 0; }
.pagehead h2 { margin: 0; }
.pagehead .lede { margin: 4px 0 0; }
/* Aligned to the lede's baseline rather than the title's: the button is
 * the same weight as the supporting line, not the heading. */
.pagehead .btn { flex: none; margin-bottom: 2px; }

/* The role and status, right-aligned so they form a column the eye can
 * run down. A role buried in a sentence cannot be compared between rows,
 * which is the one thing this page is scanned for. */
.acctmeta { display: flex; align-items: center; gap: 8px; flex: none; }

/* The audit search bar.
 *
 * Above the rows rather than beside the heading: it filters what is
 * below it, and a control that acts on a list belongs with the list
 * rather than with the page title.
 */
.auditbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
}
.auditsearch {
  flex: 1;
  min-width: 0;
  max-width: 340px;
}
/* "so far", not a total -- a total would need a second query over the
 * whole table on every keystroke to answer a question nobody asked. */
.auditcount {
  font-size: 12.5px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The traffic card's interface picker.
 *
 * In the card's heading rather than above the chart: it chooses what the
 * whole card is about, including the two big figures, not just what the
 * trace draws.
 */
.ifacepick {
  font-size: 12.5px;
  padding: 3px 8px;
  border-radius: var(--r-control);
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--ink);
  max-width: 11rem;
}
/* Shown only when the choice differs from the saved one -- a Keep button
 * that is always there invites pressing it to find out what it does. */
.ifacekeep { font-size: 12px; padding: 3px 10px; }

@media (max-width: 680px) {
  /* The heading wraps rather than squeezing the picker to nothing. */
  .dash-chart .cardhead, .card .cardhead { flex-wrap: wrap; row-gap: 8px; }
  .ifacepick { max-width: 8rem; }
}

/* A tunnel address in the stat strip: monospace, and it wraps rather
 * than pushing the strip wider. An address is read character by
 * character, which is what the mono face is for. */
.addrval code { font-size: 15px; overflow-wrap: anywhere; }

/* A group heading in the More sheet.
 *
 * The same words as the rail's, so the phone and the desktop are
 * visibly one menu. Without them the sheet was a flat list and "DNS &
 * DHCP is under Network on the desktop but nowhere in particular here"
 * was a fair complaint -- the structure existed and the phone hid it.
 */
.sheet-group {
  padding: 14px 18px 6px;
  font-size: 11px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--faint);
}
.sheet-group:first-child { padding-top: 6px; }

/* The addresses on the tunnel, as a wrapping row of chips.
 *
 * A list somebody scans to answer "is this device on it", which is why
 * they are chips rather than lines: the eye compares a row faster than
 * it reads a column, and the list is short by nature.
 */
.addrlist { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.addrlist code.tag { font-family: var(--mono); font-size: 11.5px; }

/* A list of devices to tick.
 *
 * Rows rather than a multi-select: a native multi-select needs ctrl-click
 * to pick a second item, loses the selection on a stray click, and shows
 * one line per device with no room for the address underneath. This is
 * the same information somebody reads on the Clients card.
 */
.picklist {
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  max-height: 18rem;
  overflow-y: auto;
  margin: 4px 0 2px;
}
.pickrow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 12px;
  cursor: pointer;
}
.pickrow + .pickrow { border-top: 0.5px solid var(--rule); }
.pickrow:hover { background: var(--sunk); }
.pickrow .grow { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.pickname { font-size: 13.5px; }
/* The address under the name, smaller: it is how the panel identifies the
 * device and how somebody confirms they ticked the right one, but it is
 * not what they were looking for. */
.picksub {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--faint);
  overflow-wrap: anywhere;
}

/* A card that needs an answer before a timer runs out.
 *
 * The band at the top of every page says the same thing, and that was
 * not enough: somebody who has just pressed Save in a drawer is looking
 * at the page they saved on, not at the top of the window.
 */
.warncard { border: 1px solid var(--privl-solid); }

/* A row that is listed but not this page's to edit.
 *
 * A WireGuard tunnel appears in the interface list because it IS an
 * interface, and clicking it used to open the addressing editor -- which
 * would write an /etc/network/interfaces stanza for something wg-quick
 * already owns. It goes to the VPN page instead, and says so rather than
 * looking identical to the rows that do open an editor.
 */
tr.notmine .name::after {
  content: ' \00b7 on the VPN page';
  color: var(--faint);
  font-size: 11.5px;
  font-family: var(--sans);
}

/* The busiest devices, under the clients ring.
 *
 * A table rather than a list of rows built from divs: it is tabular data
 * with a header, and a screen reader should be told that rather than
 * having to infer it from the layout.
 */
.busylist {
  /* Inset to the card's own gutter.
   *
   * The card pads its DIV children, not a table appended beside them, so
   * this ran edge to edge: the header read DEVICE hard against the left
   * border and TOTAL against the right, outside the padding every other
   * row on the card respects. Stated here rather than by wrapping the
   * table in a div, because the table is the thing that is misaligned
   * and a wrapper would hide that from whoever reads this next.
   */
  width: calc(100% - 36px);
  margin: 14px 18px 4px;
  border-collapse: collapse;
  font-size: 13px;
}
.busylist th {
  text-align: left;
  font-weight: 500;
  color: var(--muted);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: 0 0 6px;
  border-bottom: 1px solid var(--line);
}
.busylist th.num, .busylist td.num { text-align: right; }
.busylist td {
  padding: 7px 0;
  border-bottom: 1px solid var(--line-faint, var(--line));
  vertical-align: baseline;
}
.busylist tr:last-child td { border-bottom: 0; }
/* The figures in a tabular face so columns line up as they change --
 * a rate that jitters between 9Mb/s and 10Mb/s should not shift the
 * column it sits in. */
.busylist td.num { font-variant-numeric: tabular-nums; color: var(--ink); }
.busylist .who { display: block; }
/* The address under the name, quietly: it is what somebody needs when two
 * devices are called the same thing, and nothing more. */
.busylist .sub {
  display: block;
  color: var(--muted);
  font-size: 11px;
  margin-top: 1px;
}
/* What the panel guesses a device is, under its name in the network and
 * DHCP tables (1.91.8): quiet, because it is a guess and the name is what
 * somebody scans for. The hover says where the guess came from. */
td .devkind {
  display: block;
  color: var(--muted);
  font-size: 11.5px;
  margin-top: 1px;
}
.busylist td.empty {
  color: var(--muted);
  padding: 12px 0;
  border-bottom: 0;
}

/* The WAN plot's labels, in HTML over the SVG rather than inside it.
 *
 * The SVG is stretched non-uniformly to fill the card, which is right for
 * the traces and wrong for text -- on a wide screen it drew an 11px label
 * three times its natural width and made a sans face look monospace.
 * Positioned in percentages of the plot's height so they track the two
 * bands, which sit at the top and just past the middle of it.
 */
.traffic-plot { position: relative; }
.traffic-labels { position: absolute; inset: 0; pointer-events: none; }
.traffic-lbl {
  position: absolute;
  left: 4px;
  right: 4px;
  display: flex;
  /* One label since 1.91.13, the shared ceiling, at the right end of the
   * dashed line it names (drawn at 12 of 88 units). */
  justify-content: flex-end;
  top: 0;
  font-size: 11px;
  line-height: 1;
  color: var(--muted);
}
/* Tabular figures on the ceiling only, so it does not shift as the scale
 * changes -- and NOT on the words, where equal-width digits were never the
 * point. */
.traffic-lbl .ceil { font-variant-numeric: tabular-nums; }

/* Home's cards (1.99.0): the Busiest devices table sits a little below its
 * heading, as it did beside the ring. */
.listbody .busylist { margin-top: 10px; }
.listbody { padding: 0 0 8px; }

/* Editing Home: a bar under the status line with Edit, and on each card
 * move up, move down and remove. */
/* Home's Edit, at the end of the status line (1.102.1). */
.hero2 .heroedit { flex: none; padding: 3px 10px; font-size: 12.5px; }
.homecards > * + * { margin-top: 14px; }
.card.homecard { position: relative; }
.cardedit {
  display: flex; gap: 6px; justify-content: flex-end;
  padding: 8px 12px; border-top: 1px solid var(--rule); background: var(--sunk);
}
.homeeditrow { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 16px; }
.homeeditrow .spacer { flex: 1; }
.homeempty .inner { padding: 18px 16px; }

/* A gauge drawn as bars: a label and a figure over each meter. */
.gaugebars { padding: 14px 16px 4px; }
.gaugebar .l { display: flex; font-size: 12.5px; color: var(--muted); }
.gaugebar .l .n { margin-left: auto; color: var(--ink); font-variant-numeric: tabular-nums; }

/* A status card's sentence, coloured by its level. */
.statustext { font-size: 15px; font-weight: 600; }
.statustext.bad { color: var(--danger); }
.statustext.warn { color: var(--caution); }

/* The busiest-devices table speaks the chart's colour language. */
.busylist td.down { color: var(--marine); }
.busylist td.up { color: var(--up-text); }
/* Idle is quiet, whichever direction: the row that is moving data should
 * be found by colour before it is found by reading. */
.busylist td.idle { color: var(--faint); }
/* The total is history, not activity, so it does not compete with the
 * live figures beside it. */
.busylist td.total { color: var(--muted); }
/* Device names in the page's face, not monospace. The broad `td` rule
 * sets mono for tables of values, which is right for the figures here and
 * wrong for "MacBookPro" and "Living-Room", which are names somebody
 * reads rather than values they compare. */
.busylist td:first-child { font-family: var(--sans); }
.busylist .sub { font-family: var(--mono); }

/* --- sections you can tell apart (1.116.0, decision 107) ----------------
 *
 * The Apps, Modules and NAS pages were one grey: every card the same, every
 * row in the same mono, so nothing said where one kind of thing stopped and
 * the next began. Each menu section now has a hue -- Network blue, Storage
 * green, Apps violet, Device grey -- carried in two places only: the icon
 * tile beside a name, and a band down the left of its card. A state is a
 * pill with a dot; green is on, grey is off, amber is busy, red is wrong.
 * Everything else stays as quiet as it was. */
.h-network { --hue: var(--marine); --hue-wash: var(--marine-wash); }
.h-storage { --hue: var(--in);     --hue-wash: var(--in-wash); }
.h-apps    { --hue: var(--up-text); --hue-wash: var(--violet-wash); }
.h-device  { --hue: var(--local);  --hue-wash: var(--local-wash); }

.itile {
  flex: none;
  width: 40px; height: 40px; border-radius: 11px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--hue-wash, var(--sunk)); color: var(--hue, var(--muted));
}
.itile svg { width: 22px; height: 22px; }
.itile.small { width: 30px; height: 30px; border-radius: 8px; }
.itile.small svg { width: 17px; height: 17px; }

/* A card of one section: its band. Inset, so the card's corners hold. */
.panelcard.banded, .rows.banded { box-shadow: inset 4px 0 0 var(--hue, var(--rule)), var(--lift); }

/* A card's head with a tile: the name above what it is. */
.panelcard > .head.ident { gap: 14px; padding-top: 14px; padding-bottom: 14px; align-items: center; }
.ident .what { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ident .what .title { font-size: 16px; font-weight: 600; color: var(--ink); }
.ident .what .sub { font-size: 13px; font-weight: 400; color: var(--muted); }

.pill {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  padding: 3px 11px 3px 9px; border-radius: var(--r-chip);
  font-size: 12.5px; font-weight: 500; white-space: nowrap;
  background: var(--sunk); color: var(--muted);
}
.pill::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill.on   { background: var(--in-wash);      color: var(--in); }
.pill.busy { background: var(--caution-wash); color: var(--caution); }
.pill.bad  { background: var(--danger-wash);  color: var(--danger); }
.pill.off  { background: var(--sunk);         color: var(--faint); }
.pill.plain::before { display: none; }

/* A group's heading: what it is, and how many. */
.grouphead {
  display: flex; align-items: center; gap: 10px;
  margin: 30px 0 12px 2px;
}
.grouphead .title { font-size: 17px; font-weight: 600; color: var(--ink); margin: 0; }
.grouphead .count {
  min-width: 24px; padding: 1px 8px; border-radius: var(--r-chip);
  background: var(--hue-wash, var(--sunk)); color: var(--hue, var(--muted));
  font-size: 12.5px; font-weight: 600; text-align: center;
}
.grouphead .note { font-size: 13px; color: var(--faint); }
.grouphead .spacer { flex: 1; }

/* A box of like things inside a card: an app's folders, its old copies. */
.box { background: var(--sunk); border-radius: var(--radius); padding: 4px 14px; margin-top: 12px; }
.box > .boxhead {
  display: flex; align-items: baseline; gap: 8px;
  padding: 10px 0 6px; font-size: 13px; font-weight: 600; color: var(--ink);
}
.box > .boxhead .spacer { flex: 1; }
.box > .boxhead .dim { font-weight: 400; color: var(--muted); }

/* One thing in a box: an icon, its name in words, where it is in mono. */
.frow {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 0; border-top: 1px solid var(--rule);
}
.frow:first-child, .boxhead + .frow { border-top: none; }
.frow > .ic { flex: none; color: var(--muted); display: inline-flex; }
.frow > .ic svg { width: 18px; height: 18px; }
.frow > .what { flex: 1 1 auto; min-width: 0; }
.frow > .what .name { font-size: 14px; color: var(--ink); }
.frow > .what .path { font-family: var(--mono); font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.frow > .what .who { font-size: 13px; color: var(--muted); }
.frow > .what .bad { margin-top: 4px; }
.frow > .chip {
  flex: none; padding: 2px 9px; border-radius: var(--r-chip);
  background: var(--surface); color: var(--muted); font-size: 12px;
}
.frow > .chip.disk { background: var(--in-wash); color: var(--in); }
.frow > .chev { flex: none; color: var(--faint); font-size: 18px; }
.frow > select { flex: none; width: 11rem; }
.frow.fwrow { cursor: pointer; margin: 0 -8px; padding-left: 8px; padding-right: 8px; border-radius: 8px; }
.frow.fwrow:hover { background: var(--surface); }
.frow.fwrow:focus-visible { outline: 2px solid var(--marine); outline-offset: -2px; }

/* A module's row on the Modules page: a .row, with its tile. */
.row.modrow { align-items: flex-start; gap: 12px 14px; padding: 15px 0; }
.row.modrow > .grow .label { font-size: 15.5px; font-weight: 600; }
.row.modrow > .grow .value { margin-top: 2px; }
.row.modrow > .side { display: flex; align-items: center; gap: 10px; margin-left: auto; align-self: center; }
.row.modrow > .detail { padding-left: 54px; }
.row.modrow > .detail:empty { display: none; }
@media (max-width: 640px) {
  .row.modrow > .detail { padding-left: 0; }
}

/* --- 1.117.0: actions where they belong, and things said once -----------
 *
 * The operator's review of 1.116.3's pages. A card's buttons sit beside
 * what they act on, not in a ruled footer of their own: .fwactions is a
 * drawer's row of form buttons, and on a card it spent seventy pixels on a
 * lone Eject. */
.cardacts { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 14px; }
.cardacts:empty { display: none; }
.panelcard > .head .btn { flex: none; padding: 5px 12px; min-height: 30px; font-size: 13px; }

/* A sentence with its one button at its end: "The panel is connected" and
 * Disconnect. */
.lineact { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.lineact > .text { flex: 1 1 18rem; min-width: 0; margin: 0; }

/* A path inside words: mono for the path, the words in the page's face. */
.inpath { font-family: var(--mono); font-size: .92em; overflow-wrap: anywhere; }

/* How full a disk is: the bar, then the figures. */
.diskuse { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px; align-items: center; margin: 2px 0 12px; }
.diskuse .meter { grid-column: 1 / -1; height: 8px; border-radius: 4px; }
/* In the card's own hue until it is getting full. */
.diskuse .meterfill:not(.warm):not(.hot) { background: var(--hue, var(--marine)); }
.diskuse .used { font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; }
.diskuse .free { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* What has to happen first, as steps somebody can take. */
.before { margin: 4px 0 0; font-size: 13px; color: var(--muted); }
.before > p { margin: 0 0 2px; }
.before ul { margin: 0; padding-left: 18px; }
.before li { margin: 3px 0; }
.before a { color: var(--marine); }

/* An address to type somewhere else, with a button that copies it. */
.frow > .what .addr { font-family: var(--mono); font-size: 13.5px; color: var(--ink); overflow-wrap: anywhere; }
.frow > .btn { flex: none; padding: 4px 11px; min-height: 28px; font-size: 12.5px; }
.box > .boxfoot { padding: 4px 0 10px; font-size: 13px; color: var(--muted); }

/* A list whose items share one condition, said once above them. */
.appgets .sub { margin: 3px 0 2px; padding-left: 16px; list-style: none; }
.appgets .sub li { margin: 1px 0; }
.appgets .sub .port { font-variant-numeric: tabular-nums; color: var(--ink); }
/* Where a thing is, in words with its paths in mono (1.117.0). */
.frow > .what .where { font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; }
/* A link drawn as a button is a button: no underline, centred like one
 * (1.117.0: "Open Jellyfin" was underlined beside Stop and Remove). */
a.btn { display: inline-flex; align-items: center; text-decoration: none; }
a.btn:hover { text-decoration: none; }
/* Remove..., Enable and Disable side by side, one size (1.117.0). */
.row.modrow > .side .btn { padding: 6px 13px; min-height: 32px; font-size: 13px; }
/* The top bar's buttons on one line (1.117.1). Sign out wrapped to two
 * lines on a phone and hung below the bar, once 1.117.0 gave quiet
 * buttons an outline: the bar is 46px and the button did not fit beside
 * the switcher. It keeps its width now, and the switcher is what gives. */
.top .btn { flex: none; white-space: nowrap; padding: 4px 12px; min-height: 30px; font-size: 13px; }
.top .switcher { flex: 0 1 auto; min-width: 0; text-overflow: ellipsis; }
.top .ver { flex: none; }
@media (max-width: 640px) {
  .top .switcher { margin-left: 4px; }
}
/* The NAS page's mirror (1.119.0): a disk missing is the one thing on the
 * card said in red; disk errors and a check that found damage in amber. */
.callout.bad { border-left-color: var(--danger); background: var(--danger-wash); }
.panelcard .inner p.warn-text { color: var(--caution); }
.panelcard .inner > .alarm { margin-bottom: 14px; }

/* A line that opens what it is about (1.120.0): the Activity card's and the
 * Recent tab's. A row, not a blue underlined link; it lights on hover. */
a.change.rowlink { color: inherit; text-decoration: none; cursor: pointer; }
a.change.rowlink:hover, a.change.rowlink:focus-visible { background: var(--sunk); }
a.change.rowlink .what > div:first-child::after { content: ' \203A'; color: var(--faint); }
.listbody .showall { padding: 8px 16px 0; font-size: 13px; }
.listbody .showall a { color: var(--marine); text-decoration: none; }
/* The line of short figures and Show all as a button (1.145.2, the Health
 * card compact when all is well). */
.listbody .listfigs { padding: 12px 16px 2px; font-size: 14px; font-variant-numeric: tabular-nums; color: var(--ink); }
.listbody .showall .linkbtn { background: none; border: 0; padding: 0; font: inherit; color: var(--marine); cursor: pointer; }
.listbody .showall .linkbtn:hover, .listbody .showall .linkbtn:focus-visible { text-decoration: underline; }
.drawerlist { margin: 0 -4px; }
.recentlist { padding: 0; }
.recentlist .change .when { width: auto; min-width: 128px; }
/* The one it was opened at: an Activity line on Home, or an audit entry. */
.change.focus, .row.auditrow.focus { background: var(--marine-wash); }

/* Accounts for shares only, among the panel's (1.120.0): the storage hue's
 * wash and its edge, and a tag saying so -- told apart without reading. */
.blrow.shareonly { background: var(--in-wash); box-shadow: inset 3px 0 0 var(--in); }
.tag.shares { color: var(--in); border-color: var(--in); background: transparent; }
.acctkinds { flex-wrap: wrap; }
/* Which kind of account, in the Add drawer: two choices, each said. */
.kindpick { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 14px; }
.kindpick .kind { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; text-align: left;
  padding: 10px 12px; border: 1px solid var(--rule); border-radius: 10px; background: var(--surface);
  color: var(--ink); font: inherit; cursor: pointer; }
.kindpick .kind.on { border-color: var(--marine); box-shadow: inset 0 0 0 1px var(--marine); background: var(--marine-wash); }
.kindpick .kindname { font-weight: 600; font-size: 13.5px; }
.kindpick .kindwhat { font-size: 12px; color: var(--muted); }
@media (max-width: 520px) { .kindpick { grid-template-columns: 1fr; } }

/* Home's line about a version another panel passed on (1.120.0). */
.homeupdate .callout { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0 0 14px; }
.homeupdate .callout > span { flex: 1; min-width: 200px; }
.homeupdate .callout .btn { text-decoration: none; }

/* What is using the machine, on the Status page (1.121.0). */
.proctable { width: 100%; border-collapse: collapse; font-size: 13px; }
.proctable th { text-align: left; font-weight: 500; color: var(--muted); font-size: 12px; padding: 4px 6px; }
.proctable th.num, .proctable td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.proctable td { padding: 7px 6px; border-top: 0.5px solid var(--rule); vertical-align: top; }
.proctable .sub { display: block; color: var(--faint); font-size: 12px; }
.proctable tr.procgroup { cursor: pointer; }
.proctable tr.procgroup:hover, .proctable tr.procgroup:focus-visible { background: var(--sunk); }
.proctable .procname { font-weight: 500; }
.proctable tr.procrow td:first-child { padding-left: 26px; }
.proctable tr.procrow td { border-top-style: dashed; }
.procnote { font-size: 12px; }

/* The Network page's Devices card (1.124.0, the operator's design A):
 * four tiles for the line, then a row per device. */
.spark.down .spark-line { stroke: var(--down); }
.spark.down .spark-fill { fill: var(--marine-wash); }
.spark.up .spark-line { stroke: var(--up); }
.spark.up .spark-fill { fill: var(--violet-wash); }
.spark.count .spark-line { stroke: var(--local); }
.spark.count .spark-fill { fill: var(--local-wash); }
.devtiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-bottom: 1px solid var(--rule); }
.devtile { display: flex; flex-direction: column; gap: 4px; padding: 16px 20px; min-width: 0; }
.devtile + .devtile { border-left: 1px solid var(--rule); }
.devlabel { font-size: 13px; color: var(--muted); }
.devbig { font-size: 28px; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; }
.devbig.down { color: var(--down); }
.devbig.up { color: var(--up-text); }
.devspark { display: block; min-width: 0; }
.devspark .spark { margin-top: 2px; height: auto; }
.devfoot { font-size: 12px; color: var(--faint); }
.devdots { display: flex; flex-wrap: wrap; gap: 5px; padding: 12px 0 10px; }
.devdot { width: 12px; height: 12px; border-radius: 50%; }
.devdot.on { background: var(--in); }
.devdot.off { background: transparent; box-shadow: inset 0 0 0 1.5px var(--faint); }
.devcols, .devrow { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1.6fr) minmax(0, 1fr) 130px; gap: 20px; align-items: center; padding: 10px 20px; }
.devcols { font-size: 12px; color: var(--faint); text-transform: uppercase; letter-spacing: .04em; border-bottom: 1px solid var(--rule); }
.devcols .right { text-align: right; }
.devrow { width: 100%; text-align: left; font: inherit; color: inherit; background: none; border: 0; border-bottom: 1px solid var(--rule); cursor: pointer; min-height: 44px; }
.devrow:hover, .devrow:focus-visible { background: var(--sunk); }
.devname { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.devname strong { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.devnow { display: flex; flex-direction: column; gap: 5px; }
.devbarrow { display: flex; align-items: center; gap: 10px; }
.devbar { flex: 1; height: 8px; border-radius: 4px; background: var(--sunk); display: flex; overflow: hidden; }
.devbar.short { flex: 0 0 64px; height: 6px; }
.devbar .fill { display: block; height: 100%; border-radius: 4px; }
.devbar .fill.down { background: var(--down); }
.devbar .fill.up { background: var(--up); }
.devbar .fill.count { background: var(--local); }
.devrate { flex: 0 0 104px; text-align: right; font-family: var(--mono); font-size: 13px; }
.devrate.down { color: var(--down); }
.devrate.up { color: var(--up-text); }
.devconns { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
.devconn { min-width: 36px; text-align: right; font-family: var(--mono); font-size: 14px; }
.devhint { margin: 0; padding: 12px 20px; font-size: 13px; }
.devempty { margin: 0; padding: 16px 20px; }
.devstats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 8px 0 18px; }
.devstat { display: flex; flex-direction: column; gap: 2px; }
.devstat .devbig { font-size: 22px; }
.devspark.tall { margin: 4px 0 14px; }
.devspark.tall .spark { height: 72px; }
.devfacts { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 10px 16px; margin: 16px 0 0; font-size: 14px; }
.devfacts dt { color: var(--muted); }
.devfacts dd { margin: 0; overflow-wrap: anywhere; }
@media (max-width: 900px) {
  .devtiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .devtile:nth-child(3) { border-left: 0; }
  .devtile:nth-child(n+3) { border-top: 1px solid var(--rule); }
  .devcols { display: none; }
  .devrow { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px 16px; }
  .devrow .devspark { display: none; }
}
.devname .devkind { display: block; font-size: 13px; color: var(--muted); font-weight: 400; }

/* --- the household page (1.125.0) ---------------------------------------
 * Phone first: one column, large type, and buttons a thumb finds (44px). */
.hh { max-width: 640px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
.hhhead h2 { margin-bottom: 2px; }
.hhstatus { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8px 18px;
  padding: 24px; border-radius: var(--r-hero); background: var(--surface); box-shadow: var(--lift-raised); }
.hhmark { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center;
  font-size: 30px; font-weight: 600; background: var(--in-wash); color: var(--in); }
.hhtitle { margin: 0; font-size: 22px; line-height: 1.25; }
.hhsub { margin: 4px 0 0; color: var(--muted); font-size: 15px; }
.hhstatus.warn .hhmark, .hhstatus.unknown .hhmark, .hhstatus.restarting .hhmark { background: var(--caution-wash); color: var(--caution); }
.hhstatus.down .hhmark, .hhstatus.retry .hhmark { background: var(--danger-wash); color: var(--danger); }
.hhstatus.down, .hhstatus.retry { box-shadow: inset 4px 0 0 var(--danger), var(--lift-raised); }
.hhoffer { grid-column: 1 / -1; }
.hhbtn { min-height: 44px; padding: 10px 18px; font-size: 15px; }
.hhoffer .hhbtn { width: 100%; }
.hhbtn.armed { background: var(--danger); border-color: var(--danger); color: #fff; }
.hhads { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: space-between;
  padding: 12px 16px; border-radius: var(--r-card); background: var(--caution-wash); color: var(--caution); }
.hhads .btn { min-height: 44px; }
.hhhome { padding: 18px 20px; }
.hhcardtitle { margin: 0 0 10px; font-size: 16px; }
.hhpeople { display: flex; flex-direction: column; }
.hhperson { display: flex; align-items: center; gap: 14px; padding: 10px 0; }
.hhperson + .hhperson { border-top: 1px solid var(--rule); }
.hhwho { min-width: 0; }
.hhname { font-weight: 600; font-size: 16px; }
.hhdevs { color: var(--muted); font-size: 14px; overflow-wrap: anywhere; }
.hhnone { margin: 4px 0; }
.hhavatar { flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
  font-weight: 600; font-size: 17px; background: var(--local-wash); color: var(--local); }
.hhavatar.small { width: 32px; height: 32px; font-size: 14px; }
.c-blue   { background: var(--marine-wash); color: var(--marine); }
.c-violet { background: var(--violet-wash); color: var(--up-text); }
.c-green  { background: var(--in-wash); color: var(--in); }
.c-amber  { background: var(--caution-wash); color: var(--caution); }
.c-teal   { background: var(--teal-wash); color: var(--teal); }
.c-grey, .c-house { background: var(--local-wash); color: var(--local); }
.hhactions { display: flex; flex-direction: column; gap: 10px; }
.hhask { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  padding: 12px 16px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--lift); }
.hhq { color: var(--muted); font-size: 15px; }
.hhmsg:empty { display: none; }
@media (max-width: 520px) {
  .hhask { flex-direction: column; align-items: stretch; }
  .hhask .hhbtn { width: 100%; }
  .hhstatus { padding: 20px; }
}

/* The household role: this one page, no menu (1.125.0). */
body.household-only .rail, body.household-only .tabs, body.household-only .switcher,
body.household-only .top .ver { display: none; }
body.household-only .main { padding-bottom: 24px; }

/* People, on the Accounts page; a colour chosen by its chip. */
.peoplesec { margin-top: 28px; }
.people-head h3 { margin: 0; }
.personrow { display: flex; align-items: center; gap: 12px; }
.colourchips { display: flex; gap: 10px; flex-wrap: wrap; }
.colourchip { width: 36px; height: 36px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; }
.colourchip.c-blue   { background: var(--marine); }
.colourchip.c-violet { background: var(--up-text); }
.colourchip.c-green  { background: var(--in); }
.colourchip.c-amber  { background: var(--caution); }
.colourchip.c-teal   { background: var(--teal); }
.colourchip.c-grey   { background: var(--local); }
.colourchip.on { border-color: var(--ink); box-shadow: 0 0 0 2px var(--surface) inset; }

/* "Belongs to", at the top of a device's drawer. */
.devowner { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0 0 18px;
  padding-bottom: 14px; border-bottom: 1px solid var(--rule); }
.devownersel { min-height: 40px; min-width: 180px; }
.devownersaid { font-size: 13px; }

/* Debian's updates on the Maintenance page (1.129.0): the restart they ask
 * for as a line with its button, the switch beside its words, a failed
 * run in the error colour. */
.osupdates .oscall { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 4px 0 10px; }
.osupdates .oscall > span { flex: 1; min-width: 200px; }
.osupdates .setval.check { justify-content: flex-end; text-align: right; }
.osupdates .setval.bad { color: var(--danger); }

/* A device's internet paused (1.133.0): the Network page's drawer and the
 * household page's own devices -- a state, then the choices or Resume. */
.devpause { margin: 0 0 18px; }
.devnameline { display: flex; align-items: center; gap: 8px; min-width: 0; }
.devnameline > strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pbadge.devpaused { font-size: 11px; padding: 2px 8px; flex: none; }
.devpauserow { display: flex; align-items: center; gap: 12px; background: var(--sunk); border-radius: 12px;
  padding: 10px 14px; min-height: 48px; }
.devpausename { flex: 1; font-size: 15px; }
.devpausestate { flex: none; display: flex; align-items: center; gap: 10px; }
.devpausenote { font-size: 12px; margin: 6px 14px 0; }
/* Pausing the internet (1.135.3; 1.136.0): At home now marks a person
 * whose internet, or a device's, is paused -- an amber ring on their
 * initial and an amber line -- and opens the pause drawer; its rows are a
 * name, then "On" and Pause… or an amber badge and Resume. */
.hhperson.tap { width: 100%; background: none; border: 0; border-radius: 10px; font: inherit; color: inherit;
  text-align: left; cursor: pointer; margin: 0 -8px; padding: 10px 8px; width: calc(100% + 16px); }
.hhperson.tap:hover { background: var(--sunk); }
.hhperson.tap:focus-visible { outline: 2px solid var(--marine); outline-offset: -2px; }
.hhperson + .hhperson.tap, .hhperson.tap + .hhperson { border-top: 1px solid var(--rule); }
.hhavatar.paused { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--caution); }
.hhpaused { font-size: 13px; font-weight: 600; color: var(--caution); }
.hhchev { margin-left: auto; color: var(--faint); font-size: 22px; line-height: 1; }
.hhpdrawer .hhprow { padding-left: 0; padding-right: 0; }
.hhpnote { font-size: 12px; margin-top: 14px; }
.hhprow { display: flex; align-items: center; gap: 12px; padding: 11px 16px; min-height: 48px; }
.hhprow + .hhprow { border-top: 0.5px solid var(--rule); }
.hhprow-name { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.hhprow-label { font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hhprow.person .hhprow-label { font-weight: 600; }
.hhprow.person .hhprow-label { white-space: normal; }
.hhprow-sub { font-size: 12px; color: var(--muted); }
.hhprow-state { flex: none; display: flex; align-items: center; gap: 10px; }
.pon { font-size: 14px; color: var(--muted); }
.ppauselink { background: none; border: 0; padding: 6px 2px; font: inherit; font-size: 15px; font-weight: 600;
  color: var(--marine); cursor: pointer; }
.ppauselink:hover { text-decoration: underline; }
.pbadge { font-size: 13px; font-weight: 600; color: var(--caution); background: var(--caution-wash);
  padding: 4px 10px; border-radius: 999px; white-space: nowrap; }
/* A device's priority on the line (1.149.0): High and Boosted in the
 * panel's blue, Low muted -- beside the name in the list, and in the
 * drawer. */
.qbadge { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; white-space: nowrap; flex: none; }
.qbadge.high { color: var(--marine); background: var(--marine-wash); }
.qbadge.low { color: var(--muted); background: var(--sunk); }
.devpausestate .qbadge { font-size: 13px; padding: 4px 10px; }
.devpausestate .qsel { width: auto; min-height: 36px; }
/* Priority's choice and its boost wrap under each other on a phone. */
.devpausestate:has(.qsel) { flex-wrap: wrap; justify-content: flex-end; flex: 1 1 auto; }
.presume { border: 0; border-radius: 999px; padding: 8px 16px; font: inherit; font-size: 15px; font-weight: 600;
  color: var(--marine); background: var(--marine-wash); cursor: pointer; }
.presume:hover { filter: brightness(.97); }
.ppauselink:focus-visible, .presume:focus-visible { outline: 2px solid var(--marine); outline-offset: 2px; }
.presume[disabled] { opacity: .5; cursor: progress; }
@media (max-width: 460px) {
  .hhprow-state { gap: 8px; }
  /* A person's row with a badge and a button: the state under the name. */
  .hhprow.person:has(.presume) { flex-wrap: wrap; row-gap: 8px; }
  .hhprow.person:has(.presume) .hhprow-name { flex-basis: 100%; }
}
/* Pause schedules (1.140.0): a device's schedule said under its name and
 * opened from there; a card with the state, the times and the change
 * button; the editor's two sets of times. */
.hhschedlink { align-self: flex-start; background: none; border: 0; padding: 2px 0 0; font: inherit; font-size: 12px;
  color: var(--marine); cursor: pointer; text-align: left; }
.hhschedlink:hover { text-decoration: underline; }
.hhschedlink:focus-visible, .hhschedx:focus-visible { outline: 2px solid var(--marine); outline-offset: 2px; }
.hhsched { margin-top: 16px; padding: 14px 16px; background: var(--sunk); border-radius: 12px; }
.hhschedhead { font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 8px; }
.hhschedhead.big { font-size: 17px; color: var(--ink); margin: 4px 0 12px; }
.hhschedstate { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.hhschednote { font-size: 14px; font-weight: 600; }
.hhschedrow { display: flex; gap: 12px; padding: 6px 0; font-size: 15px; }
.hhschedrow + .hhschedrow { border-top: 0.5px solid var(--rule); }
.hhschedset { flex: none; width: 96px; color: var(--muted); }
.hhschedby { font-size: 12px; margin: 8px 0 0; }
.hhsched .cardacts { margin-top: 12px; }
.hhschededit > p.dim { font-size: 13px; margin: 0 0 14px; }
.hhschedgroup { padding: 12px 14px; background: var(--sunk); border-radius: 12px; margin-bottom: 12px; }
.hhschedgroup > p.dim { margin: 0 0 4px; font-size: 14px; }
.hhschedtime { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.hhschedtime input[type=time] { font: inherit; font-size: 16px; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--rule); background: var(--surface); color: var(--ink); min-width: 0; flex: 1; max-width: 140px;
  color-scheme: light dark; }
.hhschedx { border: 0; background: none; color: var(--muted); font-size: 16px; width: 36px; height: 36px;
  border-radius: 999px; cursor: pointer; flex: none; }
.hhschedx:hover { background: var(--caution-wash); color: var(--caution); }
.hhback { display: block; margin: -4px 0 6px; }
/* The daily internet time (1.141.0): left today under a person in At
 * home now; the amounts picked from a list. */
.hhleft { font-size: 12px; color: var(--muted); margin-top: 1px; }
/* The checks, for an administrator (1.144.0): under the answer, opened
 * from a link; each check its mark, its name and its figures. */
.hhcheckbox { margin: -4px 0 12px; }
.hhchecklink { font-size: 14px; padding: 4px 2px; }
.hhchecks { margin-top: 8px; padding: 16px 20px; }
.hhchecks .hhcardtitle { margin: 14px 0 6px; }
.hhchecks .hhcardtitle:first-child { margin-top: 0; }
.hhcheck { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0; }
.hhcheck + .hhcheck { border-top: 0.5px solid var(--rule); }
.hhcheckmark { flex: none; width: 18px; text-align: center; font-weight: 600; color: var(--in); }
.hhcheck.warn .hhcheckmark { color: var(--caution); }
.hhcheck.bad .hhcheckmark { color: var(--danger, var(--privl)); }
.hhcheckwords { display: flex; flex-direction: column; min-width: 0; }
.hhcheckname { font-size: 14px; font-weight: 500; }
.hhcheckfig { font-size: 13px; color: var(--muted); overflow-wrap: anywhere; }
.hhcheck.warn .hhcheckfig, .hhcheck.bad .hhcheckfig { color: var(--ink); }
.hhamount { font: inherit; font-size: 16px; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--rule);
  background: var(--surface); color: var(--ink); min-width: 160px; color-scheme: light dark; }
.hhsched + .hhsched { margin-top: 12px; }

/* The choice sheet (1.135.3, chooseSheet): the app's action sheet. At the
 * bottom of a narrow screen, in the middle of a wide one. */
.choose-scrim { position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,.45);
  display: flex; align-items: center; justify-content: center; padding: 16px; }
.choose { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 8px; }
.choose-group { background: var(--sunk); border-radius: 14px; overflow: hidden; box-shadow: var(--lift-raised); }
.choose-head { padding: 14px 16px 12px; text-align: center; border-bottom: 0.5px solid var(--rule); }
.choose-title { font-size: 13px; font-weight: 600; color: var(--muted); }
.choose-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
.choose-item { display: block; width: 100%; background: var(--sunk); border: 0; padding: 15px 16px; font: inherit;
  font-size: 18px; color: var(--marine); cursor: pointer; text-align: center; }
.choose-group .choose-item + .choose-item { border-top: 0.5px solid var(--rule); }
.choose-item:hover, .choose-item:focus-visible { background: var(--marine-wash); outline: none; }
.choose-cancel { border-radius: 14px; font-weight: 600; background: var(--surface); box-shadow: var(--lift-raised); }
@media (max-width: 680px) {
  .choose-scrim { align-items: flex-end; padding: 8px 8px calc(8px + env(safe-area-inset-bottom)); }
}

/* The Wi-Fi page (1.145.0, decisions 216-219). The Now card is the words
 * beside the join code, the code dropping below them on a phone; the code
 * keeps its white in the dark theme too, because a camera wants it. */
.wifinowgrid { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-start; }
.wifiwords { flex: 1 1 260px; min-width: 0; }
.wifijoin { flex: 0 0 auto; margin: 0 auto; text-align: center; }
.wifiform .fwactions { border-top: 0; margin-top: 10px; padding-top: 6px; }
.wifijoin figcaption { font-size: 12.5px; margin-top: 6px; max-width: 184px; }
.qrcode { display: block; margin: 0 auto; border-radius: 6px; box-shadow: 0 0 0 0.5px var(--rule); }
.wifiname { font-weight: 600; }
.wifipass { font-family: var(--mono); font-size: 13px; }
.wifinote { margin: 12px 0 0; font-size: 12.5px; }
.wififield { align-items: center; }
.wififield .setval { display: flex; flex-direction: column; align-items: stretch; gap: 4px; text-align: left; }
.wififield .setlabel { flex-basis: 84px; }
.wififield input[type=text], .wififield input[type=password], .wififield select { width: 100%; max-width: 360px; }
.wifihint { font-size: 12px; }
.wificonfirm { margin: 12px 0; padding: 4px 12px; border-radius: 8px; background: var(--sunk); }
.wifidev { align-items: center; }
.wifisignal { font-size: 12.5px; white-space: nowrap; color: var(--muted); }
.wifisignal.s4, .wifisignal.s3 { color: var(--in); }
.wifisignal.s1 { color: var(--caution); }
