/* The site's one stylesheet, served at /static/site.css under a
   content-hashed URL that every page links (see pages._statics). */
:root {
  --bg: #fbfaf8; --fg: #1c1b19; --muted: #6b6862; --line: #e3e0da;
  --card: #ffffff; --accent: #1c5d99; --accent-soft: #eaf1f8;
  --accent-fg: #ffffff;
  --active: #1a7a4c; --future: #7a5c1a; --lapsed: #8a5a2b; --ended: #8a2b2b;
  /* The map's overlay palette, separate from the state colors above.
     Restricted airspace wears the red every EFB and chart viewer draws a
     TFR in -- but bold, bright and barely translucent, because the chart's
     own printed reds and browns are muted and a boundary must outshout
     them, not blend in. Upcoming boundaries take the same apps' scheduled
     orange. A NOTAM's own point or radius claim is not restricted airspace
     and stays teal, a hue the chart barely uses. */
  --restrict: #e8112d; --restrict-soon: #e07c00; --shape-point: #0d7d8f;
  /* Advisories, in ForeFlight's own assignments so a reader keeps the
     vocabulary they already have: red for SIGMETs of every type
     (convective included), yellow for convective outlooks, and the
     AIRMETs by hazard -- orange turbulence, blue icing, purple IFR,
     magenta mountain obscuration. The SIGMET red is deliberately not the
     restriction red: a TFR and a SIGMET are different claims. */
  --adv-sigmet: #d32f2f; --adv-outlook: #b08900;
  --adv-conv: #d32f2f; --adv-turb: #b45309; --adv-ice: #2563eb;
  --adv-ifr: #7c3aed; --adv-mtn: #c026d3;
  /* A followed flight's track: a magenta the restriction reds, the point
     teal and the advisory hues all leave alone, because it is the one line
     on this map that is about a single aircraft and not about a place. */
  --track: #c2185b;
  /* The map's ground: what shows where the chart has no tiles -- which,
     zoomed out to the country, is most of the screen. The map page sets it
     inline per basemap to the chart's own empty-paper colour (basemaps.py:
     the sectional's ocean blue, the IFR charts' white), so coverage ends
     like a chart running out of paper. This is only the fallback for a
     canvas no basemap has claimed. */
  --ground: #c8d2d9;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
/* --accent-fg is what sits ON the accent. The dark accent is a light blue, so
   the light scheme's white-on-accent fails contrast there; it takes the page
   background instead. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #17181a; --fg: #e6e4e0; --muted: #97948e; --line: #2e3033;
    --card: #1e2023; --accent: #7fb2e0; --accent-soft: #1c2a36;
    --accent-fg: #17181a;
    --active: #63c295; --future: #d5b46a; --lapsed: #d69a6a; --ended: #e08b8b;
    /* Middle lightness, not the pastel the rest of this scheme takes: the
       chart tiles stay light in every scheme, so a shape's backdrop is the
       light chart wherever there is one and the dark ground only beyond
       it. These have to hold on both. */
    --restrict: #f43f4f; --restrict-soon: #ffa040; --shape-point: #1cadc4;
    --adv-sigmet: #ef5350; --adv-outlook: #eab308;
    --adv-conv: #ef5350; --adv-turb: #d97706; --adv-ice: #60a5fa;
    --adv-ifr: #a78bfa; --adv-mtn: #e879f9;
    --track: #ff6ea8;
    /* Fallback only, like the light one: the map page's inline per-basemap
       ground wins in both schemes, because the chart tiles it must meet
       stay light everywhere. */
    --ground: #26333d;
  }
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font: 15px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 20px 64px; }
header.top { border-bottom: 1px solid var(--line); background: var(--card); }
header.top .wrap { display: flex; align-items: baseline; gap: 18px; padding: 14px 20px; }
header.top h1 { font-size: 17px; margin: 0; letter-spacing: -0.01em; }
header.top h1 a { color: var(--fg); }
header.top nav { margin-left: auto; display: flex; gap: 16px; font-size: 13px; }
.sub { color: var(--muted); font-size: 13px; }

input, select, button {
  font: inherit; color: var(--fg); background: var(--bg);
  border: 1px solid var(--line); border-radius: 6px; padding: 7px 9px; min-width: 0;
}
input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
button.go {
  background: var(--accent); color: var(--accent-fg); border-color: transparent;
  font-weight: 600; cursor: pointer; padding: 8px 18px;
}
button.go:hover { filter: brightness(1.08); }
.hint { font-size: 12px; color: var(--muted); }

/* Rendered for assistive tech, invisible otherwise -- the map search's
   submit button and the timeline's spoken status line use it. */
.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  border: 0; overflow: hidden; clip-path: inset(50%); }

.banner {
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  background: var(--accent-soft); border-radius: 7px; padding: 10px 14px;
  margin: 16px 0; font-size: 13px;
}
.banner.warn { border-left-color: var(--lapsed); }
.banner.bad { border-left-color: var(--ended); }
.banner b { font-variant: small-caps; letter-spacing: 0.04em; }

/* The filter strip, on /search. Each filter is a <details> whose panel holds
   the inputs that belong to it -- place, text, dates, kind, state -- and all
   of the panels' inputs are inside the one GET form, so submitting from any
   panel carries every filter. No script: a <details> opens before anything
   loads. The summary wears a column header's look, because these controls
   used to be the column headers and the table below should still read as
   theirs. */
details.pop { position: relative; display: inline-block; }
details.pop > summary { list-style: none; cursor: pointer; white-space: nowrap;
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  font-weight: 600; color: var(--muted); }
details.pop > summary::-webkit-details-marker { display: none; }
details.pop > summary::after { content: " ▾"; opacity: 0.7; }
details.pop > summary:hover, details.pop[open] > summary { color: var(--accent); }
/* The header wears its own filter's value, so a filtered column says so
   without being opened. */
details.pop summary .val { color: var(--accent); text-transform: none;
  letter-spacing: normal; font-weight: 600; }
details.pop .panel { position: absolute; z-index: 30; top: calc(100% + 4px);
  left: 0; min-width: 250px; padding: 12px; display: flex;
  flex-direction: column; gap: 9px; background: var(--card);
  border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
  font-size: 13px; font-weight: 400; letter-spacing: normal;
  text-transform: none; color: var(--fg); white-space: normal; text-align: left; }
details.pop.right .panel { left: auto; right: 0; }
.panel label { display: flex; flex-direction: column; gap: 4px; font-size: 11px;
  color: var(--muted); font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; }
.panel label.check { flex-direction: row; align-items: center; gap: 6px;
  text-transform: none; font-weight: 400; font-size: 13px; color: var(--fg); }
.panel button.go { align-self: flex-start; padding: 6px 14px; }
/* The effective-date direction, toggled by its own arrow in the strip. */
a.sort { font-size: 13px; margin-left: 2px; }
a.sort:hover { text-decoration: none; }
a.sort.off { color: var(--muted); opacity: 0.5; }
/* The toolbar above the tables: the range and the pager on the left, the
   filter strip on the right. */
.pager.toolbar { gap: 14px; }
.pager.toolbar .filters { display: flex; gap: 14px; align-items: center;
  flex-wrap: wrap; margin-left: auto; }

table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { text-align: left; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--muted); font-weight: 600;
  padding: 8px 10px; border-bottom: 1px solid var(--line); white-space: nowrap; }
td { padding: 9px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
tr:hover td { background: var(--card); }
td.num { font-family: var(--mono); font-size: 13px; white-space: nowrap; }
td.when { font-family: var(--mono); font-size: 12px; color: var(--muted);
  white-space: nowrap; }
td.body { white-space: pre-wrap; font-family: var(--mono); font-size: 12.5px;
  line-height: 1.45; max-width: 620px; }
.scroll { overflow-x: auto; }

/* The headings of the per-state results tables: where the per-row state pill
   used to be, colored as the pill was. */
h2.part { margin: 24px 0 2px; font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; }
h2.part.p-upcoming { color: var(--future); }
h2.part.p-active { color: var(--active); }
h2.part.p-cancellation { color: var(--ended); }
h2.part.p-expired { color: var(--ended); }
h2.part.p-unknown { color: var(--muted); }

/* On a phone the results tables stack. Columns squeezed into 390px put the
   text -- the thing the reader came for -- off the right edge behind a
   scroll. Below this width the header row hides (the filters live in the
   toolbar, so it is only labels) and each row becomes a block: the
   identifying cells wrap onto one line, and the text follows at full width.
   A filter panel opens in place, pushing the toolbar apart, rather than
   floating -- a floated panel has no room to float into here. Only the NOTAM
   results tables do this; the reference tables (fetches, sweeps) are short
   rows of short values and survive scrolling. */
@media (max-width: 600px) {
  table.notams, table.notams tbody { display: block; }
  table.notams thead { display: none; }
  table.notams tr { display: flex; flex-wrap: wrap; align-items: baseline;
    gap: 2px 14px; padding: 10px 0; border-bottom: 1px solid var(--line); }
  table.notams th, table.notams td { display: block; border: 0; padding: 0; }
  table.notams tr:hover td { background: none; }
  table.notams td.body { flex: 1 1 100%; max-width: none; padding-top: 4px; }
  details.pop .panel, details.pop.right .panel { position: static;
    box-shadow: none; margin-top: 6px; min-width: 0; width: 100%; }
}

.pill { display: inline-block; font-size: 11px; font-weight: 600; padding: 1px 7px;
  border-radius: 999px; border: 1px solid currentColor; white-space: nowrap; }
.s-active { color: var(--active); }
.s-future { color: var(--future); }
.s-lapsed { color: var(--lapsed); }
.s-ended  { color: var(--ended); }
.tag { font-size: 11px; color: var(--muted); border: 1px solid var(--line);
  border-radius: 4px; padding: 1px 5px; }

.pager { display: flex; gap: 10px; align-items: center; margin: 18px 0;
  font-size: 13px; color: var(--muted); flex-wrap: wrap; }
.pager .nav { display: flex; gap: 6px; }
.pagebtn { display: inline-flex; align-items: center; justify-content: center;
  min-width: 32px; height: 28px; padding: 0 9px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--card); font-size: 14px;
  line-height: 1; }
a.pagebtn:hover { border-color: var(--accent); text-decoration: none; }
.pagebtn.off { color: var(--muted); background: transparent; opacity: 0.35; }
/* inline-flex above would defeat the hidden attribute (a UA default). */
.pagebtn[hidden] { display: none; }
.empty { padding: 40px 0; color: var(--muted); text-align: center; }

.crumb { font-size: 13px; color: var(--muted); margin: 20px 0 0; }
.crumb a { color: var(--muted); }
.crumb a:hover { color: var(--accent); }
form.filters { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end;
  margin: 18px 0; }
form.filters .field { display: flex; flex-direction: column; gap: 4px; }
form.filters label { font-size: 11px; color: var(--muted); font-weight: 600;
  letter-spacing: 0.04em; text-transform: uppercase; }
.sha { font-family: var(--mono); font-size: 12px; word-break: break-all; }
td.n { text-align: right; font-family: var(--mono); font-size: 13px;
  white-space: nowrap; }
th.n { text-align: right; }
dl.kv dd a { font-family: var(--mono); }
.derived { border-left: 3px solid var(--lapsed); }

.card { background: var(--card); border: 1px solid var(--line);
  border-radius: 10px; padding: 18px 20px; margin: 18px 0; }
.card h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted); margin: 0 0 12px; }
/* An endpoint path is case-sensitive; uppercasing it would document a URL
   that does not exist. */
.card h2.path { text-transform: none; letter-spacing: 0; font-family: var(--mono);
  font-size: 13.5px; color: var(--fg); }
/* The same NOTAM's other published number, on the heading of the text it is
   another rendering of. A name for this thing, not a link to another one. */
.card h2 .aka { margin-left: 12px; font-family: var(--mono); letter-spacing: 0; }
pre.notam { white-space: pre-wrap; font-family: var(--mono); font-size: 13px;
  margin: 0; line-height: 1.5; }
dl.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px;
  margin: 0; font-size: 13.5px; }
dl.kv dt { color: var(--muted); }
dl.kv dd { margin: 0; font-family: var(--mono); font-size: 13px; }
.grid2 { display: grid; gap: 18px; grid-template-columns: 1fr 1fr; }
@media (max-width: 800px) { .grid2 { grid-template-columns: 1fr; } }
h1.title { font-size: 22px; margin: 24px 0 4px; letter-spacing: -0.01em; }

/* The history of one NOTAM: one entry per update the FAA made. */
ol.history { list-style: none; margin: 0; padding: 0 0 0 20px;
  border-left: 2px solid var(--line); }
ol.history li { position: relative; padding: 0 0 20px 14px; }
ol.history li:last-child { padding-bottom: 0; }
ol.history li::before { content: ""; position: absolute; left: -25px; top: 5px;
  width: 9px; height: 9px; border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 3px var(--card); }
ol.history li.end::before { background: var(--ended); }
ol.history li.unwitnessed::before { background: var(--muted); }
ol.history .what { font-weight: 600; }
ol.history .clocks { font-family: var(--mono); font-size: 12px;
  color: var(--muted); margin-top: 2px; }
/* The date sits on the heading line, because an entry is now what happened and
   when, and those are one fact rather than two lines. */
ol.history .what .clocks { margin-left: 9px; font-weight: 400; }
ol.history .diff { margin: 7px 0 0; font-size: 12.5px; }
ol.history .diff td { padding: 3px 10px 3px 0; border: 0; vertical-align: top; }
ol.history .diff td.f { color: var(--muted); font-family: var(--mono);
  white-space: nowrap; }
ol.history .diff td.v { font-family: var(--mono); font-size: 12px;
  white-space: pre-wrap; max-width: 460px; }
.arrow { color: var(--muted); }
footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--line);
  font-size: 12px; color: var(--muted); }

/* ── the map ─────────────────────────────────────────────────────────────
   The server composes a tile grid larger than any screen, centered here and
   cropped by overflow, so one URL draws the same map everywhere. Every
   child of .plane is positioned from the *center* (left:calc(50% + Npx)),
   which is what lets the container's size not matter.

   The page owns the viewport (the shell is `bare`): a column in which the
   chart takes every pixel the listing and the timeline do not need, and
   nothing scrolls but the listing's own bar. */
body.bare { height: 100vh; height: 100dvh; display: flex;
  flex-direction: column; overflow: hidden; }
.mapfull { flex: 1; min-height: 0; display: flex; flex-direction: column;
  gap: 8px; padding: 8px; }
.canvas { position: relative; overflow: hidden; flex: 1; min-height: 120px;
  border: 1px solid var(--line); border-radius: 10px; background: var(--ground); }
.plane { position: absolute; inset: 0; }
.plane[hidden] { display: none; }
/* The live map (Leaflet) replaces the static grid when map.js runs. The
   explicit z-index makes it a stacking context, so Leaflet's tall internal
   pane z-indexes stay inside it instead of climbing over the controls. */
.livemap { position: absolute; inset: 0; z-index: 1; }
.canvas .leaflet-container { background: var(--ground); font: inherit; }
/* Leaflet's own stylesheet keeps two Safari-only workarounds from about 2014,
   and this map stands both of them down. They are the only rules on the page
   that apply to one engine, and the tiles that fail to paint here fail on one
   engine: the DOM says every tile is loaded and placed (a headless WebKit at
   this very viewport paints all of them, every time), while Safari leaves
   half the chart as bare ground -- which is a compositing fault, and these
   are the two rules that steer Safari's compositing.

   The first pins every tile container to a fixed 1600x1600 box, against
   hardware layers "stretching" while tiles load. The box is smaller than this
   chart: on a 1684-pixel canvas the tiles already reach x=1750 at rest, and
   during a zoom *out* the outgoing level is scaled to a half or a quarter, so
   its tiles reach two to four times past the box's edge. What every other
   browser gets is no size at all -- the container is an anchor for
   absolutely positioned tiles and needs none -- so that is what Safari gets.

   The second asks Safari for a sharpening resample of every tile. Nothing
   here wants it: the chart tiles are photographs of paper, drawn at the
   screen's own density, and the browser's ordinary smooth scaling is the
   better picture during a zoom's fractional scales -- which is exactly when
   the tiles go missing.

   Both selectors double up on `.livemap` because leaflet.css is linked
   *after* this file and would otherwise win the tie. */
.livemap.leaflet-safari .leaflet-tile-container { width: auto; height: auto; }
.livemap.leaflet-safari .leaflet-tile { image-rendering: auto; }
/* Leaflet positions a marker's anchor; the chip centers itself on it. */
.mkwrap { background: none; border: 0; }
.tile { position: absolute; width: 256px; height: 256px; border: 0; }
/* The weather overlay: a wash over the chart, never a coat of paint --
   the chart must stay readable through the echoes. `!important` on the live
   map's half: Leaflet writes `opacity: 1` *inline* on a tile layer's
   container as each tile fades in, and an inline style outranks any
   selector -- so without this the wash was a coat of paint. The look of the
   overlay stays one number in the stylesheet, the same discipline the
   boundaries keep. */
.tile.wx { opacity: 0.65; }
.leaflet-tile-pane .wx { opacity: 0.65 !important; }
/* Advisories: AIRMETs and SIGMETs, drawn under the restrictions. Hue
   says the hazard family -- hues the restriction palette does not use --
   and weight says the claim: a SIGMET heavier than an AIRMET, an outlook
   barely there, upcoming dashed like everything else. Fills stay near
   nothing: these polygons span states, and a wash that reads as emphasis
   on a TFR would read as paint here. */
.adv { fill-opacity: 0.05; stroke-width: 1.5; }
.adv:hover { fill-opacity: 0.12; }
.adv.sigmet { stroke-width: 2.5; }
.adv.outlook { stroke-width: 1.2; stroke-dasharray: 2 4; }
.adv.airmet { stroke-dasharray: 7 4; }
.adv.future { stroke-dasharray: 6 4; }
/* AIRMETs color by hazard... */
.adv.conv { stroke: var(--adv-conv); fill: var(--adv-conv); }
.adv.turb { stroke: var(--adv-turb); fill: var(--adv-turb); }
.adv.ice  { stroke: var(--adv-ice);  fill: var(--adv-ice); }
.adv.ifr  { stroke: var(--adv-ifr);  fill: var(--adv-ifr); }
.adv.mtn  { stroke: var(--adv-mtn);  fill: var(--adv-mtn); }
/* ...but the kind outranks the hazard (these rules stand later for that):
   every SIGMET is red whatever it warns of, every convective outlook
   yellow -- ForeFlight's assignment, kept verbatim. */
.adv.sigmet  { stroke: var(--adv-sigmet);  fill: var(--adv-sigmet); }
.adv.outlook { stroke: var(--adv-outlook); fill: var(--adv-outlook); }

/* Traffic. With script the icons are tar1090's own -- silhouettes carrying
   their fill (the altitude rainbow) and outline inline -- so the CSS only
   clears the divIcon chrome. The no-script dart keeps the hue-free ink
   look: it has no script to color it honestly by altitude. */
.tfcmk { fill: var(--fg); stroke: var(--card); stroke-width: 1.2; }
.tfcwrap { background: none; border: 0; }
/* The one aircraft being followed, in the track's own colour so the icon
   and the line it belongs to read as one thing. */
.tfcmk.sel { fill: var(--track); stroke: var(--card); stroke-width: 1.6; }
.tfcwrap.sel { filter: drop-shadow(0 0 3px var(--track)); }
/* The live icon's SVG carries its altitude color as a fill *attribute*,
   which any stylesheet rule outranks -- so the selected aeroplane trades
   the rainbow for the track's own colour here, accents (fill="none")
   left alone. */
.tfcwrap.sel path:not([fill="none"]) { fill: var(--track); }
/* A followed flight's whole recorded track. Solid is what had been flown by
   the instant on the strip; dashed is what had not -- a stretch the archive
   can draw only because it is looking at the flight from afterwards, and
   which must never read as though the aircraft had already been there. The
   dot is where it was at the instant, and exists only when the instant is
   inside the flight. */
.fltline { fill: none; stroke: var(--track); stroke-width: 2.5;
  stroke-linejoin: round; stroke-linecap: round; }
.fltline.future { stroke-dasharray: 7 6; stroke-width: 2.2; }
/* The casing under it, the way a chart draws a route over its own clutter:
   the same line in the page's own colour, wider. Without it a followed
   flight is a two-pixel thread over a screen that may hold a thousand
   markers, a wall of TFR red and three hundred aircraft -- the one thing
   the reader actually asked for, and the least visible thing on the map.
   Dashed where the line is dashed, or the gaps would fill and the whole
   track would read as already flown. */
.fltcase { fill: none; stroke: var(--card); stroke-width: 6.5; opacity: 0.75;
  stroke-linejoin: round; stroke-linecap: round; }
.fltcase.future { stroke-dasharray: 7 6; stroke-width: 6; }
.fltpos { fill: var(--track); stroke: var(--card); stroke-width: 2; }
.shapes { position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%); overflow: visible; }
/* The overlay's color says what KIND of claim a shape is: red for a
   published boundary -- restricted airspace, drawn the way the chart apps
   pilots already read draw it, with the TFR in the heaviest line -- and
   teal for a NOTAM's own point or radius claim. Upcoming boundaries take
   the scheduled orange AND the long dash, a cancellation notice the short
   gray one: hue follows the convention, but no state rides on hue alone. */
.shape { fill: var(--shape-point); fill-opacity: 0.09; stroke: var(--shape-point);
  stroke-width: 1.5; }
.shape:hover { fill-opacity: 0.18; }
.shape.future { stroke-dasharray: 6 4; }
.shape.cancellation { fill: none; stroke: var(--muted); stroke-dasharray: 2 4; }
.shape.tfr { fill: var(--restrict); stroke: var(--restrict);
  stroke-width: 3; fill-opacity: 0.16; }
.shape.tfr:hover { fill-opacity: 0.26; }
/* The bold claim is the stroke, not the fill: a text-defined area can be
   the size of a marine sanctuary, and a strong fill that reads as emphasis
   on a small TFR repaints the whole chart under a big one. */
.shape.area { fill: var(--restrict); stroke: var(--restrict);
  stroke-width: 2; fill-opacity: 0.06; }
.shape.area:hover { fill-opacity: 0.14; }
.shape.tfr.future, .shape.area.future { fill: var(--restrict-soon);
  stroke: var(--restrict-soon); }
/* A marker is a count, and a count is a link: it opens the place's sidebar. */
.mk { position: absolute; transform: translate(-50%, -50%); z-index: 5;
  min-width: 19px; height: 19px; padding: 0 5px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--accent-fg); font-size: 10.5px;
  font-weight: 600; line-height: 1; border: 1px solid var(--card);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); }
.mk:hover { text-decoration: none; filter: brightness(1.12); z-index: 7; }
.mk.mid { min-width: 24px; height: 24px; font-size: 11.5px; z-index: 6; }
.mk.big { min-width: 30px; height: 30px; font-size: 12.5px; z-index: 6; }
.mk.sel { outline: 2px solid var(--fg); outline-offset: 1px; z-index: 8; }
/* The overlaid controls: every one a plain link, because the page must pan
   and zoom before any script loads. */
.mapctl { position: absolute; z-index: 10; display: flex; gap: 4px;
  font-size: 13px; }
/* Direct children only: the search control nests a whole form (with rows,
   labels and a visually-hidden submit) that must not inherit chip looks. */
.mapctl > a, .mapctl > span, .mapctl > button { display: grid;
  place-items: center; min-width: 28px; height: 28px; padding: 0 7px;
  background: var(--card); border: 1px solid var(--line); border-radius: 6px;
  font-weight: 600; }
.mapctl > span { color: var(--muted); opacity: 0.6; }
.mapctl > a:hover, .mapctl > button:hover { border-color: var(--accent);
  text-decoration: none; }
.mapctl > button { cursor: pointer; font-size: 14px; }
/* `display: grid`/`flex` above would silently defeat the `hidden`
   attribute -- it is only a UA default -- and two controls rely on it:
   the geolocation button until script reveals it, and the search control
   while script has it put away behind "/". */
.mapctl > button[hidden] { display: none; }
.mapctl[hidden] { display: none; }
/* The position button holds the top-left corner, on the layers chip's
   line across the top edge. */
.geoctl { left: 10px; top: 10px; }
.zoomctl { left: 10px; top: 48px; flex-direction: column; }
.panctl { left: 52px; top: 48px; display: grid; gap: 4px;
  grid-template-columns: repeat(3, 28px); grid-template-rows: repeat(3, 28px); }
.panctl .pn { grid-area: 1 / 2; } .panctl .pw { grid-area: 2 / 1; }
.panctl .pe { grid-area: 2 / 3; } .panctl .ps { grid-area: 3 / 2; }
/* The layer picker: one chip that opens the stack, top layer first. The
   rows in a group are alternatives (the checkmark and the filled row say
   which is on); one row of each group may draw, stacked in the order the
   groups are listed. */
.layersctl { right: 10px; top: 10px; display: block; }
.layersctl > summary { list-style: none; cursor: pointer; display: grid;
  place-items: center; height: 28px; min-width: 28px; padding: 0 7px;
  background: var(--card); border: 1px solid var(--line); border-radius: 6px;
  font-size: 16px; }
.layersctl > summary::-webkit-details-marker { display: none; }
.layersctl > summary:hover, .layersctl[open] > summary {
  border-color: var(--accent); color: var(--accent); }
.lpanel { position: absolute; z-index: 30; right: 0; top: calc(100% + 4px);
  min-width: 230px; padding: 6px; background: var(--card);
  border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16); }
.lgroup + .lgroup { border-top: 1px solid var(--line); margin-top: 6px;
  padding-top: 4px; }
.lgroup h3 { margin: 4px 8px 2px; font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted); font-weight: 600; }
.lrow { display: grid; grid-template-columns: 16px 1fr; column-gap: 6px;
  align-items: baseline; padding: 5px 8px; border-radius: 6px;
  color: var(--fg); }
.lrow:hover { background: var(--accent-soft); text-decoration: none; }
.lrow.on { color: var(--accent); background: var(--accent-soft);
  font-weight: 600; }
.lmark { text-align: center; color: var(--accent); }
.lrow .hint { grid-column: 2; font-weight: 400; }
/* The search box, bottom-left so the list opens upward over the map. A real
   form (place with no ll = center on it); map.js adds the matching list. */
.searchctl { left: 10px; bottom: 10px; }
.searchctl .sbox { display: block; position: relative; width: 210px;
  padding: 0; border: 0; background: none; min-width: 0; height: auto; }
.searchctl input { width: 100%; font-size: 13px; padding: 6px 9px;
  background: var(--card); }
/* Wider than the box it hangs off, because a flight's row is three
   identifiers and a span -- "SWA3016 · N8720L · B38M  08-19 14:09–15:13Z" --
   and a list that ellipsises the type off every one of them is a list that
   cannot be chosen from. (The touch layout below sets its own edges.) */
.searchctl ul {
  position: absolute; z-index: 30; left: 0; right: -160px;
  bottom: calc(100% + 4px); margin: 0; padding: 4px; list-style: none;
  max-height: 240px; overflow-y: auto; background: var(--card);
  border: 1px solid var(--line); border-radius: 7px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14); }
.searchctl li { display: flex; align-items: baseline; gap: 8px;
  padding: 4px 8px; border-radius: 5px; cursor: pointer; font-size: 12.5px;
  white-space: nowrap; font-weight: 400; }
.searchctl li[aria-selected=true] { background: var(--accent-soft);
  color: var(--accent); }
.searchctl li .nm { overflow: hidden; text-overflow: ellipsis; }
.searchctl li .off { color: var(--muted); font-family: var(--mono);
  font-size: 11px; margin-left: auto; flex: none; }
/* A flight row wears the track's colour, so the list says which of the two
   things the box finds a row is before it is chosen. */
.searchctl li.flt .nm { color: var(--track); }
.searchctl li.none { cursor: default; color: var(--muted); font-size: 12px; }
.searchctl li.none[aria-selected=true] { background: transparent;
  color: var(--muted); }
/* Which flight is being followed, above the "not drawn" badge on the same
   edge: both are chips that say what the picture is doing that the picture
   cannot say about itself. */
.flightctl { right: 10px; bottom: 44px; font-size: 12px;
  max-width: min(340px, calc(100% - 100px)); }
.flightctl .fname { color: var(--track); border-color: var(--track);
  font-weight: 600; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; display: block; line-height: 26px; opacity: 1; }
.flightctl .fname.gone { color: var(--muted); border-color: var(--line); }
.flightctl a { color: var(--muted); font-size: 15px; }
.unplacedctl { right: 10px; bottom: 10px; font-size: 12px; }
.unplacedctl a, .unplacedctl span { font-weight: 600; }
.unplacedctl a { color: var(--lapsed); border-color: var(--lapsed); }
/* Honesty that used to live in the page's margins rides on the chart:
   the coverage banner floats top-center. The status prose (whose chart,
   which frame, what a cap trimmed) is the Layers chip's hover title, and
   the bottom-center belongs to the weather legend when weather is on --
   its colors sampled from the archived mosaics' own palettes. */
.canvas .banner.floats { position: absolute; z-index: 12; top: 10px;
  left: 50%; transform: translateX(-50%); margin: 0;
  max-width: min(620px, calc(100% - 300px)); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); }
.legendctl { left: 50%; bottom: 10px; transform: translateX(-50%);
  z-index: 9; flex-direction: column; gap: 2px; align-items: stretch;
  width: min(320px, calc(100% - 320px)); padding: 5px 10px 6px;
  background: color-mix(in srgb, var(--card) 85%, transparent);
  border-radius: 7px; }
.legendctl .ltitle { font-size: 10.5px; font-weight: 600;
  color: var(--muted); text-align: center; }
.legendctl .lbar { height: 8px; border-radius: 3px;
  border: 1px solid var(--line); }
.legendctl .lticks { display: flex; justify-content: space-between;
  font-size: 10px; color: var(--muted);
  font-variant-numeric: tabular-nums; }
/* Two states the pixels cannot state for themselves. `lstale`: the frame is
   the best the archive has for the instant and is measurably older than it.
   `lgap`: there is no frame at all, which on a weather layer is otherwise
   indistinguishable from calm air -- so it takes the legend's place and
   keeps its size, because a box that says nothing is drawn must be as
   visible as the drawing would have been. */
.legendctl.lstale { border: 1px solid var(--lapsed); }
.legendctl .lwarn { font-size: 10px; font-weight: 600; text-align: center;
  color: var(--lapsed); }
.legendctl.lgap { border: 1px solid var(--ended);
  background: color-mix(in srgb, var(--card) 92%, var(--ended)); }
.legendctl.lgap .ltitle { color: var(--ended); font-variant: small-caps;
  letter-spacing: 0.04em; font-size: 11.5px; }
/* The hatching that stands for a frame there is none of. Diagonal red
   stripes over the whole viewport, at an alpha that leaves the chart legible
   under them -- the point is that the reader cannot mistake this screen for
   a quiet sky, not that the map goes away. Under the markers (z-index 5) and
   over the tiles, which is where the missing frame itself would have been,
   and never in the way of a click. */
.wxgap { position: absolute; inset: 0; z-index: 4; pointer-events: none;
  background: repeating-linear-gradient(45deg,
    color-mix(in srgb, var(--ended) 26%, transparent) 0 9px,
    transparent 9px 22px); }
@media (max-width: 900px) {
  .canvas .banner.floats { max-width: calc(100% - 20px); }
  .legendctl { width: min(320px, calc(100% - 170px)); }
}
/* A touch screen with the live map up (`.live`: the pan and zoom links
   are gone, freeing the top row): the search box rides the top of the
   chart instead of a band of its own at the bottom -- overlay, not offset
   -- beside the geolocation chip and clear of the on-screen keyboard,
   which opens over the bottom half at exactly the moment the box is in
   use. The match list drops downward under it. Without script
   nothing here applies: the click-controls still hold the top row, and
   the box keeps the bottom. */
@media (hover: none), (pointer: coarse) {
  .canvas.live .searchctl { top: 48px; left: 48px; right: 10px; bottom: auto; }
  .canvas.live .searchctl .sbox { width: auto; }
  .canvas.live .searchctl ul { top: calc(100% + 4px); bottom: auto; right: 0; }

}

/* The timeline: one row -- window controls, the scrubber, `Now` at the end
   -- and nothing underneath it. Bands are sweep gaps (the error bars of any
   answer inside them), the wall is where the reach's floor is, the ticks
   are the scale, and the bubble above the thumb is the instant itself. */
.timeline { border: 1px solid var(--line); border-radius: 10px;
  background: var(--card); padding: 8px 12px; flex: none; }
.tlrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tlwin { display: flex; gap: 4px; }
/* "Earlier" at the floor: dimmed and inert, rather than a control that
   answers a click with the window it already shows. */
.tlwin .pagebtn.off { pointer-events: none; }
/* Play and speed are the script's own controls, wearing the window
   buttons' chip; `.on` is playing, and says so in the active color. */
button.pagebtn { cursor: pointer; font: inherit; font-size: 14px; }
button.pagebtn:hover { border-color: var(--accent); }
#tlplay.on { border-color: var(--active); color: var(--active);
  background: var(--accent-soft); }
#tlspeed { font-family: var(--mono); font-size: 11.5px;
  font-variant-numeric: tabular-nums; }
/* The scrubber. Taller than a decorative strip, because it is the control
   the whole page is read through: a grab target first, a picture second.
   Overflow stays visible for the bubble's sake -- everything drawn inside
   the strip is clamped to it by arithmetic instead. */
.track { position: relative; flex: 1 1 220px; height: 32px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  min-width: 140px;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.18); }
/* The scale: minor ticks as stubs, majors taller and wearing the clock. */
.tick { position: absolute; bottom: 0; width: 1px; height: 7px;
  background: var(--line); pointer-events: none; }
.tick.maj { height: 12px; background: var(--muted); }
.tick.maj > span { position: absolute; bottom: 13px; left: 0;
  transform: translateX(-50%); font-family: var(--mono); font-size: 9px;
  color: var(--muted); letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; }
/* The legend the ticks are read by: what one of them is. */
.tlstep { font-family: var(--mono); font-size: 11px; color: var(--muted);
  white-space: nowrap; }
.gapband { position: absolute; top: 0; bottom: 0; opacity: 0.55;
  background: repeating-linear-gradient(135deg, var(--lapsed) 0 2px,
    transparent 2px 6px); }
/* The floor: the strip stops here, so the mark is a wall, not a tick. */
.tstart { position: absolute; top: 0; bottom: 0; width: 2px;
  background: var(--muted); }
.thumb { position: absolute; top: 0; bottom: 0; width: 3px;
  background: var(--accent); transform: translateX(-50%); z-index: 2; }
/* The grab handle: what says this line is draggable before anyone drags it. */
.thumb::after { content: ""; position: absolute; left: 50%; top: 50%;
  width: 11px; height: 11px; transform: translate(-50%, -50%) rotate(45deg);
  background: var(--accent); border-radius: 2px; }
.thumb.live, .thumb.live::after { background: var(--active); }
.thumb.off { width: auto; background: none; transform: none; padding: 0 3px;
  color: var(--accent); font-size: 12px; line-height: 30px; }
.thumb.off::after { content: none; }
.thumb.off.live { color: var(--active); }
/* The instant, in a bubble above the strip, centered on the thumb. Its
   `left` is clamped (in the markup and in map.js alike) so it never leaves
   the bar; over the map's own bottom controls it wins, because it exists
   exactly while the reader is looking at it. */
.thumblab { position: absolute; bottom: calc(100% + 7px); z-index: 20;
  transform: translateX(-50%); pointer-events: none;
  font-family: var(--mono); font-size: 11px; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  padding: 2px 6px; border-radius: 5px; color: var(--fg);
  background: var(--card); border: 1px solid var(--accent);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); }
.thumblab.live { border-color: var(--active); color: var(--active); }
.tlform { display: flex; gap: 6px; align-items: center; }
/* `Now` wears the map controls' chip exactly -- same box as the button that
   jumps to your position, because it is the same kind of promise about the
   other axis: put me where I actually am. */
.tlform button.now { display: grid; place-items: center; height: 28px;
  min-width: 28px; padding: 0 7px; font-size: 14px; font-weight: 600;
  background: var(--card); border: 1px solid var(--line); border-radius: 6px;
  cursor: pointer; white-space: nowrap; }
.tlform button.now:hover { border-color: var(--accent); }
.tlform button.now[aria-pressed=true] { border-color: var(--active);
  color: var(--active); background: var(--accent-soft); }
/* On a phone the strip takes its own full-width line -- squeezed between
   the buttons and the legend it was too small to read, let alone (once
   map.js makes it one) to drag -- and grows into a finger-sized target. */
@media (max-width: 700px) {
  .tlrow { row-gap: 6px; }
  .track { order: -1; flex: 1 1 100%; height: 36px; }
  .thumb.off { line-height: 34px; }
  .tlwin { order: 1; }
  .tlstep { order: 2; }
  .tlform { order: 3; margin-left: auto; }
}

/* The listing: one place's whole answer as a bar above the timeline, with
   the only scrollbar on the page. */
.bottombar { flex: none; max-height: 40dvh; overflow-y: auto;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--card); padding: 10px 16px 14px; }
.sidehead { display: flex; align-items: baseline; gap: 10px;
  position: sticky; top: -10px; background: var(--card); z-index: 3;
  padding: 10px 0 6px; margin-top: -10px; }
.sidehead h2 { margin: 0; font-size: 16px; }
.sidehead .close { margin-left: auto; font-size: 20px; line-height: 1;
  color: var(--muted); }
.sidehead .close:hover { color: var(--fg); text-decoration: none; }
.bottombar h2.part { margin: 12px 0 2px; }
.bottombar .pager { margin: 8px 0; }
.bottombar td.body { max-width: none; font-size: 12px; }
.bottombar table { font-size: 12.5px; }
.bottombar td, .bottombar th { padding: 5px 8px; }
/* Collapsible partitions (the map's listing folds Upcoming by default).
   The summary is the partition heading plus its count, because folding may
   hide rows but never the fact of them. */
details.partwrap > summary { list-style: none; cursor: pointer; }
details.partwrap > summary::-webkit-details-marker { display: none; }
details.partwrap > summary h2.part { display: inline-block; }
details.partwrap > summary h2.part::before { content: "\25B8  "; }
details.partwrap[open] > summary h2.part::before { content: "\25BE  "; }
details.partwrap > summary .count { color: var(--muted); font-weight: 600;
  margin-left: 6px; }
