* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  display: grid;
  grid-template-rows: auto 1fr auto;
  font-family: var(--font-mono);
  background: var(--bg);
  color: var(--txt);
}
/* Only the dashboard (index.html) has a #ticker row between the header and
   main; every other page sharing this stylesheet (privacy.html, call.html,
   sysop.html, offline.html, ...) has just header + main and relies on the
   base rule's second track being 1fr so main fills the remaining height.
   Scoped to #dash so those pages are untouched. */
#dash { grid-template-rows: auto auto minmax(0, 1fr) auto; }
/* Elk van de vier expliciet op zijn eigen rij. Zonder dit schuift de hele
   stapel op zodra #ticker leeg is: `#ticker:empty { display: none }` haalt hem
   uit het raster, main belandt dan op de auto-rij van de ticker en groeit naar
   zijn max-content, en de banddrukbalk krijgt de 1fr-rij en houdt 1px over.
   Gemeten op een leeg tickerscherm: main 760px, #strip 1px. */
#dash > #top { grid-row: 1; }
#dash > #ticker { grid-row: 2; }
#dash > #main { grid-row: 3; }
#dash > #strip { grid-row: 4; }

#top {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 13px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}
#node { color: var(--txt-strong); letter-spacing: .16em; font-weight: 600; }
#live { font-size: 9px; letter-spacing: .1em; color: var(--dim); }
#live[data-state="up"] { color: var(--accent-go); }
#live::before {
  content: ""; display: inline-block; width: 5px; height: 5px;
  border-radius: 50%; margin-right: 5px; background: currentColor;
}
#views, #spectrum, #bands, #modes { display: flex; gap: 3px; }
#views { margin-left: 4px; }
#spectrum { margin-left: 14px; }
#modes { margin-left: 14px; }
.chip {
  font-size: 10px; padding: 3px 7px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 3px;
  background: none; color: var(--dim); font-family: inherit;
}
.chip[aria-pressed="true"] { background: var(--line); color: var(--txt); }
/* The count on the inactive half is the point of it: it says how much is
   happening on the spectrum you are not looking at. It has to stay legible
   there, so it is dimmed relative to its own chip rather than hidden. */
.chip-count {
  margin-left: 5px; padding-left: 5px; border-left: 1px solid var(--line);
  font-variant-numeric: tabular-nums; opacity: 0.75;
}
.chip--spectrum[aria-pressed="true"] .chip-count { opacity: 1; }

/* The plots page puts the toggle on its own line under the subtitle. */
.plots #spectrum { margin: 8px 0 0; }
#meta { margin-left: auto; display: flex; align-items: center; gap: 11px; font-size: 10px; color: var(--dim); }
#meta button, #meta a {
  font: inherit; color: var(--dim); text-decoration: none; cursor: pointer;
  border: 1px solid var(--line); border-radius: 3px; padding: 3px 7px; background: none;
}

/* Theme toggle: a pill switch, last in #meta on every page (theme.js wires
   the click on all of them, not just the dashboard). The moon glyph rides
   on the knob rather than sitting fixed in the track, so there is exactly
   one thing sliding and exactly one thing to read. Colour comes entirely
   from tokens already defined for other uses (--line-soft track,
   --txt-strong/--panel knob) — the switch never needed a colour of its own,
   which is the point of the token system. Position, not colour, carries
   the two states: dark (the default, no [data-theme] override needed) has
   the knob at the right; :root[data-theme="light"] moves it left. */
#theme-toggle {
  position: relative; flex: none; padding: 0; margin: 0;
  width: 34px; height: 18px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--line-soft); cursor: pointer;
}
#theme-toggle:focus-visible { outline: 2px solid var(--accent-cool); outline-offset: 2px; }
.theme-toggle-knob {
  position: absolute; top: 1px; left: 1px;
  width: 14px; height: 14px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--txt-strong); color: var(--panel);
  transform: translateX(16px);
  transition: transform .15s ease;
}
:root[data-theme="light"] .theme-toggle-knob { transform: translateX(0); }
.theme-toggle-knob svg { width: 10px; height: 10px; fill: currentColor; }

/* Opening ticker (app.js ticker.js): newest event first, wraps onto more
   than one line under heavy traffic rather than growing without limit — the
   module itself caps and expires entries, this just bounds how tall that
   can ever get on screen. Empty (no live openings yet) collapses to nothing
   so it never reserves dead space above the main view. */
#ticker {
  list-style: none; margin: 0; padding: 6px 13px;
  display: flex; flex-wrap: wrap; gap: 6px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  max-height: 68px; overflow-y: auto;
}
#ticker:empty { display: none; }
.ticker-item {
  display: flex; align-items: baseline; gap: 6px;
  padding: 3px 8px;
  border: 1px solid var(--line); border-radius: 3px;
  background: var(--bg);
  font-size: 10.5px;
}
.ticker-text { color: var(--txt-strong); }
.ticker-count { color: var(--dim); font-size: 9px; }
/* A path starting from the visitor's own country is directly workable, not
   just a hint that a country elsewhere can hear something — the accent
   border plus the explicit badge say so without repeating the sentence. */
.ticker-item--home { border-color: var(--accent-go); }
.ticker-badge {
  color: var(--accent-go); font-size: 8.5px; letter-spacing: .06em;
  text-transform: uppercase;
}

main { display: grid; grid-template-columns: 1fr var(--rail-width); min-height: 0; }
/* The bandscope and the map share one grid cell and one column: the toggle
   swaps which of them is visible instead of reflowing the layout. */
#map, #scope { grid-column: 1; grid-row: 1; position: relative; overflow: hidden; }
/* Rows are flex children, not a fixed height each: a visible row with spots
   shares what's left of #scope's height with the other active rows (see
   .scope-row's flex: 1 1 0), while a visible row with none takes a narrow
   fixed strip instead (.scope-row--empty) — the point of the whole layout
   (task 1 of the pile-up fix): fewer visible or busy bands means more
   height, and therefore more vertical room to stack collisions, per band. */
#scope { overflow-y: auto; display: flex; flex-direction: column; }
main[data-view="map"] #scope { display: none; }
main[data-view="scope"] #map { display: none; }
#rail { border-left: 1px solid var(--line); background: var(--panel); overflow-y: auto; }
.rail-head {
  display: flex; align-items: baseline; gap: 6px; position: relative;
  padding: 7px 11px; border-bottom: 1px solid var(--line);
  font-size: 9px; letter-spacing: .14em; color: var(--dim);
}
#rail-sub { margin-left: auto; }
/* The "?" toggle for the score explanation. A native <details>/<summary> so
   it opens on click and on Enter, reads with a screen reader and needs no
   JS — a hover reveal was considered and rejected: this is four sentences
   meant to be read, and hover both dismisses on pointer drift and is
   unreachable by keyboard or touch. Kept visually quiet (dim, small, no
   fill) since the spot list is the content here, this is reference. */
.rail-help { display: inline; }
.rail-help > summary {
  display: inline-block; list-style: none; cursor: pointer;
  width: 12px; height: 12px; line-height: 10px; text-align: center;
  font-size: 9px; letter-spacing: 0; color: var(--dim);
  border: 1px solid var(--line); border-radius: 50%;
}
.rail-help > summary::-webkit-details-marker,
.rail-help > summary::marker { display: none; content: ""; }
.rail-help > summary:hover,
.rail-help > summary:focus-visible { color: var(--txt); border-color: var(--dim); }
.rail-help[open] > summary { color: var(--txt-strong); border-color: var(--dim); }
/* Absolutely positioned against .rail-head, not inserted into the document
   flow — opening it must not push #rail-note or #rail-list down, since
   that's the actual spot list the sidebar exists to show. */
.rail-help-body {
  position: absolute; left: 11px; right: 11px; top: 100%; margin-top: 4px;
  z-index: 2; background: var(--panel); border: 1px solid var(--line);
  border-radius: 4px; padding: 8px 10px;
  font-size: 10px; letter-spacing: normal; text-transform: none;
  line-height: 1.45; color: var(--txt);
}
.rail-help-body p { margin: 0 0 6px; }
.rail-help-body p:last-child { margin-bottom: 0; }
.rail-help-body strong { color: var(--txt-strong); font-weight: 600; }
#rail-note { margin: 0; padding: 6px 11px; border-bottom: 1px solid var(--line); font-size: 9px; color: var(--dim); }
#rail-list { list-style: none; margin: 0; padding: 0; }

#strip { display: flex; border-top: 1px solid var(--line); background: var(--panel); }

/* Callsign → QRZ links (web/js/format.js qrzLink), used in the rail, the
   bandscope marks, the call-lookup recent list and the sysop tables. A
   dotted underline reads as a link on the dark panel without borrowing the
   browser's default blue, and without the callsign losing its own colour
   context (e.g. --txt-strong in .rail-call). */
.callsign-link { color: var(--accent-cool); text-decoration: none; border-bottom: 1px dotted var(--accent-cool); }
.callsign-link:hover, .callsign-link:focus { border-bottom-style: solid; }

/* Smal scherm. Drie dingen gaan hier anders, alle drie omdat een telefoon
   geen verkleinde desktop is.

   1. De zijbalk gaat naar boven en krijgt de vrije hoogte. Op een breed
      scherm staan de bandscope en de zeldzame spots naast elkaar en zie je ze
      allebei; onder elkaar zie je alleen wat bovenaan staat, en dan is "welke
      zeldzame stations zijn nu te werken" het antwoord waarvoor iemand op zijn
      telefoon kijkt. De bandscope blijft eronder staan, met 34vh in plaats
      van 55vh: gemeten op een scherm van 900px houdt de zijbalk daarmee 397px
      over tegen 306px voor de bandscope, en eet een lege band geen half scherm
      meer op.
   2. #top mag afbreken. Zonder wrap duwt de knoppenrij de pagina breder dan
      het scherm, en dan scrollt de hele site horizontaal -- inclusief de
      bandscope, die daardoor niet meer op zijn eigen breedte te lezen is.
   3. #strip scrollt zelf. Elf banddrukblokken passen nooit op een
      telefoonbreedte; ze mogen hun eigen balk uit schuiven, niet de pagina. */
@media (max-width: 900px) {
  /* minmax(0, 1fr), niet 1fr: een 1fr-baan heeft een min-content bodem, en de
     zijbalk vol spots is dan hoger dan het scherm. De pagina groeit daardoor
     onder de vouw uit en de banddrukbalk staat niet meer waar hij hoort. */
  main { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) 34vh; }
  #rail { grid-row: 1; min-height: 0; border-left: 0; border-bottom: 1px solid var(--line); }
  #map, #scope { grid-row: 2; }
  #top { flex-wrap: wrap; row-gap: 7px; }
  #spectrum, #modes, #meta { margin-left: 0; }
  /* De band- en moderijen krijgen elk een eigen regel en schuiven daarbinnen:
     zeventien chips die afbreken maken de kop drie regels hoog, en dat kost
     dezelfde hoogte als het probleem dat hierboven wordt opgelost. */
  #bands, #modes, #meta { flex: 1 1 100%; min-width: 0; overflow-x: auto; }
  /* Een grid-item heeft min-width: auto, dus de kop kan de pagina breder maken
     dan het scherm: zijn min-content is de breedste knoppenrij die hij bevat,
     en die rijen hebben `flex-basis: 100%`, wat tijdens die meting terugvalt op
     hun eigen max-content. Gemeten: 412px scherm, 480px pagina, en dus schuift
     alles horizontaal mee -- ook de bandscope, die daardoor niet meer op zijn
     eigen breedte te lezen is. Nul zetten laat elke rij op schermbreedte staan
     en binnen zijn eigen balk schuiven. */
  #dash > * { min-width: 0; }
  #strip { overflow-x: auto; }
  /* Elf cellen die de breedte delen worden elk 35px breed, en dan past "160m"
     met zijn aantal er niet meer in. Vaste breedte, en de balk schuift.
     Via #strip, want .band-cell zelf staat verderop in dit bestand en zou bij
     gelijke specificiteit alsnog winnen. */
  #strip .band-cell { flex: 0 0 92px; }
}

/* De great-circle kaart (map.js): een schijf, geen rechthoek. De SVG schaalt
   daarom met 'meet' en houdt links en rechts ruimte over; het onderschrift
   staat in die ruimte en zegt vanaf welk punt de kaart kijkt. */
#map-centre {
  position: absolute; top: 8px; left: 11px; margin: 0; z-index: 2;
  font-size: 9px; color: var(--dim); pointer-events: none;
}
#map .land path { fill: var(--line); stroke: none; }
/* Ringen en spaken zijn de schaal, niet de inhoud: afleesbaar, en verder weg
   achter de marks. De randcirkel is het antipodenpunt en mag iets steviger
   staan, want die begrenst waar de kaart nog iets kan tonen. */
.map-ring { fill: none; stroke: var(--line-soft); stroke-width: .6; }
.map-ring--rim { stroke: var(--line); stroke-width: 1; }
.map-spoke { stroke: var(--line-soft); stroke-width: .5; }
.map-ring-label, .map-bearing {
  font-family: var(--font-mono); font-size: 8px; fill: var(--dim);
  text-anchor: middle; dominant-baseline: middle;
  paint-order: stroke; stroke: var(--bg); stroke-width: 3px;
}
/* De vier hoofdrichtingen dragen de oriëntatie van de hele kaart, de getallen
   ertussen zijn aflezing. Dus verschil in gewicht, niet in kleurtje. */
.map-bearing--cardinal { fill: var(--txt); font-size: 10px; }
/* Het middelpunt is de bezoeker zelf, in dezelfde kleur waarin de ticker
   "werkbaar vanaf hier" zegt. */
.map-home { fill: var(--accent-go); }
.map-spot { cursor: pointer; }
.map-spot circle { fill: var(--accent-none); }
/* Dezelfde modekleuren als de bandscope (tokens.css), want een FT8-stip hoort
   in beide views dezelfde stip te zijn. */
.map-spot circle[data-mode="CW"]   { fill: var(--mode-cw); }
.map-spot circle[data-mode="SSB"]  { fill: var(--mode-ssb); }
.map-spot circle[data-mode="FT8"]  { fill: var(--mode-ft8); }
.map-spot circle[data-mode="FT4"]  { fill: var(--mode-ft4); }
.map-spot circle[data-mode="RTTY"] { fill: var(--mode-rtty); }
.map-spot circle[data-mode="PSK"]  { fill: var(--mode-psk); }
.map-spot:hover circle, .map-spot:focus circle { stroke: var(--txt-strong); stroke-width: 1; }
/* Dezelfde "nieuw" marker als in de bandscope, met dezelfde keyframes en
   dezelfde twee custom properties uit map.js markEl(). Een SVG-element heeft
   geen ::after, dus hier is de ring een echte <circle>; transform-box zet het
   schaalpunt op het midden van die cirkel in plaats van op de oorsprong van
   het hele tekenvlak, waar hij zonder wegvliegt. */
.map-ping {
  pointer-events: none; transform-box: fill-box; transform-origin: center;
  animation: scope-ping 1.4s ease-out infinite;
  animation-delay: var(--spot-age, 0s);
}
@media (prefers-reduced-motion: reduce) {
  .map-ping { animation: none; transform: scale(2.1); opacity: calc(var(--spot-ping, 1) * .3); }
}

/* Bandscope: one row per band, all rows sharing #scope's height equally
   (flex: 1 1 0 below). The row order never reshuffles and a selected band
   with no spots still holds its lane — only which rows are visible changes,
   via [hidden], and that is driven by the band chips (app.js draw()). A
   hidden row is removed from the flex layout entirely, so the remaining
   rows grow to fill the space it leaves — the filter doubles as a zoom. */
.scope-row {
  display: grid; grid-template-columns: 40px 1fr 34px; align-items: center;
  gap: 8px; flex: 1 1 0; min-height: 0; padding: 0 11px;
  border-bottom: 1px solid var(--line-soft);
}
.scope-row[hidden] { display: none; }
/* A band with no spots in the window still gets a row — narrow and fixed,
   just enough to show its label, its frequency scale and that it is empty,
   not a full share of height it would do nothing with (Change 2). The
   bands that do have spots keep flex: 1 1 0 above and divide everything
   this leaves between them. scope.js render() decides this from the live
   spot counts before it measures any track's pixel height, so a lane's
   room to stack collisions (maxRows) reflects the height it actually ends
   up with here, never a stale one from before this class was set. */
.scope-row--empty { flex: 0 0 34px; }
.scope-label { font-size: 10px; color: var(--dim); }
.scope-count { font-size: 10px; color: var(--dim); text-align: right; }
/* The lane stacks the track (spots) above the tick strip (frequency scale),
   so both share the row's height without either one having to guess the
   other's size. */
.scope-lane {
  position: relative; height: 100%; min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
}
.scope-track {
  position: relative; flex: 1 1 auto; min-height: 0;
  background: var(--line-soft); border-radius: 2px;
}
/* A placed spot: an 8px dot anchored to the row it was stacked into
   (scope.js placeSpots), counted up from the track's bottom edge so a
   pile-up grows as a visible column instead of spreading around a
   vertical centre. */
.scope-spot {
  position: absolute; left: 0; bottom: 0; transform: translateX(-50%);
  width: 8px; height: 8px; padding: 0; border: 0; border-radius: 50%;
  background: var(--accent-none); cursor: pointer;
}
/* A spot the lane ran out of rows for (scope.js render()'s `overflow`):
   drawn as a thin sliver spanning the full track height at its true
   frequency rather than an 8px dot at a guessed row, so it cannot be
   mistaken for an ordinarily-placed mark, and dimmed further so the marks
   that did get a precise row stay the more prominent read. Still a real
   link with a real tooltip — overflow is compressed, not dropped. */
.scope-spot--overflow {
  top: 0; bottom: 0; height: auto; width: 2px; border-radius: 1px;
  /* Measured contrast floor, not a look — do not "tidy" this back toward .5.
     At .5, blended over --line-soft, four of the six --mode-* colours drop
     below 3:1 in dark (ft4 2.35, rtty 2.18, psk 2.36, cw 2.99) and all but
     one do in light (checked against light's own --line-soft, worse there
     since blending toward a light track halves the luminance gap WCAG
     measures, rather than barely denting it the way blending toward a near-
     black one does). .75 is the value both themes need: dark's worst case
     (rtty) clears to 3.42, light's worst case (ft8) clears to 3.18. Recheck
     both themes' six --mode-* colours against --line-soft (see
     theme-report.md for the method) before ever lowering this. */
  opacity: .75;
}
/* One dedicated token per mode (tokens.css) — color here is the only thing
   that says which mode a spot is, so FT8 and FT4 each need their own hue,
   not a shared one. Applies to overflow slivers too, same rule either way. */
.scope-spot[data-mode="CW"]   { background: var(--mode-cw); }
.scope-spot[data-mode="SSB"]  { background: var(--mode-ssb); }
.scope-spot[data-mode="FT8"]  { background: var(--mode-ft8); }
.scope-spot[data-mode="FT4"]  { background: var(--mode-ft4); }
.scope-spot[data-mode="RTTY"] { background: var(--mode-rtty); }
.scope-spot[data-mode="PSK"]  { background: var(--mode-psk); }
/* A spot spotted in the last minute (scope.js NEW_WINDOW_MS): a ring in the
   spot's own mode colour grows out of the mark and fades, about once a
   second, so the one dot that just landed is findable in a lane that already
   holds fifty. It is a pseudo-element, so it moves nothing and leaves the
   mark's own age fade alone. The two custom properties come from scope.js
   markNew() and are what make the ring survive a redraw: --spot-age is a
   negative animation-delay that resumes the cycle at the phase the spot's
   real age has reached, --spot-ping runs 1 -> 0 over the minute so the last
   rings are faint and the marker ends by fading out, not by stopping. */
.scope-spot--new::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: inherit; border-radius: inherit;
  animation: scope-ping 1.4s ease-out infinite;
  animation-delay: var(--spot-age, 0s);
}
@keyframes scope-ping {
  0%   { transform: scale(1); opacity: calc(var(--spot-ping, 1) * .55); }
  70%  { opacity: calc(var(--spot-ping, 1) * .14); }
  100% { transform: scale(2.6); opacity: 0; }
}
/* The same marker on an overflow sliver, widened instead of scaled both
   ways: a sliver already spans the full track height, so a round ring would
   grow out of the lane rather than around the mark. */
.scope-spot--overflow.scope-spot--new::after { animation-name: scope-ping-x; }
@keyframes scope-ping-x {
  0%   { transform: scaleX(1); opacity: calc(var(--spot-ping, 1) * .55); }
  100% { transform: scaleX(7); opacity: 0; }
}
/* Reduced motion keeps the marker and drops the movement: "this spot is new"
   is information, repeating it once a second is only how it is delivered. */
@media (prefers-reduced-motion: reduce) {
  .scope-spot--new::after {
    animation: none; transform: scale(2.1);
    opacity: calc(var(--spot-ping, 1) * .3);
  }
  .scope-spot--overflow.scope-spot--new::after { transform: scaleX(4); }
}
.scope-tip {
  display: none; position: absolute; bottom: calc(100% + 5px); left: 50%;
  transform: translateX(-50%); white-space: nowrap; z-index: 5;
  background: var(--panel); color: var(--txt); border: 1px solid var(--line);
  border-radius: 3px; padding: 4px 7px; font-size: 10px;
}
.scope-spot:hover .scope-tip, .scope-spot:focus .scope-tip { display: block; }
/* The "+N" badge next to the overflow slivers: how many spots on this band
   are compressed that way. Quiet and out of the way, top corner of the
   track, never part of the frequency axis itself. */
.scope-overflow-badge {
  position: absolute; top: 3px; right: 4px; z-index: 4; pointer-events: none;
  font-size: 8.5px; line-height: 1; color: var(--txt); background: var(--panel);
  border: 1px solid var(--line); border-radius: 3px; padding: 2px 4px;
}
/* Frequency scale: both band edges plus intermediate ticks (scope.js
   tickEls()), quiet enough that the marks stay the thing the eye lands on.
   A thin rule instead of a background keeps it out of the way of the track
   above it while still reading as "this is the axis". */
.scope-ticks {
  position: relative; flex: 0 0 11px; border-top: 1px solid var(--line-soft);
}
.scope-tick {
  position: absolute; top: 2px; transform: translateX(-50%);
  font-size: 8px; color: var(--dim); white-space: nowrap;
}
.scope-tick--start { transform: translateX(0); }
.scope-tick--end { transform: translateX(-100%); }

.rail-item {
  display: grid; grid-template-columns: 26px 1fr auto; gap: 8px;
  align-items: center; padding: 8px 11px;
  border-bottom: 1px solid var(--line-soft);
}
.rail-score { font-size: 14px; font-weight: 600; text-align: center; }
.rail-score.hi { color: var(--accent-hot); }
.rail-score.md { color: var(--accent-warn); }
.rail-score.lo { color: var(--dim); }
.rail-call { font-size: 12.5px; color: var(--txt-strong); letter-spacing: .04em; }
.rail-meta { font-size: 9px; color: var(--dim); margin-top: 2px; }
.rail-freq { font-size: 11px; color: var(--txt); text-align: right; }
.rail-freq em { display: block; font-style: normal; font-size: 8.5px; color: var(--dim); }

.band-cell { flex: 1; padding: 6px 9px; border-right: 1px solid var(--line-soft); }
.band-cell:last-child { border-right: 0; }
.band-label { display: flex; justify-content: space-between; font-size: 9.5px; color: var(--dim); }
.band-label b { color: var(--txt); font-weight: 500; }
.band-bar { height: 3px; background: var(--line-soft); border-radius: 2px; margin-top: 4px; overflow: hidden; }
.band-bar u { display: block; height: 100%; text-decoration: none; background: var(--accent-go); }
/* The bar takes its band's own colour, the same token the activity heatmap
   fills its cells with, so a band reads the same on both pages. --accent-go
   above stays the fallback for anything not listed. */
.band-cell[data-band="160m"] .band-bar u { background: var(--band-160m); }
.band-cell[data-band="80m"]  .band-bar u { background: var(--band-80m); }
.band-cell[data-band="60m"]  .band-bar u { background: var(--band-60m); }
.band-cell[data-band="40m"]  .band-bar u { background: var(--band-40m); }
.band-cell[data-band="30m"]  .band-bar u { background: var(--band-30m); }
.band-cell[data-band="20m"]  .band-bar u { background: var(--band-20m); }
.band-cell[data-band="17m"]  .band-bar u { background: var(--band-17m); }
.band-cell[data-band="15m"]  .band-bar u { background: var(--band-15m); }
.band-cell[data-band="12m"]  .band-bar u { background: var(--band-12m); }
.band-cell[data-band="10m"]  .band-bar u { background: var(--band-10m); }
.band-cell[data-band="6m"]   .band-bar u { background: var(--band-6m); }
.band-cell[data-band="4m"]   .band-bar u { background: var(--band-4m); }
.band-cell[data-band="2m"]   .band-bar u { background: var(--band-2m); }
.band-cell[data-band="70cm"] .band-bar u { background: var(--band-70cm); }
.band-cell[data-band="23cm"] .band-bar u { background: var(--band-23cm); }
.band-cell[data-band="13cm"] .band-bar u { background: var(--band-13cm); }
.band-cell[data-band="9cm"]  .band-bar u { background: var(--band-9cm); }
.band-cell[data-band="6cm"]  .band-bar u { background: var(--band-6cm); }
.band-cell[data-band="3cm"]  .band-bar u { background: var(--band-3cm); }
.band-cell[data-band="QO-100"] .band-bar u { background: var(--band-qo100); }
.band-cell[data-band="1.2cm"]  .band-bar u { background: var(--band-12cm); }
.band-trend { font-size: 8px; margin-top: 3px; }
.band-trend.up { color: var(--accent-go); }
.band-trend.down { color: var(--accent-hot); }
.band-trend.flat { color: var(--dim); }

/* Sysop page */
#login {
  width: 260px; margin: 60px auto;
  display: flex; flex-direction: column; gap: 10px;
  padding: 18px;
  border: 1px solid var(--line);
  background: var(--panel);
  border-radius: 4px;
}
#login input {
  font: inherit;
  background: var(--bg);
  color: var(--txt);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 6px 8px;
}
#login button {
  font: inherit;
  color: var(--txt);
  background: var(--line-soft);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 7px 8px;
  cursor: pointer;
}
#login button:hover { background: var(--line); }
#login-error { font-size: 10px; color: var(--accent-hot); min-height: 1em; margin: 0; }

#panels {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 14px; padding: 16px; overflow-y: auto; align-content: start;
}
#panels section {
  border: 1px solid var(--line);
  background: var(--panel);
  border-radius: 4px;
  padding: 11px 13px;
}
#panels h2 {
  margin: 0 0 8px;
  font-size: 11px; letter-spacing: .1em; color: var(--dim); font-weight: 600;
}
#panels h3 {
  margin: 12px 0 8px;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
  font-size: 10px; letter-spacing: .08em; color: var(--dim); font-weight: 600;
}
#panels table { width: 100%; border-collapse: collapse; font-size: 11px; }
#panels td { text-align: left; padding: 4px 6px; border-bottom: 1px solid var(--line-soft); }
#panels tr:first-child td { color: var(--dim); font-size: 9.5px; letter-spacing: .06em; }

/* Call lookup & activity-plots pages */
#call-main, #plots-main { display: block; max-width: 900px; margin: 0 auto; padding: 20px 16px; }

#lookup { display: flex; gap: 8px; }
#lookup input {
  flex: 1; font: inherit; background: var(--panel); color: var(--txt);
  border: 1px solid var(--line); border-radius: 3px; padding: 8px 10px;
}
#lookup button {
  font: inherit; color: var(--txt); background: var(--line-soft);
  border: 1px solid var(--line); border-radius: 3px; padding: 8px 14px; cursor: pointer;
}
#lookup button:hover { background: var(--line); }
#call-error { font-size: 10px; color: var(--accent-hot); min-height: 1em; margin: 10px 0 0; }

#call-summary { display: flex; gap: 24px; margin: 18px 0; }
.call-stat { font-size: 10px; color: var(--dim); }
.call-stat b { display: block; font-size: 18px; color: var(--txt-strong); font-weight: 600; }

#call-bands { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 18px; }
#call-bands .band-cell {
  flex: 0 0 150px; border: 1px solid var(--line-soft); border-radius: 3px; padding: 8px 10px;
}

#call-recent { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }

/* Legal & offline pages */
#legal-main {
  display: block; max-width: 640px; margin: 0 auto; padding: 30px 16px 60px;
  line-height: 1.6; font-size: 12.5px;
}
#legal-main h1 { margin: 0 0 18px; font-size: 16px; color: var(--txt-strong); }
#legal-main h2 {
  margin: 24px 0 8px; font-size: 11px; letter-spacing: .08em; color: var(--dim); font-weight: 600;
}
#legal-main p { margin: 0 0 12px; }
#legal-main code {
  font-family: inherit; background: var(--panel); border: 1px solid var(--line);
  border-radius: 3px; padding: 1px 5px;
}
#legal-main a { color: var(--accent-cool); }

#plots-sub { font-size: 11px; color: var(--dim); }
#heatmap { overflow-x: auto; }
.plot-label { fill: var(--dim); font-size: 8px; }
.plot-cell[data-band="160m"] { fill: var(--band-160m); }
.plot-cell[data-band="80m"]  { fill: var(--band-80m); }
.plot-cell[data-band="60m"]  { fill: var(--band-60m); }
.plot-cell[data-band="40m"]  { fill: var(--band-40m); }
.plot-cell[data-band="30m"]  { fill: var(--band-30m); }
.plot-cell[data-band="20m"]  { fill: var(--band-20m); }
.plot-cell[data-band="17m"]  { fill: var(--band-17m); }
.plot-cell[data-band="15m"]  { fill: var(--band-15m); }
.plot-cell[data-band="12m"]  { fill: var(--band-12m); }
.plot-cell[data-band="10m"]  { fill: var(--band-10m); }
.plot-cell[data-band="6m"]   { fill: var(--band-6m); }
.plot-cell[data-band="4m"]    { fill: var(--band-4m); }
.plot-cell[data-band="2m"]    { fill: var(--band-2m); }
.plot-cell[data-band="70cm"]  { fill: var(--band-70cm); }
.plot-cell[data-band="23cm"]  { fill: var(--band-23cm); }
.plot-cell[data-band="13cm"]  { fill: var(--band-13cm); }
.plot-cell[data-band="9cm"]   { fill: var(--band-9cm); }
.plot-cell[data-band="6cm"]   { fill: var(--band-6cm); }
.plot-cell[data-band="3cm"]   { fill: var(--band-3cm); }
.plot-cell[data-band="QO-100"]{ fill: var(--band-qo100); }
.plot-cell[data-band="1.2cm"] { fill: var(--band-12cm); }
