/*
 * status.gpt.nexus — design tokens and the [2a] overview.
 *
 * Values are the design handoff's, verbatim. Nothing here is rounded to a
 * "nicer" number and nothing is a system default standing in for a specified
 * value.
 *
 * Two rules that hold everywhere and are easy to break by habit:
 *   - RADIUS IS 0. The only curves on the page are glyph circles, the live dot,
 *     the progress ring and the radial glow — all of which are shapes, not
 *     surfaces. No card, button, chip, input or strip cell is ever rounded.
 *   - NO SHADOWS. Elevation is carried by `--card` against `--bg` and by
 *     hairline `--line` borders.
 */

/* ------------------------------------------------------------------ tokens */

/*
 * Light is the BASE and dark is an override, in both directions:
 *   [data-theme="dark"]                          -> explicit dark
 *   prefers-color-scheme:dark + [data-theme=system] -> follow the OS
 * The document ships with data-theme="system", so the OS is honoured with no
 * JavaScript at all and a stored override only has to change one attribute.
 */
:root {
  --bg: #f4f3f0;
  --fg: #111110;
  --mute: #6b6a66;
  --dim: rgba(17, 17, 16, 0.38);
  --faint: rgba(17, 17, 16, 0.06);
  --line: rgba(17, 17, 16, 0.13);
  --card: #fbfaf8;
  --acc: #f04e23;
  --grid: rgba(17, 17, 16, 0.055);
  --up: oklch(58% 0.14 152);
  --deg: oklch(66% 0.15 68);
  --down: oklch(55% 0.19 26);
  --upbg: oklch(58% 0.14 152 / 0.12);
  --degbg: oklch(66% 0.15 68 / 0.14);
  --downbg: oklch(55% 0.19 26 / 0.12);
  --hatch: repeating-linear-gradient(135deg, rgba(23, 23, 15, 0.14) 0 1px, transparent 1px 5px);
  color-scheme: light;
}

:root[data-theme="dark"] {
  --bg: #0f0f0e;
  --fg: #f1efe8;
  --mute: rgba(241, 239, 232, 0.6);
  --dim: rgba(241, 239, 232, 0.36);
  --faint: rgba(241, 239, 232, 0.07);
  --line: rgba(241, 239, 232, 0.14);
  --card: #161615;
  --acc: #ff6a3d;
  --grid: rgba(241, 239, 232, 0.045);
  --up: oklch(76% 0.15 152);
  --deg: oklch(80% 0.14 72);
  --down: oklch(70% 0.18 26);
  --upbg: oklch(76% 0.15 152 / 0.13);
  --degbg: oklch(80% 0.14 72 / 0.14);
  --downbg: oklch(70% 0.18 26 / 0.14);
  --hatch: repeating-linear-gradient(135deg, rgba(241, 239, 232, 0.16) 0 1px, transparent 1px 5px);
  color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] {
    --bg: #0f0f0e;
    --fg: #f1efe8;
    --mute: rgba(241, 239, 232, 0.6);
    --dim: rgba(241, 239, 232, 0.36);
    --faint: rgba(241, 239, 232, 0.07);
    --line: rgba(241, 239, 232, 0.14);
    --card: #161615;
    --acc: #ff6a3d;
    --grid: rgba(241, 239, 232, 0.045);
    --up: oklch(76% 0.15 152);
    --deg: oklch(80% 0.14 72);
    --down: oklch(70% 0.18 26);
    --upbg: oklch(76% 0.15 152 / 0.13);
    --degbg: oklch(80% 0.14 72 / 0.14);
    --downbg: oklch(70% 0.18 26 / 0.14);
    --hatch: repeating-linear-gradient(135deg, rgba(241, 239, 232, 0.16) 0 1px, transparent 1px 5px);
    color-scheme: dark;
  }
}

/* ------------------------------------------------------------------- base */

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

html,
body {
  min-height: 100%;
}

body {
  margin: 0;
  color: var(--fg);
  background-color: var(--bg);
  /* 40px grid: two 1px gradients, drawn under everything. */
  background-image: linear-gradient(var(--grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid) 1px, transparent 1px);
  background-size: 40px 40px;
  font-family: Inter, system-ui, sans-serif;
  font-size: 14px;
  letter-spacing: -0.005em;
  -webkit-font-smoothing: antialiased;
}

a {
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: -2px;
}

.mono {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
}

/* The mono uppercase label with the 6px orange square. */
.lbl {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--mute);
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.lbl::before {
  content: "";
  width: 6px;
  height: 6px;
  background: var(--acc);
  flex: none;
}

.page {
  max-width: 1120px;
  margin: 0 auto;
  padding-bottom: 32px;
}

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

/* --------------------------------------------------------------- keyframes */

@keyframes nxIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes nxPulse {
  0% {
    box-shadow: 0 0 0 0 var(--up);
  }
  100% {
    box-shadow: 0 0 0 9px transparent;
  }
}
@keyframes nxBreathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}
@keyframes nxBlink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}

/* --------------------------------------------------------------- top bar */

.topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 36px;
  border-bottom: 1px solid var(--line);
}

.topbar__brand {
  display: flex;
  align-items: center;
  gap: 12px;
}

/*
 * Sized by HEIGHT with an automatic width: the mark's viewBox is 246x342, so a
 * 22x22 box would squash it. No filter — the paths are `currentColor`.
 */
.topbar__mark {
  height: 22px;
  width: auto;
  color: var(--fg);
  display: block;
}

.topbar__host {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 13px;
  color: var(--mute);
}

.topbar__meta {
  display: flex;
  align-items: center;
  gap: 20px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 12px;
  color: var(--mute);
}

.topbar__group {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The 390 top bar is the mark and a dot + age. No ring, no Subscribe, no host. */
.topbar__meta--mobile {
  display: none;
}

.live-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--up);
  flex: none;
  /* steps(1): a hard on/off, never a fade. It reads as a heartbeat, not a glow. */
  animation: nxBlink 2s steps(1) infinite;
}

/*
 * STALE: nothing on this page may read as a live signal.
 *
 * [2d]'s top bar is a dashed, static, --mute dot reading "not live · checked
 * 47 min ago", with no ring and no countdown — because a blinking green dot
 * beside "this page stopped updating" is the exact false green this product
 * exists to detect. The rest of that screen's treatment lives further down under
 * the stale banner: dashed card borders, muted rows, and strips frozen and
 * hatched forward at `freshness.lastTickAt`.
 */
.is-stale .live-dot {
  background: transparent;
  border: 1.5px dashed var(--mute);
  animation: none;
}

.is-stale #next-group {
  display: none;
}

.is-stale .word {
  color: var(--mute);
}

.subscribe {
  text-decoration: none;
  color: var(--bg);
  background: var(--fg);
  padding: 8px 14px;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* ---------------------------------------------------------------- glyphs */

/*
 * Shape carries state; hue is secondary. `--g` is the size for the context.
 * The `unknown` glyph never animates — every other glyph moving is what says
 * "this is a live reading", and we do not have one.
 */
.glyph {
  --g: 8px;
  width: var(--g);
  height: var(--g);
  flex: none;
  display: inline-block;
}

.glyph[data-state="up"] {
  border-radius: 50%;
  background: var(--up);
}
.glyph[data-state="degraded"] {
  background: var(--deg);
  transform: rotate(45deg);
}
.glyph[data-state="down"] {
  background: var(--down);
}
.glyph[data-state="unknown"] {
  border-radius: 50%;
  background: transparent;
  border: 1.5px dashed var(--mute);
}

/*
 * ALARM HUE FOLLOWS THE EFFECT ON NEXUS, NOT THE VENDOR'S OWN STATE.
 *
 * This is the vocabulary's own rule applied, not a new one: shape carries the
 * state, hue is secondary. A vendor genuinely down whose outage reaches nothing
 * of ours keeps its `■` — the state is true and stays legible — and drops to
 * `--mute`, so it reads as information rather than as an alarm. `effectOnNexus`
 * is the single judgement that sets `data-hue`; nothing in CSS decides it.
 *
 * ONLY `background` MOVES. `border-radius` and `transform` are what draw the
 * circle and the diamond, so the four shapes survive a demotion intact — which
 * is the whole reason this can be done at all.
 *
 * `unknown` IS EXCLUDED, and not for tidiness: it is the one hollow glyph, drawn
 * with `background: transparent` and a dashed `--mute` border. Filling it would
 * turn `◌` into `●` and invent a fifth reading — a demoted `no signal` that looks
 * like `up`.
 */
.glyph[data-hue="mute"]:not([data-state="unknown"]) {
  background: var(--mute);
}

/* ----------------------------------------------------------- verdict cards */

.verdicts {
  display: grid;
  grid-template-columns: 1.25fr 1fr;
  gap: 12px;
  padding: 24px 36px 0;
}

.card {
  background: var(--card);
  border: 1px solid var(--line);
}

.card--verdict {
  padding: 30px 32px 28px;
  position: relative;
  overflow: hidden;
}

.glow {
  position: absolute;
  inset: auto auto -60px -40px;
  width: 260px;
  height: 260px;
  border-radius: 50%;
  pointer-events: none;
}

.verdict {
  display: flex;
  align-items: center;
  gap: 18px;
  margin-top: 22px;
  position: relative;
}

.verdict .glyph {
  --g: 16px;
}
.verdict .glyph[data-state="degraded"] {
  --g: 14px;
}
.verdict .glyph[data-state="up"] {
  animation: nxPulse 2.4s ease-out infinite;
}
.verdict .glyph[data-state="degraded"] {
  animation: nxBreathe 2.4s ease-in-out infinite;
}
.verdict .glyph[data-state="down"] {
  animation: nxBreathe 1.6s ease-in-out infinite;
}

.word {
  font: 800 56px/1 "Playfair Display", Georgia, serif;
  letter-spacing: -0.02em;
}

.sub {
  margin: 16px 0 0;
  font: italic 700 20px/1.3 "Playfair Display", Georgia, serif;
  color: var(--mute);
  max-width: 440px;
  position: relative;
}

.facts {
  display: flex;
  flex-wrap: wrap;
  gap: 26px;
  margin-top: 22px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 12px;
  color: var(--mute);
  position: relative;
}

.facts b {
  font-weight: inherit;
  color: var(--fg);
}

/*
 * [2f] at 390: ONE line of mono, not a wrapping row of labelled facts.
 *
 * The desktop row's 26px gaps put three clauses on three lines at this width,
 * which is taller than the headline above it. The design writes them as a single
 * `·`-separated sentence instead, and it is one span rather than several so it
 * wraps as prose if it has to.
 */
.facts--line {
  gap: 0;
  margin-top: 14px;
  font-size: 11.5px;
  line-height: 1.5;
}

.facts i {
  font-style: normal;
  color: var(--dim);
}

/* ----------------------------------------------------------------- chips */

.chips {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 20px;
}

.chip {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--fg);
  font-family: inherit;
  font-size: 12.5px;
  text-align: start;
  padding: 0;
  transition: border-color 120ms ease-out;
}

.chip__head {
  display: grid;
  grid-template-columns: 8px 90px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 7px 10px;
  width: 100%;
  background: transparent;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.chip__glyph {
  --g: 7px;
}

.chip__short {
  font-weight: 500;
}

.chip__impact {
  color: var(--mute);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip__dur {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  color: var(--dim);
  font-size: 11px;
  white-space: nowrap;
}

.chip:hover,
.chip:focus-within {
  border-color: var(--fg);
}

.chip:hover .chip__dur,
.chip:focus-within .chip__dur {
  color: var(--fg);
}

.chip__caret::before {
  content: " →";
}

.chip[aria-expanded="true"] .chip__caret::before,
.chip.is-open .chip__caret::before {
  content: " ↑";
}

.chip:not(:hover):not(:focus-within):not(.is-open) .chip__caret::before {
  content: "";
}

/* 0fr -> 1fr is the only height transition that works on content-sized rows. */
.chip__body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 160ms ease-out;
}

.chip.is-open {
  border-color: var(--fg);
}

.chip.is-open .chip__body {
  grid-template-rows: 1fr;
}

.chip__bodyinner {
  overflow: hidden;
}

.chip__facts {
  border-top: 1px solid var(--line);
  padding: 10px 10px 12px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 16px;
  font-size: 12px;
  color: var(--mute);
}

.chip__facts b {
  font-weight: inherit;
  color: var(--fg);
}

.chip__links {
  grid-column: 1 / -1;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  color: var(--fg);
}

.chip__links span {
  color: var(--mute);
}

/*
 * A chip that does not expand — [2f] and [2d].
 *
 * [2f]: Nexus itself is down, so the vendor incidents are context and get the
 * weight of context. [2d]: the board has stopped, so there are no current facts
 * to expand onto, and the chip carries `last seen` where the duration was —
 * because a duration computed from ticks stops advancing when the ticks do, and
 * a frozen figure reads exactly like a moving one.
 */
/* [2f] — bare name tags, wrapping in a row: context, not the story. */
.chips--tags {
  flex-direction: row;
  flex-wrap: wrap;
  gap: 6px;
}

.chips--tags .chip--flat {
  padding: 5px 10px 5px 8px;
}

/* [2d] — a column of dashed rows, each with `last seen` where a duration was. */
.chips--stale .chip--flat {
  display: grid;
  grid-template-columns: 8px 1fr auto;
  gap: 10px;
  padding: 7px 10px;
}

.chip--flat {
  display: flex;
  gap: 7px;
  align-items: center;
  font-size: 12.5px;
  text-decoration: none;
  color: var(--fg);
}

.chip--stale {
  border-style: dashed;
  color: var(--mute);
}

.chip--flat .chip__dur {
  color: var(--dim);
}

/* ------------------------------------------------ [2h] last-resolved row */

/*
 * The row that keeps the right card from being a headline and a sentence on the
 * commonest day of all. It is a REAL incident off `/api/v1/incidents`, never a
 * placeholder — the glyph is at half strength because the thing it describes is
 * over.
 */
.lastres {
  display: grid;
  grid-template-columns: 8px 1fr auto;
  gap: 10px;
  align-items: center;
  margin-top: 18px;
  border-top: 1px solid var(--line);
  padding-top: 12px;
  font-size: 12.5px;
  text-decoration: none;
  color: var(--fg);
}

.lastres__glyph {
  --g: 7px;
  opacity: 0.5;
}

.lastres__text {
  color: var(--mute);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lastres:hover .lastres__text {
  text-decoration: underline;
  text-decoration-color: var(--acc);
  text-underline-offset: 3px;
}

.lastres__when {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  color: var(--dim);
  white-space: nowrap;
}

/* ----------------------------------------------------------- group cards */

.groups {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  padding: 12px 36px 0;
}

.group {
  padding: 18px 20px 10px;
}

.group__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 10px;
}

.group__name {
  font: 800 16px/1.2 "Playfair Display", Georgia, serif;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.group__summary {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  color: var(--mute);
  flex: none;
  white-space: nowrap;
}

.row {
  display: grid;
  grid-template-columns: 14px 1fr 80px 52px;
  gap: 10px;
  align-items: center;
  padding: 8px 0;
  border-top: 1px solid var(--line);
  font-size: 13px;
  text-decoration: none;
  color: var(--fg);
  transition: background-color 120ms ease-out;
}

/*
 * The hover fill bleeds to the card edge without the row itself being
 * full-bleed, so the hairline border stays inset. Two spread shadows rather
 * than negative margins, which would drag the border with them.
 */
.row:hover {
  background: var(--faint);
  box-shadow: -20px 0 0 var(--faint), 20px 0 0 var(--faint);
}

.row:hover .row__name {
  text-decoration: underline;
  text-decoration-color: var(--acc);
  text-underline-offset: 3px;
}

.row:hover .row__value::after {
  content: " →";
}

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

.row__value {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  text-align: right;
  white-space: nowrap;
}

/*
 * The strip and its tooltip share a positioning context.
 *
 * The wrapper exists only so the tooltip can be absolutely positioned against
 * the strip it describes. It must not introduce a size of its own — it sits in a
 * fixed grid column and the strip inside it keeps its own height.
 */
.strip-wrap {
  position: relative;
  min-width: 0;
}

.strip {
  display: flex;
  gap: 1.5px;
  height: 10px;
}

.cell {
  flex: 1;
  min-width: 0;
}

/*
 * [2k]: a hovered cell takes a 1px `--fg` outline.
 *
 * An OUTLINE rather than a border, because a border would change the cell's box
 * and shove every hour after it sideways — a timeline that moves under the
 * pointer is worse than no highlight at all.
 */
.cell:hover {
  outline: 1px solid var(--fg);
}

/*
 * Observed: THAT HOUR's state colour, set inline per cell — a strip is a sequence
 * of hours that each had their own state, not one colour stretched across a row.
 * Not-yet-observed: hatched, never blank and never a gap.
 */
.cell--observed {
  background: var(--faint);
}

.cell--unobserved {
  background-image: var(--hatch);
}

/*
 * A VENDOR CAVEAT ON AN HOUR WE MEASURED: a mark ON the cell, never instead of it.
 *
 * The cell keeps its state colour underneath — that is our measurement and it is
 * the ground. This adds a 2px caveat-coloured rule along the top edge, drawn with
 * an inset box-shadow so it costs no box and cannot shift the strip sideways the
 * way a border would (the same reason `.cell:hover` uses an outline).
 *
 * IT IS BOUNDED BY CONSTRUCTION AND THAT IS THE POINT. 2px of a 10px cell cannot
 * become the cell however many ticks carried the claim — which is the whole
 * defect this file is part of fixing, where a caveat sized as a fill turned 571
 * successful probes into a day of solid orange.
 */
.cell--caveat {
  box-shadow: inset 0 2px 0 var(--deg);
}

.ruler {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0 2px;
  border-top: 1px solid var(--line);
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 10px;
  color: var(--dim);
}

/*
 * THE ENDS OF A GROUP RULER NEVER WRAP. THE MIDDLE DOES.
 *
 * The middle slot is normally four words (`hatched = not yet observed`) and on
 * [2f] it is a whole sentence — `◌ suppressed = withheld while Nexus API is
 * down` — which is wider than a 340px group card can hold. With every item
 * shrinkable the ENDS gave way first, so `−24 h` broke across two lines and the
 * axis read as broken while the note it was making room for read as fine.
 *
 * Scoped to the group card: `.ruler--chart` is a five-tick axis on the detail
 * screens where every label is an end of something and none of them is prose.
 */
.group .ruler > span:first-child,
.group .ruler > span:last-child {
  flex: none;
  white-space: nowrap;
}

.group .ruler > span:nth-child(2) {
  min-width: 0;
  text-align: center;
}

/* ---------------------------------------------------------------- legend */

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  padding: 22px 36px 0;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11.5px;
  color: var(--mute);
}

.legend__item {
  display: flex;
  align-items: center;
  gap: 8px;
}

.legend__note {
  margin-left: auto;
  color: var(--dim);
}

/* ---------------------------------------------------------- stale banner */

/*
 * [2d], in full: the banner, hollow verdicts, dashed card borders, and strips
 * frozen and hatched forward.
 *
 * ONE RULE GOVERNS ALL OF IT — nothing on a stopped board may read as a live
 * signal. That is why the treatment is not a filter or an opacity on the page: a
 * dimmed green row still says green. The states, the words and the values are
 * changed in the view model (`rowOf`, `nexusVerdictFor`), and what is left here
 * is only the part that is genuinely presentational.
 */
.stale-banner {
  background: var(--acc);
  color: #111;
  padding: 14px 36px;
  font-size: 13.5px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}

.stale-banner__lead {
  display: flex;
  gap: 12px;
  align-items: center;
  min-width: 0;
}

.stale-banner__lead b {
  font-weight: 600;
}

.stale-banner__rest {
  opacity: 0.75;
}

/*
 * A dashed ring turning once every six seconds — the ONLY moving thing on a
 * stopped board, and not a state glyph. It says "something is still trying",
 * which is true: the page keeps refetching whether or not the prober does.
 */
.stale-banner__spin {
  width: 10px;
  height: 10px;
  flex: none;
  border-radius: 50%;
  border: 1.5px dashed #111;
  animation: nxSpin 6s linear infinite;
}

@keyframes nxSpin {
  to {
    transform: rotate(360deg);
  }
}

.stale-banner__meta {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  opacity: 0.8;
  white-space: nowrap;
}

/*
 * DASHED BORDERS EVERYWHERE. The hairline that says "this is a panel of current
 * readings" becomes the same dashed line the `◌` glyph uses, at card scale.
 */
.is-stale .card {
  border-style: dashed;
}

/* The headline, the group name and every row go to --mute: nothing is asserted. */
.is-stale .group__name,
.is-stale .row {
  color: var(--mute);
}

.is-stale .row__value {
  color: var(--dim) !important;
}

/*
 * The strip freezes and hatches FORWARD.
 *
 * Observed cells drop to 40% — they are real readings, and they are old. Cells
 * past the last tick are hatched outright, because nothing observed them at all.
 * The boundary is `freshness.lastTickAt`, computed in `stripCells`, never a
 * fraction of the strip.
 */
.is-stale .cell--observed {
  opacity: 0.4;
}

.cell--frozen {
  background-color: transparent;
  background-image: var(--hatch);
}

/* -------------------------------------------------------------- skeleton */

.skeleton-block {
  background: var(--faint);
}

.skeleton-block--headline {
  animation: nxBreathe 1.6s ease-in-out infinite;
}

/* ------------------------------------------------------------- entrance */

/*
 * Applied only on the FIRST paint of real data. A refetch patches text in
 * place, so values swap with no animation — a card that re-animates every 60s
 * would read as new information arriving when nothing changed.
 */
.is-entering .card,
.is-entering .verdicts > *,
.is-entering .groups > * {
  animation: nxIn 0.7s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: var(--enter-delay, 0ms);
}

.is-entering .groups > * {
  animation-duration: 0.6s;
}

/* ---------------------------------------------------------- mobile / 390 */

.mobile-only {
  display: none;
}

@media (max-width: 899px) {
  .verdicts {
    grid-template-columns: 1fr;
  }
  .groups {
    grid-template-columns: 1fr;
  }

  /*
   * THE STALE BANNER STACKS WITH THE CARDS, AND NOT AT 480.
   *
   * Its three parts — the sentence, `Nothing below is current.` and the
   * `LAST TICK … · STALE AFTER …` meta — need about 830px on one line. The
   * mobile treatment started at 480, which left a 419px-wide band in which the
   * desktop banner did not fit and nothing said so. Measured on the 47-minute
   * board: at 481–510 the meta lands ON TOP of "Nothing below is current."
   * (meta x=141, that phrase ending at x=177); from there to ~820 the sentence
   * wraps into a 112px-tall stack of three narrow columns.
   *
   * 899 is the width at which this page stops being a two-column layout at all,
   * which is the same fact about the same page, so the banner uses it too.
   */
  .stale-banner {
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
  }
}

@media (max-width: 480px) {
  .desktop-only {
    display: none;
  }
  .mobile-only {
    display: block;
  }

  .page {
    padding: 0 16px 24px;
  }

  .topbar {
    padding: 18px 4px 12px;
    border-bottom: 0;
  }
  .topbar__mark {
    height: 20px;
    width: auto;
  }
  .topbar__meta {
    font-size: 11px;
    gap: 6px;
  }
  .live-dot {
    width: 5px;
    height: 5px;
  }

  .topbar__meta--mobile {
    display: inline-flex;
  }

  .verdicts {
    display: block;
    padding: 0;
  }
  /* Adjacent VISIBLE cards only — the hidden desktop pair is still in the DOM. */
  .mobile-only + .mobile-only {
    margin-top: 8px;
  }

  .card--verdict {
    padding: 22px 20px;
  }
  .lbl {
    font-size: 10.5px;
  }
  .verdict {
    gap: 12px;
    margin-top: 12px;
  }
  .verdict .glyph {
    --g: 12px;
  }
  .verdict .glyph[data-state="degraded"] {
    --g: 10px;
    /* The mobile mock's diamond is static. */
    animation: none;
  }
  .word {
    font-size: 36px;
  }
  .sub {
    margin-top: 10px;
    font-size: 15px;
  }

  .chips {
    gap: 5px;
    margin-top: 14px;
  }
  .chip {
    font-size: 12px;
  }
  .chip__head {
    grid-template-columns: 7px 1fr auto;
    padding: 6px 9px;
  }
  .chip__glyph {
    --g: 6px;
  }
  /* The impact clause has no room at 390 and is dropped, per the mockup. */
  .chip__impact {
    display: none;
  }
  .chip__dur {
    font-size: 10.5px;
  }

  .mobile-heading {
    display: flex;
    justify-content: space-between;
    padding: 20px 4px 8px;
    font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
    font-size: 11px;
    color: var(--mute);
  }

  /*
   * 62px, not the mockup's 46.
   *
   * [2a] mobile draws 46px because every value there is a latency. [2f] mobile
   * draws 66px because its values are `◌ supp.`, and the column has to hold the
   * widest thing it can be asked to say — at 46px `◌ supp.` clips to `◌ sup`,
   * which is not a shorter label, it is a different one. 62px is the narrowest
   * that holds both, and it comes out of a name column that ellipsises anyway.
   */
  .mobile-rows .row {
    grid-template-columns: 14px 1fr 72px 62px;
    padding: 9px 4px;
  }
  .mobile-rows .row:hover {
    box-shadow: none;
  }
  .mobile-rows .strip {
    height: 9px;
  }
  .mobile-rows .row__value {
    color: var(--mute);
  }

  .mobile-groups {
    margin-top: 14px;
  }
  .mobile-group {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 4px;
    border-top: 1px solid var(--line);
    font-size: 13px;
    text-decoration: none;
    color: var(--fg);
  }
  .mobile-group__dots {
    display: flex;
    gap: 6px;
    align-items: center;
  }
  .mobile-group__dots .glyph {
    --g: 7px;
  }

  /*
   * The banner STACKS at 390 and drops its meta.
   *
   * Three parts laid out `space-between` with a `nowrap` meta overprinted each
   * other here: the bold sentence, the muted one and `LAST TICK … STALE AFTER …`
   * all landed on the same 358px. The design's own mobile banner is two lines and
   * no meta, so that is what this is — the timestamp is repeated in the top bar
   * directly below either way.
   */
  /* The column and its gap come from the 899 block; only the box differs here. */
  .stale-banner {
    padding: 12px 16px;
    font-size: 12.5px;
    margin: 0 -16px;
    gap: 2px;
  }

  .stale-banner__lead {
    flex-wrap: wrap;
    gap: 8px;
    line-height: 1.45;
  }

  /*
   * The meta SHRINKS here, it does not vanish. `.desktop-only` / `.mobile-only`
   * pick which of the two the banner renders, and the mobile one is the short
   * `last tick 12:27 UTC` — the one number on this banner that says when the
   * board stopped, which is what a reader needs to judge everything below it.
   */
  .stale-banner__meta {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: none;
  }
}

/* --------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
 * [2b] target detail and [2i] group page
 *
 * Both share the overview's tokens, cards, glyphs and verdict typography; what
 * follows is only what those two screens add. Same two rules hold: radius 0
 * everywhere but the glyph shapes, and no shadows.
 * ======================================================================== */

/* ------------------------------------------------------- breadcrumb top bar */

/*
 * The detail top bar carries a breadcrumb where the overview carries the mark
 * and the countdown ring. The ring is deliberately absent rather than hidden:
 * this page's charts refresh on the bucket, not on the tick, so a countdown to
 * the next tick would be a promise about the wrong thing.
 */
.topbar--crumb {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 12.5px;
}

.crumb {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

.crumb__back,
.crumb__up {
  text-decoration: none;
  color: var(--mute);
}

.crumb__back:hover,
.crumb__up:hover {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: var(--acc);
  text-underline-offset: 3px;
}

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

.crumb__here {
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A single card fills the row rather than sitting at 1.25fr with a hole beside it. */
.verdicts--single {
  grid-template-columns: 1fr;
}

/* ------------------------------------------------------------ detail layout */

.detail-grid {
  display: grid;
  grid-template-columns: 1fr 340px;
  gap: 12px;
  padding: 12px 36px 0;
}

.card--content {
  padding: 22px 24px 18px;
}

.card--uptime {
  padding: 22px 24px 6px;
  /* The blocks are separated by their own rules; the last must not double up
     with the card's own bottom edge. */
  align-self: start;
}

/* A card heading and its mono meta, sharing one baseline. */
.sect {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}

/*
 * The gap between a card's sections.
 *
 * EVERY BLOCK THAT CAN PRECEDE A HEADING IS LISTED, including [2e]'s two
 * replacements. A selector list that omits one is not a smaller gap, it is NO
 * gap — the heading lands flush against the block above it — and it fails
 * silently on exactly the screen the new block was added for.
 */
.sect + .sect,
.chart + .sect,
.ruler--chart + .sect,
.spark + .sect,
.nochart + .sect,
.nospark + .sect {
  margin-top: 28px;
}

.sect__title {
  font: 800 16px/1.2 "Playfair Display", Georgia, serif;
}

.sect__meta {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  color: var(--mute);
  text-align: right;
}

/* The verdict card's clause: `Root` + italic `of 7 checks` on one line. */
.verdict--dep {
  gap: 16px;
  flex-wrap: wrap;
}

/*
 * A word beside a clause must break at the CLAUSE, never inside itself.
 * `7 checks` broke across two lines at 56px with `suppressed` stranded beside the
 * second, which reads as two separate figures.
 */
.verdict--dep .word {
  white-space: nowrap;
}

.verdict__clause {
  font: italic 700 20px/1.2 "Playfair Display", Georgia, serif;
  color: var(--mute);
}

/*
 * A verdict glyph that must NOT animate.
 *
 * Every animating glyph on this site is saying "this is a live reading". The
 * Effect-on-Nexus card states a conclusion read off the declared dependency
 * graph, so its glyph is static in the design and static here.
 */
.verdict--static .glyph {
  animation: none !important;
}

/* 13px body prose — distinct from `.sub`, which is the Playfair italic. */
.prose {
  margin: 16px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--mute);
  position: relative;
}

.deps {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 16px;
}

.dep {
  display: flex;
  gap: 6px;
  align-items: center;
  font-size: 12px;
  border: 1px solid var(--line);
  padding: 4px 9px 4px 7px;
  text-decoration: none;
  color: var(--fg);
  transition: border-color 120ms ease-out;
}

.dep:hover {
  border-color: var(--fg);
}

.dep__glyph {
  --g: 6px;
}

/* --------------------------------------------------- 96-bucket chart + tips */

.chart {
  position: relative;
  margin-top: 14px;
}

.bars {
  display: flex;
  gap: 2px;
  align-items: flex-end;
}

/*
 * A bar is a STACK of proportional segments, bottom first, not a single block.
 *
 * `flex-direction: column-reverse` puts the FIRST child at the BOTTOM, and
 * `bucketSegments` emits `up` first — so green is the ground and whatever else
 * the quarter hour held sits on top of it. Reversing here rather than reversing
 * the array keeps the data in the order it reads in (`up`, `degraded`, `down`)
 * and leaves the visual order to the thing that is visual.
 */
.bar {
  flex: 1;
  min-width: 0;
  transform-origin: bottom;
  display: flex;
  flex-direction: column-reverse;
  overflow: hidden;
}

.bar__seg {
  display: block;
  width: 100%;
  /* Its height is a percentage set inline from the tick share. `flex-shrink: 0`
     because a flex child's default shrink would quietly renormalise the shares
     the moment rounding put them a hair over 100% — turning a measured
     proportion into an approximated one with nothing saying so. */
  flex-shrink: 0;
}

/*
 * A VENDOR CAVEAT ON A BUCKET WE MEASURED. Same rule as `.cell--caveat`: a 2px
 * rule along the top of the bar, over a fill that stays entirely our own
 * measurement. It has no share of the bar and cannot take one.
 */
.bar--caveat {
  box-shadow: inset 0 2px 0 var(--deg);
}

/* Not-yet-observed is hatched at FULL height, never a gap and never a short bar:
   a short bar would read as a small measurement rather than as none. */
.bar--unobserved {
  background-image: var(--hatch);
}

.bar:hover {
  outline: 1px solid var(--fg);
}

.is-entering .bars.is-sweeping .bar {
  animation: nxSweep 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: var(--sweep-delay, 0ms);
}

@keyframes nxSweep {
  from {
    transform: scaleY(0);
  }
  to {
    transform: scaleY(1);
  }
}

/*
 * The bucket tooltip. Solid --fg on --bg so it is never mistaken for part of the
 * chart, and it appears after 80ms with no fade — a tooltip that animates in is
 * a tooltip you have to wait for twice.
 */
.tip {
  position: absolute;
  bottom: calc(100% + 9px);
  left: var(--tip-x, 50%);
  transform: translateX(-50%);
  background: var(--fg);
  color: var(--bg);
  padding: 7px 10px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  line-height: 1.5;
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
  /* Clamped inside the card so an edge bucket cannot push it off. */
  max-width: 100%;
}

.tip__what {
  opacity: 0.7;
}

.tip__arrow {
  position: absolute;
  left: 50%;
  bottom: -5px;
  width: 10px;
  height: 10px;
  background: var(--fg);
  transform: translateX(-50%) rotate(45deg);
}

.ruler--chart {
  border-top: 0;
  padding: 6px 0 0;
}

/* ------------------------------------------------------- [2e] empty state */

/*
 * The block that stands where the chart would be.
 *
 * HATCHED AT FULL HEIGHT WITH THE REASON WRITTEN ACROSS IT. 96 hatched bars
 * would be the same texture and a different claim — "we looked at ninety-six
 * quarter hours and saw nothing" — about a target nothing has ever probed. The
 * label sits on a `--card` plate so it is readable over the hatching.
 */
.nochart {
  height: 56px;
  margin-top: 14px;
  background-image: var(--hatch);
  display: flex;
  align-items: center;
  justify-content: center;
}

.nochart span {
  background: var(--card);
  padding: 3px 10px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--mute);
}

.nospark {
  height: 70px;
  margin-top: 12px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: flex-end;
  padding-bottom: 10px;
  font-size: 13px;
  color: var(--dim);
}

/*
 * The CTA. A link-only target's page cannot answer the question a reader has, so
 * it hands them to the one that can — with no radius and no shadow, like every
 * other filled control on the site.
 */
.cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 22px;
  text-decoration: none;
  color: var(--bg);
  background: var(--fg);
  padding: 11px 16px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  letter-spacing: 0.12em;
  position: relative;
}

/*
 * DASHED CARDS on the link-only screen, for the same reason [2d]'s are dashed:
 * the hairline that says "this panel holds readings" becomes the dashed line the
 * `◌` glyph uses. Set on the body so every card on the screen agrees.
 */
.is-linkonly .card {
  border-style: dashed;
}

/* The one card that is NOT about the missing readings keeps its hairline. */
.is-linkonly .card--effect {
  border-style: solid;
}

/* ------------------------------------------------------- latency sparkline */

/*
 * The sparkline. The SVG stretches to the box; the type over it does not.
 */
.spark {
  margin-top: 12px;
  position: relative;
  line-height: 0;
}

.spark svg {
  display: block;
  overflow: visible;
}

.spark__label {
  position: absolute;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 10px;
  line-height: 1;
  color: var(--dim);
  pointer-events: none;
}

.spark__label--gap {
  left: 8px;
  top: 5px;
}

/* Just above the dashed midline, right-aligned — as the design places it. */
.spark__label--scale {
  right: 8px;
  top: 34px;
}

/* The r3 end dot, positioned as a fraction of the box so `none` cannot squash it. */
.spark__dot {
  position: absolute;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: var(--acc);
  pointer-events: none;
}

.is-entering .spark__line--drawing {
  animation: nxDraw 1.6s 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}

@keyframes nxDraw {
  to {
    stroke-dashoffset: 0;
  }
}

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

.irow {
  display: grid;
  grid-template-columns: 12px 1fr 110px 130px 1fr;
  gap: 12px;
  align-items: center;
  padding: 12px 0 4px;
  margin-top: 10px;
  border-top: 1px solid var(--line);
  font-size: 13px;
  text-decoration: none;
  color: var(--fg);
  transition: background-color 120ms ease-out;
}

.irow + .irow {
  margin-top: 0;
}

.irow:hover {
  background: var(--faint);
  box-shadow: -12px 0 0 var(--faint), 12px 0 0 var(--faint);
}

.irow:hover .irow__title {
  text-decoration: underline;
  text-decoration-color: var(--acc);
  text-underline-offset: 3px;
}

.irow__title {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.irow__state {
  font-size: 12px;
  font-weight: 500;
}

.irow__meta {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11.5px;
  color: var(--mute);
}

.irow__when {
  font-size: 12px;
  color: var(--mute);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------------------- uptime windows */

.uw {
  padding: 16px 0 14px;
  border-bottom: 1px solid var(--line);
}

.uw__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
}

.uw__label {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 12px;
  color: var(--mute);
}

/* Silkscreen, and only here: the handoff reserves it for uptime percentages. */
.uw__pct {
  font: 400 26px/1 Silkscreen, "JetBrains Mono", ui-monospace, monospace;
}

/*
 * The three-state split, drawn where a single percentage would be.
 *
 * NOT Silkscreen at 26px. Three terms at that size do not fit the row, and the
 * point of the split is that it is READABLE in place — a figure that overflows
 * or wraps mid-term is a worse answer than the wrong one it replaced. The
 * monospace face keeps the three percentages column-aligned against each other,
 * and each term is coloured by its own state with its glyph in the label, so the
 * shape carries the meaning and the hue is secondary — the same rule the glyph
 * vocabulary holds everywhere else on this page.
 *
 * It wraps as a whole at narrow widths, never inside a term: `flex-wrap` on the
 * container with `white-space: nowrap` on each term is what buys that.
 */
.uw__split {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 4px 10px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 12px;
  line-height: 1.3;
  text-align: right;
}

.uw__term {
  white-space: nowrap;
}

.uw__term b {
  font-weight: 700;
}

/* "no data" is not a number and must not be set as one. */
.uw__nodata {
  font: italic 700 20px/1 "Playfair Display", Georgia, serif;
  color: var(--mute);
}

/*
 * The coverage bar. Hatched track = the window; solid fill = the fraction we
 * actually observed.
 *
 * IT IS A METER, NOT A TIMELINE. The fill is anchored left because a bar has to
 * start somewhere, and it says HOW MUCH was observed, never WHEN — which is why
 * the basis line under it is mandatory and states the tick counts in words.
 */
.uw__track {
  margin-top: 10px;
  height: 6px;
  background-image: var(--hatch);
  position: relative;
  overflow: hidden;
}

.uw__fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  background: var(--fg);
  transform-origin: left;
}

.is-entering .uw__fill.is-growing {
  animation: nxGrow 1s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: var(--grow-delay, 0ms);
}

@keyframes nxGrow {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.uw__basis {
  margin-top: 8px;
  font-size: 12px;
  color: var(--mute);
  line-height: 1.45;
}

/* ------------------------------------------------------- [2i] group table */

.table-wrap {
  padding: 12px 36px 0;
}

.card--table {
  padding: 18px 24px 6px;
}

.thead,
.trow {
  display: grid;
  grid-template-columns: 14px 190px 1fr 190px 80px 90px;
  gap: 14px;
  align-items: center;
}

.thead {
  padding: 0 0 10px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 10.5px;
  letter-spacing: 0.08em;
  color: var(--dim);
  text-transform: uppercase;
}

.thead > span:nth-child(5),
.thead > span:nth-child(6) {
  text-align: right;
}

.trow {
  padding: 11px 0;
  border-top: 1px solid var(--line);
  font-size: 13.5px;
  text-decoration: none;
  color: var(--fg);
  transition: background-color 120ms ease-out;
}

.trow:hover {
  background: var(--faint);
  box-shadow: -12px 0 0 var(--faint), 12px 0 0 var(--faint);
}

.trow:hover .trow__name {
  text-decoration: underline;
  text-decoration-color: var(--acc);
  text-underline-offset: 3px;
}

.trow__name {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.trow__vendor,
.trow__latency {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  color: var(--mute);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.trow__vendor {
  font-size: 11px;
}

.trow__latency {
  font-size: 12px;
  text-align: right;
}

.trow__state {
  font-size: 12px;
  text-align: right;
  font-weight: 500;
  /* The glyph character is part of the label, so a wrap breaks `◆ degraded`
     across two lines with the shape stranded above the word. */
  white-space: nowrap;
}

.strip--24 {
  height: 14px;
}

/*
 * A row with NO strip at all — a link-only target, which has no probe and so has
 * no observation history, ever. The design's own words go in the column;
 * 24 hatched cells would claim we looked and saw nothing, which is a different
 * and false statement.
 */
.strip-none {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  color: var(--dim);
  white-space: nowrap;
}

/* ==========================================================================
 * [2c] vendor incident and [2g] Nexus incident
 *
 * Same tokens, same cards, same verdict typography as everything above. Two
 * things here are load-bearing rather than decorative and are commented at the
 * rule: the DASHED dependent chip, and the `opened` marker's position.
 * ======================================================================== */

/* The vendor status page in the facts row is the one link inside a fact. */
.facts__link {
  color: var(--fg);
  text-decoration: none;
}

.facts__link:hover {
  text-decoration: underline;
  text-decoration-color: var(--acc);
  text-underline-offset: 3px;
}

/*
 * A DASHED chip, and it is the chip-scale echo of the `◌` glyph.
 *
 * These seven readings were WITHHELD, not taken. A solid chip would present a
 * suppressed target as one we measured — which is the single most expensive
 * misreading this screen can produce, because it turns one outage into eight.
 */
.dep--dashed {
  border-style: dashed;
  color: var(--mute);
}

/* ------------------------------------------------------------ dependency edge */

.edges {
  margin-top: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.edge {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 10px;
  align-items: center;
  font-size: 12.5px;
}

.edge__node {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 8px 10px;
  border: 1px solid var(--line);
  min-width: 0;
  text-decoration: none;
  color: var(--fg);
  transition: border-color 120ms ease-out;
}

a.edge__node:hover {
  border-color: var(--fg);
}

/* The subject carries its own state's colour; the dependent carries a hairline. */
.edge__node--degraded {
  border-color: var(--deg);
  background: var(--degbg);
}
.edge__node--down {
  border-color: var(--down);
  background: var(--downbg);
}
.edge__node--up {
  border-color: var(--up);
  background: var(--upbg);
}

.edge__label {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 10.5px;
  color: var(--dim);
  white-space: nowrap;
}

/* ----------------------------------------------------------------- timeline */

.timeline {
  position: relative;
  margin: 22px 0 0 5px;
  padding-left: 24px;
  border-left: 1px solid var(--line);
}

.tevent {
  position: relative;
  padding-bottom: 24px;
}

/*
 * The dot sits ON the rule, so it needs a ring of card colour to punch the line
 * out behind it — `outline`, not a border, because a border would move the dot.
 */
.tevent__dot {
  position: absolute;
  left: -29px;
  top: 4px;
  --g: 9px;
  outline: 4px solid var(--card);
}

.tevent__when {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  color: var(--mute);
}

.tevent__text {
  font-size: 14px;
  margin-top: 3px;
  line-height: 1.45;
}

.is-entering .tevent {
  animation: nxIn 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: var(--enter-delay, 0ms);
}

/* ------------------------------------------------------- the incident strip */

.card--istrip {
  padding: 22px 24px 18px;
  align-self: start;
}

/*
 * The ruler, and the one rule on this page that is about honesty rather than
 * looks.
 *
 * `−96 h` and `now` are the ends of the axis and sit at the ends. The `opened`
 * marker is POSITIONED, at its true fraction of that axis, because a
 * `space-between` third label lands at the midpoint whatever the data says — and
 * a word reading `opened` at the midpoint of a time axis is a claim that the
 * incident opened halfway through the window. When the opening is off the axis
 * entirely the marker is not drawn at all and `.istrip__offaxis` says so in
 * words instead.
 */
.istrip__axis {
  position: relative;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 10px;
  color: var(--dim);
  margin-top: 6px;
  /* Room for the marker's own row above the two end labels. */
  padding-top: 14px;
}

.istrip__ends {
  display: flex;
  justify-content: space-between;
}

.istrip__opened {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  white-space: nowrap;
}

.istrip__opened::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -8px;
  width: 1px;
  height: 6px;
  background: var(--dim);
}

/*
 * Pulled hard left, beside `−96 h`, rather than left in the middle slot.
 *
 * `space-between` would centre it, and a phrase sitting at the midpoint of a time
 * axis reads as a position even when its words deny one. It belongs against the
 * oldest edge, which is the direction the opening actually lies in.
 */
.istrip__offaxis {
  color: var(--dim);
  margin-right: auto;
  margin-left: 10px;
}

/* --------------------------------------------- other open incidents / recovery */

.oi__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-top: 22px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  font-size: 12.5px;
  color: var(--mute);
}

.oi__count {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  color: var(--fg);
}

.oi {
  display: grid;
  grid-template-columns: 8px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 6px 0;
  font-size: 12.5px;
  text-decoration: none;
  color: var(--fg);
  transition: background-color 120ms ease-out;
}

.oi:hover {
  background: var(--faint);
  box-shadow: -12px 0 0 var(--faint), 12px 0 0 var(--faint);
}

.oi:hover .oi__short {
  text-decoration: underline;
  text-decoration-color: var(--acc);
  text-underline-offset: 3px;
}

.oi__short {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.oi__dur {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px;
  color: var(--dim);
  white-space: nowrap;
}

.oi__empty,
.recovery {
  margin: 12px 0 0;
  font-size: 12.5px;
  color: var(--mute);
  line-height: 1.5;
}

.recovery {
  margin-top: 22px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

/* ------------------------------------------------- detail / group at ≤ 899 */

@media (max-width: 899px) {
  .detail-grid {
    grid-template-columns: 1fr;
  }
}

/*
 * The 390 layout.
 *
 * The handoff draws no mobile mockup for [2b] or [2i] — every mobile artboard is
 * an overview. So this is NOT a recreation of a drawn screen: it is the minimum
 * that keeps both screens readable and horizontally scroll-free at 390, using
 * the same reductions the overview's own 390 rules already establish (smaller
 * verdict word, tighter gutters, dropped columns that have no room). Flagged as
 * a gap rather than presented as the design's.
 */
@media (max-width: 480px) {
  .topbar--crumb {
    font-size: 11px;
    padding: 18px 4px 12px;
    gap: 10px;
  }

  .crumb {
    gap: 8px;
  }

  .detail-grid,
  .table-wrap {
    padding: 8px 0 0;
  }

  .card--content,
  .card--uptime,
  .card--table {
    padding: 18px 16px 12px;
  }

  .verdict__clause {
    font-size: 15px;
  }

  .sect__title {
    font-size: 15px;
  }

  .sect__meta {
    font-size: 10px;
  }

  /* Five columns of mono do not fit; the row keeps its title, state and when. */
  .irow {
    grid-template-columns: 10px 1fr auto;
    gap: 8px;
    row-gap: 2px;
  }
  .irow__meta {
    grid-column: 2 / -1;
  }
  .irow__when {
    grid-column: 2 / -1;
    white-space: normal;
  }

  /* The vendor host has no room at 390 and is dropped; the row still links. */
  .thead,
  .trow {
    /* `◆ degraded` is the widest state label and must not wrap; 78px holds it
       at 12px mono-adjacent type. */
    grid-template-columns: 12px 1fr 66px 78px;
    gap: 8px;
  }
  .thead > span:nth-child(4),
  .trow__vendor {
    display: none;
  }
  .thead > span:nth-child(3) {
    display: none;
  }
  /*
   * The whole WRAPPER goes, not just the strip inside it.
   *
   * The row is a four-column grid at this width and carries six children; hiding
   * the strip alone leaves its wrapper occupying a column, which pushes the last
   * two cells onto an implicit second row. `display: none` on the element that
   * is actually the grid item is the only thing that removes a column.
   */
  .trow .strip-wrap,
  .strip-none {
    display: none;
  }
  .thead > span:nth-child(5),
  .thead > span:nth-child(6) {
    text-align: right;
  }
}
