/* SALSA dashboard styles. Palette: dataviz reference system (light + dark). */

:root {
  color-scheme: light;
  --surface: #fcfcfb;
  --plane: #efeee9;          /* 1.13:1 under the card: a card has to be a step, not a hint */
  --ink: #0b0b0b;
  --ink-2: #52514e;
  --muted: #898781;          /* chart chrome and non-text marks only */
  --text-muted: #6c6b66;     /* small muted text: >= 4.5:1 on plane, surface and wash */
  --grid: #e1e0d9;
  --axis: #c3c2b7;
  --border: rgba(11, 11, 11, 0.14);
  --wash: rgba(11, 11, 11, 0.05);
  --scroll-shadow: rgba(11, 11, 11, 0.16);
  /* Every slot clears 3:1 on --surface, and every set that actually shares a plot --
     GHI/RHI/POA/BOA and x1..x6 -- passes scripts/validate_palette.js on all pairs, which two
     of the three sets failed before. Eight colours that all clear 3:1 on a near-white surface
     cannot also all be told apart: the sRGB ceiling is below the validator's floor, and the
     legend, the end labels and the table view are what cover that. Two hues had to move:
     amber at 3:1 lands on top of slot 2's orange, and a green near slot 3 cannot be separated
     from it. Hue is held within 6 degrees of the dark value, so a channel keeps its identity
     across a theme change. */
  --series-1: #2a78d6;       /* GHI    blue    4.30:1 */
  --series-2: #df6401;       /* RHI    orange  3.43:1 */
  --series-3: #009965;       /* POA    green   3.56:1 */
  --series-4: #67428b;       /* BOA    plum    7.50:1 */
  --series-5: #d36aa7;       /*        rose    3.20:1 */
  --series-6: #456000;       /*        olive   6.99:1 */
  --series-7: #5144ac;       /*        indigo  7.38:1  (the station never reaches slots 7-8;
                                  against slot 4 this pair separates by less than the floor) */
  --series-8: #da534f;       /*        red     3.84:1 */
  /* A status mark is not text, so it needs 3:1 -- and it needs it against the plane as well
     as the card, in case it ever moves off one. Four of these were under 3:1; --warning was
     at 1.79 and --unknown at 1.75. */
  --good: #009d01;
  --warning: #b27c00;
  --serious: #d06a41;
  --critical: #d03b3b;
  --neutral: #888680;
  --unknown: #88877d;
  --focus: #2a78d6;
  /* Device map. One sequential blue, five steps taken at 0, 3, 6, 9 and 12 of the documented
     13-step ramp the spectrogram already uses, so the dashboard has a single sequential
     language. Never an amber: it would sit next to --warning and read as a fault. The label
     token is a property of its own step, so the pairing survives the dark-mode reversal. */
  --pv-0: #cde2fb;
  --pv-0-label: #0b0b0b;
  --pv-1: #86b6ef;
  --pv-1-label: #0b0b0b;
  --pv-2: #3987e5;
  --pv-2-label: #0b0b0b;
  --pv-3: #1c5cab;
  --pv-3-label: #ffffff;
  --pv-4: #0d366b;
  --pv-4-label: #ffffff;
  /* Structure in the drawing: three recessive greys, because --grid and --axis are tuned for
     chart chrome and go the wrong way round on the dark surface. */
  --rack-env: #d9d8ce;
  --rack-part: #c9c8bd;
  --rack-rail: #b0afa3;
  /* The hue the 3D layer paints a selected solid with. It is rack.js's own material colour,
     not --focus: the swatch in that view's legend has to be the colour on the canvas. */
  --rack-accent: #1f6fd0;
  --gutter: 16px;
  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
  /* One series each for space, radius, type and motion, so a value on this page is chosen
     from a set rather than invented. Layout space is a multiple of 4; the 2 px half-step is
     for a control's own box, which is sized by its min-height rather than by the page rhythm. */
  --s-1: 2px;
  --s-2: 4px;
  --s-3: 8px;
  --s-4: 12px;
  --s-5: 16px;
  --s-6: 20px;
  --s-7: 24px;
  --s-9: 48px;
  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 12px;
  --r-pill: 999px;
  /* Negative tracking only at >= 17 px, none at 13-15 px, positive only on the eyebrow -- which
     is the page's one uppercase style. No web font is possible under font-src 'self', so the
     instrument feel comes from weight, case, tracking and figure style instead of a typeface. */
  --t-figure: 1.625rem;
  --t-h1: 1.25rem;
  --t-h2: 1.0625rem;
  --t-h3: 0.9375rem;
  --t-body: 0.875rem;
  --t-meta: 0.8125rem;
  --t-micro: 0.75rem;
  --t-eyebrow: 0.6875rem;
  /* A refresh every 30-60 s: motion is here to say what changed, never to decorate. */
  --dur-1: 90ms;
  --dur-2: 160ms;
  --dur-3: 260ms;
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  --content: 1280px;
  --measure: 68ch;          /* ~78 characters at 13 px: the top of the comfortable band */
  --sticky-h: 0px;          /* measured from the filter bar itself, see app.js */
  /* A solid hairline, so it composites the same over the card and over the plane. Never a
     glyph colour: at 1.18:1 on the plane a rule is readable as an edge and nothing else. */
  --rule: #dedcd5;
  --pop-shadow: 0 2px 10px rgba(11, 11, 11, 0.10);   /* only for what floats over a canvas */
  --pad-card: 16px;
  --track: #88877d;         /* a control boundary, so >= 3:1 rather than --grid's 1.29 */
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    color-scheme: dark;
    --surface: #1a1a19;
    --plane: #0a0a0a;
    --ink: #ffffff;
    --ink-2: #c3c2b7;
    --muted: #898781;
    --text-muted: #9a9892;
    --grid: #2c2c2a;
    --axis: #383835;
    --border: rgba(255, 255, 255, 0.16);
    --wash: rgba(255, 255, 255, 0.06);
    --scroll-shadow: rgba(0, 0, 0, 0.55);
    --series-1: #3b92f2;
    --series-2: #b05e14;
    --series-3: #008362;
    --series-4: #7f5fb7;
    --series-5: #dd629d;
    --series-6: #8c9f00;
    --series-7: #8084e4;
    --series-8: #c56365;
    --good: #0ca30c;
    --warning: #fab219;
    --serious: #ec835a;
    --critical: #e05654;
    --neutral: #9a9892;
    --unknown: #7f7e78;
    --focus: #3987e5;
    /* The ramp is reversed for the dark surface: "more" must stay "further from the page",
       and on #1a1a19 that is the pale end. Each step keeps the label colour its own hex needs. */
    --pv-0: #0d366b;
    --pv-0-label: #ffffff;
    --pv-1: #1c5cab;
    --pv-1-label: #ffffff;
    --pv-2: #3987e5;
    --pv-2-label: #0b0b0b;
    --pv-3: #86b6ef;
    --pv-3-label: #0b0b0b;
    --pv-4: #cde2fb;
    --pv-4-label: #0b0b0b;
    --rack-env: #393936;
    --rack-part: #4a4945;
    --rack-rail: #5c5b55;
    --rack-accent: #6aa8f0;
    --rule: #2c2c2a;
    --pop-shadow: 0 2px 12px rgba(0, 0, 0, 0.60);
    --track: #767570;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --surface: #1a1a19;
  --plane: #0a0a0a;
  --ink: #ffffff;
  --ink-2: #c3c2b7;
  --muted: #898781;
  --text-muted: #9a9892;
  --grid: #2c2c2a;
  --axis: #383835;
  --border: rgba(255, 255, 255, 0.16);
  --wash: rgba(255, 255, 255, 0.06);
  --scroll-shadow: rgba(0, 0, 0, 0.55);
  --series-1: #3b92f2;
  --series-2: #b05e14;
  --series-3: #008362;
  --series-4: #7f5fb7;
  --series-5: #dd629d;
  --series-6: #8c9f00;
  --series-7: #8084e4;
  --series-8: #c56365;
  --good: #0ca30c;
  --warning: #fab219;
  --serious: #ec835a;
  --critical: #e05654;
  --neutral: #9a9892;
  --unknown: #7f7e78;
  --focus: #3987e5;
  --pv-0: #0d366b;
  --pv-0-label: #ffffff;
  --pv-1: #1c5cab;
  --pv-1-label: #ffffff;
  --pv-2: #3987e5;
  --pv-2-label: #0b0b0b;
  --pv-3: #86b6ef;
  --pv-3-label: #0b0b0b;
  --pv-4: #cde2fb;
  --pv-4-label: #0b0b0b;
  --rack-env: #393936;
  --rack-part: #4a4945;
  --rack-rail: #5c5b55;
  --rack-accent: #6aa8f0;
  --rule: #2c2c2a;
  --pop-shadow: 0 2px 12px rgba(0, 0, 0, 0.60);
  --track: #767570;
}

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

/* A control the page injects later must not fall through to the UA's own typeface. Every
   control rule below already says this; saying it once here makes it true of the next one. */
button, input, select, textarea { font: inherit; }
/* Several cards are <figure> elements, which the UA gives `margin: 1em 40px`. The sky card
   sat 40 px inside every other card and 80 px narrower than the grid because of it. */
figure { margin: 0; }

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

body {
  margin: 0;
  background: var(--plane);
  color: var(--ink);
  font: var(--t-body)/1.45 var(--font);
  /* Every digit on this page is a measurement: proportional figures make a value jump
     sideways when it refreshes, and a slashed zero is the one instrument cue a system font
     will give you. Sentences opt back out below. */
  font-variant-numeric: tabular-nums slashed-zero lining-nums;
  font-optical-sizing: auto;
  padding: 0 max(var(--gutter), env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(var(--gutter), env(safe-area-inset-left));
  overflow-x: hidden;
}

h1, h2, h3 { margin: 0; font-weight: 600; }
h1 { font-size: var(--t-h1); line-height: 1.25; letter-spacing: -0.018em; }
h2 { font-size: var(--t-h2); line-height: 1.3; letter-spacing: -0.012em; }
h3 { font-size: var(--t-h3); line-height: 1.35; letter-spacing: -0.005em; }
/* A section's heading is the page's middle step; a card's heading is one below it. The page
   had no size between 16.8 px and 28 px, so the hierarchy had a hole in the middle. */
.section-head h2 { font-size: var(--t-h1); letter-spacing: -0.018em; }
p { margin: 0; }

.secondary { color: var(--ink-2); }
.muted { color: var(--text-muted); }
.small { font-size: var(--t-meta); }
.nowrap { white-space: nowrap; }
/* A phrase that moves to the next line as a whole, and wraps inside only when it cannot fit. */
.keep { display: inline-block; }
[hidden] { display: none !important; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

a { color: inherit; }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-sm); }

.skip-link { position: absolute; left: -9999px; top: 0; }
.skip-link:focus { left: var(--gutter); top: 8px; background: var(--surface); padding: 6px 10px; z-index: 10; }

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

.site-header {
  max-width: var(--content);
  margin: 0 auto;
  padding: max(14px, env(safe-area-inset-top)) 0 10px;
  display: grid;
  gap: 6px;
}
/* Title and "Sign out" share one line at every width; a long title wraps inside its column. */
.header-top { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px 12px; }
.header-actions { display: flex; align-items: center; gap: 10px; }
.header-status { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-1) var(--s-4); }
/* The status pill and the three readings are one sentence, so they are separated by a mark
   rather than by three rows of their own. The mark hangs off the item in front of it, not the
   one behind: a flex row that wraps carries each item's own boxes with it, so a leading mark
   would open a line. `:has(~ ...)` rather than `+`: a part the API left empty is skipped, and
   the part before it still finds a successor and keeps its separator. */
.header-status > *:not(:empty):has(~ *:not(:empty))::after {
  content: "\00a0·";
  color: var(--text-muted);
}

/* The masthead: the station itself, with the wordmark and the university's crest on a scrim
   at the foot of the photograph. Everything on that scrim is white in both themes, because
   what it has to be legible against is the picture, not the page. The photograph is cropped
   by the browser rather than by the asset, so one file serves every width; the crop keeps the
   dish at the left and the array across the frame, which is what makes the place recognisable. */
.masthead {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--wash);
}
.masthead-photo { display: block; }
.masthead-photo img {
  display: block;
  width: 100%;
  height: clamp(200px, 26vw, 400px);
  object-fit: cover;
  /* The file is a third sky; anchoring high keeps that share when the band is
     wider than the file and the crop has to give something up at the foot. */
  object-position: 50% 30%;
  background: var(--wash);
}
.masthead-bar {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px 24px;
  padding: 64px 18px 14px;
  color: #ffffff;
  background: linear-gradient(to top,
    rgba(6, 8, 10, 0.92) 0%, rgba(6, 8, 10, 0.80) 30%, rgba(6, 8, 10, 0.40) 64%, rgba(6, 8, 10, 0) 100%);
}
/* The lockup is the masthead's only wordmark, so it carries the size on its own. */
.masthead-title { display: grid; min-width: 0; font-size: var(--t-h2); }
.masthead-lockup { display: block; width: auto; height: 64px; }
.masthead-crest { display: block; flex: none; width: 208px; height: auto; }
.masthead-caption { display: flex; flex-wrap: wrap; gap: 0 10px; }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .masthead-photo img { filter: brightness(0.84); }
}
:root[data-theme="dark"] .masthead-photo img { filter: brightness(0.84); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px 3px 6px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: var(--surface);
  font-weight: 600;
  font-size: var(--t-h3);
}

.status-icon { display: inline-flex; width: 16px; height: 16px; flex: none; }
.status-icon svg { width: 16px; height: 16px; display: block; }
.si-good { fill: var(--good); }
.si-warning { fill: var(--warning); }
.si-serious { fill: var(--serious); stroke: var(--serious); stroke-width: 1; stroke-linejoin: round; }
.si-critical { fill: var(--critical); }
.si-mark-light { fill: none; stroke: #ffffff; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.si-mark-dark { fill: none; stroke: #0b0b0b; stroke-width: 1.9; stroke-linecap: round; }
.si-dot-dark { fill: #0b0b0b; }
.si-ring { fill: none; stroke: var(--neutral); stroke-width: 1.6; }
.si-ring-unknown { stroke: var(--unknown); stroke-dasharray: 2.4 2.2; }
.si-mark-neutral { fill: none; stroke: var(--neutral); stroke-width: 1.6; stroke-linecap: round; }

.banner {
  max-width: var(--content);
  margin: 4px auto 8px;
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  padding: 8px 12px;
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--t-body);
}

/* ---------------------------------------------------------------- filters */

.filters {
  max-width: var(--content);
  margin: 6px auto 14px;
  display: grid;
  gap: 6px;
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--plane);
  padding: 8px 0;
  /* The bar's own edge, and only once the page has moved under it. */
  border-bottom: 1px solid transparent;
}
.filters.is-stuck { border-bottom-color: var(--rule); }
.sticky-sentinel { height: 1px; }
/* The one in-page link on the page landed its target under the sticky bar. --sticky-h is
   measured from the bar itself in app.js, because its height depends on the width, on the
   pointer, and on whether the custom-range row is open. */
main > section[id], .card[id] { scroll-margin-top: calc(var(--sticky-h) + var(--s-4)); }
/* Every control stays on screen: the groups wrap instead of scrolling sideways. */
.filter-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); }
.range-row {
  display: grid;
  grid-template-columns: minmax(0, max-content) auto;
  justify-content: start;
  align-items: center;
  gap: 12px;
}

.seg {
  display: inline-flex;
  flex: none;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 2px;
}
.seg button {
  font: inherit;
  font-size: var(--t-meta);
  color: var(--ink-2);
  background: transparent;
  border: 0;
  border-radius: var(--r-md);
  padding: 5px 9px;
  min-height: 32px;
  cursor: pointer;
  white-space: nowrap;
}
.seg button:hover { background: var(--wash); color: var(--ink); }
.seg button[aria-pressed="true"] { background: var(--ink); color: var(--surface); font-weight: 600; }
/* The group has a 1px border and 2px of padding, so the global +2px offset lands the ring
   exactly on that border. Pull it inside. */
.seg button:focus-visible { outline-offset: -1px; }
/* Three pressed segments sit side by side. Fully inverting all three puts three solid slabs
   in the quietest part of the page — louder, in dark mode, than any reading. Only the control
   that changes the data keeps the inversion; the two that change presentation are switches. */
#tz-group button[aria-pressed="true"], #theme-group button[aria-pressed="true"] {
  background: var(--wash); color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink-2);
}

.btn {
  font: inherit;
  font-size: var(--t-meta);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  gap: 6px;
  min-height: 34px;
  padding: 5px 12px;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover { background: var(--wash); }
.btn-quiet { background: transparent; border-color: transparent; color: var(--ink-2); min-height: 32px; padding: 4px 8px; }
.btn-primary { background: var(--ink); color: var(--surface); border-color: var(--ink); font-weight: 600; }
.btn-primary:hover { opacity: 0.9; background: var(--ink); }
.btn-small { min-height: 32px; padding: 2px 10px; font-size: var(--t-meta); }

.custom-range { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3) 12px; font-size: var(--t-meta); }
.custom-range label { display: inline-flex; align-items: center; gap: 6px; }
.custom-range input {
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 4px 6px;
  min-height: 32px;
}

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

/* minmax(0, 1fr): a wide table scrolls inside its own box instead of widening the page. */
main { max-width: var(--content); margin: 0 auto; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-5); }
main > section { min-width: 0; }
.section-head { display: grid; gap: var(--s-1); margin-bottom: var(--s-4); }
/* A line of prose stops at ~78 characters however wide the page gets. */
.masthead-caption, .site-footer p, .section-head p, .chart-titles p, .chart-foot p,
.spectrum-ref-note, .rack-idle, .rack-detail-note, .health-row-note, .chart-empty-text {
  max-width: var(--measure);
  font-variant-numeric: normal;     /* prose, not a column of figures */
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--pad-card);
  min-width: 0;
}
.card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-3); margin-bottom: 8px; }
.card-title { display: grid; gap: 2px; min-width: 0; }

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

/* The data refreshes every 30-60 s, so motion here says what changed and nothing else. A
   control echoes the pointer; a control that changes what it is takes a little longer to say
   so. Focus is never animated: it has to be where the eye is, immediately. Nor is layout, nor
   a plot canvas -- a transition on a canvas smears the curve. A card is a page element and
   gets no hover state at all. */
.btn, .btn-quiet, .btn-small, .seg button, .seg-small button, .chip, .legend button,
.rack-ix, .rack-toggle, table.data tbody tr {
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
.seg button[aria-pressed="true"], .chip[aria-pressed="true"], .rack-toggle[aria-pressed="true"] {
  transition-duration: var(--dur-2);
  transition-timing-function: var(--ease-in-out);
}
.is-loading { opacity: 0.55; transition: opacity var(--dur-3) var(--ease-out); }
.tiles, .charts, .daily, .health-grid { transition: opacity var(--dur-3) var(--ease-out); }

/* ---------------------------------------------------------------- tiles */

.tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-4); }
.tile { display: grid; gap: 2px; align-content: start; }
/* Label, value, detail and note sit on shared rows, so values line up across a row of tiles
   even when one label wraps. Browsers without subgrid keep each tile's own rows. */
@supports (grid-template-rows: subgrid) {
  .tile { grid-row: span 4; grid-template-rows: subgrid; row-gap: 2px; align-content: stretch; }
}
.tiles > .tile:last-child:nth-child(odd) { grid-column: 1 / -1; }
/* Labels start at the top of their shared row; the key lines up with the first line of text. */
.tile-label { font-size: var(--t-meta); color: var(--ink-2); display: flex; align-items: flex-start; align-self: start; gap: 6px; }
.tile-label .key { margin-top: 8.5px; margin-top: calc(0.5lh - 1px); }
.tile-value { font-size: var(--t-figure); font-weight: 600; line-height: 1.08; letter-spacing: -0.02em; }
.tile-unit { font-size: var(--t-meta); font-weight: 400; color: var(--ink-2); margin-left: 3px; }
.tile-sub { font-size: var(--t-meta); color: var(--ink-2); }
.tile-line { font-weight: 500; }
/* A tile's detail block carries one line per instrument; a reserved slot stays quiet. */
.tile-line { display: block; }
.tile-line-quiet { color: var(--text-muted); }
.tile-note { font-size: var(--t-micro); color: var(--text-muted); }
.tile-stale .tile-value { color: var(--ink-2); }

/* Line keys are painted as backgrounds so the dashed key really shows dashes at 2 px. */
.key {
  --key: var(--muted);
  display: inline-block;
  flex: none;
  width: 18px;
  height: 2px;
  background: var(--key);
  border-radius: 0;
  vertical-align: middle;
}
.key-dashed { background: repeating-linear-gradient(90deg, var(--key) 0 4px, transparent 4px 7px); border-radius: 0; }
.key-slot-1 { --key: var(--series-1); }
.key-slot-2 { --key: var(--series-2); }
.key-slot-3 { --key: var(--series-3); }
.key-slot-4 { --key: var(--series-4); }
.key-slot-5 { --key: var(--series-5); }
.key-slot-6 { --key: var(--series-6); }
.key-slot-7 { --key: var(--series-7); }
.key-slot-8 { --key: var(--series-8); }
.key-muted { --key: var(--muted); }

/* ---------------------------------------------------------------- charts */

.charts { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-5); }
.charts > .section-head { grid-column: 1 / -1; margin-bottom: 0; }
/* A grid row makes every card as tall as the tallest in it. Without this the slack is
   shared out between title, legend and plot, opening three voids inside the card; it belongs
   at the foot, in one piece. */
.chart-card { margin: 0; display: grid; gap: var(--s-3); align-content: start; }
.chart-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-3); }
.chart-head-controls { display: flex; align-items: center; gap: 6px; flex: none; flex-wrap: wrap; justify-content: flex-end; }
.chart-titles { display: grid; gap: 1px; min-width: 0; }
.chart-body { position: relative; min-height: 220px; }
.chart-plot { position: relative; width: 100%; overflow: hidden; }
/* Placed inside the plotting area (u-over) so the message never covers the axes. */
.chart-empty {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none; padding: 0 10px;
}
.chart-empty-text {
  max-width: 100%;
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--t-body);
  text-align: center;
  padding: 6px 10px;
  border-radius: var(--r-md);
}

.legend { display: flex; flex-wrap: wrap; gap: 2px 4px; margin: 0; padding: 0; list-style: none; }
.legend button {
  font: inherit;
  font-size: var(--t-meta);
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: var(--r-md);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  min-height: 32px;
  cursor: pointer;
}
.legend button:hover { background: var(--wash); }
.legend button[aria-pressed="false"] { color: var(--text-muted); }
.legend button[aria-pressed="false"] .legend-label { text-decoration: line-through; }
.legend button[aria-pressed="false"] .key { opacity: 0.35; }
.legend .legend-note { color: var(--text-muted); }

/* uPlot overrides */
.uplot { font-family: var(--font); width: 100%; }
.uplot .u-over { touch-action: pan-y; }
.u-cursor-x { border-right: 1px solid var(--muted) !important; }
.u-cursor-y { display: none; }
.u-select { background: var(--wash); }

.tooltip {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  min-width: 140px;
  max-width: 260px;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--pop-shadow);
  padding: 8px 10px;
  font-size: var(--t-meta);
}
.tooltip-time { color: var(--ink-2); margin-bottom: 4px; }
.tooltip-row { display: grid; grid-template-columns: auto 18px 1fr; align-items: center; gap: 6px; line-height: 1.5; }
.tooltip-value { font-weight: 600; text-align: right; }
.tooltip-label { color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tooltip-foot { color: var(--text-muted); margin-top: 3px; }

/* Tables scroll sideways inside their box; edge shadows show there is more to either side. */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background:
    linear-gradient(to right, var(--surface) 40%, transparent) left center / 28px 100% no-repeat local,
    linear-gradient(to left, var(--surface) 40%, transparent) right center / 28px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, var(--scroll-shadow), transparent) left center / 10px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, var(--scroll-shadow), transparent) right center / 10px 100% no-repeat scroll,
    var(--surface);
}
.table-wrap-scroll { max-height: 360px; overflow-y: auto; }
table.data { border-collapse: collapse; width: 100%; font-size: var(--t-meta); }
table.data th, table.data td { padding: 5px 8px; text-align: right; white-space: nowrap; border-bottom: 1px solid var(--grid); }
table.data th:first-child, table.data td:first-child { text-align: left; }
/* The page's one uppercase style, and the only place it is used besides the chip label: a
   column head is a name for a column, not a word in a sentence. */
table.data thead th {
  position: sticky; top: 0; background: var(--surface); color: var(--ink-2);
  font-size: var(--t-eyebrow); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  z-index: 1; vertical-align: bottom; box-shadow: inset 0 -1px 0 var(--rule);
}
table.data tbody tr:hover { background: var(--wash); }
.cell-sub { display: block; color: var(--text-muted); font-size: var(--t-micro); font-weight: 400; }
.cell-bar { display: block; height: 3px; margin-top: 3px; border-radius: var(--r-sm); background: var(--muted); max-width: 100%; }
.bar-slot-1 { background: var(--series-1); }
.bar-slot-2 { background: var(--series-2); }
.bar-slot-3 { background: var(--series-3); }
.bar-slot-4 { background: var(--series-4); }
.bar-slot-5 { background: var(--series-5); }
.bar-slot-6 { background: var(--series-6); }
.bar-slot-7 { background: var(--series-7); }
.bar-slot-8 { background: var(--series-8); }
.table-empty { padding: 16px; }
table.data caption { text-align: left; font-size: var(--t-micro); color: var(--text-muted); padding: 6px 0 4px; }
table.data caption .cell-sub { display: block; }
.table-wrap table.data + table.data { margin-top: 12px; }
table.data th .key { margin-right: 2px; }
table.data tbody th { text-align: left; font-weight: 400; color: var(--ink); background: transparent; position: static; }
.cell-warn { display: inline-flex; vertical-align: -3px; margin-right: 3px; }
.cell-warn svg { width: 13px; height: 13px; }

/* ---------------------------------------------------------------- spectrum */

/* A full-width row that never enters the two-column chart grid: the device map and the
   spectrometers' own product both need the whole width to be read at all. */
.lead { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-7); }
.spectrum-card { gap: 10px; }
.spectrum-controls {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--s-3);
}
.btn-step { font-size: var(--t-h2); line-height: 1; padding-left: 10px; padding-right: 10px; }
/* A toggle that is on looks on, the same way the range and theme buttons do. */
.chart-head .btn[aria-pressed="true"], .spectrum-controls .btn[aria-pressed="true"] {
  background: var(--ink); color: var(--surface); border-color: var(--ink); font-weight: 600;
}
.btn:disabled { opacity: 0.4; cursor: default; }
.btn:disabled:hover { background: var(--surface); }
.spectrum-slider {
  -webkit-appearance: none; appearance: none;
  width: 100%; min-width: 0; margin: 0;
  background: transparent;
  height: 28px;
}
.spectrum-slider::-webkit-slider-runnable-track {
  height: 4px; border-radius: var(--r-sm); background: var(--track);
}
.spectrum-slider::-moz-range-track { height: 4px; border-radius: var(--r-sm); background: var(--track); }
.spectrum-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 18px; margin-top: -7px;
  border-radius: 50%; background: var(--ink); border: 2px solid var(--surface);
}
.spectrum-slider::-moz-range-thumb {
  width: 18px; height: 18px;
  border-radius: 50%; background: var(--ink); border: 2px solid var(--surface);
}
.spectrum-slider:disabled { opacity: 0.45; }
.spectrum-slider:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-sm); }
.spectrum-stamp { font-size: var(--t-meta); display: flex; flex-wrap: wrap; gap: 0 2px; }
.spectrum-stamp-time { font-weight: 600; }
.spectrum-body { display: grid; gap: 4px; }
/* The two panels share one wavelength axis: only the lower one draws it. */
/* --- the all-sky cameras ------------------------------------------------------
   The server sends each frame as a square that bounds the lens's disc; the round
   mask is here so the bytes on the wire stay a plain WebP. The pair sits side by
   side wherever two will fit and stacks below that. */
.sky-shots {
  display: grid; gap: 14px; margin-top: 10px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  justify-items: center;
}
.sky-shot { display: grid; gap: 6px; justify-items: center; min-width: 0; width: 100%; }
.sky-disc-wrap {
  position: relative; width: 100%; max-width: 440px; aspect-ratio: 1 / 1;
  border-radius: 50%; overflow: hidden;
  /* The disc never fills a square, so what shows through has to be a real token
     rather than white: in dark mode a white ring around the sky is glaring. */
  background: var(--surface); border: 1px solid var(--border);
}
.sky-disc { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Before the first frame of the day both discs are empty, and at full width that is most of
   a screen of blank circles. An empty pair states the same thing at a quarter of the size. */
.sky-shots.is-empty .sky-disc-wrap { max-width: 260px; }
.sky-empty {
  position: absolute; inset: 0; margin: 0;
  display: grid; place-items: center; text-align: center; padding: 0 12px;
}
.sky-label { font-size: var(--t-meta); color: var(--text-muted); }

/* Sky coordinates over a frame: rings of equal elevation, spokes of equal bearing, and the
   sun where it stood when that frame was taken. White with a dark halo in both themes --
   this sits on a photograph, not on the page's own surface, and a sky is bright at noon and
   black at night. The lines scale with the disc; the letters and numbers do not, so they
   stay the size they are meant to be read at however wide the card is. */
.sky-grid, .sky-marks { position: absolute; inset: 0; pointer-events: none; }
.sky-grid { width: 100%; height: 100%; filter: drop-shadow(0 0 1.5px rgba(0, 0, 0, 0.9)); }
.sky-ring, .sky-spoke { fill: none; stroke: #fff; stroke-width: 1; vector-effect: non-scaling-stroke; }
.sky-ring { stroke-opacity: 0.55; }
.sky-spoke { stroke-opacity: 0.4; }
.sky-mark {
  position: absolute; transform: translate(-50%, -50%);
  font-size: var(--t-micro); line-height: 1; color: #fff; opacity: 0.85;
  font-variant-numeric: tabular-nums; text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
  white-space: nowrap;
}
.sky-point { font-size: var(--t-meta); font-weight: 700; opacity: 1; }
.sky-sun {
  position: absolute; width: 18px; height: 18px; margin: -9px 0 0 -9px;
  border-radius: 50%; border: 2px solid #ffc53d;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 9px rgba(255, 197, 61, 0.75);
}
.sky-shots.no-grid .sky-grid, .sky-shots.no-grid .sky-mark { display: none; }

/* The discs and the sun's own two angles are one block. Side by side once there is width
   for it: two discs alone leave most of a desktop row empty, and the angles are the numbers
   that say where to look on them. */
.sun-charts { display: grid; gap: var(--s-5); align-content: start; }

.spectrum-panel { display: grid; gap: 2px; min-width: 0; position: relative; }
.panel-caption { font-size: var(--t-micro); color: var(--text-muted); }
.spectrum-none { padding: 24px 0; text-align: center; }
.legend-reserved {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 6px; font-size: var(--t-meta); color: var(--text-muted);
}
.legend-reserved .key { opacity: 0.35; }
.calib { font-size: var(--t-meta); border-top: 1px solid var(--border); padding-top: 8px; }
.calib summary { cursor: pointer; color: var(--ink-2); padding: 6px 0; }
.calib summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-sm); }
.calib[open] summary { margin-bottom: 6px; }
.calib > * + * { margin-top: 6px; }

/* ------------------------------------------------- chart footers & references */

/* The footer under a chart: what the shading means, and what is connected to what. */
.chart-foot { display: grid; gap: var(--s-3); border-top: 1px solid var(--border); padding-top: 8px; }
.chart-foot > p { margin: 0; }
/* The same ink as the curtailment band on the plot, so the sentence and the shading
   are recognisably the same mark: a neutral wash with a hairline lid, never a status hue. */
.chart-foot-mark {
  display: inline-block;
  width: 22px; height: 11px;
  vertical-align: -1px;
  background: var(--wash);
  border-top: 1px solid var(--axis);
  border-radius: 0;
}

/* A compact segmented control for a control that sits inside a card. */
.seg-small button { font-size: var(--t-meta); padding: 3px 9px; min-height: 32px; }

.spectrum-ref-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
.spectrum-ref-note { margin: 0; }
.check { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-meta); color: var(--ink-2); cursor: pointer; }
.check input { width: 16px; height: 16px; accent-color: var(--focus); margin: 0; }
.check:focus-within { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-sm); }

/* ---------------------------------------------------------------- spectrogram */

/* The colour scale legend: a continuous encoding must state its bounds, and the
   bounds differ per channel (each is scaled to its own peak). */
.heat-scale { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 8px; font-size: var(--t-micro); color: var(--text-muted); }
.heat-scale-bar {
  flex: 0 0 96px;
  height: 8px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  /* background-image is set from the ramp in JS so light and dark share one source. */
}
.heat-scale-text { }
.heat-gap { display: inline-flex; align-items: center; gap: 5px; }
/* The card's own surface behind a hairline: the same nothing the plot leaves unpainted. */
.heat-gap-swatch {
  width: 12px; height: 8px;
  background: var(--surface);
  border: 1px solid var(--axis);
  border-radius: var(--r-sm);
}

/* ---------------------------------------------------------------- device map */

/* The elevation is inline SVG authored in CSS pixels, so every length below is the length
   it will be on screen at any card width. rack.css is deliberately not loaded: it styles
   *, html, body and button for a standalone page and would reset this one. */

.rack-card { gap: 10px; }
.rack-body { display: grid; gap: 14px; min-width: 0; }
.rack-figure { min-width: 0; display: grid; gap: 0; align-content: start; }
.rack-map { position: relative; min-width: 0; min-height: 90px; }
.rack-svg { display: block; width: 100%; height: auto; }
/* The loading strip sits over the drawing, which stays readable underneath. It is a caption,
   never a shield: the drawing below it stays clickable, and only the strip's own text (which
   may carry a link) takes the pointer back. */
.rack-note {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  display: flex; align-items: center; justify-content: center; padding: 0 10px;
}
.rack-note .chart-empty-text { max-width: 40em; pointer-events: auto; }
/* A load that failed leaves its message on the card until the reader tries again, so that one
   flows under the drawing instead of covering it for good. */
.rack-note-static {
  position: static; z-index: auto; justify-content: flex-start; padding: 8px 0 0;
}
.rack-failed { margin: 0; padding: 10px 0; }

/* -- the drawing itself */
/* Frame, boom and enclosures are each a part's own bounding box from the STEP export, so
   they are drawn as an open outline: a filled shape would claim an extent the file cannot. */
.rack-env { fill: none; stroke: var(--rack-env); stroke-width: 1; }
.rack-part { fill: none; stroke: var(--rack-part); stroke-width: 1; }
.rack-rail { fill: var(--rack-rail); }
.rack-hatch-line { stroke: var(--muted); }
.rack-mod-back { fill: var(--wash); }
.rack-mod { stroke: var(--surface); stroke-width: 1; cursor: pointer; }
.rack-mod-s0 { fill: var(--pv-0); }
.rack-mod-s1 { fill: var(--pv-1); }
.rack-mod-s2 { fill: var(--pv-2); }
.rack-mod-s3 { fill: var(--pv-3); }
.rack-mod-s4 { fill: var(--pv-4); }
/* "The charger reports nothing" is never a value on the scale: hatch, not colour. */
.rack-mod-none {
  fill: url(#rack-hatch); stroke: var(--muted); stroke-width: 1.25; stroke-dasharray: 4 3;
}
/* Nothing is known yet, or this station has no charger data at all. */
.rack-mod-blank { fill: var(--wash); stroke: var(--border); }
.rack-mod:hover { stroke: var(--focus); stroke-width: 2; }
.rack-off { display: none; }
.rack-mod-id { font-size: 12.5px; font-weight: 600; text-anchor: middle; pointer-events: none; }
.rack-mod-w {
  font-size: 11px; text-anchor: middle; pointer-events: none;
}
.rack-mod-t0 { fill: var(--pv-0-label); }
.rack-mod-t1 { fill: var(--pv-1-label); }
.rack-mod-t2 { fill: var(--pv-2-label); }
.rack-mod-t3 { fill: var(--pv-3-label); }
.rack-mod-t4 { fill: var(--pv-4-label); }
.rack-mod-tn { fill: var(--ink); }

/* -- instruments: the dot is the position, the numbered badge is only its label */
.rack-inst { fill: none; stroke: var(--muted); stroke-width: 1; }
.rack-dot { fill: var(--ink-2); }
.rack-leader { stroke: var(--rack-part); stroke-width: 1; }
/* The badge and its numeral are drawn over the hit circle, so they must let the click
   through to it: it is the only node that carries the device id. */
.rack-badge { fill: var(--surface); stroke: var(--ink-2); stroke-width: 1.5; pointer-events: none; }
.rack-badge-n {
  font-size: 11px; font-weight: 600; text-anchor: middle; dominant-baseline: central;
  fill: var(--ink); pointer-events: none;
}
.rack-hit { fill: transparent; pointer-events: all; cursor: pointer; }
.rack-hit:hover + .rack-badge { stroke: var(--focus); stroke-width: 2.25; }
/* The selected badge inverts rather than filling with --focus: 11 px text on the focus hue
   is 4.2:1 in dark and 4.5:1 in light, and this is the same ink-on-surface reversal a pressed
   chip already uses. The focus hue stays on the ring, the dot, the leader and the box. */
.rack-is-sel.rack-badge { fill: var(--ink); stroke: var(--focus); stroke-width: 2; }
.rack-is-sel.rack-badge-n { fill: var(--surface); }
.rack-is-sel.rack-dot { fill: var(--focus); }
.rack-is-sel.rack-inst { stroke: var(--focus); stroke-width: 1.5; }
.rack-is-sel.rack-leader { stroke: var(--focus); stroke-width: 1.75; }
.rack-sel-box { fill: none; stroke: var(--focus); stroke-width: 2.5; pointer-events: none; }
/* Keyboard focus on a chip or an index row echoes onto the drawing; it is not selection. */
.rack-focus-ring {
  fill: none; stroke: var(--focus); stroke-width: 1.5; stroke-dasharray: 3 3;
  pointer-events: none;
}

/* -- dimensions */
.rack-dim { stroke: var(--rack-part); stroke-width: 1; fill: none; }
.rack-dim-bg { fill: var(--surface); }
.rack-dim-t { font-size: 11px; text-anchor: middle; fill: var(--text-muted); }
/* The array fills the top third and the boom hangs through the rest, so the lower left of the
   drawing is empty. A drawing states its own view there rather than leaving the corner blank. */
.rack-view-note { font-size: 11px; fill: var(--text-muted); }

/* -- legend */
.rack-legend { align-items: center; gap: 2px 14px; font-size: var(--t-meta); }
.rack-legend li { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-2); }
.rack-scale-bar {
  flex: 0 0 110px; width: 110px; height: 8px; border-radius: var(--r-sm); border: 1px solid var(--border);
  forced-color-adjust: none;
  /* background-image is built in JS from the same five tokens the modules use. */
}
.rack-hatch-swatch {
  flex: none; width: 16px; height: 11px; border-radius: var(--r-sm); forced-color-adjust: none;
  border: 1.25px dashed var(--muted);
  background: repeating-linear-gradient(45deg, var(--muted) 0 1px, transparent 1px 5px);
}
.rack-badge-swatch {
  flex: none; width: 13px; height: 13px; border-radius: 50%; forced-color-adjust: none;
  border: 1.5px solid var(--ink-2); background: var(--surface);
}

/* -- chips: the modules, in plan order, for people who would rather not aim at a rectangle */
.rack-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 10px; }
.rack-chips-label {
  font-size: var(--t-eyebrow); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-muted); margin-right: var(--s-2);
}
.chip {
  font: inherit; font-size: var(--t-meta); color: var(--ink); background: transparent;
  border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 3px 10px; min-height: 32px; cursor: pointer;
}
.chip:hover { background: var(--wash); }
.chip[aria-pressed="true"] {
  background: var(--ink); color: var(--surface); border-color: var(--ink); font-weight: 600;
}

/* -- the panel: the same eight instruments, always legible, always in the same order */
.rack-panel { min-width: 0; display: grid; gap: 10px; align-content: start; container: rack-panel / inline-size; }
.rack-index { display: grid; border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; }
.rack-ix {
  display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; gap: var(--s-3); align-items: center;
  font: inherit; font-size: var(--t-meta); text-align: left; color: var(--ink);
  background: transparent; border: 0; border-top: 1px solid var(--grid);
  padding: 6px 9px; cursor: pointer;
}
.rack-ix:first-child { border-top: 0; }
.rack-ix:hover { background: var(--wash); }
/* The list clips its rows so its corners stay round, which would cut the left and right sides
   off the default ring and leave a focused row wearing two stray horizontal lines. */
.rack-ix:focus-visible { outline-offset: -2px; border-radius: 0; }
.rack-ix[aria-pressed="true"] { background: var(--wash); box-shadow: inset 2px 0 0 var(--focus); }
.rack-ix-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--ink-2);
  font-size: 10.5px; font-weight: 600; line-height: 1;
}
.rack-ix-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rack-ix-value {
  color: var(--ink-2); font-size: var(--t-micro); white-space: nowrap;
}
/* The name is the key to the numbered badge, so it is never truncated. The panel is narrow on a
   phone and again in the first hundred pixels of the two-column layout, where the window is
   wide -- so the test is the panel's own width, and the reading drops to its own line. */
@container rack-panel (max-width: 384px) {
  .rack-ix { grid-template-columns: 18px minmax(0, 1fr); row-gap: 1px; align-items: start; }
  .rack-ix-n { margin-top: 1px; }
  .rack-ix-name { white-space: normal; }
  .rack-ix-value { grid-column: 2; white-space: normal; }
}
/* The same shape the login page uses for a message that must be read. */
.rack-detail {
  display: grid; align-content: start;
  border: 1px solid var(--border); border-left: 3px solid var(--focus); border-radius: var(--r-md);
  background: var(--wash); padding: 10px 12px;
}
/* Nothing selected: the box is taller than its one sentence wherever it takes up the column's
   slack, so the sentence sits in the middle of it instead of hanging from the top edge. */
.rack-detail.is-idle { align-content: center; }
.rack-detail-title { font-size: var(--t-h3); font-weight: 600; margin-bottom: 6px; }
.rack-dl {
  display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 3px 12px;
  margin: 0; font-size: var(--t-meta);
}
.rack-dl dt { color: var(--text-muted); }
.rack-dl dd { margin: 0; color: var(--ink-2); min-width: 0; }
.rack-detail-note { margin-top: 7px; }
.rack-idle { font-size: var(--t-meta); color: var(--text-muted); max-width: 46em; }
.rack-idle p + p { margin-top: 6px; }

/* -- the 3D view, styled fresh here: rack.css is for the standalone page only */
/* rack.js's legend paints its "selected" swatch with var(--accent), a token its own
   stylesheet defines and this one deliberately does not load. Naming it here rather than at
   :root keeps it out of the dashboard's vocabulary: without it the swatch resolves to nothing
   and the one key that explains the selection colour comes out blank. */
.rack-3d, .rack-prose { --accent: var(--rack-accent); }
.rack-3d { display: grid; gap: var(--s-3); }
.rack-3d-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; }
.rack-3d-groups { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* Layer switches, not a choice between four things. The pressed-chip black would put four
   solid slabs across the card and still not say which layer is on; a switch reads by its
   border and its weight, and the canvas beside it is the real answer. */
.rack-toggle[aria-pressed="true"] {
  background: var(--wash); color: var(--ink); border-color: var(--ink-2); font-weight: 600;
}
.rack-toggle[aria-pressed="false"] { color: var(--text-muted); border-style: dashed; }
.rack-stage {
  position: relative; height: 360px; border: 1px solid var(--border); border-radius: var(--r-md);
  overflow: hidden; background: var(--plane); touch-action: none;
}
.rack-stage canvas { display: block; width: 100%; height: 100%; }
/* rack.js reveals this itself when WebGL is missing; until then it stays out of the way. */
.rack-stage > #fallback {
  display: none; padding: 18px; font-size: var(--t-body); color: var(--ink-2); max-width: 46em;
}
.rack-tag {
  position: absolute; left: 10px; bottom: 9px; margin: 0;
  white-space: pre-line; font-size: var(--t-micro); line-height: 1.5;
  color: var(--ink-2); pointer-events: none;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  padding: 3px 6px; border-radius: var(--r-sm);
}
.rack-3d-legend { display: flex; flex-wrap: wrap; gap: 5px 16px; font-size: var(--t-micro); color: var(--ink-2); }
.rack-3d-legend span { display: inline-flex; align-items: center; gap: 7px; }
.rack-3d-legend i {
  display: inline-block; flex: none; width: 16px; height: 11px; border-radius: var(--r-sm);
  forced-color-adjust: none;
}
.rack-prose { font-size: var(--t-meta); }
.rack-prose p { margin: 0; }
.rack-prose > * + * { margin-top: 6px; }

/* ---------------------------------------------------------------- daily */

/* Seven tables of four to six numeric columns, each stacked full width. Two columns fit
   side by side from 1440 and three once the page is at its full width -- a six-column table
   needs about 660 px before .table-wrap starts scrolling sideways, and three columns do not
   clear that until the content is 2160. */
#daily-tables { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-6); }
@media (min-width: 1440px) { #daily-tables { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 2208px) { #daily-tables { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.daily-type { margin-bottom: 0; }
.daily-type h3 { margin-bottom: 6px; }

/* ---------------------------------------------------------------- health */

/* Columns, not a grid. These cards are independent and of wildly different heights, and a
   grid row makes each one as tall as the tallest beside it: at four columns that left more
   bare plane than card. Flowing them column-major packs them and reads correctly for a list
   of separate states. */
.health-grid { column-gap: var(--s-5); }
.health-card { display: grid; gap: var(--s-3); align-content: start; break-inside: avoid; margin-bottom: var(--s-5); }
.health-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
.health-level { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-meta); font-weight: 600; color: var(--ink-2); }
.health-rows { list-style: none; margin: 0; padding: 0; display: grid; }
.health-row {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr);
  gap: 2px 8px;
  padding: 7px 0;
  border-top: 1px solid var(--grid);
  font-size: var(--t-body);
}
.health-row:first-child { border-top: 0; }
.health-row .status-icon { margin-top: 2px; }
.health-row-label { color: var(--ink-2); }
.health-row-value { overflow-wrap: anywhere; }
.health-row-note { color: var(--text-muted); font-size: var(--t-micro); overflow-wrap: anywhere; }
.health-row-text { display: grid; gap: 1px; min-width: 0; }

.site-footer { max-width: var(--content); margin: 24px auto 0; display: grid; gap: 2px; }

/* ---------------------------------------------------------------- login */

/* The sign-in page is the one page with nothing to plot, so the site gets the whole of it:
   the photograph full bleed behind a scrim, the card and the crest on top. */
.login-page {
  display: grid;
  grid-template-rows: 1fr auto;
  justify-items: center;
  align-items: center;
  min-height: 100vh;
  min-height: 100dvh;
}
.login-photo { position: fixed; inset: 0; z-index: -1; display: block; }
.login-photo img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
.login-photo::after {
  content: "";
  position: fixed;
  inset: 0;
  background: linear-gradient(to bottom, rgba(6, 8, 10, 0.52) 0%, rgba(6, 8, 10, 0.68) 100%);
}
.login-footer {
  display: grid;
  justify-items: center;
  gap: var(--s-3);
  padding-bottom: 8px;
  color: rgba(255, 255, 255, 0.9);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}
.login-crest { display: block; width: 168px; height: auto; }
.login-credit { font-size: var(--t-micro); }
.login-lockup { display: block; width: auto; height: 50px; }
/* One wordmark is shown and the other is not: the card takes its background from the theme. */
.only-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .only-light { display: none; }
  :root:where(:not([data-theme="light"])) .only-dark { display: block; }
}
:root[data-theme="dark"] .only-light { display: none; }
:root[data-theme="dark"] .only-dark { display: block; }
.login-card {
  width: 100%;
  max-width: 380px;
  display: grid;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 22px;
  margin: 24px 0;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34);
}
.login-form { display: grid; gap: var(--s-3); }
.login-form input {
  font: inherit;
  font-size: 16px; /* prevents iOS zoom on focus */
  color: var(--ink);
  background: var(--plane);
  border: 1px solid var(--axis);
  border-radius: var(--r-md);
  padding: 10px 12px;
  min-height: 44px;
}
.login-form .btn { min-height: 44px; font-size: var(--t-h2); margin-top: 4px; }
.login-message { border-left: 3px solid var(--critical); padding: 4px 10px; background: var(--wash); border-radius: var(--r-sm); }

/* ---------------------------------------------------------------- phones */

/* The detail list drops to one column before a definition would wrap to two words a line. */
@media (max-width: 479px) {
  .rack-dl { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .rack-dl dt { margin-top: 7px; }
  .rack-dl dt:first-child { margin-top: 0; }
}

@media (max-width: 559px) {
  h1 { font-size: var(--t-h2); }
  .heat-scale-bar { flex-basis: 64px; }
  .rack-scale-bar { flex-basis: 64px; width: 64px; }
  .rack-scale-steps { display: none; }
  .seg-small button { padding-left: 6px; padding-right: 6px; }
  /* A card with two controls has no room for them beside the title on a phone: the title
     block keeps the first line to itself and the controls take the next. */
  .chart-head { flex-wrap: wrap; }
  .chart-head-controls { flex-basis: 100%; justify-content: flex-start; }
  .chart-titles { flex: 1 1 auto; }
  .spectrum-controls { gap: 6px; }
  .masthead-bar { padding: 48px 12px 10px; gap: 10px 12px; }
  .masthead-lockup { height: 46px; }
  .masthead-crest { width: 150px; }
  .login-lockup { height: 40px; }
  .login-crest { width: 140px; }
  table.data th, table.data td { padding: 5px 6px; }
}

/* Narrow screens: the range buttons take a full row; time zone and theme share the next. */
@media (max-width: 759px) {
  .filter-row > .seg { flex: 1 1 0; }
  #range-group { flex-basis: 100%; }
  .seg button { flex: 1 1 auto; padding-left: 5px; padding-right: 5px; }
}

/* A sticky bar of two or three rows would cover too much of a phone screen. */
@media (max-width: 759px), (max-height: 500px) {
  .filters { position: static; }
}

/* Touch screens: comfortable targets (44 px for the main controls). */
@media (pointer: coarse) {
  .seg button { min-height: 44px; }
  .seg-small button { min-height: 36px; }
  .check input { width: 20px; height: 20px; }
  .btn, .btn-quiet { min-height: 44px; }
  .btn-small { min-height: 36px; padding-left: 12px; padding-right: 12px; }
  .legend button { min-height: 36px; }
  .chip { min-height: 36px; }
  .rack-ix { min-height: 44px; }
  .spectrum-slider { height: 44px; }
  .spectrum-slider::-webkit-slider-thumb { width: 22px; height: 22px; margin-top: -9px; }
  .spectrum-slider::-moz-range-thumb { width: 22px; height: 22px; }
  .calib summary { padding-top: 9px; padding-bottom: 9px; }
  .custom-range input { min-height: 44px; font-size: 16px; } /* 16 px: no iOS zoom on focus */
}

/* ---------------------------------------------------------------- wider screens */

@media (min-width: 560px) {
  .tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .tiles > .tile:last-child:nth-child(odd) { grid-column: auto; }
  .tiles > .tile:last-child:nth-child(4n + 2) { grid-column: span 3; }
  .tiles > .tile:last-child:nth-child(4n + 3) { grid-column: span 2; }
}

@media (min-width: 760px) {
  .sun-charts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .health-grid { column-count: 2; }
}

@media (min-width: 1000px) {
  :root { --gutter: 24px; --pad-card: 20px; }
  /* A section boundary has to be plainly larger than a card boundary, or seven sections read
     as one long run of cards. Three times the card gap, and the type does the rest. */
  main { gap: var(--s-9); }
  .charts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The drawing keeps the wider column; the panel needs only enough for its longest name. */
  .rack-body { grid-template-columns: minmax(0, 1.55fr) minmax(280px, 1fr); }
  /* The index is always eight rows and the drawing is always 1.77:1, so the two columns end at
     different heights. The detail box takes the slack; it still grows past it when a selection
     needs more room, which is why the track is a minmax and not a fixed height. */
  .rack-panel { align-content: stretch; grid-template-rows: auto minmax(64px, 1fr); }
}

/* ------------------------------------------------------------- wide screens */

/* Above 1000 px the page had no breakpoint at all: 1280 px of content and the rest of the
   screen left blank, which is half of a 2560 px display. The content width opens from 1440
   and stops at 2160, because the page's atomic unit is a chart card and three of them at the
   top of a card's comfortable range is 2118 px. Past that nothing reads better -- so the
   margin at 2560 is a margin, not waste. A sentence is capped at --measure separately, so a
   wider page never means a wider paragraph. */
@media (min-width: 1200px) {
  .sky-block { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: start; }
  .sun-charts { grid-template-columns: minmax(0, 1fr); }
  /* The range label and the CSV button join the controls: the bar was two rows, the second
     of them two-thirds empty, and every row it takes is a row the anchor has to clear. */
  .filters { grid-template-columns: minmax(0, auto) minmax(0, 1fr); align-items: center; column-gap: var(--s-7); }
  .filter-row { grid-column: 1; grid-row: 1; }
  .range-row { grid-column: 2; grid-row: 1; justify-self: end; }
  .custom-range { grid-column: 1 / -1; grid-row: 2; }
}

@media (min-width: 1340px) {
  .tiles { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
  .tiles > .tile:last-child:nth-child(n) { grid-column: auto; }
}

@media (min-width: 1440px) {
  :root { --content: min(100%, 2160px); }
  /* The drawing takes the width the card gains; the panel needs only its longest name. */
  .rack-body { grid-template-columns: minmax(0, 1.9fr) minmax(320px, 0.9fr); }
  /* Two discs side by side, once there is width for them to be bigger rather than just
     further apart. Below this the card would only grow taller for nothing. */
  .sky-disc-wrap { max-width: min(100%, 520px); }
  /* The two spectrum cards side by side. They are already one instrument: picking a minute on
     the spectrogram loads that minute's curve into the card beside it, and stacked, that
     happens off screen. Both plots also stop being letterboxes at half the width. The guard
     is there because the spectrogram card hides itself when no channel is live, and a lone
     spectrum card pinned to column 1 would leave a hole beside it. */
  #spectrum-section:has(> #chart-spectrogram:not([hidden])) {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
  }
  #spectrum-section:has(> #chart-spectrogram:not([hidden])) > #chart-spectrum { grid-column: 1; }
  #spectrum-section:has(> #chart-spectrogram:not([hidden])) > #chart-spectrogram { grid-column: 2; }
  /* Two discs and no more, so they stay a pair instead of drifting to opposite corners of a
     card that is now most of the screen wide. */
  .sky-shots { max-width: 1054px; margin-inline: auto; }
}

/* The health console is the tallest thing on the page -- 37 % of it. A grid row is as tall as
   its tallest card, so the saving is not linear in the column count: three columns take 2911
   and four take 2320, against 3245 stacked in two. Three waits until 1600 because the cards
   get taller as they get narrower, and at 1440 the extra column buys almost nothing. */
@media (min-width: 1600px) {
  .health-grid { column-count: 3; }
}

@media (min-width: 1960px) {
  .health-grid { column-count: 4; }
  /* Three chart columns, and never four: a card's plot needs about 512 px to carry eight time
     ticks at uPlot's `space: 64`, and four columns fall under that and collide on a 30-day
     axis. It is the x axis that rules this out, not taste. */
  .charts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* The drawing and the sky discs side by side. This waits until the page is at its full width,
   because a half-width rack card shrinks the elevation below what it gets at 1280 -- the one
   change on this page that would make an existing thing smaller. Both guards are needed: a
   station with no cameras, or with the map still loading, must not get a half-width card with
   a hole beside it. Where :has() is unsupported the block simply does not apply. */
@media (min-width: 2208px) {
  main:has(#sky-section:not([hidden])):has(#rack-section:not([hidden])) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
  main:has(#sky-section:not([hidden])):has(#rack-section:not([hidden])) > * { grid-column: 1 / -1; }
  main:has(#sky-section:not([hidden])):has(#rack-section:not([hidden])) > #rack-section { grid-column: 1; }
  main:has(#sky-section:not([hidden])):has(#rack-section:not([hidden])) > #sky-section { grid-column: 2; }
  /* In that right-hand column the block is half a page wide again, so its own two halves
     go back to stacking and the pair of angle charts sits side by side under the discs. */
  main:has(#rack-section:not([hidden])) > .sky-block { grid-template-columns: minmax(0, 1fr); }
  main:has(#rack-section:not([hidden])) > .sky-block .sun-charts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The blanket rule only covered transitions, and the page now has one animation. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

@media (forced-colors: active) {
  .seg button[aria-pressed="true"] { outline: 2px solid CanvasText; }
  .key { forced-color-adjust: none; }
  /* The module tint, the scale and the two swatches are the encoding: keep them. */
  .rack-mod, .rack-scale-bar, .rack-hatch-swatch, .rack-badge-swatch { forced-color-adjust: none; }
  .chip[aria-pressed="true"] { outline: 2px solid CanvasText; }
  .rack-ix[aria-pressed="true"] { outline: 2px solid CanvasText; outline-offset: -2px; }
}

