/* Webhooker status page.
   Hand-written, no framework, no build step: the file is embedded in the
   binary as it stands here. Colours are declared once as custom properties and
   redefined for dark mode, so every rule below is theme-agnostic.

   The page is laid out as an instrument panel rather than a deck of cards: one
   framed list, hairline rules between services, readings set in tabular
   figures. Cobalt is chrome only — the mark, links, focus — because the
   colours that mean something here are the three state colours, and a brand
   accent competing with them would be the one decoration that costs a reader a
   second during an outage. */

:root {
  color-scheme: light dark;

  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;

  /* `--page` is the grey the page sits on; `--surface` is what is raised off
     it — the header, the service panel, a state pill. */
  --page: #eef1f7;
  --surface: #ffffff;
  --border: #dfe4ed;
  --border-strong: #c9d1de;
  --text: #0d1320;
  --text-muted: #5a6478;

  --cobalt: #2a58d8;

  /* State colours differ in lightness as well as hue, and every use of them is
     paired with a word or a height, so nothing on this page depends on telling
     red from green.

     Each state has two tokens. The plain one fills bars and dots, where the
     shape carries the meaning and the hue only has to be told apart. The
     `-text` one is the same state set as words on `--surface`, where the glyphs
     have to be read: the fills for up and degraded measure 3.44:1 and 2.64:1
     against white, both below the 4.5:1 that WCAG AA asks of body text. Down
     already passed, and keeps its value; it has a token of its own so that a
     later change to the palette cannot quietly reintroduce the problem. */
  --state-up: #0f9d76;
  --state-up-text: #04735a;
  --state-degraded: #d9901f;
  --state-degraded-text: #8a5a00;
  --state-down: #c8372d;
  --state-down-text: #c8372d;
  --state-unknown: #8d97a3;
  --bar-no-data: #ccd2d9;

  /* The wash behind the status band and an incident row: the state colour at
     the strength of a tint, never at the strength of a fill. */
  --wash: 8%;
  --wash-border: 28%;

  --radius: 14px;
  --radius-small: 10px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --page: #070c14;
    --surface: #101825;
    --border: #212b3a;
    --border-strong: #354156;
    --text: #e7edf6;
    --text-muted: #94a0b4;

    --cobalt: #6d96ff;

    /* On `--surface` these measure 8.99:1, 9.15:1 and 6.48:1, so the fills
       are already readable as text and the two tokens coincide. */
    --state-up: #2ec99f;
    --state-up-text: #2ec99f;
    --state-degraded: #e5aa3d;
    --state-degraded-text: #e5aa3d;
    --state-down: #f0705f;
    --state-down-text: #f0705f;
    --state-unknown: #79838f;
    --bar-no-data: #313947;

    --wash: 13%;
    --wash-border: 34%;
  }
}

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--page);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.5;
  text-rendering: optimizeLegibility;
}

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

.page {
  width: 100%;
  max-width: 880px;
  margin-inline: auto;
  padding-inline: 24px;
}

/* The icon set. Every glyph is stroked in `currentColor` and sized by its
   box, so a mark takes the colour of the state it sits in. The core dot of
   each glyph is filled rather than stroked, which is what holds the set
   together at 20px. */

.icon {
  width: 100%;
  height: 100%;
  display: block;
}

/* Site header ------------------------------------------------------------- */

.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.site-header__inner {
  display: flex;
  align-items: center;
  min-height: 52px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 0.9375rem;
}

.brand__mark {
  flex: none;
  width: 24px;
  height: 24px;
}

.brand__name {
  font-weight: 640;
  letter-spacing: -0.012em;
}

/* Not a tagline: it names which Webhooker surface this is, and sits in the
   muted weight because the reader came for the state, not the label. */
.brand__section {
  color: var(--text-muted);
}

/* Holds the mark's gradient definition and nothing else. */
.brand__gradient {
  position: absolute;
  width: 0;
  height: 0;
}

/* Status band ------------------------------------------------------------- */

/* The one loud element on the page. It is washed in the state's own colour and
   carries the only oversized type, so what is happening now is legible from
   across a desk and everything below it can stay quiet. */
.status-band {
  --state: var(--state-unknown);
  --state-text: var(--text-muted);
  border-bottom: 1px solid
    color-mix(in srgb, var(--state) var(--wash-border), var(--border));
  background: color-mix(in srgb, var(--state) var(--wash), var(--surface));
}

.status-band--up {
  --state: var(--state-up);
  --state-text: var(--state-up-text);
}

.status-band--degraded {
  --state: var(--state-degraded);
  --state-text: var(--state-degraded-text);
}

.status-band--down {
  --state: var(--state-down);
  --state-text: var(--state-down-text);
}

/* A grid rather than a row, so the dot stays beside the headline when the
   headline wraps: on a phone a centred dot drifts down beside the timestamps
   and stops reading as the status light. */
.status-band__inner {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 16px;
  padding-block: 26px 22px;
}

/* The status light, and the page's only motion: one ring leaves the dot on a
   slow cycle, the way a probe leaves the prober. It is the single thing on the
   page that says the numbers below are live rather than a snapshot, so it is
   allowed to move — and it stops the moment the reader asks for less motion. */
.status-band__pulse {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--state) 18%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--state) 34%, transparent);
}

.status-band__pulse::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1.5px solid var(--state);
  opacity: 0;
  animation: radar 3.4s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
}

@keyframes radar {
  0% {
    transform: scale(1);
    opacity: 0.55;
  }
  70%,
  100% {
    transform: scale(1.85);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .status-band__pulse::after {
    animation: none;
  }
}

.status-band__dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--state);
}

.status-band__headline {
  margin: 0;
  font-size: clamp(1.5rem, 1.1rem + 1.8vw, 2.125rem);
  font-weight: 660;
  line-height: 1.12;
  letter-spacing: -0.026em;
}

.status-band__meta {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  margin: 7px 0 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.status-band__meta > span + span::before {
  content: "· ";
}

/* The count is the second fact a reader wants and the first they can act on,
   so it is set in the state's own colour rather than the muted grey. */
.status-band__count {
  font-weight: 580;
  color: var(--state-text);
}

.status-band__meta time {
  font-family: var(--font-mono);
  font-size: 0.75rem;
}

/* Main -------------------------------------------------------------------- */

main.page {
  padding-block: 22px 40px;
}

/* Open incidents ---------------------------------------------------------- */

.open-incidents {
  display: grid;
  gap: 10px;
  margin-bottom: 18px;
}

.open-incident {
  display: flex;
  gap: 12px;
  padding: 13px 15px;
  border: 1px solid color-mix(in srgb, var(--state) var(--wash-border), var(--border));
  border-left-width: 3px;
  border-left-color: var(--state);
  border-radius: var(--radius-small);
  background: color-mix(in srgb, var(--state) var(--wash), var(--surface));
  --state: var(--state-down);
  --state-text: var(--state-down-text);
}

.open-incident--degraded {
  --state: var(--state-degraded);
  --state-text: var(--state-degraded-text);
}

.open-incident__glyph {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  color: var(--state-text);
}

.open-incident__headline {
  margin: 0;
  font-weight: 600;
  color: var(--state-text);
}

.open-incident__meta {
  margin: 3px 0 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.open-incident__meta time {
  font-family: var(--font-mono);
  font-size: 0.75rem;
}

/* The upstream's own words. Set in mono because it is a machine string, and
   wrapped anywhere because a URL in it must not push the row sideways. */
.open-incident__error {
  margin: 8px 0 0;
  padding: 7px 9px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* Service list ------------------------------------------------------------ */

/* One frame around the whole list, divided by hairlines. Separate cards gave
   every service its own border, shadow and gutter, which spent a third of the
   page on the space between services rather than on the services. */
.services {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.service {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 24px;
  row-gap: 9px;
  padding: 14px 18px 15px;
}

.service + .service {
  border-top: 1px solid var(--border);
}

/* A service in trouble is lit along its edge and washed behind, so a reader
   scrolling a long list finds the failing row without reading a single pill.
   An inset shadow rather than a border: a left border would shift the row's
   contents out of line with the rows above it. */
.service--degraded {
  box-shadow: inset 3px 0 0 var(--state-degraded);
  background: color-mix(in srgb, var(--state-degraded) 5%, var(--surface));
}

.service--down {
  box-shadow: inset 3px 0 0 var(--state-down);
  background: color-mix(in srgb, var(--state-down) 5%, var(--surface));
}

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

/* The glyph says what kind of thing the row is: a probed endpoint, or a
   component read out of one endpoint's status document. */
.service__glyph {
  flex: none;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  color: var(--cobalt);
}

.service__name {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 620;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 9px 2px 7px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  font-size: 0.75rem;
  font-weight: 560;
  white-space: nowrap;
}

.state__dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--state-unknown);
}

.state--up {
  color: var(--state-up-text);
  border-color: color-mix(in srgb, var(--state-up) 45%, var(--border));
  background: color-mix(in srgb, var(--state-up) 8%, var(--surface));
}

.state--up .state__dot {
  background: var(--state-up);
}

.state--degraded {
  color: var(--state-degraded-text);
  border-color: color-mix(in srgb, var(--state-degraded) 45%, var(--border));
  background: color-mix(in srgb, var(--state-degraded) 10%, var(--surface));
}

.state--degraded .state__dot {
  background: var(--state-degraded);
}

.state--down {
  color: var(--state-down-text);
  border-color: color-mix(in srgb, var(--state-down) 45%, var(--border));
  background: color-mix(in srgb, var(--state-down) 10%, var(--surface));
}

.state--down .state__dot {
  background: var(--state-down);
}

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

/* Uptime figures ---------------------------------------------------------- */

/* Four readings on the right of the name, the way a meter row sits beside its
   label. No rules between them: the columns are held by their own rhythm and
   by tabular figures, and four hairlines in a 220px strip is noise. */
.uptime-figures {
  display: grid;
  grid-template-columns: repeat(4, minmax(46px, auto));
  column-gap: 18px;
  text-align: right;
}

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

.uptime-figure__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.9375rem;
  font-weight: 560;
  letter-spacing: -0.03em;
}

/* A percentage drawn from part of a window states less than a full one does,
   and is set to look like it. Without this the boldest number in the row is
   the one with the least behind it. What it was drawn from is on the cell's
   tooltip rather than under it. */
.uptime-figure__value--partial {
  color: var(--text-muted);
  font-weight: 460;
}

.uptime-figure__value--empty {
  color: var(--text-muted);
  font-size: 0.8125rem;
}

.uptime-figure__label {
  font-size: 0.6875rem;
  font-weight: 520;
  color: var(--text-muted);
}

.uptime-figures__missing {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
  text-align: left;
}

/* Uptime bars ------------------------------------------------------------- */

.uptime-history {
  grid-column: 1 / -1;
}

.uptime-bars {
  display: block;
  width: 100%;
  height: 30px;
}

.uptime-bar {
  fill: var(--bar-no-data);
}

.uptime-bar--up {
  fill: var(--state-up);
}

.uptime-bar--degraded {
  fill: var(--state-degraded);
}

.uptime-bar--down {
  fill: var(--state-down);
}

.uptime-bar--no-data {
  fill: var(--bar-no-data);
}

/* The hover answers a pointer that is already on a day, which is the one kind
   of motion this page has room for below the status light. */
.uptime-bar:hover {
  fill-opacity: 0.6;
}

/* The span every track in the panel covers, said once at the foot of it. */
.services__axis {
  display: flex;
  justify-content: space-between;
  margin: 0;
  padding: 9px 18px 11px;
  border-top: 1px solid var(--border);
  font-size: 0.6875rem;
  color: var(--text-muted);
}

/* Incidents --------------------------------------------------------------- */

.incidents {
  margin-top: 26px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.incidents__title,
.legend__title {
  margin: 0 0 12px;
  font-size: 0.9375rem;
  font-weight: 620;
  letter-spacing: -0.01em;
}

.incidents__empty {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.incidents__list {
  display: grid;
  gap: 11px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.incident {
  display: flex;
  gap: 11px;
}

/* Resolved, so the mark is the state the service ended in rather than the one
   it passed through. */
.incident__glyph {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 2px;
  color: var(--state-up-text);
}

.incident__headline {
  margin: 0;
  font-size: 0.875rem;
  font-weight: 560;
}

.incident__meta {
  margin: 1px 0 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.incident__meta time {
  font-family: var(--font-mono);
}

/* Legend ------------------------------------------------------------------ */

.legend {
  margin-top: 26px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.legend__items {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 8px 18px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.8125rem;
}

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

/* Drawn with the same viewBox as the bars and stretched the same way, so a
   swatch carries the height the reader will see in the track. */
.legend__swatch {
  flex: none;
  width: 10px;
  height: 22px;
}

.legend__note {
  margin: 12px 0 0;
  font-size: 0.75rem;
  color: var(--text-muted);
  max-width: 68ch;
}

/* Footer ------------------------------------------------------------------ */

.page-footer {
  border-top: 1px solid var(--border);
  padding-block: 16px 28px;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.page-footer p {
  margin: 0;
  max-width: 68ch;
}

/* Narrow screens ---------------------------------------------------------- */

/* The readings drop under the name rather than shrinking beside it: four
   columns and a name in 360px leaves neither of them readable. */
@media (max-width: 680px) {
  .page {
    padding-inline: 16px;
  }

  .status-band__inner {
    column-gap: 13px;
    padding-block: 22px 20px;
  }

  .status-band__pulse {
    width: 32px;
    height: 32px;
  }

  /* One fact per line, so the separators have nothing to separate: a line
     that opens with a stray middle dot reads as a bullet list nobody asked
     for. */
  .status-band__meta {
    flex-direction: column;
  }

  .status-band__meta > span + span::before {
    content: none;
  }

  .status-band__dot {
    width: 12px;
    height: 12px;
  }

  .service {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 10px;
    padding: 13px 15px 14px;
  }

  .uptime-figures {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    column-gap: 10px;
    text-align: left;
  }

  .uptime-bars {
    height: 26px;
  }

  .services__axis {
    padding-inline: 15px;
  }
}

@media (max-width: 400px) {
  .service__identity {
    flex-wrap: wrap;
    row-gap: 6px;
  }

  .service__name {
    white-space: normal;
  }
}
