/* optical.satnogs.org — design system v0.2 (design pass 2026-08-21)

   One design system, three surfaces. This file is the token source of truth
   for the network site; the station's own pages (satnogs-optical-client,
   web/design.py) carry the same token block, plus the night theme that only
   a station surface needs. Components never reference a literal colour.

   Themes. Dark is primary: operators read these pages beside hardware after
   dark. Light is declared ONCE, as the light half of every light-dark()
   token, and `color-scheme` decides — so there is one block, one selector,
   and no duplicated palette. A browser without light-dark() keeps the plain
   dark values declared first and never sees a white page.

   Type. Three named faces for the interface, actually loaded: Archivo for
   headings, Source Sans 3 for body, IBM Plex Mono for every measurement, path
   and id. A fourth, Open Sans, is the *identity's* face and is used by the
   wordmark and nothing else -- it is the face the parent SatNOGS sets its own
   name in, and the lockup exists to say this is that project's optical branch
   (the planning repo's brand/README.md). The files are self-hosted under
   /static/fonts (the only route the CSP allows); until they are fetched the
   metric-compatible system stacks stand in, and nothing on the page depends
   on the difference. */

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/archivo-600.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/source-sans-3-400.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/source-sans-3-600.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* The identity's face: Light for OPTICAL, Bold for SatNOGS. Two weights, one
   word each, loaded for the wordmark in the header and the footer of every
   page -- which is why they are subset to latin like the rest. */
@font-face {
  font-family: "Open Sans";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/static/fonts/open-sans-300.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Open Sans";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/open-sans-700.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/static/fonts/ibm-plex-mono-500.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---------- tokens: dark, declared plainly first ---------- */
:root {
  color-scheme: dark;
  --bg: #0A0F14;
  --bg-deep: #06090C;
  --surface: #101720;
  --surface-2: #16202B;
  /* Under a surface, not on it: the band a grouped table puts between
     its groups, so a group header reads as a seam and not as a row. */
  --surface-sub: #0D141B;
  --line: #1E2B38;
  --line-strong: #2C3E4F;
  --ink: #E4EDF4;
  --ink-2: #A3B4C2;
  --ink-3: #6F8394;
  --ink-4: #4B6070;
  --accent: #56A8E0;
  --accent-2: #8CC6EF;
  --accent-tint: #1B3A52;
  --accent-line: #2E5B7D;
  --warm: #E8B24A;
  --warm-tint: #2A2008;
  --warm-line: #5C4715;
  --ok: #4CC48E;
  --warn: #DFA83A;
  --serious: #E07E42;
  --crit: #E45B5B;
  --on-accent: #0A0F14;
  /* chart series + grid */
  --s1: var(--accent);
  --s2: var(--serious);
  --s3: var(--ok);
  --s4: #B98CE8;
  --chart-grid: var(--surface-2);
  --chart-axis: var(--line-strong);
  /* A pin the filter has dimmed. Grey rather than a faded colour: a
     station filtered out has not left the network, and a pin at 40 % of
     its own hue still reads as the state it was filtered out of. */
  --pin-dim: #3A4A58;
  /* fixed, never themed */
  --familybar: #0B1219;
  --lsf: #6672D8;
  /* the identity (planning repo, brand/): ink, glint, grey, paper. These are
     the mark's own colours and they do not follow the theme -- the glint on
     the trail is the same yellow on a white page, a black page, a sticker and
     a laser-cut plate, which is the entire point of having one. The ring is
     the exception: it is drawn in currentColor so it inverts with the page,
     exactly as the identity's "on black / on white" pair does. */
  --glint: #FFB528;
  --brand-ink: #0B0D10;
  --brand-grey: #6E7479;
  --brand-paper: #FFFFFF;
  /* space & radius */
  --s1-px: 4px; --s2-px: 8px; --s3-px: 12px; --s4-px: 16px; --s5-px: 22px; --s6-px: 32px; --s7-px: 48px;
  --r1: 3px;
  --r2: 4px;
  /* type */
  --font-ui: "Source Sans 3", "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", "Cascadia Code", Consolas, ui-monospace, monospace;
  --font-display: "Archivo", "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
  --font-brand: "Open Sans", "Noto Sans", "Segoe UI", system-ui, sans-serif;
  --t-display: 600 40px/42px var(--font-display);
  --t-h1: 600 28px/32px var(--font-display);
  --t-card: 600 17px/22px var(--font-display);
  --t-label: 600 11px/1 var(--font-display);
  --t-body: 400 16px/26px var(--font-ui);
  --t-mono: 500 15px/22px var(--font-mono);
}

/* ---------- light: one block, one selector ----------
   The light palette lives here and nowhere else: the left half of each
   light-dark(). `color-scheme` picks the half — the system's preference by
   default, or the operator's explicit choice via data-theme. */
@supports (color: light-dark(#000, #fff)) {
  :root {
    color-scheme: light dark;
    --bg: light-dark(#F4F7F9, #0A0F14);
    --bg-deep: light-dark(#E9EEF2, #06090C);
    --surface: light-dark(#FFFFFF, #101720);
    --surface-2: light-dark(#EEF2F5, #16202B);
    --surface-sub: light-dark(#F9FBFC, #0D141B);
    --line: light-dark(#D7DEE5, #1E2B38);
    --line-strong: light-dark(#B9C4CE, #2C3E4F);
    --ink: light-dark(#101720, #E4EDF4);
    --ink-2: light-dark(#3F5060, #A3B4C2);
    --ink-3: light-dark(#5B6B7A, #6F8394);
    --ink-4: light-dark(#8A98A6, #4B6070);
    --accent: light-dark(#1F6FB2, #56A8E0);
    --accent-2: light-dark(#155A92, #8CC6EF);
    --accent-tint: light-dark(#DDEBF7, #1B3A52);
    --accent-line: light-dark(#9FC3E3, #2E5B7D);
    --warm: light-dark(#8A5A05, #E8B24A);
    --warm-tint: light-dark(#FBF1D8, #2A2008);
    --warm-line: light-dark(#E2C98A, #5C4715);
    --ok: light-dark(#0E7A4E, #4CC48E);
    --warn: light-dark(#8A5A05, #DFA83A);
    --serious: light-dark(#B4503A, #E07E42);
    --crit: light-dark(#B23434, #E45B5B);
    --pin-dim: light-dark(#A7B4C0, #3A4A58);
    --on-accent: light-dark(#FFFFFF, #0A0F14);
    --s4: light-dark(#6B3FA0, #B98CE8);
  }
}
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

/* ---------- base ---------- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font: var(--t-body);
  -webkit-text-size-adjust: 100%;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-2); text-decoration: underline; }
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}
h1, h2, h3 { font-family: var(--font-display); color: var(--ink); }
.t-display { font: var(--t-display); letter-spacing: -0.022em; }
.t-h1 { font: var(--t-h1); letter-spacing: -0.016em; }
.t-card { font: var(--t-card); }
.t-label, .eyebrow { font: var(--t-label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); }
.mono, .num, .t-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.t-mono { font: var(--t-mono); }
.muted { color: var(--ink-3); }
.small { font-size: 13px; line-height: 1.45; }
.unit { color: var(--ink-3); font-family: var(--font-mono); font-size: 0.85em; }
.prose { max-width: 68ch; color: var(--ink-2); text-wrap: pretty; }
.prose strong { color: var(--ink); font-weight: 600; }

/* ---------- family bar + site header ---------- */
.fam-bar {
  background: var(--familybar); /* fixed in both themes: the thread to the sibling sites */
  border-bottom: 1px solid rgba(228, 237, 244, 0.08);
  font-size: 12px;
}
.fam-bar .wrap { display: flex; gap: 18px; align-items: center; padding: 6px 24px; max-width: 1200px; margin: 0 auto; }
.fam-bar a { color: #6F8394; }
.fam-bar a.active { color: #E4EDF4; font-weight: 600; }
.fam-bar .lsf { margin-left: auto; color: #6F8394; }

.site-header { background: var(--surface); border-bottom: 1px solid var(--line); }
.site-header .wrap { display: flex; align-items: center; gap: 28px; max-width: 1200px; margin: 0 auto; padding: 12px 24px; min-height: 56px; }
/* The lockup. Mark, then the two words stacked: Bold for SatNOGS, Light
   uppercase at 0.38em tracking for OPTICAL. Both lines are ink -- the identity
   distinguishes the second word by weight and case, not by colour, and the
   glint stays on the trail where it means "a pass was seen". The trailing
   letter-space of OPTICAL is pulled back so the two words align on the left
   and the block optically centres against the mark. */
.wordmark { display: flex; align-items: center; gap: 10px; color: var(--ink); }
.wordmark .mark { width: 26px; height: 26px; display: block; flex: none; }
.wordmark .words { display: flex; flex-direction: column; gap: 2px; font-family: var(--font-brand); line-height: 1; }
.wordmark b { font-size: 18px; font-weight: 700; letter-spacing: -0.015em; }
.wordmark .opt { font-size: 11px; font-weight: 300; letter-spacing: 0.38em; margin-right: -0.38em; text-transform: uppercase; }
.site-nav { display: flex; gap: 20px; font-size: 14px; }
.site-nav a { color: var(--ink-2); }
.site-nav a.active { color: var(--ink); font-weight: 600; border-bottom: 2px solid var(--accent); padding-bottom: 2px; }
.site-header .auth { margin-left: auto; display: flex; gap: 10px; align-items: center; min-height: 22px; }
.site-header .auth-state { display: flex; gap: 10px; align-items: center; min-height: 22px; }
.site-header .auth-state[hidden] { display: none; }
.btn.sign-in { display: inline-flex; align-items: center; gap: 7px; }
/* The LSF mark keeps its own brand colour on both themes: it is somebody's
   identity, not a decorative icon. */
.lsf-mark { height: 12px; width: auto; display: block; flex: none; }
.site-header .who { max-width: 18ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.theme-toggle {
  font: 500 12px var(--font-mono); color: var(--ink-3); background: none;
  border: 1px solid var(--line); border-radius: var(--r1); padding: 4px 9px; cursor: pointer;
  min-height: 22px;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--line-strong); }

/* ---------- page scaffold ---------- */
.page-body { max-width: 1200px; margin: 0 auto; padding: 28px 24px 64px; }
main.page-body[data-narrow] { max-width: 760px; }
.crumbs { font-size: 13px; color: var(--ink-3); margin-bottom: 14px; font-family: var(--font-ui); }
.crumbs a { color: var(--ink-3); }
.crumbs .sep { margin: 0 6px; }
h1.page-title { font: var(--t-h1); letter-spacing: -0.016em; margin: 0 0 4px; text-wrap: balance; }
h1.page-title.display { font: var(--t-display); letter-spacing: -0.022em; }
.page-sub { color: var(--ink-2); margin: 0 0 24px; max-width: 68ch; text-wrap: pretty; }
.section { margin-top: 40px; }
.section > h2, .section-head h2 { font: 600 20px/1.25 var(--font-display); margin: 0 0 14px; }
.section-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.section-head h2 { margin: 0; }
.section-head .asof { font: 500 13px var(--font-mono); color: var(--ink-3); }
.head-row { margin-bottom: 4px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.head-row .page-title { margin: 0; }
.page-sub.loc { margin-bottom: 8px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.badge-row { margin-bottom: 24px; }

/* ---------- the station head ----------
   Name and place on the left, the three live readouts on the right, one rule,
   and under it what needs doing beside the detail that stays folded away. One
   rule and no boxes: a head made of bordered pills reads as six small cards
   and hides which of them the operator is meant to act on. */
.station-head { margin-bottom: 26px; }
.head-top { display: flex; gap: 16px 32px; align-items: flex-end; justify-content: space-between; flex-wrap: wrap; }
.head-top .head-row { margin-bottom: 2px; }
.head-top .page-sub.loc { margin-bottom: 0; }
.head-id { font: 500 15px/1 var(--font-mono); color: var(--ink-3); }
.loc-note { color: var(--ink-3); }
.readouts { display: flex; gap: 30px; flex-wrap: wrap; }
.readout { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; text-align: right; }
.readout .eyebrow { margin: 0; }
.readout-v { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
             font: 500 15px/20px var(--font-mono); color: var(--ink); }
.readout-v .status { font-size: 15px; }
.readout-v [data-last-contact] { color: var(--ink-2); }
.readout-v .sep { color: var(--ink-4); }
/* The clock readout is the timing badge with its chrome taken off: one
   component, the same words, no box. The sync state shows only when it is
   not locked, and the bias belongs to the detail and to the Timing card. */
.readouts .timing-badge { border: 0; border-radius: 0; background: none; font: inherit; }
.readouts .timing-badge > span { padding: 0; border-left: 0; }
.readouts .timing-badge > span + span::before { content: "·"; color: var(--ink-4); margin: 0 7px; }
.readouts .timing-badge .src, .readouts .timing-badge.unvalidated .src {
  background: none; letter-spacing: 0.04em;
}
.readouts .timing-badge .state-locked, .readouts .timing-badge .bias { display: none; }

.head-detail { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.head-detail > summary { display: flex; align-items: center; gap: 8px 18px; flex-wrap: wrap;
                         cursor: pointer; list-style: none; font-size: 14px; }
.head-detail > summary::-webkit-details-marker { display: none; }
.head-notes { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.head-note { color: var(--ink-2); }
.head-note.warm { color: var(--warm); }
.head-note.warm::before { content: "• "; }
.head-more { margin-left: auto; color: var(--accent); }
.head-more::after { content: " →"; }
.head-detail[open] .head-more::after { content: " ↑"; }
.head-detail > summary:hover .head-more { color: var(--accent-2); text-decoration: underline; }
.head-detail .badge-row { margin: 14px 0 0; }
.chips-row { margin-top: 16px; }
.chips-row .chip { white-space: normal; line-height: 1.3; } /* sentences, not labels: let them wrap on a phone */
.identity-block { margin-bottom: 20px; }
.session-block { margin-top: 8px; }

.cols { display: grid; gap: 20px; }
/* A grid item's min-width is auto: one unbreakable mono URL in a card made the
   column wider than a phone and the page scrolled sideways. Zero lets the
   column stay the column, and long paths break where they must. */
.cols > *, .stat-grid > * { min-width: 0; }
.prose, .callout, ol.numbered li, .step p { overflow-wrap: anywhere; }
.g-2 { grid-template-columns: 1fr 1fr; }
.g-3 { grid-template-columns: 1fr 1fr 1fr; }
.g-4 { grid-template-columns: repeat(4, 1fr); }
.g-2-1 { grid-template-columns: 2fr 1fr; }
.g-1-2 { grid-template-columns: 1fr 2fr; }
@media (max-width: 900px) { .g-2, .g-3, .g-4, .g-2-1, .g-1-2 { grid-template-columns: 1fr; } }

/* ---------- cards ---------- */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  padding: 18px 20px;
}
.card.inset { background: var(--surface-2); padding: 12px 16px; margin-bottom: 12px; }
/* A flush card drops its padding so a table can meet the border. That also
   stripped the head row of its gutter: the eyebrow sat a cell-padding to the
   LEFT of the first column heading and the as-of count ran into the right
   border. Give the head its own gutter, at the cell padding of the table it
   heads, so the label starts where the first heading starts and the count
   stops where the last column stops. A card whose table tightens its cells
   re-declares the gutter and the head follows it. */
.card.flush { padding: 0; --flush-gutter: 12px; }
.card.flush > .eyebrow,
.card.flush > h2.t-card { margin: 0; padding: 14px var(--flush-gutter); }
.card.flush > .eyebrow + h2.t-card { padding-top: 0; }
.card.warm { border-color: var(--warm-line); }
.card.accent { border-color: var(--accent-line); }
.card > .eyebrow:first-child { margin-top: 2px; }
.eyebrow { margin: 0 0 12px; display: flex; align-items: baseline; gap: 10px; }
.eyebrow .asof { margin-left: auto; font: 400 11px var(--font-mono); letter-spacing: 0; text-transform: none; }
.card h2.t-card, .card > h3 { font: var(--t-card); margin: 0 0 8px; }

/* ---------- stat tiles: 44 px mono value, warm recolours value + label ---------- */
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
.stat-grid.two { grid-template-columns: repeat(2, minmax(0, 260px)); }
@media (max-width: 900px) { .stat-grid, .stat-grid.two { grid-template-columns: 1fr 1fr; } }
.stat .stat-value {
  font: 600 44px/1 var(--font-mono); font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em; color: var(--ink);
}
.stat .stat-value .unit { font: 400 15px var(--font-mono); color: var(--ink-3); margin-left: 6px; letter-spacing: 0; }
.stat .stat-label { font: var(--t-label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); margin-top: 8px; }
.stat .stat-note { color: var(--ink-3); font-size: 13px; line-height: 1.4; margin-top: 5px; }
.stat.warm .stat-value, .stat.warm .stat-label { color: var(--warm); }
.stat.absent .stat-value { color: var(--ink-3); }
/* A value that is a word, not a figure: a ten-character date wrapped mid-value
   at the headline size. Same tile, a size the value fits in. */
.stat.long .stat-value { font-size: 30px; letter-spacing: -0.01em; }

/* The residual panels carry their own labels at a fixed type size, so scaling
   the whole SVG down to a 390 px phone renders them at about 4 px. Wide content
   scrolls inside its own box here rather than shrinking: the chart keeps a
   legible minimum and the card, not the page, is what moves sideways. */
/* ---------- the interactive residual panels (uPlot) ----------
   The server's SVG is what loads and what a reader without JavaScript keeps;
   `.residual-chart.is-live` means static/residuals.js has an interactive chart
   actually on screen and has hidden the fallback. Everything here is scoped
   under that class, so a page that never upgrades never pays for these rules.

   uPlot is themed through OUR tokens rather than its own stylesheet defaults,
   for the same reason charts.py takes its series colours from --s1..--s3: two
   sources of truth for a colour drift, and the drift shows up as an
   interactive chart that disagrees with the picture beside it. */
.residual-live { display: grid; gap: 10px; }
.residual-chart { position: relative; }
.residual-chart .u-title {
  font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-3);
}
.residual-chart .u-legend { font: 400 12.5px var(--font-mono); color: var(--ink-2); }
.residual-chart .u-legend .u-marker { border-radius: 2px; }
/* uPlot marks a hidden series by dimming the row; make that unmistakable,
   because toggling a series off is how a reader separates two sources whose
   values coincide. */
.residual-chart .u-legend .u-off .u-label { opacity: 0.45; text-decoration: line-through; }
.residual-chart .u-legend .u-series { cursor: pointer; }
.residual-chart .u-select { background: var(--accent-wash, rgba(86, 168, 224, 0.12)); }
.residual-chart .u-cursor-x, .residual-chart .u-cursor-y {
  border-color: var(--chart-axis);
}

/* The hover read-out. Positioned by script inside the panel, so it is clamped
   to the chart rather than the viewport and never leaves the card. */
.chart-tip {
  position: absolute; z-index: 30; pointer-events: none;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r1); box-shadow: 0 8px 22px rgba(0, 0, 0, 0.35);
  padding: 8px 10px; max-width: 320px;
  font: 400 12.5px/1.45 var(--font-ui); color: var(--ink-2);
}
.chart-tip[hidden] { display: none; }
.chart-tip .tip-head {
  font-family: var(--font-mono); font-size: 12px; color: var(--ink);
  margin-bottom: 5px;
}
.chart-tip .tip-row { display: grid; gap: 1px; margin-top: 5px; }
.chart-tip .tip-row b { color: var(--ink); font-weight: 600; }
.chart-tip .tip-row span { color: var(--ink-3); font-family: var(--font-mono); font-size: 11.5px; }
.chart-actions { margin-top: 2px; }

/* A source whose numbers are another source's numbers. Said, not hidden: see
   web/data.py:_mark_coincidences. */
.coincides { color: var(--ink-3); font-size: 12.5px; }

.chart-scroll { overflow-x: auto; max-width: 100%; }
/* charts._svg wraps the <svg> in .chart, and `.chart svg` is width:100% -- so
   the minimum has to be set on the wrapper, not on the svg, or the svg simply
   fills whatever narrow box it is given and the labels scale away with it. */
.chart-scroll > .chart { min-width: 560px; }

/* ---------- chips: the semantic vocabulary ---------- */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font: 500 12px/1 var(--font-mono);
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: var(--r1); padding: 6px 9px; color: var(--ink-2);
  white-space: nowrap;
}
.chip.version { color: var(--accent-2); border-color: var(--accent-line); background: var(--accent-tint); }
.chip.hint { border-style: dashed; border-color: var(--ink-4); background: transparent; color: var(--ink-2); } /* advisory */
.chip.warm { color: var(--warm); background: var(--warm-tint); border-color: var(--warm-line); }             /* discovery */
.chip.sample { color: var(--warm); border-style: dashed; border-color: var(--warm-line); background: transparent; white-space: normal; line-height: 1.3; } /* long enough to wrap on a phone */
.chip.build { color: var(--ink-2); }

/* ---------- status(): dot + word, never colour alone ---------- */
.status { display: inline-flex; align-items: center; gap: 7px; font: 400 14px/1.2 var(--font-ui); color: var(--ink); }
.status .dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--ink-3); }
.status .word { font-weight: 400; }
.status.ok .dot { background: var(--ok); }
.status.warn .dot { background: var(--warn); }
.status.serious .dot { background: var(--serious); }
.status.crit .dot { background: var(--crit); }
.status.muted, .status.absent { color: var(--ink-3); }
.status.muted .dot, .status.absent .dot { background: transparent; border: 1.5px solid var(--ink-3); width: 8px; height: 8px; } /* absent is a value */

/* ---------- timing_badge — SOURCE ‖ sync ‖ σ ‖ bias ---------- */
.timing-badge {
  display: inline-flex; align-items: stretch; flex-wrap: wrap;
  font: 500 12px/1.2 var(--font-mono);
  border: 1px solid var(--line-strong); border-radius: var(--r1); overflow: hidden; white-space: nowrap;
  background: var(--surface);
}
.timing-badge > span { padding: 7px 9px; border-left: 1px solid var(--line-strong); display: inline-flex; align-items: center; gap: 5px; }
.timing-badge > span:first-child { border-left: 0; }
.timing-badge .src { background: var(--surface-2); color: var(--accent-2); letter-spacing: 0.06em; font-weight: 600; }
.timing-badge .state .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; background: currentColor; }
.timing-badge .state-locked { color: var(--ok); }
.timing-badge .state-holdover { color: var(--warn); }
.timing-badge .state-unsync { color: var(--crit); }
.timing-badge .val { color: var(--ink-2); }
.timing-badge .val.absent { color: var(--ink-3); }
.timing-badge.unvalidated { border-color: var(--warm-line); }
.timing-badge.unvalidated > span { border-left-color: var(--warm-line); }
.timing-badge.unvalidated .src { background: var(--warm-tint); color: var(--warm); }
.timing-badge.unvalidated .state-unsync { color: var(--warn); }

/* ---------- tables: option A, roomy 44 px rows ---------- */
.table-wrap { overflow-x: auto; max-width: 100%; }
table.data { width: 100%; border-collapse: collapse; font-size: 14px; }
table.data th {
  text-align: left; font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-3); padding: 10px 12px;
  border-bottom: 1px solid var(--line-strong); background: var(--surface-2);
  white-space: nowrap;
}
table.data td { padding: 0 12px; height: 44px; border-bottom: 1px solid var(--line); vertical-align: middle; }
table.data tr:last-child td { border-bottom: 0; }
table.data td.num, table.data th.num { text-align: right; }
table.data td.wrap { white-space: normal; height: auto; padding-top: 10px; padding-bottom: 10px; }
table.data td.nowrap { white-space: nowrap; } /* a word and its age stay on one line; the wrap scrolls */
table.data td.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
table.data tbody tr:hover td { background: rgba(86, 168, 224, 0.04); }
.table-foot {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 10px 12px; border-top: 1px solid var(--line); font-size: 13px; color: var(--ink-3);
}
.table-foot .pager { margin-left: auto; display: flex; gap: 8px; }
.table-foot .pager a, .table-foot .pager span { font: 500 13px var(--font-ui); padding: 8px 12px; border: 1px solid var(--line-strong); border-radius: var(--r1); color: var(--ink); }
.table-foot .pager span[aria-disabled="true"] { color: var(--ink-4); border-style: dashed; }
/* ---------- the object table's controls: search, regime chips, sort, paging
   (web/pages.py, static/objects.js). The table is server-rendered and sorted
   before any of this runs; these styles dress controls that already work as a
   plain form, and the .live class the script adds is what turns the headings
   into something that looks clickable. ---------- */
.table-tools {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px; border-bottom: 1px solid var(--line-strong); background: var(--surface-2);
}
.table-tools form { gap: 8px; }
.code-input.find-input {
  font: 400 15px var(--font-mono); letter-spacing: 0; text-align: left;
  width: 260px; max-width: 100%;
}
.filter-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.tools-count { margin-left: auto; font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }

button.filter-chip { cursor: pointer; gap: 7px; }
button.filter-chip:hover { border-color: var(--ink-3); color: var(--ink); }
button.filter-chip .c { color: var(--ink-3); font-variant-numeric: tabular-nums; }
button.filter-chip.on { color: var(--accent-2); border-color: var(--accent-line); background: var(--accent-tint); }
button.filter-chip.on .c { color: var(--accent-2); }

/* The four regimes read as four regimes, and never by colour alone: the word
   is in the chip. The tint only makes a column of 500 rows scannable. */
.chip.regime-leo { color: var(--ok); border-color: var(--ok); background: transparent; }
.chip.regime-meo { color: var(--accent-2); border-color: var(--accent-line); background: transparent; }
.chip.regime-geo { color: var(--warm); border-color: var(--warm-line); background: transparent; }
.chip.regime-heo { color: var(--serious); border-color: var(--serious); background: transparent; }

/* Sortable headings. Inert until the script marks the table .live -- a heading
   that looks like a button and does nothing is worse than a plain heading. */
table.data th.sort { white-space: nowrap; }
.live table.data th.sort { cursor: pointer; user-select: none; }
.live table.data th.sort:hover { color: var(--ink); }
.live table.data th.sort::after {
  content: "\2195"; margin-left: 6px; color: var(--ink-4); font-weight: 400;
}
.live table.data th.sort.asc::after { content: "\2191"; color: var(--accent-2); }
.live table.data th.sort.desc::after { content: "\2193"; color: var(--accent-2); }
.live table.data th.sort:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
table.data.objects td .absent { color: var(--ink-3); }
.list-empty { padding: 18px 12px; margin: 0; }

/* List.js writes the page numbers into this nav; it is empty in the served
   HTML and stays empty without the script, which is why it carries no border
   of its own. */
.pagination { display: flex; gap: 6px; flex-wrap: wrap; padding: 0; margin: 0; list-style: none; }
.pagination:not(:empty) { padding: 10px 12px; border-top: 1px solid var(--line); }
.pagination li { list-style: none; }
.pagination a.page {
  display: inline-block; min-width: 34px; text-align: center;
  font: 500 13px var(--font-mono); padding: 7px 9px; border-radius: var(--r1);
  border: 1px solid var(--line-strong); color: var(--ink); text-decoration: none;
}
.pagination a.page:hover { border-color: var(--ink-3); text-decoration: none; }
.pagination li.active a.page { color: var(--accent-2); border-color: var(--accent-line); background: var(--accent-tint); }
.pagination li.disabled a.page { border: 0; color: var(--ink-4); pointer-events: none; }

/* Twelve columns is a lot of table. The secondary and the DERIVED ones fold
   away first on a narrow screen; what the network measured itself -- tracks,
   measurements, when it was last seen -- never folds. */
@media (max-width: 1100px) {
  table.data.objects .col-lo { display: none; }
  .code-input.find-input { width: 100%; }
  .tools-count { margin-left: 0; }
}

/* ---------- the stations table: seven columns that fold rather than scroll
   This is the one table whose width the page does not control: a station name
   is whatever its owner typed, and a declared position adds a chip. It used to
   answer that by scrolling sideways -- 110 px of it at full width, 919 px on a
   phone, with Last night off the edge of the card at every size.

   It folds instead. The breakpoints below start from the table's own
   measured minimum plus the page's 48 px of gutter, and each was then moved
   up until a width sweep from 320 px to 2560 px reported zero overflow at
   every step: the arithmetic alone put every fold 12 to 42 px too late, which
   showed as a thin band of sideways scroll just above each breakpoint.

   Fold order is by what survives elsewhere. Owner and the id ride along under
   the name (``pages._fold_ins``). Location survives on the map directly above,
   where every placed station is a pin. The measurement count survives on the
   station's own page, and the build on it too. Sky has nowhere else to be, so
   it folds into the Name cell instead of away. Station, Name, Last seen,
   Solving and Last session are the reason somebody opened the list, and never
   fold.

   The rungs moved down on 2026-09-11 because the columns got narrower, not
   because the ladder was wrong: Location went from "37.98° N · 23.73° E" to
   "37.98, 23.73" with the full figure on the clipboard, and a station name is
   capped at 20ch instead of 30. Measured after: all ten columns overflow the
   card by 5 px at 1080 and fit from about 1085, so the widest rung sits at
   1120 with the margin a user-typed name can eat. ---------- */
.card.flush.stations-card { --flush-gutter: 8px; }
/* Eight, not the page's ten: ten columns at 10 px overran the card by 37 px at
   every width above the widest fold, and a card that cannot hold its table is
   the defect this file's sweep exists to catch. Four pixels a column, ten
   columns, forty back. */
table.data.stations th, table.data.stations td { padding-left: 8px; padding-right: 8px; }
table.data.stations th { letter-spacing: 0.03em; }
/* A chip in a data cell is a label, not a badge; the page's chip padding
   costs this table a column's worth across seven of them. */
table.data.stations td .chip { font-size: 11.5px; padding: 1px 6px; }
table.data.stations td.col-build { white-space: nowrap; }
/* The build reads as the value it is, with the verdict a glyph beside it
   rather than a chip around a sentence. */
.build-sha { font-size: 12.5px; color: var(--ink-1); }
.build-mark { margin-left: 5px; font-size: 12.5px; line-height: 1; }
.build-mark.ok { color: var(--ok); }
.build-mark.warn { color: var(--warm); }
.build-mark.hint { color: var(--ink-3); }
/* Text for a screen reader and for nobody else: the glyph above is
   aria-hidden, so the word it stands for has to exist somewhere. Not
   display:none, which is not read out either. */
.a11y {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

table.data.stations td.col-name { white-space: nowrap; height: auto; padding-top: 8px; padding-bottom: 8px; }
/* A name is user input. Clamp it to a column's worth and let the title
   attribute carry the whole of it; without this, one long name reopens the
   sideways scroll the fold ladder exists to close. */
table.data.stations td.col-name > a {
  display: inline-block; max-width: 20ch; overflow: hidden;
  text-overflow: ellipsis; vertical-align: bottom; white-space: nowrap;
}
/* Why a station is not solving, under the word that says it is not. Wraps:
   it is a sentence fragment in a column sized for a status word, and letting
   it hold the column open would cost every other row the width. */
table.data.stations .why-line {
  display: block; line-height: 1.3; margin-top: 2px; max-width: 16ch;
  white-space: normal; color: var(--warn);
}

/* Served at every width, shown only once the matching column has gone. */
table.data.stations .fold-id, table.data.stations .fold-owner,
table.data.stations .fold-sky { display: none; line-height: 1.35; }
/* The session's own figures, under its date: a second line inside one cell
   rather than three more columns, because they are only ever read together
   and only ever after the date above them. */
table.data.stations .session-counts { display: block; line-height: 1.35; }

@media (max-width: 1120px) {
  table.data.stations .col-owner { display: none; }
  table.data.stations .fold-owner { display: block; }
}
@media (max-width: 1045px) {
  /* Build folds before Location does: the station's own page names it in
     full, with the observed date beside it, and a reader narrow enough to
     lose a column is not the reader comparing builds across the fleet. */
  table.data.stations .col-build { display: none; }
}
@media (max-width: 860px) {
  table.data.stations .col-meas { display: none; }
}
@media (max-width: 760px) {
  /* Sky has nowhere else to survive -- it is computed for this list and
     appears on no other page -- so it travels into the Name cell rather than
     being dropped. */
  table.data.stations .col-sky { display: none; }
  table.data.stations .fold-sky { display: block; margin-top: 2px; }
}
@media (max-width: 720px) {
  table.data.stations .col-id { display: none; }
  table.data.stations .fold-id { display: block; }
  table.data.stations td.col-name > a { max-width: 18ch; }
  /* The last two columns stop being one-liners rather than being dropped: a
     phone has vertical room to spare and these are the two facts the list is
     read for. */
  table.data.stations td.col-health, table.data.stations td.col-night,
  table.data.stations td.col-sky { white-space: normal; }
  table.data.stations th, table.data.stations td { padding-left: 6px; padding-right: 6px; }
  .card.flush.stations-card { --flush-gutter: 6px; }
}
@media (max-width: 420px) {
  table.data.stations td.col-name > a { max-width: 11ch; }
  /* Four headings and a 320 px phone: "Last session" is wider than the column
     its cells need, so the heading wraps rather than the table overflowing.
     Wrapping a two-word heading costs one line of card; the alternative costs
     the reader a horizontal scrollbar. */
  table.data.stations th { white-space: normal; }
  table.data.stations th, table.data.stations td { padding-left: 4px; padding-right: 4px; }
  .card.flush.stations-card { --flush-gutter: 4px; }
}

/* The list leads with its own sentence rather than hiding it behind the (i):
   a reader who has just arrived should not have to ask what they are looking
   at, and the tip is then free to say what the groups mean. */
[data-page="stations"] .page-title { margin-bottom: 4px; }
[data-page="stations"] .page-sub { margin: 0 0 14px; }

/* ---------- /stations: the fleet strip, the filter, the triage table
   The list answers "is anything wrong?" before it answers "what exists?".
   Three pieces carry that: five figures above the fold, a filter that dims
   rather than removes, and one tbody per triage group. ---------- */

/* Five figures, hairline-separated. The gap is the grid's own background
   showing through, which keeps the rule a single pixel at every zoom. */
.fleet-strip {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px;
  background: var(--line); border: 1px solid var(--line);
  border-radius: var(--r1); overflow: hidden; margin-bottom: 12px;
}
.fleet-tile { background: var(--surface); padding: 14px 16px; }
.fleet-label {
  font: var(--t-label); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3); margin: 0 0 6px;
}
.fleet-value { font: 500 26px/1 var(--font-mono); font-variant-numeric: tabular-nums; margin: 0; color: var(--ink); }
.fleet-value.ok { color: var(--ok); }
.fleet-value.warn { color: var(--warn); }
/* The denominator is context, not the figure: how many stations could be on
   the newest build at all. */
.fleet-value .fleet-of { color: var(--ink-4); }
.fleet-note { font: 400 13px/1.45 var(--font-ui); color: var(--ink-3); margin: 6px 0 0; }

/* The filter and, on the same line, what the map and the table are currently
   saying about one station. */
.station-filter {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
}
.filter-buttons { display: flex; gap: 8px; flex-wrap: nowrap; }
.filter-btn {
  flex: none; white-space: nowrap; padding: 6px 11px;
  font: 500 12px/1 var(--font-mono);
}
/* The page's own active treatment, not a glyph: a mark alone is invisible to
   a reader who cannot see it, and aria-pressed carries it to one who cannot. */
.filter-btn[aria-pressed="true"] {
  color: var(--accent-2); border-color: var(--accent-line); background: var(--accent-tint);
}
/* One line, always. The text changes on hover and on tap, and a readout
   that wraps to a second line moves the map under the finger between
   touchstart and touchend -- which on a phone is a pin that stops opening
   its station. Clamped rather than reserved: reserving three lines of empty
   space above the map costs every reader the fold. */
.filter-readout {
  margin: 0; margin-left: auto; font: 400 12px/1.4 var(--font-mono);
  color: var(--ink-3); text-align: right; min-width: 0; max-width: 100%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* One tbody per group, opened by a seam that names it and says what it
   means. Not a row of data: it spans the table and carries no station. */
/* Vertical padding only: the side padding is the table's own gutter, which
   tightens with the fold ladder, so the group's name starts exactly where
   the Station column's does at every width. */
table.data.stations tr.group-head > td {
  background: var(--surface-sub); border-top: 1px solid var(--line);
  padding-top: 8px; padding-bottom: 8px; white-space: normal;
}
table.data.stations tr.group-head .group-title {
  font: var(--t-label); letter-spacing: 0.07em; text-transform: uppercase;
}
table.data.stations tr.group-head .group-note {
  font: 400 12px/1 var(--font-mono); color: var(--ink-3); margin-left: 8px;
}
tr.group-head.attention .group-title { color: var(--serious); }
tr.group-head.healthy .group-title { color: var(--ok); }
tr.group-head.new .group-title { color: var(--ink-3); }

/* The group colour rides down the left edge of every row in it, so a row
   scrolled away from its header still says which group it is in. */
/* An inset shadow rather than a border: .card.flush drops the card's padding
   so the table can meet its edge, and a real 3 px border on the first cell
   moves every row that far right of its own heading. A shadow paints the
   same bar and costs no width. */
table.data.stations tbody tr.st-row.attention > td:first-child { box-shadow: inset 3px 0 0 var(--serious); }
table.data.stations tbody tr.st-row.healthy > td:first-child { box-shadow: inset 3px 0 0 var(--ok); }
table.data.stations tbody tr.st-row.new > td:first-child { box-shadow: inset 3px 0 0 var(--line-strong); }
table.data.stations tbody tr.st-row:hover, table.data.stations tbody tr.st-row.lit {
  background: var(--surface-2);
}
/* Filtered out, not gone: the row keeps its place and its link. */
table.data.stations tbody tr.st-row.dim { opacity: 0.42; }
table.data.stations tbody tr.st-row.dim > td:first-child { box-shadow: inset 3px 0 0 var(--line); }

/* Health: the word, and under it the one fact that produced it. */
table.data.stations .health-why {
  display: block; line-height: 1.35; margin-top: 2px; color: var(--ink-3);
  font-size: 13px; white-space: normal;
}

/* Sky now: two glyph lines, each glyph followed by the word it stands for
   and the figure behind the word. The glyph is never alone -- it is
   aria-hidden and the word carries the meaning. */
table.data.stations .sky-line { display: flex; align-items: center; gap: 6px; line-height: 1.5; }
table.data.stations .sky-line + .sky-line { margin-top: 3px; }
.sky-sun, .sky-cloud-bar { flex: none; display: block; }
.sky-sun .horizon { stroke: var(--line-strong); stroke-width: 1; }
.sky-sun .disc { fill: var(--ink-3); }
.sky-sun.up .disc, .sky-sun.up .ray { fill: var(--warm); stroke: var(--warm); }
.sky-sun.twilight .disc { fill: var(--accent-2); }
.sky-sun .ray { stroke: var(--warm); stroke-width: 1.2; stroke-linecap: round; opacity: 0.25; }
.sky-sun.up .ray { opacity: 1; }
.sky-cloud-bar .blk { fill: var(--line); }
.sky-cloud-bar .blk.on { fill: var(--ink-3); }
.sky-cloud-bar.thick .blk.on { fill: var(--ink-2); }
.sky-word { font: 400 14px/1.2 var(--font-ui); }
.sky-word.warm { color: var(--warm); }
.sky-word.twilight { color: var(--accent-2); }
.sky-word.ok { color: var(--ok); }
.sky-word.dim { color: var(--ink-2); }
.sky-word.faint { color: var(--ink-3); }

/* The coordinates moved off the list and onto the station's own head, and
   the clipboard came with them: the cell showed two decimals, the click has
   always copied the stored figure. */
.copy-coords {
  background: none; border: 0; padding: 0; margin: 0; font: inherit;
  color: inherit; cursor: default; white-space: nowrap;
}
.js .copy-coords { cursor: pointer; text-decoration: underline dotted var(--ink-4); text-underline-offset: 3px; }
.js .copy-coords:hover { color: var(--ink); text-decoration-color: var(--accent); }
.copy-coords:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The strip is five figures wide only where five fit. Below that it is two
   rows of tiles and then one column; nothing is dropped, because each of the
   five answers a question the others do not. */
@media (max-width: 900px) {
  .fleet-strip { grid-template-columns: repeat(2, 1fr); }
  .station-filter { align-items: flex-start; }
  .filter-buttons { flex-wrap: wrap; }
  .filter-readout { margin-left: 0; text-align: left; }
}
@media (max-width: 520px) {
  .fleet-strip { grid-template-columns: 1fr; }
  .fleet-value { font-size: 22px; }
}

.list-rows { display: flex; flex-direction: column; }
.list-rows .row { display: flex; align-items: baseline; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.list-rows .row:last-child { border-bottom: 0; }
.list-rows .row .right { margin-left: auto; color: var(--ink-3); }

/* ---------- key-value ---------- */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 8px 18px; margin: 0; font-size: 14px; }
.kv dt { color: var(--ink-3); }
.kv dd { margin: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--ink); overflow-wrap: anywhere; }
.kv dd .note { font-family: var(--font-ui); color: var(--ink-2); }
.kv dd.absent, .kv dd .absent { color: var(--ink-3); font-family: var(--font-ui); }

/* ---------- health list: dot + word, then the note in words ---------- */
.health-list { display: flex; flex-direction: column; gap: 0; }
.health-row { display: flex; gap: 14px; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--line); }
.health-row:last-child { border-bottom: 0; }
.health-row .h-label { flex: 0 0 120px; }
.health-row .h-label .status { font-size: 14px; }
.health-row .h-val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.health-row .h-note { color: var(--ink-2); font-size: 14px; line-height: 1.45; }

/* ---------- operations: units, uplink, pipeline ----------
   A refused upload is the one thing on this page that must be impossible to
   scroll past, so it wears a rule in the critical colour, states the reason in
   prose, and prints the server's own words in mono underneath. Backpressure
   gets the warm rule instead: the network asking a station to wait is flow
   control, and colouring it like a failure would teach the eye to skip both. */
.ops-card .ops-up { padding: 12px 0 14px; border-bottom: 1px solid var(--line); }
.ops-card .ops-up:last-of-type { border-bottom: 0; }
.ops-card .ops-up.ops-bad { border-left: 3px solid var(--crit); padding-left: 14px; margin-left: -17px; }
.ops-card .ops-up.ops-wait { border-left: 3px solid var(--warn); padding-left: 14px; margin-left: -17px; }
.ops-card .ops-why { margin: 8px 0 0; color: var(--ink-2); font-size: 14px; line-height: 1.5; max-width: 68ch; }
.ops-card .ops-bad .ops-why { color: var(--crit); }
.ops-card .ops-err {
  margin: 8px 0 6px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface-2); color: var(--ink);
  font: 500 13px/1.5 var(--font-mono); overflow-wrap: anywhere;
}
.ops-card .ops-foot { margin: 6px 0 0; color: var(--ink-3); }
.ops-card .ops-units { margin: 16px 0 4px; }
.ops-card .ops-units .chip .status { font-size: 12px; }
.ops-card .kv { margin-top: 14px; }

/* ---------- action rows: name · consequence · affordance ---------- */
.action-list { display: flex; flex-direction: column; gap: 0; }
.action-row { display: flex; gap: 14px; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--line); }
.action-row:last-child { border-bottom: 0; }
.action-row .a-name { font-weight: 600; color: var(--ink); }
.action-row .a-why { color: var(--ink-3); font-size: 13px; line-height: 1.4; }
.action-row .a-do { margin-left: auto; flex: none; }

/* ---------- buttons + inputs: 44 px minimum target under 768 px ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font: 500 14px/1 var(--font-ui); color: var(--ink);
  background: transparent; border: 1px solid var(--line-strong);
  border-radius: var(--r1); padding: 12px 16px; cursor: pointer; text-decoration: none;
}
.btn:hover { border-color: var(--accent); color: var(--ink); text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
.btn.primary:hover { background: var(--accent-2); border-color: var(--accent-2); }
.btn.ghost { background: transparent; }
.btn.hint { border-style: dashed; color: var(--ink-3); } /* not available to you yet */
.btn.small { font-size: 13px; padding: 8px 12px; }
/* The one destructive control on the site: removing a station. Outlined
   rather than filled, so it does not read as the page's primary action. */
.btn.danger { border-color: var(--crit); color: var(--crit); }
.btn.danger:hover { background: var(--crit); border-color: var(--crit); color: var(--on-accent); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.code-input {
  font: 500 24px var(--font-mono); letter-spacing: 0.12em; text-align: center;
  background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--r1);
  padding: 14px 18px; width: 100%; max-width: 340px;
}
.code-input::placeholder { color: var(--ink-3); }
textarea.code-input { font-size: 14px; letter-spacing: 0; text-align: left; max-width: none; line-height: 1.5; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.field > .t-label { display: block; }
.field input, .field textarea, .field select {
  font: 500 14px/1.5 var(--font-mono); background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--r1); padding: 10px 12px; width: 100%;
}
.field input::placeholder, .field textarea::placeholder { color: var(--ink-4); }

/* ---------- banners + callouts ---------- */
.banner {
  border: 1px solid var(--warm-line); border-radius: var(--r2);
  background: var(--warm-tint); padding: 18px 20px; margin-bottom: 20px;
}
.banner .b-lede { font: 600 18px/1.3 var(--font-display); color: var(--warm); margin: 0 0 6px; }
.banner p { margin: 0; color: var(--ink-2); max-width: 70ch; }
.banner.claimed { border-color: var(--accent-line); background: var(--accent-tint); }
.banner.claimed .b-lede { color: var(--accent-2); }
.banner.bench { border-color: var(--warm); }
.banner.retired { border-color: var(--line-strong); background: var(--surface-2); }
.banner.retired .b-lede { color: var(--ink-2); }
.station-remove-form { max-width: 560px; }
.station-remove-form .code-input { font-size: 15px; letter-spacing: 0; text-align: left; max-width: none; }
.banner-firstlight { border: 1px solid var(--warm-line); border-radius: var(--r2); background: var(--warm-tint); padding: 18px 20px; margin-bottom: 20px; }
.banner-firstlight h2 { font: 600 18px/1.3 var(--font-display); margin: 0 0 6px; color: var(--warm); }
.banner-firstlight p { margin: 0; color: var(--ink-2); max-width: 70ch; }
.callout { border: 1px solid var(--line-strong); border-left: 3px solid var(--accent); border-radius: 0 var(--r2) var(--r2) 0; background: var(--surface); padding: 12px 16px; font-size: 14px; color: var(--ink-2); }
.callout.warm { border-left-color: var(--warm); background: var(--warm-tint); border-color: var(--warm-line); color: var(--warm); }
.callout b, .callout strong { color: var(--ink); }
.callout.warm b, .callout.warm strong { color: var(--warm); font-weight: 600; }
.live-lost {
  display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--warm-line); background: var(--warm-tint); color: var(--warm);
  border-radius: var(--r2); padding: 10px 14px; margin-bottom: 16px; font-size: 14px;
}
.live-lost[hidden] { display: none; }
.live-lost .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--warm); flex: none; }

/* ---------- claim-flow stepper ---------- */
.steps { display: flex; flex-direction: column; gap: 0; counter-reset: step; }
.step { display: flex; gap: 16px; padding: 0 0 26px; position: relative; }
.step::before {
  counter-increment: step; content: counter(step);
  flex: none; width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 13px var(--font-mono);
  border: 1px solid var(--line-strong); color: var(--ink-3); background: var(--surface);
  z-index: 1;
}
.step::after { content: ""; position: absolute; left: 13.5px; top: 28px; bottom: 0; width: 1px; background: var(--line-strong); }
.step:last-child::after { display: none; }
.step.done::before { content: "✓"; color: var(--ok); border-color: var(--ok); }
.step.active::before { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
.step h3 { margin: 3px 0 4px; font: var(--t-card); }
.step p { margin: 0 0 8px; color: var(--ink-2); font-size: 14px; max-width: 60ch; }
.step .flex { margin-top: 6px; }
ol.numbered { margin: 10px 0 0; padding-left: 22px; color: var(--ink-2); font-size: 15px; }
ol.numbered li { margin-bottom: 6px; }
ol.numbered li::marker { font-family: var(--font-mono); color: var(--ink-3); }

/* ---------- charts ---------- */
.chart svg { display: block; width: 100%; height: auto; max-width: 100%; }
.c-grid { stroke: var(--chart-grid); stroke-width: 1; }
.c-axis { stroke: var(--chart-axis); stroke-width: 1; }
.c-line { stroke: var(--s1); stroke-width: 2; fill: none; stroke-linejoin: round; stroke-linecap: round; }
.c-line.ok { stroke: var(--ok); }
.c-area { fill: var(--s1); opacity: 0.12; }
.c-dot { fill: var(--s1); }
.c-dot.ok { fill: var(--ok); }
.c-dot-ring { fill: var(--surface); stroke: var(--s1); stroke-width: 2; }
.c-anno { stroke: var(--accent-line); stroke-width: 1; stroke-dasharray: 3 4; }
.c-label { fill: var(--ink-3); font: 500 10.5px var(--font-mono); }
.c-label-strong { fill: var(--accent-2); font: 500 10.5px var(--font-mono); }
.c-warm { fill: var(--warm); }
.c-trail { stroke: var(--s1); stroke-width: 2.5; stroke-linecap: round; }
.c-fov { stroke: var(--chart-axis); fill: none; }
.c-land { fill: var(--line); }
.c-map-label { paint-order: stroke; stroke: var(--surface); stroke-width: 3px; stroke-linejoin: round; }
/* station pins on the maps: the dot is a link to the station, with a wider
   invisible hit area for a finger and a ring of surface so it reads over land;
   the name appears beside it on hover and on focus */
.c-pin .c-pin-dot { fill: var(--s1); stroke: var(--surface); stroke-width: 1.5; transition: r .12s; }
.c-pin.warm .c-pin-dot { fill: var(--warm); }
/* On /stations a pin carries the row's triage state, so the map and the
   table cannot tell two different stories about one station. The halo is the
   same colour at a fifth of its strength: it makes a 4.5 px dot readable as
   a state without making it a bigger dot. */
.c-pin .c-pin-halo { fill: var(--s1); fill-opacity: 0.18; transition: r .12s ease-out; }
.c-pin.attention .c-pin-dot, .c-pin.attention .c-pin-halo { fill: var(--serious); }
.c-pin.healthy .c-pin-dot, .c-pin.healthy .c-pin-halo { fill: var(--ok); }
.c-pin.new .c-pin-dot, .c-pin.new .c-pin-halo { fill: var(--line-strong); }
/* Dimmed by the filter, not removed: grey, still drawn, still a link. */
.c-pin.dim .c-pin-dot, .c-pin.dim .c-pin-halo { fill: var(--pin-dim); }
.c-pin.dim .c-pin-halo { fill-opacity: 0.12; }
.c-pin:hover .c-pin-halo, .c-pin:focus-visible .c-pin-halo, .c-pin.lit .c-pin-halo { r: 12; }
.c-pin.lit .c-pin-dot { r: 6.5; }
.c-pin.lit .c-pin-label { display: block; }
.c-pin .c-hit { fill: none; stroke: none; pointer-events: all; }
.c-pin .c-pin-label { display: none; fill: var(--ink); font: 600 11px var(--font-ui);
  paint-order: stroke; stroke: var(--surface); stroke-width: 3px; stroke-linejoin: round; pointer-events: none; }
.c-pin:hover .c-pin-dot, .c-pin:focus-visible .c-pin-dot { r: 6.5; }
.c-pin:hover .c-pin-label, .c-pin:focus-visible .c-pin-label { display: block; }
.c-pin:focus-visible { outline: none; }
.c-pin:focus-visible .c-pin-dot { stroke: var(--ink); stroke-width: 2; }
.c-pins a:hover, .c-pins a:focus-visible { z-index: 1; }
.map-card .chart { max-width: 900px; margin: 0 auto; }
/* on a phone the whole world is ~360 px wide: the dot grows to stay visible and
   the hit area to stay tappable (viewBox units; ~2 per px at that width) */
@media (max-width: 720px) {
  .c-pin .c-pin-dot { r: 7; }
  .c-pin:hover .c-pin-dot, .c-pin:focus-visible .c-pin-dot { r: 9; }
  .c-pin .c-hit { r: 28; }
  .c-pin .c-pin-label { font-size: 16px; }
}
.c-ring { fill: none; stroke: var(--accent-line); }

/* ---------- the sky frame (the one photograph: charts.track_frame) ----------
   A real 968 x 608 stack, shown whole and never upscaled: the subject is the
   4 px pitch of the dashes along the trail, and stretching the image past its
   own pixels smooths that away as surely as cropping it would remove the sky.
   So max-width is the frame's own width and the card centres it. The image
   carries its own black, so it gets a hairline rather than a surface behind
   it; width/height are on the <img> so the card does not reflow when it
   loads. */
.sky-frame { margin: 0; max-width: 968px; }
.sky-card .sky-frame, .sky-card > p { margin-inline: auto; max-width: 968px; }
.sky-frame img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: var(--r1);
  background: #000;
}
.sky-frame figcaption { margin-top: 8px; }
.sky-frame figcaption span { white-space: nowrap; }
/* ---------- multi-source series: the residual panels ----------
   One class per element source, taken from element_source.series_token, so a
   source keeps its colour and its mark whether or not the others have data
   that night. Measured against --surface (the card these panels sit in),
   sRGB relative luminance, WCAG 2.1:

               dark        light
     --s1      6.93:1      5.28:1
     --s2      6.21:1      5.06:1
     --s3      8.23:1      5.37:1
     --s4      6.85:1      7.38:1

   All three clear 3:1 (SC 1.4.11, non-text: the marks and lines) and 4.5:1
   (SC 1.4.3, normal text: the direct labels, 11px). Against --surface-2, if a
   panel is ever put in a .card.inset, the light values fall to 4.69 / 4.50 /
   4.77 — still passing, but --s2 only just, so these panels belong on
   --surface.

   The series differ from EACH OTHER by 1.04–1.33:1. They are separable by hue
   and not by lightness, which is why charts.py draws a different mark shape
   per series and never relies on colour to tell two runs apart.

   Four tokens, four sources (element_source seeds --s1..--s4), and four mark
   shapes in charts.RESIDUAL_MARKS. --s4 arrived with the McCants source on
   2026-08-29; it is violet because no semantic token in this palette is, and
   it is the one series with no `var()` alias for that reason. A fifth source
   needs a fifth of each, added together: without its own token it inherits
   .c-dot's --s1 fill and would be a second blue circle. --s4 against
   --surface-2 in light is 6.87:1, so unlike --s2 it is comfortable in a
   .card.inset as well. */
.c-dot.s1 { fill: var(--s1); }
.c-dot.s2 { fill: var(--s2); }
.c-dot.s3 { fill: var(--s3); }
.c-dot.s4 { fill: var(--s4); }
.c-line.s1 { stroke: var(--s1); }
.c-line.s2 { stroke: var(--s2); }
.c-line.s3 { stroke: var(--s3); }
.c-line.s4 { stroke: var(--s4); }
/* the connecting run is context, the marks are the measurements */
.c-line.s1, .c-line.s2, .c-line.s3, .c-line.s4 { stroke-width: 1.25; opacity: 0.55; }
/* Between two tracks: hours in which nothing was measured. The thread is kept
   so the drift across a night is visible, and it is dotted and fainter than
   the run so it is never read as data. static/residuals.js draws the same two
   weights on its canvas. */
.c-line.c-gap { stroke-width: 1; opacity: 0.4; stroke-dasharray: 2 3; }
/* direct labels: the series' own name at the end of its own run */
.c-name { fill: var(--ink-2); font: 600 11px var(--font-mono); }
.c-name.s1 { fill: var(--s1); }
.c-name.s2 { fill: var(--s2); }
.c-name.s3 { fill: var(--s3); }
.c-name.s4 { fill: var(--s4); }
.c-lead { stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 2 3; fill: none; }
.c-panel { fill: var(--ink-3); font: 600 10.5px var(--font-mono); letter-spacing: 0.08em; }

/* ---------- toolbar (filters) ---------- */
.toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
.toolbar .filter {
  font: 13px var(--font-ui); color: var(--ink-2);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r1);
  padding: 8px 10px; display: inline-flex; gap: 6px; align-items: center;
}
.toolbar .filter b { color: var(--ink); font-weight: 600; }
.toolbar .spacer { flex: 1; }

/* ---------- footer ---------- */
.site-footer { border-top: 1px solid var(--line); margin-top: 56px; }
.site-footer .wrap {
  max-width: 1200px; margin: 0 auto; padding: 24px;
  display: flex; gap: 24px; flex-wrap: wrap; align-items: baseline;
  color: var(--ink-3); font-size: 13px;
}
.site-footer .links { display: flex; gap: 18px; }
.site-footer a { color: var(--ink-2); }

/* ---------- misc ---------- */
.flex { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.right { margin-left: auto; }
.divider { border: 0; border-top: 1px solid var(--line); margin: 18px 0; }
.tag-note { font-size: 12px; color: var(--ink-3); font-style: italic; }
.slot { display: flex; gap: 14px; align-items: center; justify-content: space-between; border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
.slot .slot-value { font: 600 26px/1 var(--font-mono); color: var(--ink-3); letter-spacing: -0.02em; }
.credit { font: 500 15px/1.5 var(--font-mono); color: var(--ink); background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: var(--r1); padding: 10px 12px; display: inline-block; margin: 8px 0; }
.url-line { font: 500 13px var(--font-mono); color: var(--ink-3); margin-top: 10px; overflow-wrap: anywhere; }

/* ---------- click to copy (components.copyable) ----------
   The value, then a small button. Without the script there is no button and
   the value is plain text (selectable, as before); with it the whole span is
   the target and the button is the visible affordance and the keyboard one. */
.copy .copy-btn { display: none; }
.js .copy { cursor: pointer; }
.js .copy .copy-text { text-decoration: underline dotted var(--ink-4); text-underline-offset: 3px; }
.js .copy:hover .copy-text { color: var(--ink); text-decoration-color: var(--accent); }
.js .copy .copy-btn {
  display: inline-flex; align-items: center; vertical-align: baseline;
  font: 500 11px/1 var(--font-mono); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3); background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: var(--r1); padding: 4px 7px; margin-left: 7px; cursor: pointer; white-space: nowrap;
}
.js .copy:hover .copy-btn, .copy-btn:focus-visible { color: var(--accent); border-color: var(--accent); }
.instrument-note { margin: 12px 0 0; }
.nearby { margin-bottom: 20px; }
.nearby-list { margin: 8px 0 0; padding-left: 20px; }
.nearby-list li { margin-bottom: 4px; }
.claim-card { margin-bottom: 20px; }
.claim-row { align-items: stretch; }
.claim-row .btn { align-self: center; }
/* The licence grant, made once, beside the button that makes it. */
.grant { margin: 12px 0 0; font-size: 14px; line-height: 1.5; }
.grant label { display: inline; cursor: pointer; }
.grant input { margin-right: 6px; vertical-align: baseline; }
.claim-notes { margin-top: 20px; }
.status-line { margin: 8px 0 0; min-height: 1em; }
.norad-input { max-width: 200px; font-size: 18px; }
.facts-kv { margin-bottom: 14px; }

/* ---------- (i): the explanation, off the page until asked for ----------
   A disclosure, not a title attribute: a button carrying aria-expanded and
   the text right after it in the DOM. Hover and focus show it with no
   script; optical.js adds click-to-pin (touch), Escape, and slides a tip that
   would run off an edge back inside the viewport. In print the text is inline. */
.info { position: relative; display: inline-flex; align-items: center; vertical-align: middle; margin-left: 6px; }
.info-i {
  position: relative; width: 18px; height: 18px; border-radius: 50%; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 11px/1 var(--font-mono); font-style: italic; letter-spacing: 0; text-transform: none;
  color: var(--ink-3); background: var(--surface-2); border: 1px solid var(--line-strong);
  cursor: help;
}
.info-i::after { content: ""; position: absolute; inset: -13px; } /* a 44 px target without a 44 px dot */
.info:hover .info-i, .info:focus-within .info-i, .info.open .info-i { color: var(--accent); border-color: var(--accent); }
.info-tip {
  position: absolute; left: -10px; top: calc(100% + 8px); z-index: 40;
  width: max-content; max-width: min(46ch, calc(100vw - 32px));
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r2);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
  padding: 10px 12px;
  font: 400 13.5px/1.5 var(--font-ui); color: var(--ink-2);
  text-align: left; text-transform: none; letter-spacing: 0; text-wrap: pretty; white-space: normal;
  display: none; /* not visibility: a hidden box still widens the page on a phone */
}
.info-tip::before { content: ""; position: absolute; left: 0; right: 0; top: -9px; height: 9px; } /* hover bridge */
.info:hover .info-tip, .info:focus-within .info-tip, .info.open .info-tip { display: block; }
.info-tip p { margin: 0 0 6px; }
.info-tip p:last-child { margin-bottom: 0; }
.info-tip ol, .info-tip ul { margin: 0; padding-left: 18px; }
.info-tip li { margin-bottom: 3px; }
.info-tip strong { color: var(--ink); }
.info-tip .mono { font-size: 12.5px; overflow-wrap: anywhere; }
h1 .info, h2 .info, h3 .info, .eyebrow .info, .page-sub .info, .a-name .info { font: 400 13px var(--font-ui); }
.eyebrow .info { margin-left: 2px; }
.eyebrow .info-i { width: 16px; height: 16px; font-size: 10px; }
.terms { display: flex; gap: 6px 18px; flex-wrap: wrap; align-items: center; margin: 14px 0 0; font-size: 13px; color: var(--ink-3); }
.terms .term { display: inline-flex; align-items: center; }
@media print {
  .info-tip { position: static; box-shadow: none; width: auto; max-width: none; display: block; margin-top: 4px; }
  .info-i { display: none; }
}

/* ---------- navigation: inline on desktop, a disclosure on mobile ----------
   Plain markup: a button and a list. Wide screens never see the button; under
   768 px the list collapses behind it only when the document is marked .js
   (web/layout.py:_THEME_SCRIPT), so a browser without the script keeps every
   link in reach. It used to be a <details> -- see layout.py:_header for why a
   closed <details> cannot be shown inline on any viewport. */
.nav-toggle { display: none; }
.site-nav .nav-links { display: flex; gap: 20px; }

@media (max-width: 767px) {
  .site-header .wrap { flex-wrap: wrap; gap: 12px; padding: 10px 16px; }
  .site-header .auth .sso-tag { display: none; }
  .site-nav { order: 3; flex-basis: 100%; flex-direction: column; align-items: stretch; gap: 0; }
  .js .nav-toggle {
    display: inline-flex; align-items: center; align-self: flex-start; cursor: pointer;
    font: 600 14px var(--font-ui); color: var(--ink-2); background: none;
    border: 1px solid var(--line-strong); border-radius: var(--r1);
    padding: 10px 16px; min-height: 44px;
  }
  .js .nav-toggle::after { content: " ▾"; color: var(--ink-3); margin-left: 4px; }
  .js .site-nav.open .nav-toggle::after { content: " ▴"; }
  .js .site-nav .nav-links { display: none; }
  .js .site-nav.open .nav-links { display: flex; }
  .site-nav .nav-links {
    flex-direction: column; gap: 0; margin-top: 8px;
    border: 1px solid var(--line); border-radius: var(--r2); background: var(--surface);
  }
  .site-nav .nav-links a {
    padding: 12px 16px; min-height: 44px; box-sizing: border-box;
    display: flex; align-items: center; border-bottom: 1px solid var(--line);
  }
  .site-nav .nav-links a:last-child { border-bottom: 0; }
  .site-nav .nav-links a.active { border-bottom: 1px solid var(--line); }
  .btn, .toolbar .filter, .theme-toggle, .table-foot .pager a { min-height: 44px; }
  .btn.small { min-height: 44px; }
}

/* ---------- mobile layout: stack everything, keep tables scrollable ---------- */
@media (max-width: 600px) {
  .page-body { padding: 20px 16px 48px; }
  .stat-grid, .stat-grid.two { grid-template-columns: 1fr 1fr; gap: 12px; }
  .stat .stat-value { font-size: 32px; }
  .stat.long .stat-value { font-size: 21px; }
  .cols { gap: 14px; }
  .section { margin-top: 28px; }
  .card { padding: 14px 16px; }
  h1.page-title { font-size: 24px; line-height: 1.15; }
  h1.page-title.display { font-size: 30px; line-height: 1.1; }
  .banner, .banner-firstlight { padding: 14px 16px; }
  .kv { grid-template-columns: 1fr; gap: 2px 0; }
  .kv dt { margin-top: 8px; }
  .head-top { align-items: flex-start; }
  .readouts { gap: 8px 20px; }
  .readout { align-items: flex-start; text-align: left; }
  .health-row { flex-wrap: wrap; }
  .health-row .h-label { flex-basis: 100%; }
  .action-row { flex-wrap: wrap; }
  .action-row .a-do { margin-left: 0; flex-basis: 100%; }
  .fam-bar .wrap { padding: 6px 16px; gap: 12px; overflow-x: auto; }
  .fam-bar .lsf { display: none; }
  .timing-badge { font-size: 11.5px; }
  .code-input { font-size: 19px; max-width: 100%; }
  .claim-row { flex-direction: column; }
  .site-footer .wrap { flex-direction: column; gap: 8px; }
}

/* ---------- no-refresh feel: view transitions (progressive) ---------- */
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation: none !important;
  }
}
