:root {
  --bg: #0f1216; --panel: #171b21; --panel2: #1e242c; --line: #2a323c;
  --fg: #e6e9ee; --muted: #8b95a3; --accent: #58b4ff; --link: #7cc4ff;
  --high: #3ecf8e; --medium: #e3b341; --disputed: #ff6b6b; --insufficient: #8b95a3; --estimated: #b48cff;
  /* Location confidence `mobile`: not a grade on the good-to-bad ramp the five
     above sit on, so a cyan of its own rather than a shade of amber or red.
     An operator-declared mobile is not a fault. */
  --mobile: #4ec9e0;
  --confirmed: #58b4ff; --probable: #8ed0ff; --possible: #6b7a8c;
  --d0: #3ecf8e; --d1: #e3b341; --d2: #5a6573;
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.45 -apple-system, system-ui, "Segoe UI", Roboto, sans-serif; }
a { color: var(--link); text-decoration: none; } a:hover { text-decoration: underline; }
code, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .92em; }
.muted { color: var(--muted); }
/* STAGING=true only (internal/web/staging.go): the corner marker that says this
   is staging.idahomesh.org, not the real directory. Fixed, so it is out of the
   landing page's flex layout; pointer-events off, so it never covers a control. */
.staging-badge { position: fixed; right: 8px; bottom: 8px; z-index: 2000; pointer-events: none; padding: 2px 8px; border-radius: 4px; background: var(--medium); color: #000; font-size: 12px; font-weight: 700; }
.top { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 1000; }
.brand { font-weight: 700; font-size: 18px; color: var(--fg); } .brand span { color: var(--accent); font-weight: 500; }
.search { display: flex; gap: 6px; flex: 1 1 320px; max-width: 640px; }
.search input { flex: 1; min-width: 0; padding: 8px 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; font-size: 15px; }
.search button, .btn { padding: 8px 12px; background: var(--panel2); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; cursor: pointer; font-size: 14px; }
.search button:hover, .btn:hover { border-color: var(--accent); }
.links { display: flex; gap: 14px; margin-left: auto; font-size: 14px; }
main { padding: 12px 16px 24px; max-width: 1200px; margin: 0 auto; }
.map { height: 380px; border: 1px solid var(--line); border-radius: 8px; background: #0b0e12; }
.facets { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; margin: 4px 0 10px; font-size: 14px; }
.facets label { cursor: pointer; } .facets input { vertical-align: -2px; margin-right: 4px; }
.legend { margin-left: auto; color: var(--muted); }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin: 0 4px 0 8px; vertical-align: -1px; }
.d0 { background: var(--d0); } .d1 { background: var(--d1); } .d2 { background: var(--d2); }
/* The map legend's declared-mobile entry: a hollow ring, matching the marker,
   which is hollow because the position is a claim and not a fix. */
.dmobile { background: transparent; border: 2px dashed var(--mobile); }
.results { margin-top: 12px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--muted); font-weight: 500; white-space: nowrap; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
tr.row { cursor: pointer; } tr.row:hover td { background: var(--panel2); }
.hash { font-family: ui-monospace, Menlo, monospace; color: var(--accent); letter-spacing: .02em; }
.role { display: inline-block; padding: 1px 7px; border-radius: 10px; background: var(--panel2); color: var(--muted); font-size: 12px; }
.badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 12px; font-weight: 600; color: #0b0e12; }
.badge.high { background: var(--high); } .badge.medium { background: var(--medium); } .badge.disputed { background: var(--disputed); }
.badge.insufficient { background: var(--insufficient); } .badge.estimated { background: var(--estimated); }
.badge.mobile { background: var(--mobile); }
.attr { display: inline-block; padding: 0 6px; border-radius: 8px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.attr.confirmed { background: var(--confirmed); color: #0b0e12; } .attr.probable { background: var(--probable); color: #0b0e12; }
/* The chip's text is --muted, not --possible: --possible on the card ground is
   3.94:1, the one AA failure on the site, and this is 11px text. The dashed
   --possible border is what still reads it as the weak grade (Zeb, 2026-09-14). */
.attr.possible { background: transparent; color: var(--muted); border: 1px dashed var(--possible); }
/* detail */
.head { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: baseline; margin: 6px 0 4px; }
.head h1 { margin: 0; font-size: 22px; font-weight: 650; }
.head .key { font-family: ui-monospace, Menlo, monospace; font-size: 12px; color: var(--muted); word-break: break-all; }
.head .copy { cursor: pointer; color: var(--link); font-size: 12px; margin-left: 6px; }
.facts { display: flex; flex-wrap: wrap; gap: 4px 18px; color: var(--muted); font-size: 13.5px; margin-bottom: 10px; }
.facts b { color: var(--fg); font-weight: 500; }
.strip { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; padding: 10px 12px; margin: 10px 0; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
.strip .txt { flex: 1 1 320px; }
.strip .lbl { color: var(--muted); font-size: 13px; margin-right: -6px; }
.strip .why { cursor: help; border-bottom: 1px dotted var(--muted); font-size: 13px; }
/* Detail dashboard: big map on the left, the two compact cards stacked beside
   it, the wide lists underneath. align-items:start everywhere — a grid row
   otherwise stretches every card to the tallest sibling (a 3-line Radio card
   rendered 1400px tall next to a neighbour table). */
.dash { display: grid; grid-template-columns: minmax(0, 2.1fr) minmax(300px, 1fr); gap: 14px; margin-top: 12px; align-items: stretch; }
.dash.nomap-side { grid-template-columns: 1fr; }
.dash .side { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
/* the compact cards are shorter than the map — let the chart take the slack */
.dash .side .grow { flex: 1; display: flex; flex-direction: column; }
.dash .side .grow #traffic { flex: 1; display: flex; flex-direction: column; min-height: 90px; }
.dash .side .grow #traffic .bars { flex: 1; height: auto; }
.dash .mapcol { display: flex; flex-direction: column; min-width: 0; }
.dash .map { flex: 1; height: auto; min-height: 520px; }
.wide-table { overflow-x: auto; }
.stack { display: grid; gap: 14px; margin-top: 14px; align-items: start; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; margin-top: 14px; align-items: start; }
.card.wide { grid-column: 1 / -1; }
.scroll { max-height: 380px; overflow: auto; margin: 0 -12px -10px; padding: 0 12px; }
/* The "there is more below" cue (app.js watchScroll adds .more while the list
   runs past the bottom). The insets match .scroll's own negative margins so the
   fade covers the rows edge to edge, and it is gone the moment you reach the
   end. pointer-events: none so it never eats a click on the last row. */
.scrollwrap { position: relative; }
.scrollwrap.more::after { content: ''; position: absolute; left: -12px; right: -12px; bottom: 0;
  height: 56px; background: linear-gradient(transparent, var(--panel) 75%); pointer-events: none; }
.scroll table thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; min-width: 0; }
.card h2 { margin: 0 0 8px; font-size: 15px; font-weight: 600; color: var(--fg); }
.card h2 small { color: var(--muted); font-weight: 400; margin-left: 6px; }
.card .empty { color: var(--muted); font-size: 14px; }
.note { color: var(--muted); font-size: 13px; margin-top: 6px; }
/* a value derived from an uncorroborated position — dashed on the map, dotted here */
.soft { border-bottom: 1px dotted currentColor; opacity: .75; }
/* separates the location-confidence toggle from the role facets beside it */
.facets .sep { margin-left: 10px; padding-left: 12px; border-left: 1px solid var(--line); }
.bars { display: flex; align-items: flex-end; gap: 2px; height: 96px; margin: 6px 0 4px; border-bottom: 1px solid var(--line); }
.bar { flex: 1; display: flex; flex-direction: column-reverse; min-width: 3px; height: 100%; }
.bar.zero::after { content: ""; display: block; height: 2px; background: var(--line); }
.bar i { display: block; width: 100%; min-height: 2px; }
.bar i.confirmed { background: var(--confirmed); } .bar i.probable { background: var(--probable); opacity: .75; } .bar i.possible { background: var(--possible); opacity: .6; }
.bar:hover i { filter: brightness(1.25); }
.axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 12px; }
.nf { padding: 30px 0; color: var(--muted); font-size: 16px; }
.leaflet-container { background: #0b0e12; font: inherit; }
.leaflet-tooltip { background: var(--panel2); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; box-shadow: none; font-size: 13px; }
.leaflet-tooltip-top:before { border-top-color: var(--line); }
.leaflet-control-attribution { background: rgba(15,18,22,.8) !important; color: var(--muted) !important; font-size: 10px; }
.leaflet-control-attribution a { color: var(--muted) !important; }
.leaflet-bar a { background: var(--panel2) !important; color: var(--fg) !important; border-color: var(--line) !important; }
.foot { padding: 12px 16px 24px; color: var(--muted); font-size: 13px; max-width: 1200px; margin: 0 auto; }
@media (max-width: 900px) {
  .dash { grid-template-columns: 1fr; }
  .dash .map { min-height: 360px; }
}
@media (max-width: 640px) {
  /* the header wraps to three rows here; pinned, it ate a sixth of the screen */
  .top { position: static; padding: 8px 10px; gap: 8px 12px; }
  /* …and the nav links ride on the brand's row rather than claiming a third
     one: 118px of header at 375px, most of it links a visitor arriving from a
     bot link did not come for. The search box keeps its own full-width row. */
  .brand { order: 1; }
  .links { order: 2; margin-left: auto; gap: 12px; font-size: 13px; }
  /* The links no longer fit beside the brand at 375px: they wrap as whole
     words, right-aligned, rather than breaking "health check" in two. Seventh
     link ("observers", 2026-10-13): it joins the existing second row rather
     than starting a third, so the header is the same 145px at 375px it was
     with six. The next one may not be free — measure before adding it. */
  .links { flex-wrap: wrap; justify-content: flex-end; row-gap: 4px; }
  .links a { white-space: nowrap; }
  .search { order: 3; flex: 1 0 100%; max-width: none; }
  /* short cells ("2 h ago", "3 of 8") stay on one line; the table scrolls sideways instead */
  td.muted, td.num { white-space: nowrap; }
  .head h1 { font-size: 20px; }
  .dash .map { min-height: 300px; }
  /* The packet map was a flat 300px here — 37% of an 812px phone, with the rest
     of the screen spent on chrome. Size it against the viewport instead: 350px
     is the measured chrome above it (header 95, title 30, facts 67, warning
     strips 61) plus the margin that keeps the "Fan-out by hop" card's heading
     above the fold. Floor 300px so a short screen is no worse than before, and
     a 60svh ceiling so a tall one still shows the card (Zeb, 2026-09-14). */
  #packet #map-packet { flex: none; height: clamp(300px, calc(100svh - 350px), 60svh); }
  main { padding: 10px; }
  .scroll { max-height: 300px; }
  /* Touch targets: a finger needs 44px, and these are the controls a phone
     visitor actually uses. Layout is unchanged — only the hit areas grow. */
  .search input, .search button { min-height: 44px; }
  .facets label { display: inline-flex; align-items: center; min-height: 44px; padding: 0 4px; }
  .facets input { width: 18px; height: 18px; vertical-align: baseline; }
  .facets .sep { margin-left: 6px; padding-left: 8px; }
  .head .copy { display: inline-flex; align-items: center; min-height: 44px; padding: 0 8px; }
  .pbar .btn { min-height: 44px; min-width: 44px; }
  .pathlist .path { min-height: 44px; padding: 6px 5px; }
  /* the fix number sits above its command rather than stealing width from it */
  .fixes li { flex-direction: column; gap: 3px; }
  .fixes li::before { width: auto; }
}
/* ---------------- status card (replaces the flat Configuration card) ----------------
   A repeater's standing: the level as the headline, the three descriptive
   metrics under it, the ordered fix list, and the checks behind the verdict in
   a disclosure. Approved mockup: llm-wiki history/repeater-health-card-design,
   2026-09-18. Everything here reuses the tokens and the .card/.badge/.chip
   primitives above; nothing introduces a colour of its own. */
/* headline: the level badge plus its sub-line */
.status-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; }
.status-head .badge { font-size: 14px; padding: 3px 12px; }
.status-sub { color: var(--muted); font-size: 14px; }
/* metric row: reliance, busy, neighbours, small and secondary to the badge */
.status-metrics { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 13.5px; }
.metric { display: flex; align-items: baseline; gap: 6px; }
.metric-lbl { color: var(--muted); border-bottom: 1px dotted var(--muted); cursor: help; }
.metric-val { font-weight: 600; }
/* configuration row: the settings the observed traffic reveals, on every state
   of the card. Same flex-wrap shape and type scale as .status-metrics above, so
   the two read as one stack; the chips and the "yes" colour are the old
   Configuration card's own rules (0.34.20), which is all that is left of it. */
.cfg-row { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 13.5px; }
.cfg-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px; min-width: 0; }
.cfg-lbl { color: var(--muted); }
/* only a label that has something to say wears the dotted "ask me" underline */
.cfg-lbl[title] { border-bottom: 1px dotted var(--muted); cursor: help; }
.cfg-sub { color: var(--muted); font-size: 12px; }
.cfg-val { font-weight: 600; }
.cfg-val.yes { color: var(--high); }
.cfg-val.muted { color: var(--muted); font-weight: 500; }
.cfg-sep { color: var(--muted); }
.cfg-row .chips { display: inline-flex; flex-wrap: wrap; gap: 5px; }
.cfg-row .chip { font-size: 12.5px; padding: 0 8px; cursor: default; }
.cfg-row .chip.other { color: var(--muted); border-style: dashed; }
/* Phone width: the three items take a line each rather than sharing 375px, and
   the chips wrap under their own label, so the card never widens the page. This
   block sits AFTER the rules it overrides — the sitewide 640px block above is
   earlier in the file than this section, where an equal selector would lose. */
@media (max-width: 640px) {
  .cfg-row { gap: 6px 14px; }
  .cfg-item { flex: 1 1 100%; }
}
/* health level badges. GOOD/OK/FAIR/LOW/SILENT/OFFLINE/UNKNOWN don't map 1:1 onto
   the existing high/medium/disputed set, so OK and SILENT get their own. OFFLINE
   wears SILENT's: both say the repeater is not on the air. */
.badge.lvl-good { background: var(--high); }
.badge.lvl-ok { background: var(--confirmed); }
.badge.lvl-fair { background: var(--medium); }
.badge.lvl-low { background: var(--disputed); }
/* WARNING is the observer ladder's only extra rung (internal/observer): GOOD OK
   FAIR LOW WARNING CRITICAL, worst last. It wears LOW's red, because on that
   ladder LOW is unreachable by construction — every rule either fails outright
   to CRITICAL or caps, and none caps at LOW (observer/health.go's package
   comment) — so the observer scale reads green, blue, amber, red, deep red with
   no new colour. If a rule ever does reach LOW, the two rungs will look alike
   and WARNING needs its own value. */
.badge.lvl-warning { background: var(--disputed); }
.badge.lvl-silent, .badge.lvl-offline { background: var(--panel2); color: var(--muted); border: 1px solid var(--line); }
.badge.lvl-unknown { background: transparent; color: var(--muted); border: 1px dashed var(--insufficient); }
/* reliance dots: 1-4 filled circles; tier 0 renders as a dash, not a badge, so
   it never reads as "0 out of 4", a real and bad score. */
.rel-dots { display: inline-flex; gap: 4px; align-items: center; }
.rel-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.rel-dot.on { background: var(--accent); }
.rel-dash { color: var(--muted); font-weight: 600; }
/* fix list: numbered, the command in a chip, the reason under it */
.fixes { margin: 12px 0 0; padding: 0; list-style: none; counter-reset: fix; }
.fixes li { counter-increment: fix; display: flex; gap: 10px; padding: 7px 0; border-top: 1px solid var(--line); }
.fixes li:first-child { border-top: none; }
.fixes li::before { content: counter(fix) "."; color: var(--muted); font-weight: 600; flex: none; width: 1.2em; }
.fixes .fix-body { min-width: 0; }
.fixes .fix-reason { color: var(--muted); font-size: 13px; margin-top: 3px; }
/* A CONFIRM sits in the same numbered list as the fixes and wears one muted
   word, because the difference is what it asks for and not how loud it is:
   giving it a colour would make "we could not check this" compete with the
   things that are actually wrong. */
.fix-tag { display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--panel2); color: var(--muted);
  font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; vertical-align: 1px; }
/* The observer hint: one line under the list, no number, no chip, no rule above
   it. It is a suggestion to the fleet, not an item on this repeater's list. */
.status-hint { font-size: 12.5px; margin-top: 8px; font-style: italic; }
.chip { display: inline-block; padding: 1px 9px; border-radius: 12px; background: var(--panel2); border: 1px solid var(--line); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
.gate-sentence { font-size: 13px; color: var(--muted); margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line); }
/* "too few observers": headline and sub-line only, no metrics, fixes or
   evidence. Deliberately not a coloured badge — it is not a graded level. */
.no-coverage { padding: 2px 0; }
.nc-headline { font-size: 17px; font-weight: 650; color: var(--muted); }
.nc-sub { color: var(--muted); font-size: 13px; margin-top: 4px; }
/* neighbours table extras */
.neighbours-card .facets { margin: 0 0 8px; }
.neighbours-card .nb-status { font-size: 12.5px; }
.dir { font-family: ui-monospace, Menlo, monospace; }
tr.below-floor td { opacity: .55; }
tr.below-floor .floor-note { display: block; color: var(--muted); font-size: 11.5px; font-style: italic; margin-top: 2px; opacity: 1; }
tr.hidden-row { display: none; }
/* ---------------- packet page (/p/<hash>) ----------------
   The fan-out in the directory's shell: the node page's .dash pattern, the
   directory's tokens, and only the per-path palette ported from idme. */
.strip.warn { background: #3a2e12; border-color: #5a4716; color: #ffd98a; font-size: 13.5px; line-height: 1.45; }
.strip.warn b { color: #ffe6b0; }
.strip.warn a { color: #ffe6b0; text-decoration: underline; }
.strip.warn code { background: #00000033; padding: .05em .35em; border-radius: 4px; }
.pbar { display: flex; align-items: center; gap: 6px; }
.pbar .btn { padding: 4px 9px; }
.pbar .pos { flex: 1; text-align: center; font-weight: 600; font-size: 13.5px; }
#p-bar input[type=range] { width: 100%; margin: 8px 0 0; accent-color: var(--accent); }
.ticks { display: flex; justify-content: space-between; color: var(--muted); font-size: 11px; padding: 0 2px; }
.pbox { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin-top: 8px; }
.pbox .big { font-size: 20px; font-weight: 600; }
.pbox .sub { color: var(--muted); font-size: 12px; }
.pbox .sub.mt { margin-top: 6px; }
.pbox .names { font-size: 12.5px; line-height: 1.5; }
/* path list: hashes, not names — names and observers live on the row's tooltip */
.pathlist .path { display: flex; align-items: center; gap: 7px; padding: 4px 5px; border-radius: 6px; cursor: pointer; font-size: 12.5px; }
.pathlist .path:hover, .pathlist .path.on { background: var(--panel2); }
.pathlist .path .sw, .pathlist .trunk .sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.pathlist .trunk .sw { background: #3a4556; margin-top: 4px; }
.pathlist .path .n, .pathlist .trunk .n { color: var(--muted); flex: none; min-width: 2.2em; text-align: right; }
.pathlist .path .h { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pathlist .trunk { display: flex; align-items: flex-start; gap: 7px; padding: 4px 5px 6px; margin-bottom: 3px; border-bottom: 1px solid var(--line); font-size: 12.5px; }
.pathlist .trunk .h { word-break: break-word; }
.pathlist .approx { opacity: .85; }
.pathlist s { color: var(--muted); }
.pleg { line-height: 1.6; }
/* the list fills the column beside a full-height map; on one column it scrolls at its own height */
@media (min-width: 901px) { .dash .side .grow .pathlist { flex: 1; max-height: 420px; min-height: 220px; } }
.mapcol { position: relative; }
.callout { position: absolute; z-index: 900; left: 50%; transform: translateX(-50%); bottom: 14px; max-width: min(560px, 92%);
  background: #1b2330f2; border: 1px solid #3a4d6a; border-radius: 10px; padding: 12px 14px; box-shadow: 0 6px 24px #0008; font-size: 13.5px; line-height: 1.45; }
.callout b { color: #fff; }
.callout code { background: #0f1620; padding: .05em .35em; border-radius: 4px; }
.callout .row { display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 10px; flex-wrap: wrap; }
.callout .btn { background: var(--accent); color: #0b1220; font-weight: 600; border: 0; }
/* the compact wording is the phone's; a desktop shows the full paragraph */
.callout .brief { display: none; }
/* On a phone the map is only 300 px tall — a callout floating over it would
   hide the whole fan, so it sits under the map instead. And on the packet page
   it goes after the two cards, not straight under the map: the first-visit
   callout used to push "Fan-out by hop" below the fold on a 375px phone
   (Zeb, 2026-09-14). display:contents flattens the two columns into the one
   flex column the ≤900px grid already collapses to, so the map, the cards and
   the callout can be ordered against each other. Desktop is untouched — the
   callout still floats over the bottom of the map. */
@media (max-width: 720px) {
  .callout { position: static; transform: none; max-width: none; margin-top: 10px; }
  #packet .dash { display: flex; flex-direction: column; }
  #packet .dash .mapcol, #packet .dash .side { display: contents; }
  #packet #map-packet { order: 1; }
  #packet .dash .side > .card { order: 2; }
  #packet .callout { order: 3; }
}
/* …and at phone width it gets the two-line text: the page already asks for an
   observer in the header and the footer, so this box stays short (Zeb, 2026-09-13). */
@media (max-width: 640px) {
  .callout { padding: 10px 12px; }
  .callout .full { display: none; }
  .callout .brief { display: inline; }
  .callout .row { margin-top: 8px; }
}
/* ---------------- packet page: everything below the fan-out ----------------
   The header strip, the decoded payload, the two tables and the raw bytes. */
/* The hash and its copy button read as one thing, so they never wrap apart. */
.ph-key { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.pcopy { padding: 2px 7px; font-size: 12px; }
/* A path as named repeaters. Chips wrap: on a phone a seven-hop path is four
   lines, not a horizontal scroll inside a cell that already scrolls. */
.chips { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.chips .arr { color: var(--muted); font-size: 12px; }
.chip { position: relative; display: inline-flex; align-items: center; gap: 5px; max-width: 100%;
  padding: 1px 8px; background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; font-size: 12.5px; }
.chip .px { font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--muted); }
.chip a { color: var(--link); }
/* unresolved: no repeater was pinned to this prefix. Dashed, with the ? the
   path list has used since idme. */
.chip.unres { border-style: dashed; color: var(--muted); }
.chip.unres::after { content: "?"; color: var(--muted); }
/* another repeater also fits */
.chip.amb::after { content: "≈"; color: var(--muted); }
/* its position contradicts the observations: struck through, as on the map */
.chip.gone a, .chip.gone { text-decoration: line-through; }
/* A chip's reason, on hover and on tap — the chip is focusable when it has one,
   so a phone gets the same sentence a desktop gets from the title. */
.chip .why { display: none; position: absolute; left: 0; bottom: calc(100% + 6px); z-index: 20;
  width: max-content; max-width: 260px; padding: 6px 9px; background: var(--panel2); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 6px 20px #0008;
  font-size: 12px; line-height: 1.4; text-decoration: none; white-space: normal; }
.chip:hover .why, .chip:focus .why, .chip:focus-within .why { display: block; }
.chip:focus { outline: 1px solid var(--accent); }
/* a plain label chip: the advert's flags, which carry no prefix and no reason */
.chip.flat { color: var(--muted); }
.chip.flat.ok { color: var(--high); border-color: var(--high); }
.chip.flat.bad { color: var(--disputed); border-color: var(--disputed); }
/* the decoded payload: a label column and a value column, no header row */
table.kv th { width: 1%; padding-right: 16px; white-space: nowrap; color: var(--muted); font-weight: 400; }
table.kv tr:last-child th, table.kv tr:last-child td { border-bottom: 0; }
.pl-text { white-space: pre-wrap; word-break: break-word; }
.pl-key { word-break: break-all; }
/* raw bytes: the toggle sits on the card's own heading */
.praw-btn { float: right; margin-top: -2px; padding: 2px 9px; font-size: 12px; }
.praw { margin-top: 6px; }
.praw-pick { color: var(--muted); font-size: 13px; margin-bottom: 6px; }
.praw-pick select { padding: 4px 6px; background: var(--panel2); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
.hexdump { margin: 0 0 10px; padding: 8px 10px; background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.55; }
@media (max-width: 600px) {
  .pcopy, .praw-btn { min-height: 32px; }
  .chip { padding: 3px 9px; }
  .hexdump { font-size: 11px; }
}

/* ---------------- /regions ----------------
   Map on the left, the tree and the commands beside it; stacked on a phone. */
.rg-intro { margin: 0 0 10px; }
.rg-find { max-width: 480px; margin-bottom: 8px; }
.rg-places { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.rg-places:empty { display: none; }
.rg-body { display: flex; gap: 16px; align-items: flex-start; }
.rg-map { flex: 1 1 60%; height: 62vh; min-height: 380px; cursor: crosshair; }
.rg-out { flex: 1 1 40%; min-width: 0; }
.rg-tree { margin: 4px 0 10px; }
.rg-node { display: flex; align-items: center; margin: 3px 0; }
.rg-box { flex: none; width: 22px; }
.rg-opt { cursor: pointer; } .rg-box input { margin: 0; vertical-align: -2px; }
.rg-cmds { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin: 0 0 8px; overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13.5px; line-height: 1.55; }
@media (max-width: 640px) {
  .rg-body { flex-direction: column; }
  .rg-map { width: 100%; height: 48vh; min-height: 300px; }
  .rg-out { width: 100%; }
  .rg-find button, #rg-copy, .rg-place { min-height: 44px; }
}
/* CRITICAL: the level below LOW, for a repeater that cannot carry multibyte
   traffic at all. A darker, flatter red than LOW's --disputed, so the two read as
   different in kind and not just in degree, with light text because the badge's
   default near-black would not clear it. Declared here rather than as a token at
   the top of the file because it is the one place it is used. */
.badge.lvl-critical { background: #b3261e; color: #fff; }
html[data-theme="light"] .badge.lvl-critical { background: #96150f; color: #fff; }

/* ---------------- fix titles and the "how" popup ----------------
   A fix reads as a title with its reason under it and a "How" link beside the
   reason; the commands live one level down, in a popup (approved design:
   scratchpad design3, 2026-09-19). The popup borrows the card's own tokens —
   panel background, line border, 8px radius — and the scrim and shadow the
   packet page's map callout already uses, centred rather than anchored. */
.fix-title { font-weight: 600; color: var(--fg); }
.fix-how { display: inline-flex; align-items: baseline; gap: 3px; margin-left: 4px;
  background: none; border: 0; padding: 0; color: var(--link); font: inherit; font-size: 13px; cursor: pointer; }
.fix-how:hover { text-decoration: underline; }
.fix-how:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.fix-how .arrow { font-size: 11px; }

.popup-overlay { position: fixed; inset: 0; z-index: 1000; background: rgba(0, 0, 0, .6);
  display: flex; align-items: center; justify-content: center; padding: 16px; }
.popup-overlay[hidden] { display: none; }
.popup { background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 6px 24px #0008; width: 100%; max-width: 460px; max-height: 85vh; overflow: auto;
  padding: 14px 16px 16px; }
.popup-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.popup-head h3 { margin: 0; font-size: 16px; font-weight: 650; color: var(--fg); line-height: 1.3; }
.popup-close { flex: none; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
  background: none; border: 1px solid var(--line); border-radius: 6px; color: var(--muted);
  font-size: 16px; line-height: 1; cursor: pointer; }
.popup-close:hover { border-color: var(--accent); color: var(--fg); }
.popup-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The <ol> numbers the steps itself: a hand-written "1." in front of its own
   marker is how the first cut of this read "1. 1. set path.hash.mode 1". A
   command step is a monospace chip, an instruction is plain sentence text, and
   the difference is load-bearing — one is pasted at a CLI and the other is not. */
.popup-steps { margin: 0 0 10px; padding: 0 0 0 1.7em; }
.popup-steps:empty { display: none; }
.popup-steps li { margin: 6px 0; }
.popup-steps li::marker { color: var(--muted); font-size: 12px; }
.popup-steps .step-text { color: var(--muted); font-size: 13px; }
.popup-note { color: var(--muted); font-size: 13px; margin: 0 0 12px; }
/* A confirm's evidence, one line per region. Plain bullets: they are readings,
   not steps, and numbering them would imply an order to work through. */
.popup-lines { margin: 0 0 12px; padding-left: 18px; color: var(--muted); font-size: 13px; }
.popup-lines li { margin: 3px 0; }
.popup-actions { display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center;
  padding-top: 10px; border-top: 1px solid var(--line); }
.popup-docs-link { font-size: 13px; }
/* The region popup's one control is a link, not a button, so it is given the
   button's own look rather than a second idea of what an action looks like. */
a.popup-open { text-decoration: none; }
a.popup-open:hover { text-decoration: none; border-color: var(--accent); }
@media (max-width: 640px) {
  .popup { padding: 12px; max-height: 92vh; }
  .popup-actions { flex-direction: column; align-items: stretch; }
  .popup-actions .btn, .popup-actions .popup-docs-link { width: 100%; text-align: center; }
  .fix-how { min-height: 24px; }
}
/* ---------------- health check (/h/<code>) ----------------
   The bot's reply as cards, above the packet page's own live fan-out: the
   grade at the size it deserves, the coverage ring beside it, and the counts the
   133-byte reply had to leave out. The grade sets --hc on the card and every
   coloured part reads that, so there is one place the palette is decided.
   GOOD and OK share the green: both mean the flood went where it should.

   Three tiles: the verdict across the top, and under it a row of two halves —
   the readings, and the repeaters that heard the sender direct. The verdict is
   a banner, one line of ring and words, so it takes the width and none of the
   height; the two readings below it are lists of the same weight and sit side
   by side. Stretched, so the pair reads as one row however short the list is.
   The tile and the row are spaced the way the two halves are spaced from each
   other, so the three cards read as one grid. */
.hc-row { display: grid; grid-template-columns: 1fr 1fr; align-items: stretch;
  gap: 14px; margin: 8px 0 14px; }
.hc-card { --hc: var(--fg); background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 16px 18px; margin: 8px 0 14px; }
.hc-row > .hc-card { min-width: 0; margin: 0; }
/* The banner's own gap, rather than whatever the two margins collapse to. */
.hc-head + .hc-row { margin-top: 0; }
.hc-card.hc-good, .hc-card.hc-ok { --hc: var(--high); }
.hc-card.hc-fair { --hc: var(--medium); }
.hc-card.hc-low { --hc: var(--disputed); }
.hc-top { display: flex; align-items: center; gap: 18px; }
.hc-verdict { min-width: 0; }
.hc-eyebrow { color: var(--muted); font-size: 11.5px; font-weight: 600; letter-spacing: .13em; text-transform: uppercase; }
/* The glow is currentColor twice over: a wide blur for the halo and a tight one
   to keep the letters solid, so it tracks --hc without a second token. */
.hc-grade { color: var(--hc); font-size: 40px; font-weight: 800; line-height: 1.1; margin-top: 2px;
  text-shadow: 0 0 30px currentColor, 0 0 7px currentColor; }
.hc-capped { color: var(--muted); font-size: 13px; margin-top: 5px; }
.hc-ring { flex: none; width: 92px; height: 92px; }
.hc-ring-bg { fill: none; stroke: var(--line); stroke-width: 8; }
.hc-ring-fg { fill: none; stroke: var(--hc); stroke-width: 8; stroke-linecap: round; }
.hc-ring-num { fill: var(--hc); font-size: 20px; font-weight: 700; text-anchor: middle; dominant-baseline: central; }
/* A `test` check is filed with its ack and graded at the health hold window,
   so for up to thirty seconds it has no grade: the ring's place takes a
   spinner the ring's size, and the grade's a countdown to the grade, in the
   muted ink — it is a wait, not a verdict. The same spinner, small, stands in
   for the one count the grade decides ("Observers reached"). Once the
   countdown is out the spinner turns on alone until the grade lands. */
.hc-spin { display: inline-block; flex: none; box-sizing: border-box; width: 16px; height: 16px;
  border: 2px solid var(--line); border-top-color: var(--fg); border-radius: 50%;
  vertical-align: middle; animation: hcSpin 1s linear infinite; }
.hc-top > .hc-spin { width: 92px; height: 92px; border-width: 8px; }
.hc-due { color: var(--muted); font-size: 22px; font-weight: 650; line-height: 1.2; margin-top: 4px; }
@keyframes hcSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .hc-spin { animation-duration: 4s; } }
/* Label over value, each row carrying its own rule, so the list reads as a run
   of readings rather than a table with a frame. One column: the tile is half
   the page, which is about what two columns had before the row. The list is
   the whole of its tile now, so it starts at the top of it and the first row
   drops the rule it would otherwise draw against the card's own edge. */
.hc-items { display: grid; grid-template-columns: 1fr; gap: 0; margin: 0; }
.hc-item { min-width: 0; padding: 9px 0; border-top: 1px solid var(--line); }
.hc-items > .hc-item:first-child { padding-top: 0; border-top: 0; }
.hc-item dt { color: var(--muted); font-size: 12.5px; }
.hc-item dd { margin: 2px 0 0; font-size: 15px; font-weight: 650; word-break: break-word; }
/* 1-byte path hashes: the check stands, but the repeaters behind it cannot be
   told apart, so there is no list — this heads the page instead, full width
   between the verdict and the row (.hc-warn-top), and the readings take the
   row alone (.hc-solo). Amber — FAIR's --medium, the token the admin's `tag
   warn` reads too — because it is the same kind of "this reading is weaker than
   it looks", and no new colour is invented for it. A check filed before its
   width was recorded still has the tile, and the warning fills that instead
   when the packet turns out to be 1-byte. */
.hc-warn { --hc: var(--medium); margin: 10px 0 0; padding: 12px 14px; background: var(--panel);
  border: 1px solid var(--hc); border-left: 3px solid var(--hc); border-radius: 10px;
  color: var(--fg); font-size: 14px; line-height: 1.5; }
.hc-warn code { padding: 1px 6px; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; color: var(--hc); }
/* Spaced as a tile is, so the banner, the warning and the row read as one
   grid; the row under it drops its own top margin, as it does under the
   banner. */
.hc-warn-top { margin: 8px 0 14px; }
.hc-warn-top + .hc-row { margin-top: 0; }
.hc-row.hc-solo { grid-template-columns: 1fr; }
/* The repeaters that heard the sender direct: the fan-out's first hops, read as
   a short list, in the tile beside the readings. Same rows-with-a-rule
   furniture as .hc-items. The repeater is the row; its neighbour count sits
   out at the right margin, muted and unwrapped, so the column of numbers can
   be read down without competing with the names.

   The tile takes its height from the row — it is the readings beside it that
   decide how tall the pair is — so a long list scrolls inside it rather than
   dragging the row down the page. height:0 keeps it out of the row's own
   sizing; the flex column then hands what the row gives it to the scroller. */
.hc-row > .hc-direct { display: flex; flex-direction: column; height: 0; min-height: 100%; }
.hc-direct h2 { flex: none; margin: 0; font-size: 15px; font-weight: 600; color: var(--fg); }
.hc-dr-list { min-height: 0; overflow: auto; overscroll-behavior: contain; }
.hc-dr { display: flex; align-items: baseline; gap: 10px; padding: 8px 0; border-top: 1px solid var(--line); }
.hc-dr:first-child { margin-top: 8px; }
.hc-dr .h { flex: 1 1 auto; min-width: 0; font-size: 14.5px; word-break: break-word; }
.hc-dr .n { flex: none; margin-left: auto; white-space: nowrap; text-align: right; color: var(--muted); font-size: 13px; }
.hc-dr s { color: var(--muted); }
.hc-dr-none { margin-top: 8px; padding: 8px 0; border-top: 1px solid var(--line); font-size: 14px; }
/* The fan-out under the row is mounted headless — no <h1> and no facts line,
   the tiles having already named the packet — so the map is what follows the
   row. The gap it leaves is the row's own, so the tiles and the map below them
   are spaced the way the two tiles are spaced from each other. It is the .dash
   margin the packet page already carries, raised 12 → 14 and only here: /p/,
   where #health stays hidden, is untouched. */
#health:not([hidden]) + #packet .dash { margin-top: 14px; }
/* Off a desktop the tiles are a column: verdict, readings, repeaters, fan-out,
   in that order. Nothing is clamped to anything then — the list runs its own
   length and the page scrolls, which is what a phone wants. */
@media (max-width: 900px) {
  .hc-row { grid-template-columns: 1fr; gap: 12px; }
  .hc-row > .hc-direct { height: auto; min-height: 0; }
  .hc-dr-list { overflow: visible; }
  /* the column's own gap, for the same reason */
  #health:not([hidden]) + #packet .dash { margin-top: 12px; }
}
@media (max-width: 600px) {
  .hc-card { padding: 14px; }
  .hc-top { gap: 14px; }
  .hc-ring, .hc-top > .hc-spin { width: 76px; height: 76px; }
  .hc-grade { font-size: 32px; }
}
/* ---------------- health check how-to (/health) ----------------
   Where a mesh user lands from a link heard on the air, usually on the handset
   beside the radio — so it is phone-first and reads straight down: what to
   send, what comes back, what the grade means. The furniture is the check
   page's, one size larger: dark rounded cards, thin rules between rows, an
   uppercase label over the headline. The grade badges carry the check page's
   own .hc-* classes and read --hc from them, so the legend here and the verdict
   there cannot drift apart. */
.hh { max-width: 760px; margin: 0 auto; padding-bottom: 8px; }
.hh-hero { padding: 26px 0 22px; }
.hh-eyebrow { color: var(--muted); font-size: 11.5px; font-weight: 600; letter-spacing: .18em; }
.hh-hero h1 { margin: 10px 0 0; font-size: 46px; font-weight: 800; line-height: 1.04; letter-spacing: -.02em; }
.hh-lead { margin: 14px 0 0; color: var(--muted); font-size: 18px; max-width: 46ch; }
/* Three rows in one card, the numeral doing the work a heading would. */
.hh-steps { list-style: none; counter-reset: hh; margin: 0; padding: 4px 18px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px; }
.hh-steps li { counter-increment: hh; display: flex; align-items: baseline; gap: 16px; padding: 16px 0;
  border-top: 1px solid var(--line); font-size: 16px; }
.hh-steps li:first-child { border-top: 0; }
.hh-steps li::before { content: counter(hh); flex: none; width: 30px; color: var(--accent);
  font-size: 28px; font-weight: 800; line-height: 1; letter-spacing: -.02em; text-align: center; }
.hh-what { min-width: 0; }
/* The word to send, as a thing to take rather than a word to retype. */
.hh-chip { display: inline-flex; align-items: center; min-height: 32px; padding: 2px 12px; margin: 0 1px;
  background: var(--panel2); color: var(--fg); border: 1px solid var(--line); border-radius: 9px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 15px; font-weight: 600;
  letter-spacing: .02em; vertical-align: middle; cursor: pointer; }
.hh-chip:hover { border-color: var(--accent); color: var(--accent); }
.hh-copied { margin-left: 8px; color: var(--high); font-size: 13px; }
/* The reply as it arrives on the handset: a bubble inside the tile, squared off
   at the corner it came from. */
.hh-example { margin-top: 14px; padding: 16px 18px; background: var(--panel); border: 1px solid var(--line); border-radius: 14px; }
.hh-bubble { margin: 0; max-width: 520px; padding: 12px 14px; background: var(--panel2); border: 1px solid var(--line);
  border-radius: 14px 14px 14px 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13.5px; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
/* Four tiles, the badge standing where a tile's label goes. */
.hh-legend { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
.hh-tile { min-width: 0; padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 14px; }
.hh-badge { --hc: var(--fg); display: inline-block; min-width: 56px; padding: 3px 10px; border-radius: 999px;
  background: var(--hc); color: #0b0e12; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-align: center; }
.hh-badge.hc-good, .hh-badge.hc-ok { --hc: var(--high); }
.hh-badge.hc-fair { --hc: var(--medium); }
.hh-badge.hc-low { --hc: var(--disputed); }
.hh-tile p { margin: 10px 0 0; color: var(--muted); font-size: 14px; line-height: 1.5; }
.hh-notes { margin-top: 16px; }
.hh-notes p { margin: 0 0 8px; color: var(--muted); font-size: 14px; }
.hh-cards { display: grid; gap: 10px; margin-top: 10px; }
.hh-card { padding: 14px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  color: var(--muted); font-size: 14px; line-height: 1.55; }
.hh-card code { padding: 1px 6px; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; color: var(--fg); }
/* One column of tiles, a headline that still fits 390px, and a chip big enough
   to hit with a thumb. */
@media (max-width: 600px) {
  .hh-hero { padding: 18px 0 16px; }
  .hh-hero h1 { font-size: 34px; }
  .hh-lead { margin-top: 10px; font-size: 16px; }
  .hh-steps { padding: 2px 14px; }
  .hh-steps li { gap: 12px; padding: 14px 0; font-size: 15px; }
  .hh-steps li::before { width: 24px; font-size: 23px; }
  .hh-chip { min-height: 36px; }
  .hh-example { padding: 12px; }
  .hh-legend { grid-template-columns: 1fr; gap: 8px; }
  .hh-tile, .hh-card { padding: 12px 14px; }
}

/* /channels: the channel list and one channel's thread (channels.js). One
   control strip — window presets and the region selector — on both pages. */
.ch-controls { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; margin: 2px 0 10px; font-size: 14px; }
.ch-win { display: inline-flex; gap: 6px; }
.ch-win-btn.on { border-color: var(--accent); color: var(--accent); }
.ch-region { color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }
.ch-region select { padding: 6px 8px; background: var(--panel2); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; font-size: 14px; }
/* A message wraps; a snippet in the list does not — the list is one line per
   channel and the thread is where the text is read. */
.ch-text { white-space: pre-wrap; word-break: break-word; min-width: 18ch; }
.ch-snip { color: var(--muted); max-width: 46ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-pkt { white-space: nowrap; }
.ch-copy { padding: 2px 7px; margin-left: 6px; font-size: 12px; }
.ch-page { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 10px; font-size: 13.5px; }
.ch-page .btn[disabled] { opacity: .45; cursor: default; }
.ch-sealed { margin-top: 14px; }
@media (max-width: 600px) {
  .ch-snip { max-width: 24ch; }
  .ch-win-btn, .ch-region select, .ch-page .btn { min-height: 44px; }
  .ch-copy { min-height: 32px; }
}
/* ---------------- the map page (/map) ----------------
   A full-viewport map with everything floating over it: the search box and the
   filters top-left, the count bottom-left, and a node's facts in a popup on
   the node's own marker. The header stays — it is how you get anywhere else —
   and the footer goes, because a map that ends 60px above the fold is not a
   full-viewport map.

   .map-tools is click-through except where a control actually is, or the
   invisible column down the left of the map would swallow every drag. */
body.mappage { display: flex; flex-direction: column; height: 100vh; height: 100svh; overflow: hidden; }
body.mappage main { flex: 1 1 auto; min-height: 0; max-width: none; margin: 0; padding: 0; }
body.mappage #mapview { height: 100%; }
body.mappage .foot { display: none; }
/* The search box moved out of the header and onto the map. */
body.mappage .top .search { display: none; }
.mapwrap { position: relative; height: 100%; }
.mapwrap #map-full { height: 100%; border: 0; border-radius: 0; }
.map-tools { position: absolute; top: 10px; left: 10px; z-index: 500; width: min(420px, calc(100% - 20px));
  display: flex; flex-direction: column; gap: 8px; pointer-events: none; }
.map-tools > * { pointer-events: auto; }
.map-search .search { max-width: none; }
.map-search input, .map-search button { background: var(--panel); }
.map-search .search button { flex: none; white-space: nowrap; }
/* The phone-only fold for the filter panel (see the 640px block below). */
.map-filters-toggle { display: none; align-self: flex-start; background: var(--panel);
  box-shadow: 0 2px 10px #0006; }
.map-filters { padding: 8px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 2px 10px #0006; font-size: 13.5px; }
.map-row { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center; }
.map-row + .map-row { margin-top: 6px; }
.map-row.legend { margin-left: 0; padding-top: 6px; border-top: 1px solid var(--line); }
.map-chk { cursor: pointer; white-space: nowrap; }
.map-chk input { vertical-align: -2px; margin-right: 4px; }
.map-wins { display: inline-flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.map-win { padding: 4px 10px; background: var(--panel2); color: var(--muted); border: 0; cursor: pointer;
  font: inherit; font-size: 13px; }
.map-win + .map-win { border-left: 1px solid var(--line); }
.map-win.on { background: var(--accent); color: #0b0e12; font-weight: 600; }
#f-region { padding: 4px 6px; background: var(--panel2); color: var(--fg); border: 1px solid var(--line);
  border-radius: 6px; font: inherit; font-size: 13px; }
.map-theme { padding: 4px 10px; font-size: 13px; }
/* What a search or Near me found, under the filters: the old landing page's
   results table, scrolling inside its own panel, and no panel at all empty. */
.map-results { max-height: 40vh; overflow: auto; padding: 4px 8px; background: var(--panel);
  border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 2px 10px #0006; font-size: 13.5px; }
.map-results:empty { display: none; }
.map-results table { margin: 0; }
.map-results .nf { margin: 6px 0; }
.map-status { position: absolute; left: 10px; bottom: 10px; z-index: 500; max-width: calc(100% - 20px);
  padding: 4px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  color: var(--muted); font-size: 13px; }
/* A marker heard in the last ten minutes. The stroke is what the age colour is
   on, so that is what breathes; prefers-reduced-motion gets a still dot. */
@keyframes mapPulse { 0%, 100% { stroke-width: 1.5; stroke-opacity: 1; } 50% { stroke-width: 6; stroke-opacity: .35; } }
.nodedot.pulse { animation: mapPulse 2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .nodedot.pulse { animation: none; } }
/* The node popup: Leaflet's own, on the marker it was opened from, on every
   viewport. It never navigates anywhere — the one link out of it is the
   "details" button — and it borrows the map furniture's tokens so it reads as
   part of the page in either tile theme. */
.nodepop .leaflet-popup-content-wrapper { background: var(--panel); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 2px 14px #0007; }
.nodepop .leaflet-popup-content { margin: 10px 12px; font-size: 13.5px; line-height: 1.45; }
.nodepop .leaflet-popup-tip { background: var(--panel); border: 1px solid var(--line); box-shadow: none; }
.nodepop .leaflet-popup-close-button { color: var(--muted); }
.nodepop .leaflet-popup-close-button:hover { color: var(--fg); }
/* The × sits in the corner the heading would otherwise run into. */
.np-head { padding-right: 12px; }
.np-name { font-weight: 650; }
.np-facts { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; margin: 8px 0 0; }
.np-facts dt { color: var(--muted); }
.np-facts dd { margin: 0; word-break: break-word; }
.np-note { margin: 8px 0 0; color: var(--muted); font-size: 12.5px; }
.np-more { display: inline-block; margin-top: 10px; }
.mv-body { margin: 0 0 14px; font-size: 15px; line-height: 1.5; }
/* Light tiles want light chrome over them, or the popup reads as a hole in the
   map. Only the map page's own furniture moves — the header, the node pages and
   everything else are the one dark theme they have always been. */
body.mappage.map-light {
  --panel: #ffffff; --panel2: #eef1f5; --line: #ccd4de; --fg: #16202b; --muted: #5c6a7a; --link: #0b6bcb;
}
body.mappage.map-light .map-filters, body.mappage.map-light .map-status, body.mappage.map-light .map-results,
body.mappage.map-light .nodepop .leaflet-popup-content-wrapper,
body.mappage.map-light .map-search input, body.mappage.map-light .map-search button,
body.mappage.map-light .map-filters-toggle { box-shadow: 0 2px 10px #0002; }
body.mappage.map-light .leaflet-container { background: #e8ecf0; }
body.mappage.map-light .map-win.on { color: #fff; }
@media (max-width: 640px) {
  /* The tools take the width they are given. The popup needs nothing here: its
     maxWidth is narrower than the narrowest phone, and Leaflet pans it in. */
  .map-tools { width: calc(100% - 20px); }
  .map-filters { font-size: 13px; }
  /* The filters start folded on a phone: open, they cover a third of the map.
     The zoom control is at the bottom right here (map.js), clear of the search. */
  .map-filters-toggle { display: inline-block; }
  .map-tools:not(.filters-open) .map-filters { display: none; }
  .map-tools.filters-open .map-filters-toggle { border-color: var(--accent); }
  .map-results { max-height: 30vh; font-size: 13px; }
}

/* / — the home page (home.js). The how-to's furniture (.hh): dark rounded
   cards, thin rules between rows, the numeral doing a heading's work. Four
   count tiles 2×2 with the map preview beside them on a desktop and under them
   on a phone; every tile, step and tool card is one link. */
.home { max-width: 960px; margin: 0 auto; padding-bottom: 8px; }
.home-hero { padding: 26px 0 20px; }
.home-hero h1 { margin: 0; font-size: 46px; font-weight: 800; line-height: 1.04; letter-spacing: -.02em; }
.home-lead { margin: 12px 0 0; color: var(--muted); font-size: 18px; max-width: 46ch; }
.home a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.home-top { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; }
.home-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.home-tile { display: flex; flex-direction: column; justify-content: center; min-width: 0; padding: 14px 16px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px; color: var(--fg); }
.home-tile:hover, .home-tool:hover, .home-steps a:hover { text-decoration: none; border-color: var(--accent); }
.home-num { font-size: 34px; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.home-lbl { margin-top: 4px; color: var(--muted); font-size: 14px; }
.home-map { position: relative; min-height: 220px; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; background: #0b0e12; }
.home-map #home-map { position: absolute; inset: 0; }
/* Over the tiles and the dots (Leaflet's panes top out at 700), under the
   attribution control (1000), so the provider's links stay reachable. */
.home-map-link { position: absolute; inset: 0; z-index: 800; border-radius: 14px; }
.home-map-link:focus-visible { outline-offset: -3px; }
.home-secs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 22px; align-items: start; }
.home-sec h2 { margin: 0 0 10px; font-size: 18px; font-weight: 700; }
.home-steps { list-style: none; counter-reset: home; margin: 0; padding: 4px 18px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px; }
.home-steps li { counter-increment: home; display: flex; align-items: baseline; gap: 14px; padding: 12px 0; border-top: 1px solid var(--line); }
.home-steps li:first-child { border-top: 0; }
.home-steps li::before { content: counter(home); flex: none; width: 24px; color: var(--accent);
  font-size: 23px; font-weight: 800; line-height: 1; text-align: center; }
.home-steps a, .home-steps .home-step-flat, .home-tool { display: flex; flex-direction: column; min-width: 0; color: var(--fg); border: 1px solid transparent; border-radius: 6px; }
.home-steps a, .home-steps .home-step-flat { flex: 1; }
/* A step that is not a link must not wear the link colour or it reads as a dead one. */
.home-steps .home-step-flat b { color: var(--fg); }
.home-steps b, .home-tool b { font-weight: 650; color: var(--link); }
.home-steps span, .home-tool span { margin-top: 2px; color: var(--muted); font-size: 14px; }
.home-tools { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin-top: 22px; }
.home-tool { padding: 14px 16px; background: var(--panel); border-color: var(--line); border-radius: 14px; }
@media (max-width: 760px) {
  .home-top, .home-secs { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
  .home-hero { padding: 18px 0 16px; }
  .home-hero h1 { font-size: 34px; }
  .home-lead { margin-top: 10px; font-size: 16px; }
  .home-tile { padding: 12px 14px; }
  .home-num { font-size: 28px; }
  .home-map { min-height: 200px; }
  .home-steps { padding: 2px 14px; }
  .home-tools { grid-template-columns: 1fr 1fr; }
}
/* ---------------- the repeater audit (/repeaters, repeaters.js) ----------------
   mc-audit's dashboard in this site's dark cards: two headlines, the priority
   panel, the table, the method. Status colours are the site's own — --high
   good, --disputed bad, --medium probable, --confirmed blue for "blocking *
   but covered" — and the fully-configured lanes are a ramp of the accent, so
   the deepest lane is the furthest along. */
#repeaters {
  --rp-lane-mb: #2f6694; --rp-lane-2b: #58b4ff; --rp-lane-s1: #8ed0ff; --rp-lane-s2: #b5e0ff;
  --rp-lane-s3: #d6eeff; --rp-lane-s4: #eef8ff;
}
#repeaters .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.rp-select { padding: 6px 8px; background: var(--panel2); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; font-size: 14px; }
.rp-banner { margin: 0 0 12px; padding: 10px 12px; background: var(--panel); border: 1px solid var(--medium); border-radius: 8px; color: var(--fg); font-size: 14px; }
.rp-banner .msg { margin-right: 6px; }
.rp-headline { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: stretch; }
.rp-hl { padding: 14px 16px; }
.rp-kicker { color: var(--muted); font-size: 13px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
.rp-bignum { display: flex; align-items: baseline; gap: 10px; margin: 4px 0 10px; }
.rp-bignum .n { font-size: 40px; font-weight: 650; line-height: 1.1; font-variant-numeric: tabular-nums; }
.rp-bignum .n i { font-style: normal; font-size: 22px; color: var(--muted); margin-left: 2px; }
.rp-bignum.good .n { color: var(--high); }
.rp-bignum .n.none { color: var(--muted); }
.rp-bignum .of { color: var(--muted); font-size: 14px; }
.rp-segbar { display: flex; height: 12px; border-radius: 6px; overflow: hidden; background: var(--panel2); }
.rp-segbar .seg { display: block; height: 100%; }
.rp-segbar .seg.yes { background: var(--high); } .rp-segbar .seg.no { background: var(--disputed); } .rp-segbar .seg.unk { background: var(--line); }
.rp-lanes { display: grid; gap: 4px; }
.rp-lane { height: 6px; border-radius: 3px; background: var(--panel2); overflow: hidden; }
.rp-lane span { display: block; height: 100%; }
.rp-lane.all { height: 10px; margin-top: 3px; background: repeating-linear-gradient(135deg, var(--line) 0 4px, var(--panel2) 4px 8px); }
.rp-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 10px; color: var(--muted); font-size: 13px; }
.rp-legend .li { display: inline-flex; align-items: center; gap: 6px; }
.rp-legend .sw { width: 10px; height: 10px; border-radius: 2px; }
.rp-priority, .rp-list, .rp-method { margin-top: 14px; padding: 12px 16px; }
.rp-pri-hd { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; }
.rp-priority h2, .rp-list h2 { margin: 0; font-size: 16px; font-weight: 600; }
.rp-pri-detail { color: var(--muted); font-size: 14px; }
.rp-link { padding: 0; background: none; border: 0; color: var(--link); font: inherit; font-weight: 600; cursor: pointer; text-decoration: underline; }
.rp-barrow { display: flex; align-items: center; gap: 10px; margin: 10px 0 8px; }
.rp-track { flex: 1; display: block; height: 8px; border-radius: 4px; background: var(--panel2); overflow: hidden; }
.rp-fill { display: block; height: 100%; background: var(--high); }
.rp-val { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rp-nudges { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 4px 16px; font-size: 14px; }
.rp-nudge { display: flex; align-items: center; gap: 8px; min-height: 28px; }
.rp-nudge a { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-nudge .muted { margin-left: auto; font-size: 12.5px; white-space: nowrap; }
.rp-list-hd { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: 8px; }
.rp-list-hd h2 { margin-right: auto; }
.rp-q { flex: 0 1 220px; min-width: 0; padding: 7px 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; font-size: 14px; }
.rp-filters { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.rp-filters .btn { padding: 6px 10px; font-size: 13.5px; }
.rp-filters .btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
.rp-tlegend { margin: 0 0 8px; color: var(--muted); font-size: 13px; }
.rp-ck, td.rp-check { color: var(--high); font-weight: 700; }
.rp-table th { padding: 0; }
.rp-table th.rp-check { width: 26px; }
.rp-sort { width: 100%; padding: 6px 8px; background: none; border: 0; color: var(--muted); font: inherit; font-weight: 500; text-align: left; white-space: nowrap; cursor: pointer; }
.rp-sort:hover { color: var(--fg); }
.rp-sort .ind { margin-left: 4px; font-size: 10px; color: var(--accent); }
.rp-table td { vertical-align: middle; }
.rp-table td.rp-c, .rp-table th.rp-c .rp-sort { text-align: center; }
.rp-table td.rp-seen, .rp-table td.rp-rel { white-space: nowrap; }
.rp-table td.rp-name { min-width: 12ch; }
.rp-chip { display: inline-block; min-width: 2.2em; padding: 0 7px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel2); color: var(--muted); font-size: 12px; font-weight: 600; text-align: center; white-space: nowrap; }
.rp-chip.good { color: var(--high); border-color: var(--high); }
.rp-chip.bad { color: var(--disputed); border-color: var(--disputed); }
.rp-chip.warn { color: var(--medium); border-color: var(--medium); }
.rp-chip.info { color: var(--confirmed); border-color: var(--confirmed); }
.rp-empty { padding: 14px 0; color: var(--muted); }
.rp-list-ft { display: flex; align-items: center; gap: 10px; justify-content: space-between; margin-top: 10px; font-size: 13.5px; }
.rp-method summary { cursor: pointer; font-weight: 600; }
.rp-method-body { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 6px 24px; color: var(--muted); font-size: 14px; }
.rp-method-body h3 { margin: 12px 0 4px; color: var(--fg); font-size: 14px; font-weight: 600; }
.rp-method-body p { margin: 0 0 8px; }
.rp-mbar { display: grid; grid-template-columns: 7em 1fr 2.5em; align-items: center; gap: 8px; margin: 4px 0; font-size: 13px; }
.rp-mbar .v { text-align: right; font-variant-numeric: tabular-nums; }
#repeaters button:focus-visible, #repeaters select:focus-visible, #repeaters input:focus-visible,
#repeaters a:focus-visible, #repeaters summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 700px) {
  .rp-headline { grid-template-columns: 1fr; }
  .rp-q { flex: 1 1 100%; }
}
@media (max-width: 600px) {
  .rp-bignum .n { font-size: 34px; }
  .rp-select, .rp-q, .rp-filters .btn, .rp-list-ft .btn, .rp-sort { min-height: 44px; }
  .rp-priority, .rp-list, .rp-method, .rp-hl { padding: 12px; }
}

/* ---------------- /observers ----------------
   The observer fleet: the map above, the short list below it, and one
   observer's telemetry in a dialog over both. Level colours are named here so
   a dot on the map and a badge in the table cannot drift apart; CRITICAL's
   value is .badge.lvl-critical's own, which is a literal rather than a token.
   Everything else reuses the .card/.badge/.facts/table and
   .popup-overlay/.popup/.bars primitives. */
#observers {
  --ob-good: var(--high); --ob-ok: var(--confirmed); --ob-fair: var(--medium);
  --ob-low: var(--disputed); --ob-warning: var(--disputed); --ob-critical: #b3261e;
}
html[data-theme="light"] #observers { --ob-critical: #96150f; }
.ob-mapwrap { margin: 4px 0 14px; }
.ob-map { height: clamp(260px, 42vh, 420px); border: 1px solid var(--line); border-radius: 8px; }
.ob-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6px; color: var(--muted); font-size: 13px; }
.ob-legend .li { display: inline-flex; align-items: center; gap: 6px; }
.ob-sw { width: 11px; height: 11px; border-radius: 50%; border: 1.5px solid var(--muted); }
.ob-sw.adv { background: var(--muted); }
.ob-sw.est { background: transparent; border-style: dashed; }
.ob-list { padding: 4px 12px 8px; }
/* The sortable headings. The control is a button filling the cell, as it is in
   /repeaters' table, so the whole heading is clickable and the keyboard
   reaches it — the rows are not focusable, and the headings must not inherit
   that. The cell gives up its padding to the button rather than padding it
   twice, which is why the two sets match the base th's 6px 8px exactly. */
.ob-table th { padding: 0; }
.ob-sort { width: 100%; padding: 6px 8px; background: none; border: 0; color: var(--muted);
  font: inherit; font-weight: 500; text-align: left; white-space: nowrap; cursor: pointer; }
.ob-sort:hover { color: var(--fg); }
.ob-sort .ind { margin-left: 4px; font-size: 10px; color: var(--accent); }
#observers button:focus-visible, #observers a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ob-table td { vertical-align: middle; }
.ob-table td.muted { white-space: nowrap; }
/* The name is the only cell allowed to wrap, and the only one wide enough to
   be worth wrapping. On a phone it does not wrap either — it truncates; see
   the media query at the foot of this block. */
.ob-table td.ob-name { min-width: 9ch; }
.ob-table tr.ob-sel td { background: var(--panel2); box-shadow: inset 2px 0 0 var(--accent); }
.ob-empty { padding: 14px 0; color: var(--muted); }
/* The packets column: a bar as a share of the busiest observer in the
   response, and the count beside it (CoreScope's treatment, which is the one
   thing on that page the fleet is read by). The track is a fixed width, so the
   counts line up down the column without spending a second cell on them, and
   the fill is the same --confirmed the traffic bars use rather than a colour
   that appears here alone. */
.ob-pph { white-space: nowrap; }
.ob-bar { display: inline-block; width: 56px; height: 10px; vertical-align: middle; margin-right: 8px;
  background: var(--panel2); border: 1px solid var(--line); border-radius: 3px; overflow: hidden; }
.ob-bar i { display: block; height: 100%; background: var(--confirmed); }
/* The count is right-aligned in a box of its own, so a column of them lines
   up on the unit the way td.num does rather than ragging off the bar. */
.ob-pphn { display: inline-block; min-width: 6ch; text-align: right; font-size: 12.5px;
  color: var(--muted); font-variant-numeric: tabular-nums; }
/* The detail dialog: the fix popup's overlay and panel, widened for the
   two-column telemetry groups and the chart under them. */
.ob-pop .popup { max-width: 640px; }
.ob-dhd { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.ob-dhd h3 { margin: 0; margin-right: auto; font-size: 16px; font-weight: 600; }
.ob-dhd h3 .badge { margin-right: 6px; vertical-align: 1px; }
.ob-node { color: var(--link); font-size: 13px; white-space: nowrap; }
.ob-finds { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 2px; }
.ob-find { display: inline-block; padding: 0 8px; border-radius: 10px; border: 1px solid var(--medium);
  color: var(--medium); font-size: 12px; font-weight: 600; white-space: nowrap; }
.ob-find.bad { border-color: var(--disputed); color: var(--disputed); }
.ob-dfacts { margin: 8px 0 2px; }
.ob-grps { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 4px 24px; align-items: start; }
.ob-grp h3 { margin: 12px 0 2px; font-size: 13px; font-weight: 600; color: var(--muted);
  letter-spacing: .04em; text-transform: uppercase; }
.ob-grp table.kv { font-size: 13.5px; }
.ob-grp table.kv td { word-break: break-word; }
/* The 24 hourly bars, as the node page draws them: .bars/.bar/.axis do the
   work, and this only sets the heading, a height that leaves room for the
   groups above it in a dialog, and the single series' colour — on the node
   page .bar i takes its fill from an attribution class, and an observer's
   receptions have no attribution to claim. */
.ob-chart { margin-top: 12px; }
.ob-chart h3 { margin: 0 0 2px; font-size: 13px; font-weight: 600; color: var(--muted);
  letter-spacing: .04em; text-transform: uppercase; }
.ob-chart .bars { height: 72px; }
.ob-chart .bar i { background: var(--confirmed); }
@media (max-width: 600px) {
  .ob-list { padding: 12px; }
  /* A finger lands on a row, so the rows get the height rather than the page
     getting a second control per row. */
  .ob-table td { padding-top: 11px; padding-bottom: 11px; }
  /* Four columns on a phone, and they fit by construction rather than by
     measurement: table-layout: fixed pins the table to the width of its
     wrapper, so no name, no count and no future column can push the list
     sideways again. The earlier five-column tuning was measured against short
     test names and real ones broke it — `MikeCorePi4ToadRepeater` is a single
     23-character word, which no amount of wrapping can narrow, and at 375px
     it took the table to 418px inside a 329px wrapper.

     What gives way, in order: region, then the bar, then the tail of a long
     name. Region is three letters that say where an observer is filed, not
     whether it is well, and the map above already says where it is; the bar
     at 18px could not resolve the fleet's own range anyway, and the count it
     decorates stays. Status and the name are what the page is read for, so
     the badge keeps its size and the name takes every pixel the other three
     columns do not need.

     The three fixed widths are each column's widest content plus the cell
     padding, and the content is sometimes the heading: the WARNING badge at
     62px, "89 min ago"/"1234 d ago" at 66px, and "Packets/hr" at 64px, which
     is wider than the five-figure count (58px) the column has to hold. They
     are set on the th, which is where table-layout: fixed reads a column's
     width — a heading that outgrew its column would hang over the next one,
     since the sort button is the only thing here that does not wrap.

     They come to 210px, and the name takes what is left: measured against the
     31 observers on staging, 119px at 375px and 64px at 320px, against a
     table that is exactly its wrapper's 329px and 274px. */
  .ob-table { table-layout: fixed; width: 100%; }
  .ob-table td, .ob-table th { padding-left: 3px; padding-right: 3px; font-size: 12.5px; }
  /* The heading's padding stays on the button inside it, so a sortable
     heading costs the row no width the measurement above did not allow for,
     and the control still clears 44px for a finger. */
  .ob-table th { padding: 0; }
  .ob-sort { padding: 11px 3px; min-height: 44px; }
  .ob-table .badge { font-size: 10.5px; padding: 1px 5px; }
  .ob-table .ob-rg { display: none; }
  .ob-table th.ob-st { width: 68px; }
  .ob-table th.ob-seen { width: 72px; }
  .ob-table th.ob-pph { width: 70px; }
  /* One line per observer, so the list reads as a list: a name too long for
     what is left of the row ends in an ellipsis, and the row's own dialog
     carries it in full in its heading. */
  .ob-table td.ob-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ob-bar { display: none; }
  .ob-pphn { min-width: 0; font-size: 11px; }
  .ob-pop .popup-close { width: 36px; height: 36px; }
}
