/* App styles. Open Props (open-props.min.css) supplies the design tokens; this file maps them to a
   small set of semantic roles and styles the handful of components the app actually uses. Both light
   and dark are supported via prefers-color-scheme. Keep it lean — no decoration without a job. */

:root {
  --surface: var(--gray-0);
  --surface-raised: #fff;
  --border: var(--gray-3);
  --text: var(--gray-9);
  /* gray-6 measured 3.32:1 on white — under AA for normal text, and this is every hint on the site. */
  --text-muted: var(--gray-7);
  /* "We did not check this" as a STATUS, which is why it is not `--border`.
     It was, and borrowing the hairline colour put it at 1.30:1 on white and 1.34:1 on the dark card --
     a status nobody can see, in both themes. That was survivable while the grey was only ever a gap
     between coloured pips; it stopped being survivable when the legend started keying it, because a key
     whose swatch is invisible explains nothing.
     gray-6 is the same value in both themes and clears the 3:1 that WCAG 1.4.11 asks of a graphical
     object: 3.32:1 on white (measured above, for text, where the bar is higher) and 4.65:1 on the dark
     card. A hairline wants to disappear; a status has to be seen, and one token cannot do both. */
  --status-no-data: var(--gray-6);
  --brand: var(--indigo-7);
  --brand-text: var(--gray-0);

  /* green-8, not green-7. On white, green-7 measured 2.75:1 -- under the 3:1 WCAG 1.4.11 asks of a
     graphical object, which is what a pip and a legend swatch both are. green-8 is 3.45:1 and reads as
     the same green. Dark mode is already clear at green-5 (7.69:1) and unchanged. */
  --status-up: var(--green-8);
  --status-down: var(--red-7);
  --status-blocked: var(--orange-7);
  /* The same two states drawn as a STROKE rather than a fill (shared/_alerts_icon).
     A fill never has to clear a contrast bar itself -- it is judged by the text sitting on it -- but a
     2px glyph is a graphical object, so WCAG 1.4.11 wants 3:1 against the card it sits on. Measured on
     --surface-raised (#fff): green-7 is 2.75:1 and FAILS, orange-7 is 3.04:1 and passes by 0.04.
     green-8 (3.45:1) and orange-8 (3.58:1) both clear it with room, one step darker and visually the
     same colour. Dark theme needs no adjustment -- green-5 is 7.69:1 and orange-5 is 6.90:1 on gray-9
     -- so only the light values move, and the glyph tokens exist to keep that asymmetry in one place
     instead of in whichever selector last needed a foreground. */
  --status-up-glyph: var(--green-8);
  --status-off-glyph: var(--orange-8);
  /* Paused: a check we chose not to run, because the target asked us to slow down. Deliberately far
     from both amber (blocked, which IS a check that ran) and the grey of no_data (nothing ran and we
     cannot say why) -- the two it would most easily be mistaken for, and they mean different things:
     blocked is an observation, no_data is ignorance, paused is a decision.
     Violet-4, NOT the -7 its siblings use, because violet is an intrinsically dark hue: violet-7
     gives the badge only 3.17:1 against --gray-10 ink where 4.5 is required, and violet-5 still only
     manages 4.14. Violet-4 is the single step in the ramp clearing both jobs this token holds --
     badge text 5.24, and 3.36 as a pip fill on the white card, where violet-3 would fail at 2.41. */
  --status-paused: var(--violet-4);
  /* Ink for the incident timeline's alerting band. Tokens for the same reason the glyph tokens above
     are: the light/dark asymmetry belongs in one place rather than in whichever selector last needed a
     foreground, and these two were the counter-example. They were written inline as the DARKEST step
     of each ramp mixed with --text, which is right over a light tint and disappears into a dark one --
     the band's background is a translucent tint of its status colour over the card, so it inverts with
     the theme and the ink has to invert with it. Composited and measured: light was fine at 8.26
     (paused glyph, needs 3:1 as a graphic) and 7.00 (unconfigured text, needs 4.5:1), while dark was
     1.24 and 1.85 -- two failures, one of them body text at well under half the required ratio.
     The dark values are picked off the ramp with margin: violet-2 measures 5.92, red-3 measures 6.62.
     (Both --violet-11 and --red-11 are defined, so the var() fallbacks these replaced were dead.) */
  --notify-paused-ink: color-mix(in oklab, var(--violet-11) 90%, var(--text));
  --notify-unconfigured-ink: color-mix(in oklab, var(--red-11) 90%, var(--text));
  /* Danger as *text* (outlined danger button): a step darker than --status-down so red-on-white clears
     AA contrast, where the status red used for fills would fall just short. */
  --danger-ink: var(--red-8);

  --radius: var(--radius-2);
}

@media (prefers-color-scheme: dark) {
  :root {
    --surface: var(--gray-10);
    --surface-raised: var(--gray-9);
    --border: var(--gray-8);
    --text: var(--gray-1);
    --text-muted: var(--gray-5);
    /* indigo-5 measured 4.20:1 on --surface-raised (gray-9), so every link inside a card failed AA.
       indigo-4 clears both dark surfaces: 5.94 on gray-10, 5.20 on gray-9. */
    --brand: var(--indigo-4);
    --brand-text: var(--gray-10);

    --status-up: var(--green-5);
    --status-down: var(--red-5);
    --status-blocked: var(--orange-5);
    /* The stroke tokens track the fills here: on gray-9 they measure 7.69:1 and 6.90:1, so the
       darkening the light theme needs would only make them muddier. */
    --status-up-glyph: var(--green-5);
    --status-off-glyph: var(--orange-5);
    /* One step lighter again, same reason as the light theme. Violet-5 would be a failing outlier
       here (4.14 on the badge, against 6.35-8.79 for its siblings); violet-3 lands in that band at
       7.31. The white-card constraint doesn't apply in dark, so the ramp has room. */
    --status-paused: var(--violet-3);
    /* Light ends of the ramps, not the dark ends the light theme uses: on the composited dark band
       these measure 5.92 and 6.62, against 1.24 and 1.85 for the values they replace. */
    --notify-paused-ink: var(--violet-2);
    --notify-unconfigured-ink: var(--red-3);
    --danger-ink: var(--red-5); /* light red already high-contrast on the dark surface */
  }
}

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

/* Base type: 17px root for a comfortable modern reading size (open-props' rem scale rides on this). */
html { font-size: 106.25%; }

body {
  margin: 0;
  font-family: var(--font-system-ui);
  font-size: var(--font-size-1);
  color: var(--text);
  background: var(--surface);
  line-height: var(--font-lineheight-3);
  /* Lock horizontal scroll: nothing may push the viewport sideways. Wide content (tables) scrolls
     inside its own .table-scroll container instead. */
  overflow-x: hidden;
}

a { color: var(--brand); }

/* Layout */

.container {
  max-width: 72rem;
  margin-inline: auto;
  padding: var(--size-5) var(--size-4);
}

/* Default vertical rhythm between a page's top-level blocks (cards, sections, framed content). Spacing
   is a container concern so views don't each re-add it: a missing .stack wrapper, or a bare section or
   turbo-frame breaking a .stack chain, was why cards sometimes touched with no gap. This collapses with
   .page-header's own bottom margin, so there's no doubling. */
.container > * + * { margin-block-start: var(--size-4); }

/* Turbo frames are display:inline by default, which drops them out of vertical flow so the margins that
   space siblings never apply around them (a card before a framed card would touch). Make them blocks. */
turbo-frame { display: block; }

/* The bar spans the full width (background + border edge to edge); its inner content is capped at the
   same width as the page container and centred, so the nav items line up with the content below. */
.site-nav {
  padding-block: var(--size-3);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
}
.site-nav__inner {
  display: flex;
  align-items: center;
  gap: var(--size-4);
  flex-wrap: wrap;
  max-width: 72rem;
  margin-inline: auto;
  padding-inline: var(--size-4);
}

.site-nav__brand {
  font-weight: var(--font-weight-7);
  color: var(--text);
  text-decoration: none;
}

/* Desktop: the bar dissolves so brand + menu items sit in one flex row; the toggle is hidden and the
   menu takes the remaining width with the spacer pushing account/sign-out to the right. */
.site-nav__bar { display: contents; }
.site-nav__toggle { display: none; }
/* The toggle shows a hamburger when closed and an X when the menu is open. */
.site-nav__icon--close { display: none; }
.site-nav.is-open .site-nav__icon--menu { display: none; }
.site-nav.is-open .site-nav__icon--close { display: block; }
.site-nav__menu {
  display: flex;
  flex: 1;
  align-items: center;
  gap: var(--size-4);
}

.site-nav__spacer { margin-inline-start: auto; }
.site-nav__account { color: var(--text-muted); }

.site-nav a { text-decoration: none; }
.site-nav a:hover { text-decoration: underline; }
/* The current section: solid text instead of link-blue, semibold, so "you are here" reads at a glance. */
.site-nav a.is-active { color: var(--text); font-weight: var(--font-weight-6); }
/* "New check": the primary create action, so it reads as an accent pill rather than a plain link. Keep
   the pill look even when it's the active section. */
.site-nav a.site-nav__cta,
.site-nav a.site-nav__cta.is-active {
  background: var(--brand);
  color: var(--brand-text);
  padding: var(--size-1) var(--size-3);
  border-radius: var(--radius);
  font-weight: var(--font-weight-6);
}
.site-nav a.site-nav__cta:hover { text-decoration: none; filter: brightness(1.06); }

/* Mobile: brand + hamburger on one row; the menu collapses to a stack, shown only when toggled. */
@media (max-width: 40rem) {
  .site-nav__bar {
    display: flex;
    flex: 1 0 100%;
    align-items: center;
    justify-content: space-between;
  }
  .site-nav__toggle {
    display: inline-flex;
    align-items: center;
    background: none;
    border: none;
    color: var(--text);
    padding: var(--size-1);
    line-height: 0;
    cursor: pointer;
  }
  .site-nav__menu {
    display: none;
    flex: 1 0 100%;
    flex-direction: column;
    align-items: stretch;
  }
  .site-nav.is-open .site-nav__menu { display: flex; }
  .site-nav__spacer { display: none; }
}

/* Fixed customer-app footer. Body reserves matching space so page content never scrolls under it. */
.app-footer {
  position: fixed;
  inset-block-end: 0;
  inset-inline: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--size-3);
  padding: var(--size-2) var(--size-4);
  /* Clear the phone's rounded corners / home indicator so the footer text isn't clipped. */
  padding-block-end: calc(var(--size-2) + env(safe-area-inset-bottom, 0px));
  font-size: var(--font-size-0);
  color: var(--text-muted);
  background: var(--surface-raised);
  border-block-start: 1px solid var(--border);
}
/* Reserve the footer's height plus the safe-area inset so page content never hides behind it. */
body.app { padding-block-end: calc(var(--size-8) + env(safe-area-inset-bottom, 0px)); }

/* Page header with an action on the right */

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--size-3);
  flex-wrap: wrap;
  margin-block-end: var(--size-4);
}

.page-header h1 { margin: 0; }

/* Title row: the icon, name, and status pill all vertically centered (the pill used to sit on the
   big text's baseline and read as misaligned). */
.page-title {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  flex-wrap: wrap;
}
.page-title > :is(h1, span) { min-width: 0; overflow-wrap: anywhere; }
/* The icon and name travel together as one unit, so a long name can't strand the icon on its own line
   (the flex-wrap algorithm was pushing the name — with its large max-content basis — to the next line,
   leaving the icon alone). The name wraps inside the group. */
.page-title__name { display: inline-flex; align-items: center; gap: var(--size-2); min-width: 0; }
.page-title__name > span { min-width: 0; overflow-wrap: anywhere; }
/* The icon tracks the name's FIRST line at every width: top-aligned, then pushed down by half the
   difference between one line box and the icon, which optically centres it on that line whether the
   name wraps or not. `1lh` is the inherited line-height, so this follows the title's type rather than
   hard-coding a number that would drift the moment the heading is resized.
   Plain `align-items: center` is right only while the name is one line; once it wraps it centres the
   icon against the whole block, beside the middle line. Plain `flex-start` is right only while it
   wraps; on one line it pins the icon to the top of a 51px line box whose 34px glyphs sit centred in
   it, which is what put the favicon and the status pill about ten pixels above the name on a phone.
   The size here has to match .check-icon--lg, which is why it is keyed to that class.

   Both declarations sit behind @supports because they are one mechanism: flex-start is only correct
   BECAUSE the margin puts the icon back. Where `lh` is unsupported (iOS Safari before 16.4, Firefox
   before 120) the margin would be dropped and the bare flex-start would top-align the icon by 12.8px
   at EVERY width -- including desktop, which the phone-only rule this replaced never touched. Those
   browsers keep `center` instead: right for a single-line name, which is nearly all of them, and
   exactly what they rendered before. */
@supports (margin-block-start: 1lh) {
  .page-title__name { align-items: flex-start; }
  .page-title__name > .check-icon--lg { margin-block-start: calc((1lh - 1.5rem) / 2); }
}
@media (max-width: 40rem) {
  /* Phone: the icon + name group takes the first line(s) so the status pill drops below it rather than
     squeezing the name. Alignment is deliberately NOT touched here. This block used to flip both rows
     to `flex-start` to get the icon beside the first line of a wrapping name, which is a real goal but
     the wrong instrument: it also top-aligned the icon and the pill on every title whose name did not
     wrap, which is nearly all of them. The first-line behaviour now comes from the margin rule above
     and holds at every width, so there is nothing left for a breakpoint to say about it. */
  .page-title__name { flex: 1 1 auto; }
}
/* align-items: center so an icon action (the pause button, a button_to <form> that would otherwise
   stretch and top-align its <button>) lines up with the self-centering .feed-icon link beside it. */
.page-header__actions { display: flex; gap: var(--size-2); flex-wrap: wrap; align-items: center; }
.button-row { display: flex; flex-wrap: wrap; gap: var(--size-2); }

/* A standout callout (e.g. the iOS install-first steps), tinted with the brand accent so it reads as
   the thing to do next, not body text. */
.callout {
  border: 1px solid color-mix(in oklab, var(--brand) 45%, var(--border));
  border-radius: var(--radius-2, 0.5rem);
  padding: var(--size-3);
  background: color-mix(in oklab, var(--brand) 8%, transparent);
}
.callout__title { font-weight: var(--font-weight-7); margin: 0; }
.callout ol { margin: var(--size-2) 0 0; padding-inline-start: var(--size-5); }
.callout li { margin-block-end: var(--size-1); }

/* A card's title line with an action opposite it. Baseline-aligned so the button sits on the
   heading's line rather than floating above it, and wrapping rather than crushing on a narrow screen. */
.card-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--size-3);
  flex-wrap: wrap;
}

/* A native <dialog>. The browser paints ::backdrop, traps focus and handles Escape, so all that is
   left here is size and spacing -- see modal_controller for why it is `showModal()` and not a div. */
.modal {
  width: min(32rem, calc(100vw - var(--size-5)));
  max-height: calc(100vh - var(--size-7));
  overflow-y: auto;
  padding: var(--size-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--text);
}
.modal::backdrop { background: rgb(0 0 0 / 0.45); }
.modal__head {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--size-3);
  margin-block-end: var(--size-3);
}
.modal__head h2 { margin: 0; }

/* The auto-save confirmation. Holds its line whether or not it has anything to say, so the checklist
   does not jump downward the first time a save lands. */
.checklist__saved {
  min-height: 1.5em;
  margin: 0;
  font-size: var(--font-size-0);
  color: var(--text-muted);
}

/* Cards */

.card {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--size-4);
}

.stack > * + * { margin-block-start: var(--size-3); }

/* Tables */

/* Responsive tables: a wide table scrolls horizontally inside its own container instead of pushing
   the page sideways. Every data table lives in a .card, so `:has` wraps them all with no per-view
   markup; `.table-scroll` is the same behavior for a table that isn't in a card.
   Scrolling is the fallback, not the plan: `data--stack` below reflows a table into labelled rows on a
   phone and is what a data table should normally do. A table that deliberately wants the grid and the
   scroll instead is marked `data--scroll`, which has no declarations of its own (this rule already
   scrolls it) and exists only so the choice is visible -- test/design/responsive_tables_test cannot
   otherwise tell a decision from an omission, and eight tables had quietly omitted it. */
.table-scroll,
.card:has(table.data) { overflow-x: auto; }

table.data {
  width: 100%;
  border-collapse: collapse;
}

/* Two stacked tables that should read as one grid (the probes and the other hosts on the admin
   census). Column widths come from the shared colgroup instead of each table's own content, because
   separate tables otherwise size independently and the columns drift apart by however much the widest
   cell differs. Fixed layout is what makes the colgroup authoritative. */
table.data--aligned { table-layout: fixed; }

table.data th,
table.data td {
  text-align: start;
  padding: var(--size-2) var(--size-3);
  border-bottom: 1px solid var(--border);
}

table.data th {
  font-size: var(--font-size-0);
  text-transform: uppercase;
  letter-spacing: var(--font-letterspacing-1);
  color: var(--text-muted);
}

/* Opt-in responsive reflow: on a phone a `data--stack` table drops its header and each row becomes a
   labelled key/value list (from each cell's data-label), so a wide table fits instead of scrolling.
   Cells need a data-label; the row stays clickable via .stretched-link. */
@media (max-width: 40rem) {
  table.data--stack, table.data--stack tbody, table.data--stack tr, table.data--stack td { display: block; }
  table.data--stack thead { display: none; }
  table.data--stack tr {
    padding: var(--size-2) 0;
    border-bottom: 1px solid var(--border);
  }
  table.data--stack tbody tr:last-child { border-bottom: none; }
  table.data--stack td {
    display: flex;
    justify-content: space-between;
    gap: var(--size-3);
    padding: var(--size-1) 0;
    border: none;
  }
  table.data--stack td::before {
    content: attr(data-label);
    color: var(--text-muted);
    font-weight: var(--font-weight-6);
    flex: none;
  }
  /* Let a long value (an IPv6 in the forensics table) wrap onto its own lines instead of overflowing
     the flex row; without min-width:0 a single unbreakable token can't shrink below its content width. */
  table.data--stack td > .mono {
    min-width: 0;
    overflow-wrap: anywhere;
    text-align: end;
  }
}

/* Status badge */

.status {
  display: inline-flex;
  align-items: center;
  gap: var(--size-1);
  font-size: var(--font-size-0);
  font-weight: var(--font-weight-6);
  padding: var(--size-1) var(--size-2);
  border-radius: var(--radius-round);
  /* Dark ink, not white. Measured against every fill this badge uses, near-white failed all of them
     (green-5 1.90, green-7 2.61, orange-7 2.89, red-7 3.65 — all under 4.5 for text this size), while
     gray-10 clears every one in both themes: light green-7 6.41, orange-7 5.79, violet-4 5.24, red-7
     4.59 (worst); dark green-5 8.79, orange-5 7.89, violet-3 7.31, red-5 6.35.
     Adding a fill means re-measuring against gray-10 — a hue can be too dark for this ink at a step
     that looks perfectly normal next to the others. Violet shipped at -7 and failed at 3.17. */
  color: var(--gray-10);
}

.status--up { background: var(--status-up); }
.status--down { background: var(--status-down); }
.status--blocked { background: var(--status-blocked); }
.status--disabled { background: var(--gray-6); }
/* Nothing has been observed yet. The SAME grey the uptime graph, its legend and the incident pips
   already draw for `no_data` (`--status-no-data`), not a new one: from a customer's side there is no
   difference between the kinds of reason we could not give them a coloured pip, and inventing a second
   grey would ask them to learn a distinction that does not pay for itself. This badge had no rule at
   all while the status was unreachable, so it would have rendered colourless the moment it became
   reachable. */
.status--no_data { background: var(--status-no-data); }
/* Something the customer switched off, or that has not switched on yet: alerts silenced, a
   notification method pending or disabled. Amber, because it is a state worth noticing rather than a
   failure.
   Renamed from `--paused`, which it had to give up: a check can now be paused BY US, because the site
   asked us to slow down, and the two were about to appear on one dashboard tile as differently
   coloured badges both reading "Paused". Different subject, different colour, so: different word. */
.status--off { background: var(--status-blocked); }
/* Whether alerts are going out, drawn as a bell instead of a pill (shared/_alerts_icon). `inline-flex`
   so the bell sits on the badge's optical centre line rather than the text baseline, which a bare
   inline svg would drop it below. The colours are the glyph tokens, not the fill tokens -- see the
   note beside them for why a stroke cannot reuse a fill's colour. */
.alerts-state {
  display: inline-flex;
  align-items: center;
}
.alerts-state--on { color: var(--status-up-glyph); }
.alerts-state--off { color: var(--status-off-glyph); }
/* A check WE stopped probing, because the target rate-limited us. */
.status--paused { background: var(--status-paused); }

/* Change-confirmation banner (ADR 0009). Text stays at full contrast and carries the state in words;
   the border tint is supplementary, so nothing reads by color alone. */
.confirm-banner {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  padding: var(--size-2) var(--size-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-weight: var(--font-weight-6);
}
.confirm-banner--pending { color: var(--text-muted); }
.confirm-banner--good { border-color: var(--status-up); }
.confirm-banner--bad { border-color: var(--status-down); }
.confirm-banner__spinner {
  flex: none;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--radius-round);
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .confirm-banner__spinner { animation: none; } }

/* Global degraded-monitoring uptime graph. (The white-on-danger combo this once claimed was "vetted" was not:
   near-white measured 3.65:1 on the light red fill and 2.63 on the dark one, so it and .btn--danger both
   use dark ink now.) */
/* Full-bleed colour, container-width content. The band should span the viewport -- a warning that
   stops at a column reads as a card rather than a state of the page -- but its text must start where
   the heading and the check rows start, and its link must end where they end. So the background lives
   on the outer element and the inner one carries the container's own measure and padding. */
.monitoring-alert {
  background: var(--status-down);
  /* Dark ink for the same reason as .status: near-white on the red fill measured 3.65:1 (light) and
     2.63:1 (dark), both under 4.5. gray-10 clears it at 4.59 and 6.35. */
  color: var(--gray-10);
  font-weight: var(--font-weight-6);
}
.monitoring-alert__inner {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  max-width: 72rem; /* the same measure as .container, so the two edges line up exactly */
  margin-inline: auto;
  padding: var(--size-2) var(--size-4);
}
.monitoring-alert__icon { flex: none; }
.monitoring-alert__message { flex: 1; }
.monitoring-alert__link { color: var(--gray-10); text-decoration: underline; } /* distinguishable, not by color alone */

/* A page-wide statement that is not a monitoring alarm: today, "we could not read your uptime history".
   Full-bleed and container-measured for the same reason .monitoring-alert is, so the two occupy one
   band and read as one kind of thing.

   Deliberately NOT a status colour. Red is taken and means "we may not be checking your sites", which
   is a claim about alerting; orange is the Blocked check outcome. This is neither, so it uses the body
   pairing the rest of the page already passes contrast at (--text on --surface-raised) and carries the
   eye with an edge rather than a fill. The icon inherits that ink, so it raises no non-text contrast
   question either -- it is decorative and the sentence carries the message. */
.page-alert {
  background: var(--surface-raised);
  color: var(--text);
  border-block-end: 2px solid var(--status-blocked);
}
.page-alert__inner {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  max-width: 72rem; /* the same measure as .container, so the two edges line up exactly */
  margin-inline: auto;
  padding: var(--size-2) var(--size-4);
}
.page-alert__icon { flex: none; }
.page-alert__message { flex: 1; }

/* Forms */

form .field { margin-block-end: var(--size-3); }

/* A field that sits in a row of buttons rather than in a column of fields. Both resets are load-bearing:
   `form .field` above adds a bottom margin that would shove the row's baseline out of line, and the base
   form styles give a select `width: 100%`, which inside a flex row makes it eat the whole line and
   squeeze the buttons off the end. */
.field--inline {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  flex: none;
}
form .field--inline { margin-block-end: 0; }
.field--inline label { margin-block-end: 0; font-weight: var(--font-weight-6); }
.field--inline select { width: auto; }

/* A static label (no associated control) that should still read like a field label. */
.field__label {
  display: block;
  font-weight: var(--font-weight-6);
  margin-block-end: var(--size-1);
}

/* Content-check rows (add/edit/delete): a type select, the text to match, and a remove button per row. */
.assertion-rows { display: flex; flex-direction: column; gap: var(--size-2); margin-block: var(--size-2); }
.assertion-row { display: flex; gap: var(--size-2); align-items: center; }
/* Both controls inherit `width: 100%` from the base form styles, which in a flex row makes the select
   claim the whole line and starve the text input to a sliver. Reset both to auto and let flex divide it:
   the select sized to its label, the value taking the rest. */
.assertion-row select { flex: 0 0 auto; width: auto; }
.assertion-row input[type="text"] { flex: 1 1 auto; width: auto; min-width: 0; }
.assertion-row__remove { flex: none; }
.assertion-rows:empty { margin-block: 0; }
@media (max-width: 40rem) {
  .assertion-row { flex-wrap: wrap; }
  .assertion-row input[type="text"] { flex: 1 1 100%; }
}

/* Notification-method chooser: the available types as selectable cards, front and center. */
.method-options {
  display: grid;
  gap: var(--size-3);
  border: 0;
  padding: 0;
  margin-block-end: var(--size-4);
}
/* Each type is a clickable card (a link): click it to go configure that type. No radios. */
.method-option {
  display: flex;
  align-items: center;
  gap: var(--size-3);
  margin: 0;
  padding: var(--size-4);
  text-decoration: none;
  color: inherit;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.method-option:hover { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); text-decoration: none; }
.method-option:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.method-option__body { display: flex; flex-direction: column; gap: var(--size-1); min-width: 0; }
.method-option__name { font-weight: var(--font-weight-6); }
.method-option__desc { color: var(--text-muted); font-size: 0.9375rem; }
/* Chevron pushed to the trailing edge, so the card reads as "go configure this". */
.method-option__go { margin-inline-start: auto; color: var(--text-muted); font-size: 1.5rem; line-height: 1; }

/* A row in the "email destinations you've added" list: address, a status pill, and a resend action. */
.email-row { display: flex; align-items: center; gap: var(--size-2); flex-wrap: wrap; }

/* A checkbox + its inline label on one line (overrides the block-label default). */
.checkbox {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  font-weight: var(--font-weight-4);
}
.checkbox input[type="checkbox"] { width: auto; }

label {
  display: block;
  font-weight: var(--font-weight-6);
  margin-block-end: var(--size-1);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="url"],
input[type="number"],
textarea,
select {
  width: 100%;
  padding: var(--size-2) var(--size-3);
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* One visible focus indicator for every keyboard-focusable element, not just form fields. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.btn:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Skip link: off-screen until focused, then pinned to the top-left so a keyboard user can jump past
   the nav straight to <main id="main">. WCAG 2.4.1 Bypass Blocks. */
.skip-link {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 100;
  padding: var(--size-2) var(--size-4);
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 0 0 var(--radius) 0;
  transform: translateY(-100%);
  transition: transform 0.15s ease;
}
.skip-link:focus {
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .skip-link { transition: none; }
}

/* Secondary/help text: muted colour carries the "secondary" signal, so keep it readable (not the 12px
   fine-print size it used to be). */
.hint { color: var(--text-muted); font-size: 0.9375rem; }

/* Monospace inline value (a resolved IP, hostname, user-agent token). Breaks rather than overflows. */
.mono { font-family: var(--font-mono); font-size: var(--font-size-0); word-break: break-all; }
/* A one-line field holding a token to copy (an API key). Not .input--code, which is the roomy
   multi-line editor and made this a 24rem box around one line. Qualified with `input` to outrank
   the base `input[type="text"]` rule's `font: inherit`. */
input.input--mono { font-family: var(--font-mono); }
.muted { color: var(--text-muted); }

/* A key to press, drawn as a key so "press c" does not read as the letter c in a sentence. */
kbd {
  font-family: var(--font-mono);
  /* As large as the sentence it sits in: the key is the instruction, not an adornment on it. */
  font-size: 1em;
  padding: 0 0.35em;
  border: 1px solid var(--border);
  border-block-end-width: 2px;
  border-radius: var(--radius-1);
}

/* Buttons */

.btn,
input[type="submit"],
button {
  display: inline-block;
  font: inherit;
  font-weight: var(--font-weight-6);
  padding: var(--size-2) var(--size-4);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--brand);
  color: var(--brand-text);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap; /* a button label wraps to the next row, never mid-word */
}

.btn--secondary {
  background: transparent;
  color: var(--brand);
  border-color: var(--border);
}

/* Lowest-emphasis action: text only, no border. For a tertiary "opt out" next to real buttons. */
.btn--ghost {
  background: transparent;
  color: var(--text-muted);
  border-color: transparent;
}
.btn--ghost:hover { color: var(--text); }

/* Dark ink on the red fill, same measurement as .status and .monitoring-alert: near-white was 3.65:1
   (light) / 2.63 (dark); gray-10 is 4.59 / 6.35. */
.btn--danger { background: var(--status-down); color: var(--gray-10); }

/* Outlined danger: a destructive action that shouldn't shout — red outline and text, no fill. For
   reversible / secondary destructive actions (stop sharing, cancel) where a filled danger button is
   too prominent. */
.btn--danger-outline {
  background: transparent;
  color: var(--danger-ink);
  border-color: var(--status-down);
}

/* The setting currently in force, among a row of buttons that each set one. Distinct from
   .is-success on purpose: that is transient feedback for something that just happened, complete with
   a pop animation, and using it for a persistent state made "the current scope" read as "that worked".
   Emphasis rather than colour, so it does not compete with the status pills for meaning. */
.btn.is-current {
  border-color: var(--brand);
  box-shadow: inset 0 0 0 1px var(--brand);
  font-weight: 600;
}

/* ...except where the setting IS a status. The test target's buttons each put it into one of the
   three states the app already has colours for, so the one in force wears that colour rather than a
   neutral emphasis: green is up, red is down, amber is blocked or degraded. Reserved for controls
   whose options map onto up/down/blocked — everywhere else .is-current alone is right, so the fills
   stay meaningful.

   Dark ink on the fill, like .is-success: near-white over green is 2.61:1. Against --gray-10 these
   are 6.4:1 (green), 5.8:1 (amber) and 4.6:1 (red) in light mode, and higher in dark, where the
   status variables lighten. */
.btn.is-current--up,
.btn.is-current--down,
.btn.is-current--blocked {
  color: var(--gray-10);
  box-shadow: none; /* the fill is the emphasis; the inset brand line would fight it */
}
.btn.is-current--up { background: var(--status-up); border-color: var(--status-up); }
.btn.is-current--down { background: var(--status-down); border-color: var(--status-down); }
.btn.is-current--blocked { background: var(--status-blocked); border-color: var(--status-blocked); }

/* Reusable success feedback ("juice"): a button briefly turns green with a checkmark on success. */
.btn.is-success {
  background: var(--status-up);
  border-color: var(--status-up);
  /* Green is the worst of the fills for near-white — 2.61:1 light, 1.90 dark. */
  color: var(--gray-10);
  animation: btn-success-pop 0.28s ease-out;
}
@keyframes btn-success-pop {
  0% { transform: scale(0.96); }
  60% { transform: scale(1.04); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .btn.is-success { animation: none; }
}

/* Flash + errors */

.flash {
  padding: var(--size-3) var(--size-4);
  border-radius: var(--radius);
  margin-block-end: var(--size-4);
}
/* A link inside a flash (e.g. the freshly-created share link): its own line, and it inherits the
   flash's colour so it stays legible on the tinted background. */
.flash__link { display: block; margin-block-start: var(--size-1); color: inherit; word-break: break-all; }

/* Fading out on dismissal. Only the opacity moves -- collapsing the height would shove the page up
   under the reader's eyes, which is the thing that makes a disappearing banner feel like a glitch. */
.flash--leaving { opacity: 0; transition: opacity 300ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .flash--leaving { transition: none; }
}

.flash--notice { background: var(--green-2); color: var(--green-10); }
.flash--alert { background: var(--red-2); color: var(--red-10); }

@media (prefers-color-scheme: dark) {
  .flash--notice { background: var(--green-11); color: var(--green-2); }
  .flash--alert { background: var(--red-11); color: var(--red-2); }
}

.errors {
  border: 1px solid var(--status-down);
  border-radius: var(--radius);
  padding: var(--size-3);
  margin-block-end: var(--size-4);
}

/* One field's rejection, sat under the input and tied to it by id (see invalid_field_attrs). Uses the
   darker danger ink rather than the fill red, so red-on-surface clears AA as text. Not colour alone —
   the field also carries aria-invalid, and the message says what's wrong. */
.field-error {
  color: var(--danger-ink);
  font-size: var(--font-size-0);
  margin-block-start: var(--size-1);
}
@media (prefers-color-scheme: dark) {
  /* --danger-ink is tuned for dark text on light; on the dark surface the lighter status red reads. */
  .field-error { color: var(--red-4); }
}

.empty {
  color: var(--text-muted);
  text-align: center;
  padding: var(--size-6) var(--size-4);
}

/* Definition list for a check's config */

dl.details {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--size-2) var(--size-5);
  margin: 0;
}

dl.details > div { display: contents; }
dl.details dt { color: var(--text-muted); }
/* `anywhere`, not `break-all`. break-all breaks between any two characters, which is what a long URL
   needs and what a sentence must never get: the Alerting row read "We wait for clce.org t / o be down
   for 2 chec / ks in a row". `anywhere` breaks a word only when it cannot fit on a line by itself, so
   prose wraps at spaces and a URL still breaks rather than overflowing.
   `anywhere` rather than `break-word` because this is a grid item: only `anywhere` lets the intrinsic
   min-content width shrink, so a long unbroken URL cannot force the column wider than the card. */
dl.details dd { margin: 0; overflow-wrap: anywhere; }

/* ── Key/value rows on a phone ────────────────────────────────────────────────────────────────────
   One rule, for every place we show a label beside a value: `dl.details` here, and the `data--stack`
   tables above.

   1. LABEL AND VALUE SHARE A LINE, joined by a colon, and wrap together as ordinary text. A two-column
      grid on a 390px screen gave the label half the width and left prose a ~20-character ribbon; as
      flowing text the value gets the whole card and short values stop costing two lines.
   2. UNLESS THE VALUE SPEAKS FOR ITSELF, in which case the label is visual noise and goes. A URL is
      recognisably a URL; "We wait for clce.org to be down for 2 checks in a row" is already a
      sentence about alerting. Mark those `detail--self-describing`; for a table, simply omit the
      cell's `data-label`.
   3. AND THE CELL THAT NAMES THE ROW NEVER TAKES ONE. Stacked, a row becomes a little card, and that
      cell is its heading, not a field inside it -- "Name: acme.com" labels the title of the card you
      are already looking at. Same for a cell holding only an action ("Edit", "View receipt"): the
      control says what it does, so a label beside it would be labelling a label. Both are just rule 2
      again, but they are the two that come up in every table, so they are worth naming.

   The label is hidden from the eye, never from assistive technology: a screen reader reaches the value
   with no surrounding layout to explain it, so "URL" is doing real work there even when it is
   redundant on screen. Hence the clip rather than `display: none`.

   Judge the ROW, not its length. "Any success (2xx)" is short and still needs its label; a URL is
   sometimes long and never does. Length decides nothing here, which is why nothing measures it. */
@media (max-width: 40rem) {
  dl.details { display: block; }
  dl.details > div { display: block; }
  dl.details > div + div { margin-block-start: var(--size-2); }
  dl.details dt, dl.details dd { display: inline; }
  dl.details dt::after { content: ": "; }

  dl.details .detail--self-describing dt {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  dl.details .detail--self-describing dt::after { content: none; }

  /* The table equivalent: a cell with no `data-label` is one whose value speaks for itself, so it gets
     no generated label rather than an empty one taking up a gap. */
  table.data--stack td:not([data-label])::before { content: none; }
}

/* Dashboard status tiles */

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: var(--size-3);
}

.status-tile {
  /* REQUIRED, not cosmetic. The title is a .stretched-link, whose ::after is `position: absolute;
     inset: 0` -- so it sizes to the nearest POSITIONED ancestor. Without this the tile is not one,
     the overlay resolves against the initial containing block, and every tile lays an invisible
     full-page sheet over the document. The last one in DOM order wins, so a click anywhere on the
     dashboard opened that check, and the sheet also swallowed the dragover events the group drop
     zones needed. One missing line, two unrelated-looking bugs.

     `tr.linked-row` carries the same declaration for the same reason; the class is only half a
     pattern without it. */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--size-2);
  text-decoration: none;
  color: var(--text);
}

.status-tile:hover { border-color: var(--brand); }
.status-tile__head { display: flex; align-items: center; justify-content: space-between; gap: var(--size-3); }
.status-tile__badges { display: inline-flex; align-items: center; gap: var(--size-2); flex: none; }
.status-tile__name { font-weight: var(--font-weight-6); }

.dash-group h2 { font-size: var(--font-size-3); margin-block-end: var(--size-3); }
.dash-group h2 a { color: var(--text); text-decoration: none; }
.dash-group h2 a:hover { text-decoration: underline; }

/* Pricing */

.pricing-toggle {
  display: inline-flex;
  gap: var(--size-1);
  padding: var(--size-1);
  border: 1px solid var(--gray-4);
  border-radius: var(--radius-3);
}

.pricing-toggle__tab {
  border: 0;
  background: transparent;
  padding: var(--size-1) var(--size-3);
  border-radius: var(--radius-2);
  cursor: pointer;
  font: inherit;
  color: var(--text);
}

.pricing-toggle__tab.is-active {
  background: var(--brand);
  color: var(--brand-text);
}

.pricing-toggle__save { font-size: var(--font-size-0); opacity: 0.85; }

.price { font-size: var(--font-size-4); margin: 0; }
.price strong { font-size: var(--font-size-6); }

.plan-current {
  margin: 0;
  font-weight: var(--font-weight-6);
  color: var(--green-7);
}
@media (prefers-color-scheme: dark) {
  .plan-current { color: var(--green-5); }
}

/* Trial promise on pricing/checkout: real information the user must read, so full body colour and
   size, not fine print. The <strong> inside carries the emphasis. */
.trial-note {
  margin: 0;
  color: var(--text);
  line-height: 1.5;
}

/* Checkout: our own page hosting Stripe's Payment Element, which themes to light/dark via the
   Appearance API — so it inherits the app surface instead of clashing. */
.checkout__summary {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-bottom: var(--size-2);
  border-bottom: 1px solid var(--rule, var(--gray-4));
}
.checkout__submit { width: 100%; }
.btn:disabled { opacity: 0.6; cursor: default; }

.checkout__or {
  display: flex;
  align-items: center;
  gap: var(--size-3);
  color: var(--gray-6);
  font-size: var(--font-size-0);
}
.checkout__or::before, .checkout__or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--gray-4);
}

/* Notification override checklist: tick which destinations a check or group notifies, with the
   reason (account default / group / this check) beside each. Uses only the semantic tokens, so it
   follows light/dark and a future palette without edits here. */
.checklist { list-style: none; padding: 0; margin: 0; }
.plain-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--size-1); }

/* Dashboard "needs attention": ongoing incidents surfaced above the check grid. */
.attention-list { display: flex; flex-direction: column; gap: var(--size-2); }
.attention-row {
  display: flex;
  align-items: center;
  gap: var(--size-3);
  text-decoration: none;
  color: inherit;
  border-left-width: 4px;
}
.attention-row--down { border-left-color: var(--status-down); }
.attention-row--blocked { border-left-color: var(--status-blocked); }
.attention-row--paused { border-left-color: var(--status-paused); }
/* Same hover as a dashboard tile: both are cards standing for a check, so they should answer the
   pointer the same way. This used to fill with gray-1, which is a LIGHT token -- fine on the light
   theme, a white slab in dark mode.
   Left border excluded: it carries the status colour, and a plain `border-color` on hover outranks
   `.attention-row--down` and would swap the one bit of the row that is not decoration. */
.attention-row:hover {
  border-block-color: var(--brand);
  border-inline-end-color: var(--brand);
}
/* A fixed column for the badge, so the names start at one x-position. Reserving the icon's space fixed
   half of it; the other half was the badge itself, which is 52px wide for "Down" and 63px for "Paused",
   and that difference walked every name to a different place. Widening the pill rather than padding
   after it keeps the pills a column too, which is the part the eye actually follows. */
.attention-row .status { min-width: 5rem; justify-content: center; }
.attention-row__name { display: inline-flex; align-items: center; gap: var(--size-2); font-weight: var(--font-weight-6); }
.attention-row__meta { margin-inline-start: auto; color: var(--gray-6); font-variant-numeric: tabular-nums; }
.attention-row__go { color: var(--gray-5); font-size: 1.3em; line-height: 1; }

/* Pagy pager (incident history and other lists). */
.pager .pagy { display: flex; justify-content: center; flex-wrap: wrap; gap: var(--size-1); }
.pager a, .pager .current, .pager .gap {
  padding: var(--size-1) var(--size-2);
  border-radius: var(--radius-2);
  color: var(--brand);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.pager a:hover { background: var(--gray-2); }
/* --brand-text, not a hardcoded near-white: --brand flips to a LIGHT indigo in dark mode, where white
   text on it is unreadable. The token already carries the right ink for each theme. */
.pager .current { background: var(--brand); color: var(--brand-text); }
.pager .gap, .pager a[aria-disabled="true"] { color: var(--gray-5); pointer-events: none; }
.checklist > li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--size-3);
  padding-block: var(--size-2);
  border-bottom: 1px solid var(--border);
}
.checklist > li:last-child { border-bottom: 0; }
.checklist label { display: inline-flex; align-items: baseline; gap: var(--size-2); font-weight: var(--font-weight-4); }

/* Onboarding: the single-URL entry moment. A centered block with the URL box and its button on one
   spaced row (they had no gap before). Token-driven, wraps on narrow screens. */
.onboarding { max-width: 34rem; margin-inline: auto; padding-block-start: var(--size-7); }
.onboarding__form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  margin-block-start: var(--size-4);
}
.onboarding__url { flex: 1 1 16rem; font-size: var(--font-size-2); }
/* The checks already watching what is typed (onboarding/_duplicates), a step below the field. */
.onboarding #check-duplicates-list { margin-block-start: var(--size-3); }
.onboarding-actions { display: flex; gap: var(--size-2); flex-wrap: wrap; align-items: center; margin-block-start: var(--size-3); }

/* "Taking a look" wait state: a live spinner so the inspection wait never looks dead. */
.onboarding--looking { text-align: center; }
.onboarding-spinner {
  inline-size: 2.5rem;
  block-size: 2.5rem;
  margin-inline: auto;
  margin-block-end: var(--size-4);
  border: 3px solid var(--border);
  border-block-start-color: #4263eb;
  border-radius: 50%;
  animation: var(--animation-spin);
}
@media (prefers-reduced-motion: reduce) { .onboarding-spinner { animation: none; } }

/* Uptime uptime graph: day-by-day status bars (inline SVG). Colors are the semantic status tokens, so it
   follows light/dark and a future palette. Bars stretch to fill width (preserveAspectRatio="none"). */
.uptime-graph { display: block; width: 100%; }
.uptime-graph--large { height: 44px; }
.uptime-graph--small { height: 16px; }
/* No `rx` here. The pip's rounded outline comes from a clip path on the group (see
   checks/_uptime_graph), because a pip is now a STACK of rects: rounding each one made every band
   round its own four corners, so the bands pinched apart and a notch of background showed through the
   middle of every mixed pip. */
.uptime-bar--up { fill: var(--status-up); }
.uptime-bar--down { fill: var(--status-down); }
.uptime-bar--blocked { fill: var(--status-blocked); }
.uptime-bar--paused { fill: var(--status-paused); }
.uptime-bar--no_data { fill: var(--status-no-data); }

.uptime-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--size-3);
  flex-wrap: wrap;
}
.uptime-figure { display: flex; align-items: baseline; gap: var(--size-2); }
.uptime-pct { font-size: var(--font-size-5); font-weight: var(--font-weight-7); font-variant-numeric: tabular-nums; }
/* The graph's window picker. A segmented row rather than three loose links, so it reads as one
   control with one of its options chosen -- the same shape a radio group has, which is what it is. */
.uptime-windows {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.uptime-window {
  padding: var(--size-1) var(--size-3);
  font-size: var(--font-size-0);
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
}
.uptime-window + .uptime-window { border-left: 1px solid var(--border); }
.uptime-window:hover, .uptime-window:focus-visible { background: var(--surface); color: var(--text); }
/* The chosen one carries its own background AND its own weight. Colour alone would leave the current
   window unreadable to anyone who cannot separate these two greys (WCAG 1.4.1). */
.uptime-window--current {
  background: var(--surface);
  color: var(--text);
  font-weight: var(--font-weight-6);
}
/* Five windows do not fit a phone: "Lifetime" was clipped to "Life" by the `overflow: hidden` that
   rounds the corners, which reads as a rendering fault rather than as more content.
   Tightening the padding is the fix -- the labels are short and the space between them was doing
   nothing on a screen this narrow. The scroll is the belt-and-braces: it keeps every option reachable
   at any width, in any language, without shortening labels. Only overflow-X is relaxed, so the corners
   still clip. */
@media (max-width: 40rem) {
  .uptime-windows { overflow-x: auto; max-width: 100%; }
  .uptime-window { padding-inline: var(--size-2); }
}
.uptime-legend { display: flex; gap: var(--size-3); font-size: var(--font-size-0); color: var(--text-muted); }
.uptime-legend span { display: inline-flex; align-items: center; gap: var(--size-1); }
.uptime-swatch { width: 0.7em; height: 0.7em; border-radius: 2px; }
.uptime-swatch--up { background: var(--status-up); }
.uptime-swatch--down { background: var(--status-down); }
.uptime-swatch--blocked { background: var(--status-blocked); }
.uptime-swatch--paused { background: var(--status-paused); }
/* Matches `.uptime-bar--no_data`, which is the same grey the graph draws. It had no rule at all until
   the legend started keying it -- the label rendered beside an invisible swatch, which is worse than no
   entry, because a key whose colour cannot be seen still claims to explain one. */
.uptime-swatch--no_data { background: var(--status-no-data); }


/* Incident timeline: pips across a padded window (up on each side, the impaired span coloured) at a
   resolution that adapts to duration, with a light band above marking the intervals we sent an alert. */
.incident-timeline__track { position: relative; }
.incident-pips { display: flex; gap: 1px; height: 2.5rem; }
.incident-pip { flex: 1 1 0; min-width: 0; border-radius: 1px; }
.incident-pip--up { background: var(--status-up); }
.incident-pip--down { background: var(--status-down); }
.incident-pip--blocked { background: var(--status-blocked); }
.incident-pip--paused { background: var(--status-paused); }
/* Nothing reported for this slice. Grey, matching `.uptime-bar--no_data`: ignorance is its own state
   and must not borrow the colour of an observation we did not make. */
.incident-pip--no_data { background: var(--status-no-data); }
/* `.incident-pip--mixed` was here: a diagonal gradient for a span holding two states (D22), kept in
   step with the SVG graph's <pattern> by a design test because the geometry had to be stated twice in
   two languages. Both are gone. The stripe read badly at pip size and could only say THAT a span was
   mixed; the uptime graph now divides a pip proportionally and says how much of it was which. */
/* A band over the pip row, shown only when alerting was off. Silence is the default: no band means we
   alerted as claimed. The row is a positioning context; blocks inside either span it (nothing
   configured) or sit over the exact paused stretches (left/width as % of the window). */
.incident-notify { position: relative; height: 1.25rem; margin-block-end: 2px; }
.incident-notify__block {
  position: absolute; inset-block: 0;
  display: flex; align-items: center; justify-content: center; gap: var(--size-1);
  border-radius: 2px; overflow: hidden;
  font-size: var(--font-size-0); font-weight: var(--font-weight-6);
  /* So the glyph below can be hidden by the width of THIS block rather than of the viewport. A pause
     block is sized by how much of the window it covers, which has nothing to do with screen size. */
  container-type: inline-size;
}
/* A block narrower than the bell drew it anyway, clipped by the overflow above, so a month-long
   incident's pauses rendered as a row of sliced half-glyphs that read as a rendering fault. Below the
   width that can hold one, the colour carries the meaning on its own -- which it already does either
   way, since the band is a stripe of colour with a glyph on it, not a glyph with a background. */
@container (max-width: 22px) {
  .incident-notify__block .alerts-state { display: none; }
}
/* The shared bell draws at 16px; the band is 20px tall, so it is sized down to sit inside it. */
.incident-notify__block svg { flex: none; width: 14px; height: 14px; }
/* The bell takes the band's colour, not its own amber. The band already carries the meaning in its
   background -- violet for a pause we chose, red for nothing configured -- and the note above explains
   why violet rather than a warning colour. An amber glyph would argue with both.
   Not a free choice on contrast grounds either, and it goes opposite ways per theme: on the light band
   the amber token measures 2.77 and FAILS the 3:1 a 2px glyph needs, where the band's own ink is 8.26.
   In dark it is the other way round, which is why the ink is a token that inverts (--notify-*-ink)
   rather than one expression asked to work on both. */
.incident-notify__block .alerts-state { color: inherit; }
.incident-notify__block--unconfigured {
  inset-inline: 0;
  background: color-mix(in oklab, var(--red-6) 20%, transparent);
  color: var(--notify-unconfigured-ink);
}
/* The paused family's violet, not a warning colour. This band says we deliberately stopped ALERTING
   for a stretch -- a policy, not a fault -- and yellow read as a cousin of the blocked orange in the
   pip row beneath it, so a span of suppressed notifications looked like something going wrong.
   It shares the violet with a paused PIP, which means we stopped probing. Different things, and the
   band is a separate row carrying the struck bell -- the app's glyph for a silenced ALERT -- where the
   pip row carries pause bars for a stood-down PROBE. That glyph is what separates them; the colour
   only says the part they share, which is that we stood down on purpose. (This band drew pause bars
   itself until 2026-08-18, so the colour and the glyph were both saying "probing stopped".) */
.incident-notify__block--paused {
  background: color-mix(in oklab, var(--status-paused) 24%, transparent);
  color: var(--notify-paused-ink);
}
.incident-timeline__resolution { color: var(--text-muted); }
.incident-timeline__axis {
  display: flex;
  justify-content: space-between;
  margin-block-start: var(--size-1);
  font-size: var(--font-size-0);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* A whole table row that links somewhere: a stretched link covers it, so a click anywhere navigates. */
tr.linked-row { position: relative; cursor: pointer; }
tr.linked-row:hover td { background: var(--surface); }
.stretched-link { color: inherit; text-decoration: none; }
.stretched-link::after { content: ""; position: absolute; inset: 0; }

/* Site icon (favicon / apple-touch-icon) beside a check name. object-fit keeps odd aspect ratios
   tidy; the image-fallback controller removes it if the URL 404s, so there's no broken glyph. */
.check-icon { width: 1rem; height: 1rem; border-radius: 3px; object-fit: contain; flex-shrink: 0; vertical-align: middle; }
/* Holds the icon's space when a site has none, so a column of names starts at one x-position instead
   of ragging by whether each site publishes a favicon. Most do, which is what makes the exceptions
   look like a mistake rather than a variation. */
.check-icon--absent { display: inline-block; }
.check-icon--lg { width: 1.5rem; height: 1.5rem; }
.status-tile__title { display: inline-flex; align-items: center; gap: var(--size-2); min-width: 0; }
.check-name-cell { display: inline-flex; align-items: center; gap: var(--size-2); }
.status-tile__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Share link (owner control) + the public share/status page. */
.share-bar { display: flex; flex-direction: column; gap: var(--size-2); }
.share-bar__link { display: flex; gap: var(--size-1); }
.share-link { flex: 1; min-width: 0; }
.share-bar__copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--size-3);
  background: none;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
.share-bar__icon--done { display: none; }
.share-bar__copy.is-copied .share-bar__icon--copy { display: none; }
.share-bar__copy.is-copied .share-bar__icon--done { display: inline; color: var(--green-9, #2f9e44); }
.share-bar__actions { display: flex; gap: var(--size-2); flex-wrap: wrap; }

/* Visually hidden, but available to assistive tech — for labels a sighted user gets from context
   (a column of action buttons) but a screen-reader user needs named. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Notification-channel glyphs: monochrome, brand-tinted. Decorative (aria-hidden), paired with a text
   label, so they carry no meaning of their own. */
.method-type { display: inline-flex; align-items: center; gap: var(--size-2); }
.method-icon { display: inline-flex; color: var(--brand); flex: none; }
.method-icon svg { display: block; }

/* A signed-in session row: device info on the left, the sign-out button on the right. */
.session-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--size-3);
  flex-wrap: wrap;
}
.session-row .hint { margin-block-start: var(--size-1); }

/* RSS feed affordance: just the brand-tinted icon as a link, sized to sit beside the action buttons
   with a comfortable (>=24px) target. */
.feed-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--brand);
  padding: var(--size-2);
  border-radius: var(--radius);
  line-height: 0;
}
.feed-icon:hover { background: var(--surface-raised); }
.feed-icon .method-icon { color: inherit; }

/* Icon-only action button (e.g. Pause in a page header): a brand-tinted glyph with a comfortable target,
   sitting beside the text action buttons. Same footprint as .feed-icon, but a real <button>, so reset
   the native chrome. The accessible name is on the button (aria-label); the glyph inside is decorative. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--brand);
  padding: var(--size-2);
  border: 0;
  background: none;
  border-radius: var(--radius);
  line-height: 0;
  cursor: pointer;
}
.icon-btn:hover { background: var(--surface-raised); }
.icon-btn .method-icon { color: inherit; }

.share-page { max-width: 40rem; margin-inline: auto; }
/* Eyebrow that names which kind of public page this is (Status vs Incident), so a shared link isn't
   ambiguous. */
.share-eyebrow {
  text-transform: uppercase; letter-spacing: 0.08em;
  font-size: var(--font-size-0); font-weight: var(--font-weight-7);
  color: var(--text-muted); margin: 0;
}
.danger-zone { border: 1px solid var(--red-6, #e5484d); }

/* When a check is down: the reason callout, and the highlighted config row responsible. The details
   rows are display:contents wrappers, so the flag tints the dt/dd cells (a translucent red that works
   on either theme). */
.down-reason { border: 1px solid color-mix(in oklab, var(--red-8) 55%, transparent); }
.down-reason__what { margin: 0; }
.down-reason__actions { margin-block-start: var(--size-3); display: flex; flex-direction: column; gap: var(--size-2); align-items: flex-start; }
.detail--flagged dt, .detail--flagged dd { background: color-mix(in oklab, var(--red-8) 16%, transparent); }
.public-footer { margin-block-start: var(--size-6); padding-block-start: var(--size-4); text-align: center; }

/* Printable in-app receipt. */
.receipt__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--size-3); }
.receipt__brand { font-weight: var(--font-weight-7); margin: 0; }

/* Update bar (version_watch.js): shown only when a newer release is out AND a form has unsaved input,
   so we never reload out from under an edit. The no-edit case reloads silently, so this is the rare
   fallback, not the common path. Fixed to the bottom, above content, dismissed by reloading. */
.version-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 200;
  display: none;
  align-items: center;
  justify-content: center;
  gap: var(--size-3);
  padding: var(--size-3);
  background: var(--surface-raised);
  border-block-start: 1px solid var(--border);
  box-shadow: var(--shadow-3);
}
.version-bar--visible { display: flex; }
/* Reserve room while it's up, the way .app-footer does. It appears exactly when a form has unsaved
   input, so without this a focused field low in a long form can sit behind it (WCAG 2.4.11 Focus Not
   Obscured). The bar is ~1 line plus its padding, on top of the footer's existing allowance. */
body.app:has(.version-bar--visible) {
  padding-block-end: calc(var(--size-8) + var(--size-9) + env(safe-area-inset-bottom, 0px));
}

/* A textarea holding markup, not prose: monospace, roomy, and tab-friendly (see code_editor). The
   default field is sized for a line or two of text, which makes editing a page body an exercise in
   scrolling a letterbox. */
.input--code {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--font-size-1);
  line-height: 1.5;
  tab-size: 2;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  min-height: 24rem;
  width: 100%;
}

/* The "Via" column names every channel an alert went out on -- in-app plus each email destination by
   label -- so it needs more room than the single word it used to hold. */
.delivery-via { min-width: 14ch; }

/* --- Moving a check between groups on the dashboard --- */

/* A group section as a drop target. The outline only appears mid-drag, so the dashboard is not
   permanently drawn as a set of boxes waiting to be filled. */
.dash-group.is-drop-target {
  outline: 2px dashed var(--brand);
  outline-offset: var(--size-1);
  border-radius: var(--radius);
}
.dash-group__empty { padding: var(--size-3); }
.status-tile.is-dragging { opacity: 0.5; }
/* Only tiles are draggable, and only by the pointer: `grab` on the whole card would promise dragging
   to a keyboard user, who regroups from the check's own settings page instead. */
.status-tile[draggable="true"] { cursor: grab; }
.status-tile.is-dragging { cursor: grabbing; }


/* --- Edit in place --- */

/* The trigger is a button so it is tabbable and announces as one, but it must not LOOK like a button:
   it is the heading. Everything below strips the button chrome and puts it back only on interaction. */
.inline-edit__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--size-2);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: text;
  text-align: start;
}
/* The pencil is the affordance, so it stays visible rather than appearing on hover -- a control that
   only exists once you have already found it is not an affordance. Dimmed until the heading is
   engaged, so it reads as a hint rather than as a second heading. */
.inline-edit__pencil { color: var(--text-muted); flex: none; }
.inline-edit__trigger:hover .inline-edit__pencil,
.inline-edit__trigger:focus-visible .inline-edit__pencil { color: var(--brand); }
.inline-edit__display { margin: 0; }

/* The field inherits the heading's type, so the text does not jump size or position when it becomes
   editable -- the point of editing in place is that the thing stays where it was. */
.inline-edit__input {
  font: inherit;
  font-weight: inherit;
  width: 100%;
  max-width: 24rem;
  padding: var(--size-1) var(--size-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: inherit;
}
