:root {
  color-scheme: dark light;
  --bg: #0d1117;
  --panel: #161b22;
  --panel-border: #30363d;
  --text: #e6edf3;
  --muted: #8b949e;
  --accent: #58a6ff;
  --good: #3fb950;
  --bad: #f85149;
  --warn: #d29922;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 0.92em; }

.topbar {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 14px 28px;
  border-bottom: 1px solid var(--panel-border);
  background: var(--panel);
}

.brand { font-weight: 700; font-size: 16px; color: var(--text); }
.nav { display: flex; gap: 18px; flex: 1; flex-wrap: wrap; }
.nav-link { color: var(--muted); padding-bottom: 2px; }
.nav-link[aria-current="page"] { color: var(--text); border-bottom: 2px solid var(--accent); }

.banner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 12px;
  padding: 12px 28px;
  border-bottom: 1px solid var(--panel-border);
}

.banner-stopped { background: rgba(248, 81, 73, 0.14); border-bottom-color: var(--bad); }
.banner-warn { background: rgba(210, 153, 34, 0.14); border-bottom-color: var(--warn); }
.banner span { color: var(--muted); }

.content { flex: 1; padding: 28px; max-width: 1180px; width: 100%; margin: 0 auto; }
.breadcrumbs { color: var(--muted); font-size: 13px; margin-bottom: 14px; }
.crumb-separator { padding: 0 4px; }

h1 { font-size: 22px; margin: 0 0 6px; }
h2 { font-size: 16px; margin: 0 0 12px; display: flex; align-items: center; gap: 6px; }
.muted { color: var(--muted); }
.lede { color: var(--muted); margin: 0 0 22px; }

.panel {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 18px 20px;
  margin-bottom: 20px;
}

.error-panel { border-color: var(--bad); }
.danger-panel { border-color: var(--bad); }

/* A heading and the control that governs it, on one row. The control belongs to this chart rather
   than to the page — the overview carries a second chart directly below — so it sits inside the
   panel's own header instead of above it. It wraps beneath the heading on a narrow viewport rather
   than pushing the panel into a horizontal scroll. */
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin: 0 0 12px;
}

.panel-head h2 { margin: 0; }

.range-picker { display: flex; align-items: center; gap: 8px; margin: 0; }

.range-picker-label { color: var(--muted); font-size: 13px; }

.range-picker-select {
  font: inherit;
  border-radius: 6px;
  padding: 6px 10px;
  border: 1px solid var(--panel-border);
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
}

.range-picker-select:hover { border-color: var(--accent); }
.range-picker-apply { padding: 6px 12px; }

.stat-row { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 20px; }

.stat-card {
  position: relative;
  flex: 1 1 220px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 16px 18px;
}

.stat-label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.stat-value { font-size: 26px; font-weight: 600; margin-top: 6px; }
.stat-value.good { color: var(--good); }
.stat-value.bad { color: var(--bad); }
.stat-value.warn { color: var(--warn); }
.stat-note { color: var(--muted); font-size: 12px; margin-top: 6px; }

.info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid var(--muted);
  color: var(--muted);
  font-size: 11px;
  font-style: italic;
  font-weight: 700;
  cursor: help;
}

.stat-card .info { position: absolute; top: 12px; right: 12px; }

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--panel-border); vertical-align: top; }
th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
tbody tr:last-child td { border-bottom: none; }
td.numeric { text-align: right; font-variant-numeric: tabular-nums; }

/* A table never scrolls inside the page: it takes the content's width, and a long word or address
   wraps rather than widening it. On a narrow screen each row stacks into label and value lines
   (see the 900 px media query below), so the page has one scrollbar, the document's own. */
td { overflow-wrap: anywhere; }

.timestamp { display: flex; flex-direction: column; cursor: help; }
.timestamp-relative { color: var(--text); font-weight: 600; }
.timestamp-precise { color: var(--muted); font-size: 12px; }
.timestamp-unparsed .timestamp-relative { color: var(--warn); font-weight: 600; }
.timestamp-overdue .timestamp-relative { color: var(--bad); font-weight: 600; }

.badge {
  display: inline-block;
  border-radius: 999px;
  padding: 2px 9px;
  font-size: 12px;
  border: 1px solid var(--panel-border);
  color: var(--muted);
  cursor: help;
}

.badge.on { border-color: var(--good); color: var(--good); }
.badge.off { border-color: var(--muted); color: var(--muted); }
.badge.stopped { border-color: var(--bad); color: var(--bad); }
.badge.warn { border-color: var(--warn); color: var(--warn); }
.badge.address { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; cursor: auto; }

.empty { color: var(--muted); padding: 18px 0; }

form.controls { display: flex; flex-direction: column; gap: 14px; max-width: 620px; }
form.controls label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--muted); }
form.controls label > span { display: block; }
form.controls .checkbox { flex-direction: row; align-items: center; gap: 8px; color: var(--text); font-size: 14px; }

input[type="text"] {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 8px 10px;
  font: inherit;
  width: 100%;
}

button {
  font: inherit;
  font-weight: 600;
  border-radius: 6px;
  padding: 8px 14px;
  border: 1px solid var(--panel-border);
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
  align-self: flex-start;
}

button:hover { border-color: var(--accent); }
button.danger { border-color: var(--bad); color: var(--bad); }
button.danger:hover { background: rgba(248, 81, 73, 0.12); }
button:disabled { opacity: 0.5; cursor: not-allowed; border-color: var(--panel-border); }

/*
 * The per-row terminate control. Deliberately quieter than button.danger: a table of 150 rows each
 * shouting in red reads as an alarm about the fleet rather than an available action, and this button
 * only opens a confirmation — the loud one is on the confirmation itself, where the destructive
 * click actually happens.
 */
/* A link that looks like a button, for actions that navigate rather than submit. */
.button-link {
  display: inline-block;
  text-decoration: none;
  border-radius: 6px;
  border: 1px solid var(--panel-border);
  background: var(--panel);
}
.button-link:hover { border-color: var(--accent); }

button.danger-quiet, a.danger-quiet {
  padding: 4px 10px;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--muted);
}
button.danger-quiet:hover, a.danger-quiet:hover { border-color: var(--bad); color: var(--bad); }

/*
 * The age histogram. Colours come from CSS rather than fill attributes so the chart follows the
 * theme; the geometry stays in SVG attributes because style-src forbids inline styles.
 */
.histogram { display: block; max-width: 100%; height: auto; }
.hist-bar { fill: var(--accent); }
.hist-axis { stroke: var(--panel-border); stroke-width: 1; }
.hist-grid { stroke: var(--panel-border); stroke-width: 1; opacity: 0.4; }
.hist-ytick { fill: var(--muted); font-size: 11px; text-anchor: end; dominant-baseline: middle; }
.hist-xlabel { fill: var(--muted); font-size: 11px; text-anchor: end; dominant-baseline: middle; }
.hist-value { fill: var(--text); font-size: 11px; font-weight: 600; text-anchor: middle; dominant-baseline: middle; }
.hist-axis-title { fill: var(--muted); font-size: 11px; text-anchor: middle; dominant-baseline: middle; }
/* The time chart carries its own axis classes rather than borrowing the histogram's. They style
   identically today, but they are separate charts: sharing selectors made a histogram assertion
   start matching the time chart's ticks as well as its own. */
.time-grid { stroke: var(--panel-border); stroke-width: 1; opacity: 0.4; }
.time-axis { stroke: var(--panel-border); stroke-width: 1; }
.time-ytick { fill: var(--muted); font-size: 11px; text-anchor: end; dominant-baseline: middle; }
.time-axis-title { fill: var(--muted); font-size: 11px; text-anchor: middle; dominant-baseline: middle; }
.time-xlabel { fill: var(--muted); font-size: 11px; text-anchor: start; dominant-baseline: middle; }
.time-xlabel-end { fill: var(--muted); font-size: 11px; text-anchor: end; dominant-baseline: middle; }
.timechart { display: block; max-width: 100%; height: auto; }
/* Observed and projected are drawn differently on purpose: one is a record of what the service
   measured, the other is arithmetic on deadlines that assumes nothing is created or renewed. A
   single continuous line would give the guess the authority of the measurement. */
.series-observed { fill: none; stroke: var(--accent); stroke-width: 2; }
.series-observed-point { fill: var(--accent); }
.series-projected { fill: none; stroke: var(--accent); stroke-width: 2; stroke-dasharray: 4 4; opacity: 0.75; }
.series-now { stroke: var(--panel-border); stroke-width: 1; stroke-dasharray: 2 3; }
.series-now-label { fill: var(--muted); font-size: 10px; text-anchor: middle; dominant-baseline: middle; }

.report-list { margin: 8px 0 0; padding-left: 18px; }
.report-list li { margin-bottom: 4px; }
.report-failure { color: var(--bad); }

.footer {
  display: flex;
  gap: 18px;
  padding: 14px 28px;
  border-top: 1px solid var(--panel-border);
  color: var(--muted);
  font-size: 12px;
}

/*
 * Regions whose data has not arrived. Every placeholder is shaped like what it stands in for and
 * reserves its space from the first frame, so nothing shifts when the data lands. Each one enters
 * slowly — over a second — so a fast answer retracts it, from wherever it had reached, before the
 * reader has really seen it; only a wait long enough to matter brings it fully into view.
 */
.ph {
  display: inline-block;
  position: relative;
  overflow: hidden;
  vertical-align: middle;
  height: 0.75em;
  width: 4.5em;
  border-radius: 4px;
  background: var(--panel-border);
  transform-origin: left center;
  animation: ph-enter 1s ease-out both;
}
/* The pulse lives on a layer of its own so the entrance is the only animation the bar itself plays,
   and reversing it never disturbs the pulse. */
.ph::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--muted);
  opacity: 0;
  animation: ph-pulse 1.8s ease-in-out 1s infinite;
}
.ph-value { width: 2.8em; height: 0.7em; }
.ph-cell { width: 70%; min-width: 2.5em; }
.stat-indeterminate { display: flex; align-items: center; gap: 10px; min-height: 39px; }
.awaiting { color: var(--muted); font-size: 13px; font-weight: 400; animation: ph-enter-fade 1s ease-out both; }
.awaiting.failed { color: var(--bad); }
.awaiting-line { margin: 10px 0 0; }
.ph-row td { border-bottom-color: transparent; }
/* A placeholder cell takes the lines of the cell it stands in for, so a grey row is as tall as the
   row that replaces it: two stacked lines, a badge, a stacked time (its local line shown in a
   non-UTC timezone, as a real time's is), or a row action's button. */
.ph-lines { display: flex; flex-direction: column; align-items: flex-start; }
.ph-small { font-size: 12px; }
.ph-badge { display: inline-block; font-size: 12px; padding: 2px 9px; border: 1px solid transparent; }
.ph-action { display: inline-block; font-size: 0.85rem; font-weight: 500; padding: 4px 10px; border: 1px solid transparent; }
/* Each placeholder line holds invisible sample text like the real line's, under its grey bar, so it
   wraps and widens its column as a real line does. The text is generated content: it is not part of
   the page's text and is never read out. */
.ph-text { position: relative; display: inline-block; max-width: 100%; vertical-align: top; }
.ph-text > .ph { position: absolute; left: 0; top: 50%; margin-top: -0.375em; max-width: 100%; }
.ph-text::after { visibility: hidden; }
.ph-s-name::after { content: "buildtest-shard-00"; font-weight: 700; }
.ph-s-id::after { content: "486231004"; }
.ph-s-account::after { content: "default"; }
.ph-s-status::after { content: "active"; }
.ph-s-region::after { content: "nyc3"; }
.ph-s-size::after { content: "s-4vcpu-8gb"; }
.ph-s-specs::after { content: "4 vCPU \B7  8192 MB \B7  160 GB"; }
.ph-s-address::after { content: "198.51.100.24"; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; }
.ph-s-duration::after { content: "1h 13m"; font-weight: 600; }
.ph-s-utc::after { content: "2026-08-17 19:40:00 UTC"; }
.ph-s-local::after { content: attr(data-sample); }
.ph-s-action::after { content: "Terminate\2026"; }
.ph-s-account-url::after { content: "url://digitalocean-droplets/accounts/default"; }
.ph-s-count::after { content: "12"; }
.ph-s-stop::after { content: "running"; }
.ph-s-utc-15::after { content: "2026-08-17 20:43:20 UTC"; }
.ph-s-step::after { content: "DECLARED"; }
.ph-s-actor::after { content: "operator"; }
.ph-s-detail::after { content: "Emergency mode declared for this scope; new droplet creation is refused."; }
.last-known { color: var(--muted); font-size: 12px; }
.stat-note.last-known, p.last-known { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 4px 6px; }
.last-known-value { white-space: nowrap; }
.lede .timestamp { display: inline-flex; vertical-align: top; }
.region-failure { color: var(--bad); }
.badge.unknown { border-color: var(--muted); color: var(--muted); }
.banner-unknown { padding: 6px 28px; color: var(--muted); font-size: 13px; align-items: center; }

/* The emergency banner reserves room for a current declaration and dated saved-state history, so a
 * new reading keeps the page in place. Nothing in it is cut off: a phone has no tooltip to read the
 * rest from, so a long status message wraps and the banner grows with it. */
.banner-slot { min-height: 11rem; }
.banner-slot > .banner { min-height: 11rem; flex-wrap: wrap; align-items: center; align-content: center; padding: 12px 28px; }
.banner-slot > .banner > * { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
.banner-slot > .banner-history { flex-wrap: wrap; align-content: center; gap: 2px 8px; }
.banner-history > .emergency-history-label, .banner-history > .badge { flex: 0 0 auto; }
.banner-current-history > .current-emergency-reading { flex: 0 0 100%; display: flex; flex-wrap: wrap; gap: 2px 8px; }
.current-emergency-reading > * { min-width: 0; }
.current-emergency-reading > strong { flex: 0 0 100%; line-height: 1.5; }
.current-emergency-reading > span { flex: 0 0 100%; }
.current-emergency-detail > span { display: block; min-width: 0; line-height: 1.5; }
.current-emergency-reading > a { flex: 0 0 auto; white-space: nowrap; line-height: 1.5; }
.banner-slot > .banner-stopped > span, .banner-slot > .banner-warn > span { flex: 1 1 auto; }
.banner-slot > .banner > a { flex: 0 0 auto; }
.banner-slot .region-failure { color: var(--bad); }
/* A counter's note has room for a last-known line and its stacked time (relative, local and UTC),
   filled or not. */
.stat-note { min-height: 6.5em; }
/*
 * Reserved places. Every region reserves, from the first frame, the room its expected content takes —
 * as a minimum, never a fixed height: content that turns out taller grows its region and the page
 * grows with it, so nothing ever scrolls inside the page (GOOD_DESIGN.md "Only the Page Scrolls").
 * A table reserves its rows by showing as many placeholder rows, shaped like real ones, as it last
 * held; once a region lands, the page script keeps the room it had so a shorter answer does not
 * pull what is below it up.
 */
/* A region contains its content's margins, so the room it holds is all the room its content takes. */
/* :where() adds no specificity, so a region whose own class gives it a display (the counters' flex
   row, the freshness line) keeps it. */
:where([data-slot]) { display: flow-root; }
/* The droplets page's count line has room for "awaiting data" and a stacked last-known time. */
.lede-slot { min-height: 4em; }
/* A table's header row stays in view while the page scrolls past a long table. */
.table-region thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
/* The chart's notes keep room beneath the plot for the longest of them, the one that says the line
   stops short of its window, at every width: that note is about 645 characters, so the narrower the
   page the more lines it takes (measured: 120 px at 1236 px wide and over, 158 px at 900, 225 px at
   641, 428 px at 375, 563 px at 320). Each calc() is a straight line through those measurements on
   one side of 640 px, set a little high so a classic scrollbar's width is covered; 8em is the
   desktop's. */
.chart-notes { min-height: max(8em, calc(349px - 17.65vw), calc(925px - 106vw)); margin-top: 8px; }
.chart-notes > p { margin: 0 0 6px; }
/* Reserve the chart, unreadable-age note and recorded-date lines before data arrives. */
.ages-slot { min-height: calc(292px + 8em); }
/*
 * One account's page and the admin page reserve their controls' place before the controls arrive: the
 * stop's panel is as tall as the state it is expected to show (the declared state's two forms when the
 * last-known record says a stop was declared, the clear state's one form otherwise), and the kill's
 * panel as tall as its dry-run form. Content that needs more — a long failure, a previous setup
 * attempt — grows its place. An action's result page has nothing still to arrive, and its panels take
 * the room they need.
 */
.form-slot { margin-bottom: 20px; }
.form-slot > :last-child { margin-bottom: 0; }
.stop-slot { min-height: 27rem; }
.stop-slot-declared { min-height: 40rem; }
.kill-slot { min-height: 16.5rem; }
.record-region { min-height: 14rem; }
/* An address line wraps on a narrow screen rather than being cut off. */
.lede-line { overflow-wrap: anywhere; }
/* Room for the as-of time (relative, local and UTC) and two lines beneath it: the setup note while
   connecting, or the red notice once the page is out of date, and a problem with the last-known
   record. More than that grows it. */
.freshness { min-height: 6.5rem; }
.freshness-shown { display: contents; }
/* A region that could not be loaded stops saying a read is under way. */
[data-slot-state="failed"] .awaiting, [data-slot-state="failed"] .chart-pending,
[data-slot-state="failed"] .chart-pending-label { animation: none; opacity: 1; }

/* The chart's pending band: the stretch from the last known point to the right-hand edge. It reads as
   awaited rather than empty, and gives way ahead of the line when the data arrives. */
.chart-pending { fill: var(--muted); fill-opacity: 0.1; animation: ph-enter-fade 1s ease-out both; }
.chart-pending-leaving { transform-box: fill-box; transform-origin: right center; }
.chart-pending-label { fill: var(--muted); font-size: 12px; text-anchor: middle; dominant-baseline: middle; animation: ph-enter-fade 1s ease-out both; }
.chart-projection-label { fill: var(--muted); font-size: 10px; text-anchor: end; }
.chart-boundary { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 2 3; }

/* "Up-to-date as of …": quiet while current, with a red notice once it is not. */
.freshness { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 4px 8px; margin: -12px 0 18px; font-size: 13px; color: var(--muted); }
.freshness-label, .freshness-unknown { padding-top: 1px; }
.freshness-stale { flex-basis: 100%; margin: 4px 0 0; color: var(--bad); font-weight: 600; cursor: help; }
.freshness-setup { flex-basis: 100%; margin: 0; }
.freshness .report-failure { flex-basis: 100%; margin: 0; }

/* Data arriving: rows slide in from the top one shortly after another; other values settle in. */
.row-arrive { animation: row-arrive 320ms ease-out both; }
.value-arrive { animation: value-arrive 300ms ease-out both; }

@keyframes ph-enter { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }
@keyframes ph-enter-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ph-pulse { 0%, 100% { opacity: 0; } 50% { opacity: 0.25; } }
@keyframes row-arrive { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@keyframes value-arrive { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }

/* A phone-sized screen wraps every line more, so each reserved place is taller there. */
@media (max-width: 640px) {
  .banner-slot, .banner-slot > .banner { min-height: 15rem; }
  .banner-slot .banner-history { flex-wrap: wrap; align-content: center; gap: 2px 8px; }
  
  .banner-history > .last-known-as-of { display: none; }
  .banner-history > .timestamp { flex: 0 0 100%; flex-direction: column; gap: 0; font-size: 11px; line-height: 1.25; }
  .banner-history .timestamp-precise { font-size: 10px; }

  .freshness { min-height: 10rem; }
  .lede-slot { min-height: 7em; }
  .stop-slot { min-height: 30rem; }
  .stop-slot-declared { min-height: 47rem; }
  .kill-slot { min-height: 29rem; }
}

@media (max-width: 900px) {
  /* Below the width the droplets table needs, a table's rows stack: each cell is a line of its own,
     its column's heading beside its value, so the table fits the screen instead of scrolling sideways
     inside it or widening the page. The heading row stays for screen readers only. */
  .table-wrap table, .table-wrap tbody, .table-wrap tr, .table-wrap td { display: block; }
  .table-wrap thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .table-wrap tr { padding: 8px 0; border-bottom: 1px solid var(--panel-border); }
  .table-wrap tbody tr:last-child { border-bottom: none; }
  .table-wrap td { position: relative; min-height: calc(1.5em + 6px); padding: 3px 0 3px 8.5rem; border-bottom: none; }
  .table-wrap td::before {
    /* Drawn for the eye only: the empty alternative text keeps assistive technology from reading the
       label on top of the column header it already announces. */
    content: attr(data-label) / "";
    position: absolute;
    left: 0;
    top: 3px;
    width: 8rem;
    color: var(--muted);
    font-size: 12px;
    line-height: 22px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .table-wrap td.numeric { text-align: left; }
}

/* Reduced motion: the same per-region swap, with nothing animated. */
@media (prefers-reduced-motion: reduce) {
  .ph, .awaiting, .chart-pending, .chart-pending-label, .row-arrive, .value-arrive { animation: none; }
  .ph, .awaiting { opacity: 1; transform: none; }
}

/* The pulse lives on the placeholder's own layer, so it is stopped separately: under reduced motion
   nothing on the page moves, a pulse included. */
@media (prefers-reduced-motion: reduce) {
  .ph::after { animation: none; }
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #ffffff;
    --panel: #f6f8fa;
    --panel-border: #d0d7de;
    --text: #1f2328;
    --muted: #656d76;
    --accent: #0969da;
    --good: #1a7f37;
    --bad: #cf222e;
    --warn: #9a6700;
  }
}