/* The customer surface's only stylesheet, served at /app/static/dashboard.css.
 *
 * The page CSP is `default-src 'none'; style-src 'self'` with no `script-src` at all, so no
 * template may carry a `style=` attribute, a `<style>` block or a single line of JavaScript --
 * every look on an /app page comes from this file, and the theme is chosen by the server and
 * written onto `<html data-theme=...>` before the first byte is sent. No `@import` and no CDN:
 * the CSP allows neither, and a control panel must render identically on a machine that cannot
 * reach the internet -- which is also why the one webfont below is self-hosted rather than
 * linked.
 *
 * The visual idea is a zone file, not a card deck. DNS work is ordered, aligned and literal, so
 * the panel is left-aligned with hairline rules, a fixed navigation rail, monospace reserved for
 * record data, and one bold element: the status chip, a boxed mark plus a word that says the
 * same thing twice -- once as a shape, once as text -- so no state is ever carried by colour.
 *
 * Two rule tokens and not one, because WCAG asks two different things of them. `--rule` draws
 * separators -- table lines, section edges -- which carry no information and are exempt.
 * `--rule-firm` draws the border of a CONTROL: an input, a select, a button, a theme link. That
 * is a user interface component under 1.4.11 and owes 3:1 against what it sits on, which is why
 * it is the darker of the two in every theme and not a shade chosen to look quiet.
 *
 * One webfont, self-hosted and CSP-safe: Manrope is served from this same origin
 * (`/app/static/fonts/manrope.ttf`, allowed by `font-src 'self'`), never fetched from a CDN,
 * so the panel still renders -- in the system stack `--sans` falls back to -- on a machine that
 * cannot reach the internet. `font-display: swap` means the fallback paints first rather than
 * leaving the page blank while the file loads.
 */

@font-face {
  font-family: "Manrope";
  /* The shipped file is the variable instance (`fvar` axis `wght` 200-800), not one static
     weight, so one `@font-face` covers every weight this stylesheet asks for. */
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  src: url("/app/static/fonts/manrope.ttf") format("truetype-variations");
}

/* == tokens ==================================================================================
 *
 * Light is the default and follows `prefers-color-scheme` when the reader has chosen nothing.
 * `dark` and `contrast` are explicit choices carried in the `emailmetry_app_theme` cookie.
 * `contrast` is the Okabe-Ito colour-vision-safe palette: every one of its status fills carries
 * black ink (6.1:1 or better) and a distinct mark, so deuteranopia, protanopia and tritanopia
 * all read the same states. */

:root {
  color-scheme: light;

  --paper: #eef1f5;
  --surface: #ffffff;
  --sunk: #f4f6f9;
  /* `--ink`, `--ink-quiet`, `--accent` and `--accent-wash` are the marketing brand's ink,
     slate, action blue and sky (`marketing/DESIGN.md`), so the app reads as the same product
     as the marketing site. `--accent-text` is new: `#1765ed` is 4.498:1 on `--paper`, just
     under the 4.5:1 AA floor for text, so link-coloured TEXT uses this darker sibling instead
     -- the brand blue itself is kept for fills, buttons and borders, which only owe 3:1 and
     clear it with room (see `.mark`, `a`, `.pill a`, `thead th a:hover` below). */
  --ink: #102d50;
  --ink-quiet: #56667b;
  --rule: #ccd5de;
  --rule-firm: #788593;
  --accent: #1765ed;
  --accent-text: #134e8f;
  --accent-ink: #ffffff;
  --accent-wash: #eaf3ff;

  --ok-fill: #dff0e4;
  --ok-edge: #1c6b3f;
  --ok-ink: #12492b;
  --warn-fill: #fbeed2;
  --warn-edge: #8a5a00;
  --warn-ink: #5e3d00;
  --stop-fill: #fadedd;
  --stop-edge: #a3161a;
  --stop-ink: #7d1114;
  --idle-fill: #e7ebef;
  --idle-edge: #53616f;
  --idle-ink: #35414d;
  --info-fill: #e0ecf8;
  --info-edge: #1765ed;
  --info-ink: #0e3a6b;

  --focus: #b3600b;

  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;
  --s6: 2.25rem;
  --s7: 3.5rem;

  --r1: 2px;
  --r2: 4px;
  --rail-w: 14.5rem;

  --t-micro: 0.75rem;
  --t-small: 0.8125rem;
  /* The body text size the whole surface used before this batch lifted `--t-body` to 1rem for
     prose. Data tables keep it: at 1280px the domain list's six columns were already sized to
     just fit, and a table cell that grew with the page pushed the last column off the viewport
     -- table cells, not just chips, have to stay at the size they were measured against. */
  --t-table: 0.9375rem;
  --t-body: 1rem;
  --t-lead: 1.0625rem;
  --t-h3: 1.1rem;
  --t-h2: 1.3rem;
  --t-h1: 1.75rem;

  --sans: "Manrope", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}

[data-theme="dark"] {
  color-scheme: dark;

  --paper: #0f1821;
  --surface: #17222d;
  --sunk: #1d2a36;
  --ink: #e4ecf3;
  --ink-quiet: #9fb0c0;
  --rule: #2b3947;
  --rule-firm: #6b7d90;
  /* A lighter tint of the marketing action blue (`#1765ed` mixed 35% toward white), not the
     blue itself: dark surfaces need a paler accent to read at all, and this tint already
     clears 4.5:1 as text against both dark surfaces, so `--accent-text` is the same colour --
     the split that light theme needs does not arise here. */
  --accent: #689bf3;
  --accent-text: #689bf3;
  --accent-ink: #0f1821;
  --accent-wash: #1b2c3f;

  --ok-fill: #12301f;
  --ok-edge: #5ec489;
  --ok-ink: #a8e6c1;
  --warn-fill: #33270c;
  --warn-edge: #d8a33c;
  --warn-ink: #f0cf8d;
  --stop-fill: #3a1615;
  --stop-edge: #e8817f;
  --stop-ink: #f5b9b7;
  --idle-fill: #1f2b37;
  --idle-edge: #8698a8;
  --idle-ink: #c3cfda;
  --info-fill: #16293d;
  --info-edge: #689bf3;
  --info-ink: #b6d6f7;

  --focus: #f2b134;
}

/* `auto` is what a reader who has chosen nothing gets: the light tokens above, replaced by the
   dark ones when the operating system asks for dark. Choosing `light` in the footer pins light
   whatever the operating system says, which is the difference between the two. */
@media (prefers-color-scheme: dark) {
  [data-theme="auto"] {
    color-scheme: dark;

    --paper: #0f1821;
    --surface: #17222d;
    --sunk: #1d2a36;
    --ink: #e4ecf3;
    --ink-quiet: #9fb0c0;
    --rule: #2b3947;
    --rule-firm: #6b7d90;
    --accent: #689bf3;
    --accent-text: #689bf3;
    --accent-ink: #0f1821;
    --accent-wash: #1b2c3f;

    --ok-fill: #12301f;
    --ok-edge: #5ec489;
    --ok-ink: #a8e6c1;
    --warn-fill: #33270c;
    --warn-edge: #d8a33c;
    --warn-ink: #f0cf8d;
    --stop-fill: #3a1615;
    --stop-edge: #e8817f;
    --stop-ink: #f5b9b7;
    --idle-fill: #1f2b37;
    --idle-edge: #8698a8;
    --idle-ink: #c3cfda;
    --info-fill: #16293d;
    --info-edge: #689bf3;
    --info-ink: #b6d6f7;

    --focus: #f2b134;
  }
}

/* Okabe-Ito, used as chip fills under black ink rather than as text colour: #009E73 and
   #D55E00 are 3.4:1 and 4.0:1 as text on white and would fail AA, but carry black text at
   6.1:1 and 5.4:1. The surrounding page is pushed to full black on white for the rest. */
[data-theme="contrast"] {
  color-scheme: light;

  --paper: #ffffff;
  --surface: #ffffff;
  --sunk: #f2f2f2;
  --ink: #000000;
  --ink-quiet: #2b2b2b;
  --rule: #6b6b6b;
  --rule-firm: #000000;
  /* Already an Okabe-Ito blue and already the brand family; left as is rather than swapped for
     `#1765ed`, which is not in the eight colour-vision-safe hues this theme is built from. */
  --accent: #0072b2;
  --accent-text: #0072b2;
  --accent-ink: #ffffff;
  /* Lighter than the light theme's wash needs to be: `.pill a` prints `--accent-text` on this
     background, and `--accent-text` is the full-strength Okabe-Ito blue here (there is no
     darker sibling in this theme), which only clears 4.5:1 on a wash this pale. */
  --accent-wash: #e8f3fa;

  --ok-fill: #009e73;
  --ok-edge: #000000;
  --ok-ink: #000000;
  --warn-fill: #e69f00;
  --warn-edge: #000000;
  --warn-ink: #000000;
  --stop-fill: #d55e00;
  --stop-edge: #000000;
  --stop-ink: #000000;
  --idle-fill: #f0e442;
  --idle-edge: #000000;
  --idle-ink: #000000;
  --info-fill: #56b4e9;
  --info-edge: #000000;
  --info-ink: #000000;

  /* Was Okabe-Ito vermillion (#d55e00), chosen because it read differently from the link
     colour -- but that is also `--stop-fill`, so a focused control inside a stop-coloured
     notice (`.notice.error`, `.flash-error`) drew a ring in the SAME colour as what it sat on:
     1.0:1, invisible. Every other Okabe-Ito hue in this palette is already one of the five
     status fills or the accent, and two saturated mid-tones rarely clear 3:1 against each
     other regardless of hue -- only an extreme of the range does, which this theme already
     reaches for its rules and borders. Black reaches 3:1 against all five fills (5.4:1 at the
     worst, `--stop-fill`) and the page background (21:1), and stays visually distinct from a
     `--rule-firm` border by shape alone: the 2px `outline-offset` below draws it clear of the
     element it rings, whatever the colour. */
  --focus: #000000;
}

/* == base ================================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  background: var(--paper);
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.55;
}

h1,
h2,
h3 {
  font-weight: 620;
  letter-spacing: -0.006em;
  line-height: 1.25;
}

h1 {
  font-size: var(--t-h1);
  margin: 0 0 var(--s2);
}

h2 {
  font-size: var(--t-h2);
  margin: 0 0 var(--s3);
}

h3 {
  font-size: var(--t-h3);
  margin: 0 0 var(--s2);
}

p {
  margin: 0 0 var(--s3);
  max-width: 78ch;
}

/* `--accent-text`, not `--accent`: see the token block's note on why link text and brand
   fills are not always the same shade. */
a {
  color: var(--accent-text);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

a:hover {
  text-decoration-thickness: 2px;
}

code,
pre,
kbd {
  font-family: var(--mono);
  font-size: 0.875em;
}

code {
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: var(--r1);
  padding: 0.05em 0.3em;
}

pre {
  margin: 0 0 var(--s3);
  padding: var(--s3);
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  white-space: pre-wrap;
  word-break: break-word;
  overflow-x: auto;
}

pre code {
  background: none;
  border: 0;
  padding: 0;
}

hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: var(--s5) 0;
}

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r1);
}

.skip-link {
  position: absolute;
  left: -10000px;
}

.skip-link:focus {
  left: var(--s4);
  top: var(--s4);
  z-index: 10;
  background: var(--surface);
  color: var(--ink);
  border: 2px solid var(--accent);
  border-radius: var(--r2);
  padding: var(--s3) var(--s4);
}

/* == shell ==================================================================================
 *
 * A fixed navigation rail on the left, a top bar naming the organisation, one main column and a
 * footer that carries the appearance switch. Below 60rem the rail becomes a horizontal strip
 * above the content, in the same document order, so the tab order never changes with width. */

.shell {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  min-height: 100vh;
}

.rail {
  background: var(--surface);
  border-right: 1px solid var(--rule);
  padding: var(--s4) 0 var(--s6);
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}

.mark {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 0 var(--s4);
  color: var(--ink);
  text-decoration: none;
}

.mark picture {
  display: contents;
}

.mark img {
  display: block;
  flex: none;
  height: 1.75rem;
  width: auto;
}

.mark:focus-visible {
  outline-offset: -1px;
}

/* The signed-out wordmark: not in the rail, so not full-width and not left-aligned. It sits
   above the card and the pair centre together in the space above the footer -- `margin-top:
   auto` here and `margin-bottom: auto` on `body.plain main` are the two ends of one flexbox
   auto-margin pair, so the free space in `.frame` splits between them and the footer is never
   pulled off the bottom the way a `justify-content: center` on `.frame` itself would. */
.mark-standalone {
  align-self: center;
  margin: auto 0 var(--s5);
}

.mark-standalone img {
  height: 2rem;
}

.nav {
  display: flex;
  flex-direction: column;
}

.nav a {
  display: block;
  padding: var(--s2) var(--s4);
  border-left: 3px solid transparent;
  color: var(--ink);
  text-decoration: none;
}

.nav a:hover {
  background: var(--sunk);
  text-decoration: underline;
}

.nav a[aria-current="page"] {
  border-left-color: var(--accent);
  background: var(--accent-wash);
  font-weight: 620;
}

/* The signed-in phone navigation opens on demand so the page's status remains visible
   above the fold. The desktop navigation stays in the same document order. */
.mobile-nav {
  display: none;
}

.frame {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3) var(--s5);
  align-items: baseline;
  justify-content: space-between;
  padding: var(--s3) var(--s5);
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}

.bar nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
}

.org {
  font-weight: 620;
}

.who {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s3);
}

.who form {
  margin: 0;
}

/* The reader's own name and role in the bar, and the way to their account page. It was a
   `<span>` until there was an account page to send it to; as a link it keeps the quiet
   weight it had -- a navigation control in a header is not a sentence with a link in it --
   and earns an underline on hover and a real focus ring like every other control here. */
a.actor {
  color: var(--ink-quiet);
  font-size: var(--t-small);
  /* Underlined in its resting state, and deliberately: this is the only way to the account
     page anywhere in the product, and a link that looks exactly like the plain text it
     replaced would be a page nobody finds. */
  text-decoration: underline;
  text-underline-offset: 2px;
}

a.actor:hover {
  color: var(--ink);
}

a.actor[aria-current="page"] {
  color: var(--ink);
  font-weight: 620;
}

main {
  flex: 1;
  min-width: 0;
  padding: var(--s5) var(--s5) var(--s7);
  /* A measure, not a frame. Left unbounded, an eight-column table on a 32-inch monitor puts
     the domain name and its action about a metre apart and every row becomes a saccade; the
     cap is wide enough that the table still breathes and narrow enough that a row can be
     read across. `body.plain main` is more specific and keeps the 36rem sign-in card. */
  inline-size: 100%;
  max-inline-size: 90rem;
  margin-inline: auto;
}

.crumb {
  margin: 0 0 var(--s3);
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  font-size: var(--t-small);
  color: var(--ink-quiet);
}

.crumb li::after {
  content: "/";
  margin-left: var(--s2);
  color: var(--rule-firm);
}

.crumb li:last-child::after {
  content: "";
}

section {
  margin: 0 0 var(--s6);
  min-width: 0;
}

.foot {
  border-top: 1px solid var(--rule);
  background: var(--surface);
  padding: var(--s4) var(--s5);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3) var(--s5);
  font-size: var(--t-small);
  color: var(--ink-quiet);
}

.foot p {
  margin: 0;
}

/* The centred card the signed-out pages use: sign in, invitation, reset, and every refusal
   page reached without a session. Same tokens, same footer, no rail to pages they cannot
   open -- `main` is the card, so no page needs a wrapper of its own. */
body.plain .shell {
  grid-template-columns: minmax(0, 1fr);
}

body.plain main {
  /* `flex: 0 0 auto` and not the shell's `flex: 1`: the card has to hug its content, or a
     short sign-in form is drawn as a half-empty box the height of the window. Top margin is
     `0`, not a fixed gap: `.mark-standalone` already carries the gap above the card, and this
     rule's own `auto` bottom margin is the other half of the pair that centres the two of them
     together vertically (see `.mark-standalone`). */
  flex: 0 0 auto;
  width: 100%;
  max-width: 26rem;
  margin: 0 auto auto;
  padding: var(--s5);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
}

body.plain h1 {
  margin-bottom: var(--s4);
}

/* == appearance switch ======================================================================
 *
 * Three links, not a script: each one re-requests the page it is on with `?theme=`, the server
 * stores the choice in a cookie and writes the class onto `<html>` on the way back, so the
 * chosen theme is already painted when the page first appears. */

.themes {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  margin: 0;
}

.themes a {
  display: inline-block;
  padding: var(--s1) var(--s3);
  border: 1px solid var(--rule-firm);
  border-radius: var(--r2);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
}

.themes a:hover {
  background: var(--sunk);
  text-decoration: underline;
}

.themes a[aria-current="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 620;
}

/* == status chips ===========================================================================
 *
 * The one loud element, and the reason it is safe to be loud: a chip says its state twice. The
 * mark is a distinct shape per state, the word beside it is the state in plain English, and
 * both survive a greyscale print or any of the three colour-vision deficiencies. */

.chip {
  display: inline-flex;
  /* A chip is one word or phrase, never a paragraph. In a narrow table cell at 375px
     "Administrator" wrapped one letter per line; the cell's wrapper scrolls instead. */
  white-space: nowrap;
  align-items: baseline;
  gap: var(--s2);
  padding: 0.05rem var(--s2) 0.1rem var(--s1);
  border: 1px solid var(--idle-edge);
  border-radius: var(--r2);
  background: var(--idle-fill);
  color: var(--idle-ink);
  font-size: var(--t-small);
  font-weight: 560;
  line-height: 1.5;
  white-space: nowrap;
}

.chip-mark {
  display: inline-block;
  min-width: 1.15em;
  padding: 0 0.15em;
  border-right: 1px solid currentColor;
  font-family: var(--mono);
  text-align: center;
}

.chip-ok {
  background: var(--ok-fill);
  border-color: var(--ok-edge);
  color: var(--ok-ink);
}

.chip-warn {
  background: var(--warn-fill);
  border-color: var(--warn-edge);
  color: var(--warn-ink);
}

.chip-stop {
  background: var(--stop-fill);
  border-color: var(--stop-edge);
  color: var(--stop-ink);
}

.chip-info {
  background: var(--info-fill);
  border-color: var(--info-edge);
  color: var(--info-ink);
}

/* == notices ================================================================================ */

.flash,
.notice {
  display: block;
  margin: 0 0 var(--s4);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--info-edge);
  border-left-width: 4px;
  border-radius: var(--r2);
  background: var(--info-fill);
  color: var(--info-ink);
  max-width: 78ch;
}

.flash p,
.notice p {
  margin: 0 0 var(--s2);
}

.flash p:last-child,
.notice p:last-child,
.notice h2 {
  margin-bottom: 0;
}

.notice h2 {
  font-size: var(--t-body);
  margin-top: 0;
}

.flash-ok {
  background: var(--ok-fill);
  border-color: var(--ok-edge);
  color: var(--ok-ink);
}

.flash-error,
.notice.error,
.notice-error {
  background: var(--stop-fill);
  border-color: var(--stop-edge);
  color: var(--stop-ink);
}

.critical {
  color: var(--stop-ink);
  font-weight: 620;
}

.note,
.empty {
  color: var(--ink-quiet);
  max-width: 78ch;
}

.empty {
  border: 1px dashed var(--rule-firm);
  border-radius: var(--r2);
  background: var(--sunk);
  padding: var(--s5);
  text-align: left;
}

/* == tables =================================================================================
 *
 * The core object of the panel. Every `<table>` sits in a `.table-wrap`, but on a desktop that
 * wrapper is an ordinary box and NOT a scroll container -- the page scrolls, and `thead th`
 * sticks against the viewport, which is what a zone dashboard should feel like. A nested
 * scroll box inside a scrolling page is two scrollbars arguing, and it traps a wheel gesture.
 *
 * Below the breakpoint the wrapper does become a scroll container, because a six-column table
 * has nowhere else to go on a 360px screen. Two consequences are handled there rather than
 * here. First, `overflow-x: auto` alone would not be honest: a box with one axis `auto` and
 * the other `visible` computes BOTH to `auto` (CSS Overflow 3, section 3), so the wrapper
 * becomes a vertical scroll container whether or not anyone asked -- and `position: sticky`
 * resolves against its nearest scroll container, which would leave the header stuck to the top
 * of a box that never scrolls vertically. `max-block-size` makes that container real. Second,
 * a scroll container that holds no focusable element is unreachable from a keyboard (WCAG
 * 2.1.1), which is why every wrapper carries `tabindex="0"` and a labelled `role="region"` in
 * the markup: four of these tables have no link or button in any row. */

.table-wrap {
  margin: 0 0 var(--s4);
  border-radius: var(--r2);
}

/* The safety net, on the two tables this batch owns. Everything above is about making the
   domain list FIT -- but a fit computed from today's words is not a guarantee, and what
   happened when it did not fit was the worst of the three possibilities: the last column was
   simply outside the viewport, with nothing on screen saying so. A scroll container says so.
   Both markup sites already carry `tabindex="0"`, `role="region"` and an `aria-label`, which
   is what makes a scroll container reachable from a keyboard (WCAG 2.1.1).
   The cost is stated rather than hidden: a box with `overflow-x: auto` and the other axis
   `visible` computes BOTH to `auto` (CSS Overflow 3, section 3), so this is a scroll
   container on both axes, and `position: sticky` resolves against it. Its block size is
   auto, so it never scrolls vertically and a sticky header would never move -- which is why
   the header here is explicitly not sticky rather than silently inert. The five tables that
   are not `.table-cards` keep theirs. */
.table-cards {
  overflow-x: auto;
}

.table-cards thead th {
  position: static;
}

/* The two widest tables (six and three columns) get back the horizontal padding the others
   can spare: six columns of `var(--s3)` (12px) a side cost 144px of a row before a single
   letter is drawn, which was most of the 1280px regression left once cell text and the DNS
   gutter were already back to their compact size (`--t-table`, `.dns-what`, above). `var(--s2)`
   (8px) is still a clear gap between column boundaries, not a cramped one. The narrower
   breakpoint below overrides this again for its own, already-tuned card padding. */
.table-cards th,
.table-cards td {
  padding-inline: var(--s2);
}

table {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  font-size: var(--t-table);
  font-variant-numeric: tabular-nums;
}

caption {
  caption-side: top;
  text-align: left;
  padding: 0 0 var(--s2);
  color: var(--ink-quiet);
  font-size: var(--t-small);
}

th,
td {
  padding: var(--s2) var(--s3);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--rule);
}

tbody tr:last-child th,
tbody tr:last-child td {
  border-bottom: 0;
}

thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--sunk);
  border-bottom: 1px solid var(--rule-firm);
  font-size: var(--t-small);
  font-weight: 640;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

tbody tr:hover td,
tbody tr:hover th {
  background: var(--sunk);
}

tbody th[scope="row"] {
  font-weight: 620;
  color: var(--ink-quiet);
  font-variant-numeric: tabular-nums;
}

/* == sortable headings ======================================================================
 *
 * The order a table is in is stated twice, and deliberately: `aria-sort` on the `<th>` for
 * anybody listening, and a mark after the link for anybody looking. Never by colour alone --
 * the same rule the chips follow -- because "which column is this sorted by" is exactly the
 * question a red-green reader cannot answer from a tint. Every order this list offers is
 * ascending, so one mark is the whole vocabulary. */

thead th a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted var(--rule-firm);
}

thead th a:hover {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}

thead th[aria-sort="ascending"] {
  background: var(--accent-wash);
}

thead th[aria-sort="ascending"] a::after {
  content: " \2191";
  font-weight: 700;
}

/* == the attention row ======================================================================
 *
 * A row that needs attention says so in words, in its own column, on every screen. The rule
 * down its inline start is a second signal for a reader scanning the shape of the page, not
 * the signal: switch the stylesheet off and the sentence is still there. */

tbody tr.row-attention th[scope="row"] {
  border-inline-start: 3px solid var(--warn-edge);
}

/* A state, not a paragraph. The chip is two words and must never be the thing a column
   decides to wrap: squeezed to eighty pixels it would stack one word per line and take the
   row with it, which is exactly what the sentence in this column used to do. */
td.cell-attention,
th.cell-attention {
  white-space: nowrap;
}

.cell-lookups {
  white-space: nowrap;
}

/* The client name wraps at spaces and never inside a word: a column narrowed to the width of
   "Holdings" is still readable, one narrowed to "Hold-" is not. This is the browser default
   and it is written down because the cell beside it deliberately does the opposite. */
.cell-client {
  overflow-wrap: normal;
  word-break: normal;
}

/* Hosted and customer DNS share one column. Two facts, each keeping its own word, stacked so
   the pair costs one column width instead of two -- which at 1280px was the difference
   between the table fitting and its last column being off the page. */
.cell-dns {
  min-inline-size: 13rem;
}

.dns-line {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}

.dns-line + .dns-line {
  margin-block-start: var(--s1);
}

/* No `min-inline-size`: "Hosted" and "Customer" are different lengths, and reserving a
   column-like width for the longer of the two put roughly 85px of empty gutter between the
   shorter label and its own chip. Each label sits its own tight `gap` from its chip instead;
   the two chips no longer line up left edges with each other, which costs less than the width
   the alignment used to cost the column at 1280px. */
.dns-what {
  flex: 0 0 auto;
  color: var(--ink-quiet);
  font-size: var(--t-small);
}

/* Why a row needs attention, under the name of the domain it is about: secondary weight,
   secondary colour, and a measure.
   The two sizes are a pair and both matter to how the table lays itself out. `max` is what
   the cell asks an auto-layout table for, so an unbounded sentence would ask for its whole
   length and take that share from every other column; `min` on the cell is what stops the
   same sentence being squeezed to the width of its longest word when the table is tight,
   which is the failure this whole column moved to escape. */
.row-reason {
  display: block;
  margin-block-start: var(--s1);
  max-inline-size: 26rem;
  color: var(--ink-quiet);
  font-size: var(--t-small);
  font-weight: 400;
}

/* `data-label` exists only on the two card-mode tables, so this floor reaches the domain
   column of those and no other table on the surface. Not `:has(.row-reason)`: a table column
   is one width for every row in it, so a floor that applied only to the rows that happen to
   carry a sentence today would be the same floor with a harder rule to read.

   14rem, not 15rem: batch 10's larger type left the six-column domain list a few pixels over
   1280px even after cell text and the DNS gutter went back to their compact size and the two
   dense tables' own cell padding tightened. This floor is a MINIMUM against `.row-reason`'s
   longest single word, not a maximum against a domain name -- a longer domain still gets
   whatever width its own text needs from the auto-layout table regardless of this number -- so
   the rem it gives up here still clears every word `ATTENTION_SENTENCES` (viewmodels.py)
   writes today with room to spare. */
tbody th[data-label="Domain"] {
  min-inline-size: 14rem;
}

.showing {
  margin: 0 0 var(--s2);
  color: var(--ink-quiet);
  font-size: var(--t-small);
}

/* == counters ===============================================================================
 *
 * The overview's three numbers. Each is a link to the rows behind it: a counter a reader
 * cannot open is a number they have to take on trust. */

.counters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--s3);
  margin: 0 0 var(--s5);
  padding: 0;
  list-style: none;
}

.counter {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
}

.counter > a {
  display: block;
  padding: var(--s3) var(--s4);
  color: inherit;
  text-decoration: none;
}

.counter > a:hover {
  background: var(--sunk);
}

.counter-number {
  display: block;
  font-size: 1.75rem;
  font-weight: 680;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.counter-label {
  display: block;
  color: var(--ink-quiet);
  font-size: var(--t-small);
}

/* Not colour alone: the border is doubled in width as well as changed in hue, and the number
   it frames is beside the words "need attention". */
.counter-attention {
  border-width: 2px;
  border-color: var(--warn-edge);
}

/* == filter pills ===========================================================================
 *
 * One pill per filter in force, each removing only itself. "Clear filters" removed all three
 * at once and named none of them, so a reader who could not find the domain they expected had
 * no way to tell which filter was hiding it. */

.pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin: 0 0 var(--s3);
  padding: 0;
  list-style: none;
}

.pill {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s2);
  padding: var(--s1) var(--s3);
  background: var(--accent-wash);
  border: 1px solid var(--accent);
  border-radius: 999px;
  font-size: var(--t-small);
}

.pill a {
  color: var(--accent-text);
  font-weight: 640;
}

/* The last column of a data table is where a row's own controls live. */
td.row-actions,
th.row-actions {
  text-align: right;
  white-space: nowrap;
}

td.row-actions form {
  display: inline-block;
  margin: 0 0 0 var(--s1);
}

/* == description lists ====================================================================== */

dl {
  margin: 0 0 var(--s4);
}

.domain-facts {
  display: grid;
  grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
  max-inline-size: 45rem;
  gap: 0;
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  background: var(--surface);
  overflow: hidden;
}

.domain-facts dt {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
  background: var(--sunk);
  color: var(--ink-quiet);
  font-size: var(--t-small);
  font-weight: 620;
}

.domain-facts dd {
  margin: 0;
  min-width: 0;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
}

.domain-facts dt:last-of-type,
.domain-facts dd:last-of-type {
  border-bottom: 0;
}

/* == cards and disclosures ================================================================== */

.domain-card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  margin: 0 0 var(--s4);
  padding: var(--s4) var(--s5);
}

/* The required-record card specifically: `.domain-facts` inside it caps at 45rem, and an
   unconstrained `.domain-card` otherwise stretches to fill `main` -- an empty right half on
   any screen wider than about 62rem. Capped here, on the card the table lives in, rather than
   widening `.domain-facts` everywhere it appears (the licence and facts sections use the same
   class and are meant to run the page's full measure). */
.dns-record {
  max-inline-size: 48rem;
}

/* The record card before Emailmetry's own hosted record is verified: every field is a preview
   of a value that is not live yet, not something to paste anywhere, so it reads visibly
   quieter than a real record -- the muted ink token already used for a fact's own label,
   applied here to the value beside it too. */
.dns-record-preview .domain-facts dd {
  color: var(--ink-quiet);
}

.client-group {
  margin-bottom: var(--s6);
}

.client-group > h2 {
  padding-bottom: var(--s2);
  border-bottom: 2px solid var(--rule-firm);
}

/* == timeline ================================================================================
 *
 * `history.html`'s list of versions: newest first, one `<li>` per version, its heading the date
 * and the version number so the list reads top to bottom without opening anything. Under it,
 * one sentence for what changed and one for who recorded it.
 *
 * The long tail this used to describe -- a full term-level diff and DNS publication evidence
 * behind a `<details>` per row -- is deleted, along with `rollback.html`'s own copy of the
 * list. The only `<details>` left on a row is `.confirm` below, which is not a disclosure of
 * detail but the confirm step of the one control the page offers. */

.timeline {
  list-style: none;
  margin: 0 0 var(--s4);
  padding: 0;
}

.timeline > li {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  margin: 0 0 var(--s4);
  padding: var(--s4) var(--s5);
}

.timeline > li > h3 {
  margin-top: 0;
}

/* `who()` (viewmodels.py) names a real MspUser id under "You"/"A team member" the same way
   `short_id`'s `title=` keeps a truncated id reachable -- this is the hover affordance for it.
   Named `who-actor`, not `who`: the header's own `.who` (the signed-in reader's name and
   sign-out button) is an unrelated, older class and must not pick up this rule too. */
.who-actor {
  border-bottom: 1px dotted var(--rule-firm);
}

main details {
  margin: 0 0 var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  background: var(--surface);
  padding: var(--s2) var(--s4);
}

main summary {
  cursor: pointer;
  font-weight: 620;
  padding: var(--s1) 0;
}

main pre {
  white-space: pre-wrap;
}

.domain-card code,
main pre,
main dd,
main p,
main li {
  overflow-wrap: anywhere;
}

/* The domain page's and onboarding's shared required-record card: "what we see now" against
   "what it must be", side by side above 600px and stacked below it -- see the narrow-screen
   rule further down. A long TXT value already wraps via `.domain-card code` above; this only
   has to stop the two columns themselves from forcing a wider viewport. */
.dns-compare {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s4);
  margin: var(--s4) 0 var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--rule);
}

/* A grid item's default `min-width` is `auto`, which is its content's own intrinsic width --
   an unbroken `v=spf1 ...` value would refuse to shrink below that and force the phone-width
   column to overflow even though the column TRACK above is `minmax(0, 1fr)`. Explicit, because
   `overflow-wrap: anywhere` (below, from `.domain-card code`) only wraps text once the box it
   is in has already agreed to be narrow. */
.dns-compare > div {
  min-width: 0;
}

.dns-compare h3 {
  font-size: var(--t-small);
  font-weight: 620;
  color: var(--ink-quiet);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin: 0 0 var(--s1);
}

.dns-compare p {
  margin: 0;
}

.licence-options {
  list-style: none;
  margin: 0 0 var(--s4);
  padding: 0;
}

.licence-options li {
  margin-bottom: var(--s2);
}

/* == page head and actions ================================================================== */

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s3) var(--s5);
  margin-bottom: var(--s4);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--rule);
}

.page-head h1 {
  margin: 0;
}

.filters,
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s3);
}

.filters {
  margin: 0 0 var(--s5);
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
}

.filters label {
  margin-bottom: var(--s1);
}

.actions form {
  margin: 0;
}

/* == forms ==================================================================================
 *
 * Label above field, error text inline and in the error colour, buttons that look different
 * when they cannot be pressed. No modal anywhere on this surface: a confirmation is its own
 * page, with the change written out before the button that makes it. */

form {
  margin: 0 0 var(--s4);
}

form p {
  margin: 0 0 var(--s3);
}

fieldset {
  margin: 0 0 var(--s4);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
}

legend {
  padding: 0 var(--s2);
  font-weight: 620;
}

label {
  display: block;
  margin-bottom: var(--s1);
  color: var(--ink-quiet);
  font-size: var(--t-small);
  font-weight: 560;
}

fieldset label,
label:has(input[type="checkbox"]) {
  display: inline-block;
  color: var(--ink);
  font-size: var(--t-body);
  font-weight: 400;
}

input,
select,
textarea {
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule-firm);
  border-radius: var(--r2);
  padding: var(--s2) var(--s3);
  max-width: 100%;
}

input[type="checkbox"] {
  min-width: 0;
  width: 1.05em;
  height: 1.05em;
  padding: 0;
  vertical-align: baseline;
  accent-color: var(--accent);
}

/* Wide enough for an email address or an SPF term before it wraps -- but only where there is
   room. This is inside a `min-width` query and not paired with an override further down
   because an override would have lost: `input[type="text"]` carries an attribute selector and
   beats the `main input` the narrow-viewport block used to fight it with, so on a 360px screen
   the field stayed 352px wide and pushed the whole page sideways. A floor that is never
   asserted below 601px cannot be beaten by anything, including itself. */
@media (min-width: 601px) {
  input[type="text"],
  input[type="password"],
  textarea {
    min-width: 22rem;
  }
}

textarea {
  font-family: var(--mono);
  resize: vertical;
  width: 100%;
}

input:disabled,
select:disabled,
textarea:disabled,
fieldset:disabled input,
fieldset:disabled select,
fieldset:disabled textarea {
  background: var(--sunk);
  color: var(--ink-quiet);
  cursor: not-allowed;
}

button,
.button {
  display: inline-block;
  font: inherit;
  font-weight: 560;
  padding: var(--s2) var(--s4);
  border: 1px solid var(--rule-firm);
  border-radius: var(--r2);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
}

button:hover,
.button:hover {
  background: var(--sunk);
  border-color: var(--ink-quiet);
}

/* One primary action per page: the page head's own button. */
.button-primary,
.page-head .button,
form.primary button {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.button-primary:hover,
.page-head .button:hover,
form.primary button:hover {
  background: var(--accent);
  border-color: var(--ink);
  color: var(--accent-ink);
}

/* A way back is not the thing to do on the page it is on. These sit in the page head, where
   the primary rule above would otherwise paint them, so they say so explicitly. Placed after
   that rule because it wins on order, not specificity -- both are one class plus one class. */
.button-quiet,
.page-head .button-quiet {
  background: var(--surface);
  border-color: var(--rule-firm);
  color: var(--ink);
  font-weight: 400;
}

.button-quiet:hover,
.page-head .button-quiet:hover {
  background: var(--sunk);
  border-color: var(--ink-quiet);
  color: var(--ink);
}

button:disabled,
fieldset:disabled button {
  background: var(--sunk);
  border-color: var(--rule);
  color: var(--ink-quiet);
  cursor: not-allowed;
}

/* A submitted form is waiting on the server; say so rather than letting the button look idle. */
button:active {
  transform: translateY(1px);
}

/* == money and irreversible changes =========================================================
 *
 * The confirmation pages. `before -> after` is written as a two-column block so the change is
 * legible without reading prose, and the confirm button is the only enabled control. */

.diff {
  display: grid;
  grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
  max-inline-size: 45rem;
  border: 1px solid var(--rule-firm);
  border-radius: var(--r2);
  background: var(--surface);
  overflow: hidden;
  margin: 0 0 var(--s4);
}

.diff dt {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
  background: var(--sunk);
  font-size: var(--t-small);
  font-weight: 620;
  color: var(--ink-quiet);
}

.diff dd {
  margin: 0;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--rule);
  font-variant-numeric: tabular-nums;
}

.diff dt:last-of-type,
.diff dd:last-of-type {
  border-bottom: 0;
}

.confirm-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s4);
  padding: var(--s4);
  border: 2px solid var(--rule-firm);
  border-radius: var(--r2);
  background: var(--surface);
}

.confirm-bar form {
  margin: 0;
}

.confirm-bar button {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* == the restore confirm =====================================================================
 *
 * The one control on `history.html`, as a `<details>` per restorable row: summary "Restore
 * this version", and inside it the consequence, the reason field and a button carrying the
 * same words. The disclosure IS the confirm -- this surface has no `script-src` at all, so a
 * confirm is a disclosure or it is nothing.
 *
 * Which is why the submit is drawn as the primary action and not as the generic bordered
 * button `main details` would otherwise give it: by the time it is on screen the reader has
 * opened the row, read the consequence and typed a reason, and the button is the end of that
 * sequence rather than one more thing to consider. Same three declarations as
 * `.button-primary` above, kept beside the component so the accent cannot drift. */

details.confirm > form button {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

details.confirm > form button:hover {
  background: var(--accent);
  border-color: var(--ink);
  color: var(--accent-ink);
}

/* == the QR code ============================================================================
 *
 * `emailmetry.customer.svg.safe_qr_svg` rewrites the authenticator SVG's inline styles -- which
 * `style-src 'self'` blocks -- into `fill` and `stroke` presentation attributes. Sizing it here
 * rather than on the element is the same rule: the CSP forbids a `style=` attribute. */

.qr {
  margin: 0 0 var(--s4);
}

.qr svg {
  width: 13.7rem;
  height: auto;
  display: block;
  margin-inline: auto;
  background: #ffffff;
  border: 1px solid var(--rule-firm);
  border-radius: var(--r2);
  padding: var(--s2);
}

/* The setup URI is a long, unbroken value. Keep both copyable values inside the sign-in card
   at every viewport width instead of letting a table's intrinsic width push past its border. */
.enrol-credentials {
  min-inline-size: 0;
  max-inline-size: 100%;
  margin: 0 0 var(--s4);
}

.enrol-credentials dt {
  color: var(--ink-quiet);
  font-size: var(--t-small);
  margin: 0 0 var(--s1);
}

.enrol-credentials dd {
  min-inline-size: 0;
  margin: 0 0 var(--s3);
}

.enrol-credentials code {
  display: block;
  max-inline-size: 100%;
  padding: var(--s2) var(--s3);
  overflow-wrap: anywhere;
  white-space: normal;
}

/* == motion and narrow screens ============================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  button:active {
    transform: none;
  }
}

@media (max-width: 60rem) {
  .shell {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Batch 4 fix: below this width `.rail` and `.frame` can no longer share the one row the
     desktop two-column layout gave them (only one column now, so grid auto-placement puts
     each in its own row) -- and with `grid-template-rows` unset, CSS Grid's default
     `align-content: normal` behaves as `stretch` across both of those content-sized `auto`
     rows whenever `.shell`'s own `min-height: 100vh` leaves spare block-size, which it does on
     any page shorter than the viewport. `.rail`'s track stretching, not `.rail` itself, was
     the bug: its own content stayed top-aligned inside a now-tall track, leaving a large blank
     gap below it and again below `.frame`'s content -- worst on a short page at a phone's tall
     narrow viewport. An explicit `1fr` row for `.frame` consumes all of that spare space during
     track sizing, before `align-content` ever gets a share left to redistribute, so `.rail`'s
     `auto` row is sized to its own content only. `:has(.rail)` keeps this off `body.plain`,
     whose shell has no rail at all and already centres its card by stretching to fill the one
     row it has (see `.mark-standalone`) -- giving it a second, explicit row here would leave
     that row empty and undo the centering. Within `.frame`, `main`'s existing `flex: 1` (see
     above) already absorbs whatever the `1fr` row leaves over, which is what pins `.foot` to
     the bottom instead of leaving it stranded mid-page. */
  .shell:has(.rail) {
    grid-template-rows: auto 1fr;
    align-content: start;
  }

  .rail {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    flex-direction: row;
    align-items: center;
    gap: var(--s4);
    padding: var(--s2) var(--s4);
    overflow-x: auto;
  }

  .nav {
    flex-direction: row;
    gap: var(--s1);
  }

  .nav a {
    border-left: 0;
    border-bottom: 3px solid transparent;
    white-space: nowrap;
  }

  .nav a[aria-current="page"] {
    border-left-color: transparent;
    border-bottom-color: var(--accent);
  }
}

@media (max-width: 600px) {
  /* A chip is `white-space: nowrap` because a two-word state must never stack one word per
     line inside a table column. At 390px that rule is the wrong one: the widest labels this
     product writes -- "Pointing at Emailmetry (redirect)", "Cancellation scheduled",
     "Invoiced by Emailmetry" -- are wider than the card they sit in, and a nowrap chip does
     not overflow gracefully, it simply runs off the right edge with nothing to scroll.

     Below this width a chip wraps inside its own box instead. `max-inline-size: 100%` is what
     makes that possible at all: without it the chip's intrinsic width still wins and the wrap
     never happens. The mark stays put (`.chip-mark` is `inline-block` and one character
     wide), so the shape-plus-word doctrine survives the wrap -- a two-line chip still says
     its state twice. The real fix for the worst offender was shorter words, in
     `billing_views.LicenceLine.state_label`; this is the floor under every label after it. */
  .chip {
    white-space: normal;
    max-inline-size: 100%;
    overflow-wrap: anywhere;
  }

  /* Here, and only here, the wrapper becomes the scroll container -- see the tables section
     for why both axes and a cap, and why the markup carries `tabindex="0"`. */
  .table-wrap {
    overflow: auto;
    max-block-size: 70vh;
  }

  /* ...except the two tables marked `.table-cards`, which do not scroll sideways at all:
     below this width each row becomes a card and each cell prints its own heading from
     `data-label`. An eight-column table pushed through a 360px scroll box is a table nobody
     reads -- the reader has to hold a row in their head while dragging it past a heading
     that has gone off the other side.

     `display: block` on a table part does NOT merely restyle it: in every engine that
     follows CSS Display 3, it takes the element out of the table formatting context and with
     it the implicit `table` / `rowgroup` / `row` / `cell` roles the accessibility tree was
     built from. So the two templates spell those roles out with explicit `role=` attributes,
     which survive the display change -- and the `<thead>` below is hidden from sight rather
     than with `display: none`, so that its `columnheader`s stay in the tree for the cells to
     be associated with. Every cell also prints its own label visually, the row header
     included, because a card that names only its values is a list of orphaned strings. */
  .table-cards {
    overflow: visible;
    max-block-size: none;
  }

  /* Cards are one column wide; there is nothing to stack or to reserve room for. `.dns-what`
     needs no override here any more: it carries no `min-inline-size` at any width. */
  .cell-dns {
    min-inline-size: 0;
  }

  .table-cards thead {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* `caption` is in this list for the same reason the rest of it is. Left as `table-caption`
     over a table whose parts are all `block`, it shrink-to-fits against a zero-width anonymous
     table box, and every caption on the surface set one word per line. */
  .table-cards caption,
  .table-cards table,
  .table-cards tbody,
  .table-cards tr,
  .table-cards th,
  .table-cards td {
    display: block;
    inline-size: auto;
  }

  .table-cards table {
    border: 0;
    background: none;
  }

  .table-cards tbody tr {
    margin: 0 0 var(--s3);
    padding: var(--s2) 0;
    background: var(--surface);
    border: 1px solid var(--rule);
    border-radius: var(--r2);
  }

  .table-cards tbody tr.row-attention {
    border-inline-start: 3px solid var(--warn-edge);
  }

  .table-cards tbody tr.row-attention th[scope="row"] {
    border-inline-start: 0;
  }

  .table-cards tbody th,
  .table-cards tbody td {
    border-bottom: 0;
    padding: var(--s1) var(--s3);
    text-align: left;
    white-space: normal;
  }

  .table-cards tbody td[data-label]::before,
  .table-cards tbody th[data-label]::before {
    content: attr(data-label) ": ";
    color: var(--ink-quiet);
    font-size: var(--t-small);
  }

  /* The row header is the domain, and it is the card's title, so its own label is set above
     the name rather than run into it. */
  .table-cards tbody th[data-label]::before {
    display: block;
  }

  .table-cards tbody th[scope="row"] {
    font-size: var(--t-lead);
    color: var(--ink);
  }

  /* The desktop column floor would be wider than a 320px screen once padded. */
  .table-cards tbody th[data-label="Domain"] {
    min-inline-size: 0;
  }

  .row-reason {
    max-inline-size: none;
  }

  .table-cards td.cell-attention {
    max-inline-size: none;
  }

  main,
  .bar,
  .foot {
    padding-left: var(--s4);
    padding-right: var(--s4);
  }

  .rail {
    position: relative;
    justify-content: space-between;
    flex-wrap: nowrap;
    overflow: visible;
    min-block-size: 3.5rem;
  }

  .rail > .nav {
    display: none;
  }

  .mobile-nav {
    display: block;
    margin-inline-start: auto;
  }

  .mobile-nav summary {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-block-size: 44px;
    padding: var(--s2) var(--s3);
    border: 1px solid var(--rule-firm);
    border-radius: var(--r2);
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
    list-style: none;
  }

  .mobile-nav summary::-webkit-details-marker {
    display: none;
  }

  .mobile-nav summary::after {
    content: "▾";
    font-size: var(--t-small);
  }

  .mobile-nav[open] summary::after {
    content: "▴";
  }

  .mobile-nav nav {
    position: absolute;
    z-index: 10;
    top: calc(100% - 1px);
    right: var(--s4);
    display: grid;
    min-inline-size: min(16rem, calc(100vw - 2rem));
    border: 1px solid var(--rule-firm);
    border-radius: var(--r2);
    background: var(--surface);
    box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
  }

  .mobile-nav nav a {
    display: flex;
    align-items: center;
    min-block-size: 44px;
    padding: var(--s2) var(--s4);
    border-left: 3px solid transparent;
    color: var(--ink);
    text-decoration: none;
  }

  .mobile-nav nav a + a {
    border-top: 1px solid var(--rule);
  }

  .mobile-nav nav a[aria-current="page"] {
    border-left-color: var(--accent);
    background: var(--accent-wash);
    font-weight: 620;
  }

  .domain-facts,
  .diff {
    display: block;
  }

  .domain-facts dt,
  .diff dt {
    border-bottom: 0;
  }

  .dns-compare {
    display: block;
  }

  .dns-compare > div + div {
    margin-top: var(--s3);
  }

  .domain-card {
    padding: var(--s3) var(--s4);
  }

  .bar,
  .bar nav,
  .who {
    flex-wrap: wrap;
  }

  .actions {
    display: block;
  }

  .actions form {
    margin: var(--s3) 0;
  }

  main input,
  main select,
  main textarea {
    min-width: 0;
    width: 100%;
    max-width: 100%;
  }

  main input[type="checkbox"] {
    width: 1.05em;
  }

  /* Every signed-out form's one primary submit (login, code, enrol, invite, forgot, reset)
     fills the 26rem card on a phone, where a button sized to its own label sits oddly narrow
     beside inputs that are already full width above. `button` still owes `display:
     inline-block` for the pages that keep it inline next to other controls, so this is scoped
     to the card rather than widened globally. */
  body.plain form.primary button {
    width: 100%;
  }
}

/* A rollback reason must fit its customer form, including the narrow mobile view. */
main textarea.rollback-reason {
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
}

/* Policy batch controls fit the customer editor at narrow and wide viewport sizes. */
.policy-editor fieldset {
  min-inline-size: 0;
  max-width: 100%;
  box-sizing: border-box;
}
.policy-editor textarea,
.policy-editor select {
  min-width: 0;
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}
.policy-editor legend {
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* Short editing and management tasks use a readable measure even when tables need the
   full app width. */
.policy-editor {
  max-inline-size: 48rem;
}

.overview-status {
  margin-block: var(--s4) var(--s6);
}

.overview-status h2 {
  margin-block-end: var(--s2);
}

.overview-status h2 a {
  color: var(--ink);
}

.overview-status > p {
  max-inline-size: 42rem;
  color: var(--ink-quiet);
}

.overview-secondary {
  max-inline-size: 42rem;
  padding-block-start: var(--s4);
  border-block-start: 1px solid var(--rule);
}

.overview-secondary .counters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3) var(--s6);
}

.overview-secondary .counter {
  border: 0;
  background: transparent;
}

.overview-secondary .counter > a {
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.overview-secondary .counter-number,
.overview-secondary .counter-label {
  display: inline;
  font-size: var(--t-small);
  font-weight: 400;
}

.overview-secondary .counter-label {
  margin-inline-start: var(--s1);
}

.compact-form {
  max-inline-size: 44rem;
}

main .page-head details.page-action {
  flex: 0 0 auto;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
}

.page-head details.page-action > summary {
  display: block;
  min-block-size: 44px;
  padding: var(--s2) var(--s4);
  list-style: none;
}

.page-head details.page-action > summary::-webkit-details-marker {
  display: none;
}

.page-head details.page-action[open] {
  flex-basis: 100%;
}

.page-head details.page-action[open] > summary {
  inline-size: max-content;
  margin-inline-start: auto;
}

.page-action-panel,
.page-action > .compact-form {
  max-inline-size: 40rem;
  margin: var(--s3) 0 0 auto;
}

.page-action-panel .compact-form {
  max-inline-size: 100%;
}

.page-action-panel > .note {
  max-inline-size: 40rem;
}

.team-reset[open] {
  max-inline-size: 24rem;
  white-space: normal;
  text-align: left;
}

/* A team row's controls sit behind one "Manage" disclosure. Open, it stacks them -- Resend or
   Enable, then the Reset MFA and Disable confirmations -- one per line under the summary rather
   than running them together along the row. */
.team-manage[open] {
  max-inline-size: 24rem;
  white-space: normal;
  text-align: left;
}

.team-manage[open] > form,
.team-manage[open] > details {
  display: block;
  margin: var(--s2) 0 0;
}

main .account-password {
  max-inline-size: 48rem;
  margin-block: var(--s6);
}

.account-password > summary {
  font-size: var(--t-h2);
}

.domain-routine-action {
  margin-block: var(--s3) var(--s5);
}

.dns-match-details {
  max-inline-size: 48rem;
}

.activity-filters {
  max-inline-size: 54rem;
}

main .activity-date-filter {
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
}

.activity-date-filter > summary {
  min-block-size: 2.5rem;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--rule-firm);
  border-radius: var(--r2);
  background: var(--surface);
}

.activity-date-fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  padding-block-start: var(--s3);
}

.activity-date-fields > div {
  min-inline-size: 10rem;
}

.timeline > .timeline-unchanged {
  padding: var(--s2) var(--s4);
  background: var(--sunk);
}

main .timeline-unchanged > details {
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
}

.timeline-nested {
  margin-block: var(--s3) 0;
}

.timeline-nested > li {
  margin-block-end: var(--s2);
  padding: var(--s3) var(--s4);
}

main .analysis-details {
  max-inline-size: 54rem;
  margin-block: var(--s3);
}

main .analysis-details > details,
main .analysis-details > :not(summary) details {
  margin: var(--s2) 0;
  padding: var(--s1) var(--s3);
  border-color: var(--rule);
  background: var(--sunk);
}

/* Pooled billing keeps the annual purchases in one table. The legacy licence
   view remains available while the pooled mode is disabled. */
.billing-overview,
.billing-licences,
.billing-purchases,
.billing-add,
.billing-payment,
.billing-requests {
  max-inline-size: 70rem;
}

.billing-overview {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s5);
  margin-block-end: var(--s5);
}

.billing-overview h2,
.billing-overview p {
  margin: 0;
}

.billing-counts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3) var(--s6);
  margin: 0;
}

.billing-counts > div {
  min-inline-size: 7rem;
}

.billing-counts dt {
  color: var(--ink-quiet);
  font-size: var(--t-small);
}

.billing-counts dd {
  margin: 0;
  font-size: var(--t-h2);
  font-weight: 680;
  font-variant-numeric: tabular-nums;
}

.billing-licence-wrap,
.billing-purchase {
  max-inline-size: 70rem;
}

.billing-terms {
  max-inline-size: 70rem;
  margin-block: var(--s5);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  background: var(--panel);
}

.billing-terms > summary {
  padding: var(--s2) var(--s3);
  font-weight: 620;
}

.billing-terms > .billing-purchases {
  padding: var(--s3);
  border-top: 1px solid var(--rule);
}

.billing-licence-table {
  inline-size: 100%;
}

.billing-licence-table th,
.billing-licence-table td {
  vertical-align: top;
}

.billing-licence-table td:nth-child(2) {
  overflow-wrap: anywhere;
}

.billing-domain {
  display: block;
  font-weight: 620;
}

.billing-secondary {
  display: block;
  margin-block-start: var(--s1);
  color: var(--ink-quiet);
  font-size: var(--t-small);
}

.billing-row-action {
  margin: 0;
}

main .billing-row-manage {
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
}

.billing-row-manage > summary {
  color: var(--accent-text);
  font-weight: 560;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.billing-row-manage > form {
  min-inline-size: 9rem;
  margin-block-start: var(--s2);
}

.billing-purchases {
  margin-block-start: var(--s6);
}

.billing-purchase {
  margin-block-end: var(--s3);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  background: var(--surface);
}

.billing-purchase-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
}

.billing-purchase-head h3,
.billing-purchase-summary {
  margin: 0;
}

.billing-purchase-summary {
  margin-block-start: var(--s2);
}

main .billing-purchase-actions {
  margin-block: var(--s3) 0;
}

.billing-purchase-actions form + form {
  margin-block-start: var(--s4);
  padding-block-start: var(--s4);
  border-block-start: 1px solid var(--rule);
}

.billing-add,
.billing-payment {
  margin-block-start: var(--s6);
}

.billing-add form {
  max-inline-size: 26rem;
}

.billing-payment-status {
  display: flex;
  gap: var(--s3);
  margin-block: 0 var(--s3);
}

.billing-payment-status dd {
  margin: 0;
  font-weight: 620;
}

.billing-requests {
  margin-block-start: var(--s6);
}

.billing-requests ul {
  max-block-size: 22rem;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
  border: 1px solid var(--rule);
  border-radius: var(--r2);
  background: var(--surface);
}

.billing-requests li {
  padding: var(--s2) var(--s3);
}

.billing-requests li + li {
  border-block-start: 1px solid var(--rule);
}

.billing-requests li p {
  margin-block: var(--s1) 0;
}

.billing-requests li form {
  margin-block: var(--s2) 0;
}

@media (max-width: 600px) {
  .page-head details.page-action[open] > summary,
  .page-action-panel,
  .page-action > .compact-form {
    margin-inline: 0;
  }

  .page-action-panel,
  .page-action > .compact-form {
    inline-size: 100%;
  }

  .activity-filters {
    max-inline-size: none;
  }

  .activity-date-fields,
  .activity-date-fields > div {
    display: block;
    inline-size: 100%;
  }

  .activity-date-fields > div + div {
    margin-block-start: var(--s3);
  }

  .billing-licence-wrap {
    overflow: visible;
    max-block-size: none;
    border: 0;
  }

  .billing-licence-table,
  .billing-licence-table tbody,
  .billing-licence-table tr,
  .billing-licence-table th,
  .billing-licence-table td {
    display: block;
    inline-size: 100%;
    box-sizing: border-box;
  }

  .billing-licence-table thead {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .billing-licence-table tr {
    margin-block-end: var(--s3);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--rule);
    border-radius: var(--r2);
    background: var(--surface);
  }

  .billing-licence-table th,
  .billing-licence-table td {
    border: 0;
    padding: var(--s2) 0;
    overflow-wrap: anywhere;
  }

  .billing-licence-table th {
    border-block-end: 1px solid var(--rule);
    font-weight: 680;
  }

  .billing-licence-table td {
    display: grid;
    grid-template-columns: minmax(5.5rem, 7rem) minmax(0, 1fr);
    gap: var(--s2);
  }

  .billing-licence-table td[colspan] {
    display: block;
  }

  .billing-licence-table td[colspan]::before {
    content: none;
  }

  .billing-licence-table td::before {
    content: attr(data-label);
    color: var(--ink-quiet);
    font-size: var(--t-small);
    font-weight: 620;
  }

  .billing-licence-table .chip {
    max-inline-size: 100%;
    white-space: normal;
  }

  .billing-row-manage > summary {
    min-block-size: 44px;
    display: flex;
    align-items: center;
  }

  .billing-purchase-summary {
    overflow-wrap: anywhere;
  }
}

/* == print ====================================================================================
 *
 * Batch 8: history and activity are the two pages an MSP hands a client as a PDF -- "here is
 * what changed" or "here is everything that happened this quarter" -- and neither prints
 * usefully today: the navigation rail, the theme footer and every button eat the top of each
 * sheet, a `<details>` nobody clicked stays collapsed on paper where there is no clicking, and
 * a dark or high-contrast theme would print exactly the ink it was built for a lit screen with.
 * None of that is specific to two pages in a single shared stylesheet, so the rules below apply
 * to every `/app` page a reader chooses to print; history and activity are the two this batch
 * designed and tested them against.
 *
 * `html[data-theme]` matches the attribute regardless of its value, which is one more selector
 * segment than `[data-theme="dark"]` / `[data-theme="contrast"]` carry -- so it outweighs
 * either and the print override wins no matter which theme the reader was looking at. */

@media print {
  html[data-theme] {
    color-scheme: light;
    --paper: #ffffff;
    --surface: #ffffff;
    --sunk: #ffffff;
    --ink: #000000;
    --ink-quiet: #000000;
    --rule: #000000;
    --rule-firm: #000000;
    --accent: #000000;
    /* Added alongside `--accent`: batch 10's `a { color: var(--accent-text) }` would otherwise
       print a page's link text in the screen theme's blue rather than the rest of this reset's
       ink, since this block only overrides the tokens it lists and `--accent-text` did not
       exist when it was written. */
    --accent-text: #000000;
    --accent-ink: #ffffff;
    --accent-wash: #ffffff;
    --ok-fill: #ffffff;
    --ok-edge: #000000;
    --ok-ink: #000000;
    --warn-fill: #ffffff;
    --warn-edge: #000000;
    --warn-ink: #000000;
    --stop-fill: #ffffff;
    --stop-edge: #000000;
    --stop-ink: #000000;
    --idle-fill: #ffffff;
    --idle-edge: #000000;
    --idle-ink: #000000;
    --info-fill: #ffffff;
    --info-edge: #000000;
    --info-ink: #000000;
  }

  .mark img {
    filter: brightness(0);
  }

  .rail,
  .foot,
  .filters,
  .pills,
  .skip-link,
  .page-head .actions,
  a.button,
  a.button-quiet,
  a.button-primary,
  button,
  form button {
    display: none !important;
  }

  .frame {
    margin: 0;
  }

  main {
    max-inline-size: none;
  }

  /* A reader clicked nothing before printing; every long tail this surface tucked behind a
     `<details>` -- source evidence, DNS publication evidence, the full diff -- has to be on the
     sheet anyway, or "print to PDF" hands the client a page that visibly ends mid-story. */
  main details {
    border: none;
    padding: 0;
  }

  main details > summary {
    display: none;
  }

  main details > :not(summary) {
    display: block !important;
  }

  /* Chromium 131+ keeps a closed `<details>`'s body in a UA `::details-content` box that plain
     `display: block` on its light-DOM children does not reach; Firefox has no such box and the
     rule above is already enough for it. Unverified in headless Chrome -- the render script
     this batch extends screenshots the screen stylesheet, not the print one, so this is
     belt-and-braces for the engines the owner's own printing happens in, not something that
     script can check. */
  main details::details-content {
    content-visibility: visible !important;
    display: block !important;
  }

  .chip {
    background: #fff;
    border: 1px solid #000;
    color: #000;
  }

  .timeline > li,
  .domain-card {
    break-inside: avoid;
  }
}

/* Batch 4: onboarding's compact progress header -- "1 Review", "2 Prove ownership",
   "3 Prepare" as a plain ordered list, each carrying its own done/current/to-do chip so the
   shape and the word (never colour alone) say which one is next. `aria-current="step"` marks
   the same item a screen reader would otherwise only infer from position; the border and
   background below are a sighted supplement to that chip, not a second channel replacing it.
   Existing spacing and accent tokens only -- no new colour is introduced for this. */
.stepper {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: 0 0 var(--s4);
  padding: 0;
  list-style: none;
}

.stepper li {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s2);
  padding: var(--s1) var(--s3);
  border: 1px solid var(--rule);
  border-radius: var(--r2);
}

.stepper li[aria-current="step"] {
  border-color: var(--accent);
  background: var(--accent-wash);
}

/* Batch 4 fix: a done onboarding step's receipt has to read while its `<summary>` is closed,
   which is the entire point of a receipt -- so it lives inside the summary itself, in quiet
   secondary text beside the bold label `main summary` already sets. Reusing `.note`'s ink here
   rather than its block layout, since this stays inline within the summary's own text run. */
summary .receipt {
  font-weight: 400;
  color: var(--ink-quiet);
}

/* Batch 4 fix: Clients' per-row Rename used the generic `main details` card -- a full
   border/background/padding box -- which doubled every row's height for what is, closed, a
   single word. `.inline` strips all of that and styles the summary as a same-weight link
   sitting on the same text line as the row's other actions; only once opened does its form
   drop onto its own line below, and only that row grows. */
details.inline {
  display: inline-block;
  margin: 0;
  border: 0;
  padding: 0;
  background: none;
  vertical-align: baseline;
}

details.inline > summary {
  display: inline;
  padding: 0;
  font-weight: 400;
  color: var(--accent-text);
  text-decoration: underline;
  list-style: none;
}

details.inline > summary::-webkit-details-marker {
  display: none;
}

details.inline > summary::marker {
  content: "";
}

details.inline[open] > summary {
  display: block;
}

details.inline > form {
  display: block;
  margin-top: var(--s2);
  white-space: normal;
  text-align: left;
}

/* The row's other links used to run together with a single collapsed space between them
   ("Domains Activity Add a domain"); `·` is the separator every other multi-fact line on this
   surface already uses (the breadcrumb, the page-head's "Client: X · <chip>"). Scoped to `>`
   children so it never reaches a link inside the opened rename form, and listed per pair
   rather than as `> * + *` so it stays off `team.html`'s own `.row-actions` cell, whose
   adjacent `<form>`s already carry their own spacing (`td.row-actions form`, above) and are
   not this row's kind of link at all. `span` is the suspended-client's own plain-text
   "Add a domain (unavailable)", which is not a link either -- see `clients.html`. */
td.row-actions > a + a::before,
td.row-actions > a + span::before,
td.row-actions > span + a::before,
td.row-actions > a + details.inline > summary::before,
td.row-actions > span + details.inline > summary::before {
  content: "· ";
  color: var(--ink-quiet);
}

/* Self-serve sign-up: the honeypot field. Off-screen rather than display:none, which some bots
   skip; aria-hidden and tabindex=-1 on the element keep it from people. */
.signup-trap {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
