/* ASRouteMap 2.0 — the three-bar map shell (M-07 T1, SPEC §13, ADR-0016).
 *
 * Layout principle: the map is fixed full-viewport and the chrome floats
 * over it. The prototype sized the map by subtraction
 * (`#map { height: calc(100% - 30px) }` against a percentage chain
 * starting at the left bar's 100vh), which is why its map collapsed
 * whenever a bar height changed. Nothing here is sized by subtraction: the
 * map owns the whole viewport, and bars overlay it.
 *
 * z-index ladder (one place, so nothing has to win a specificity fight):
 *   map            0
 *   tz band     1500   (inside .asrm-stage — see the note below)
 *   rails       2000
 *   corner      2100   (the bottom-left column, above an open rail panel)
 *   hover tip   2200   (above every island: it follows the cursor)
 *   tile error  2500   (must beat rails — it explains why the map is blank)
 *   drawer      3000
 *   pop-over    3100   (top-most: it is anchored to the thing you clicked)
 *
 * The hover tip's rung is the owner's 2026-10-06 report: at 20 it lost to
 * every island in this list, so a tooltip near the bottom-right corner
 * vanished under the lat/lon readout and the scale bar. It is transient,
 * pointer-events: none and one line tall — nothing on the ladder needs to
 * lose to it except the chrome it was disappearing under.
 *
 * The tz band's rung only means something because the band is appended
 * *inside* `.asrm-stage`. `position: fixed` starts a stacking context
 * whatever its z-index, so while the bands were children of `<body>` at
 * 1500 the whole stage painted at its own `auto` level and the band lay
 * over the zoom, lat/lon and credit islands — whose 2000 was trapped
 * inside the stage and could not reach out. Raising the islands' number
 * could never have fixed it. Confirmed by pixel, not by reading: magenta
 * islands against a green band, sampled in the overlap — bands in `<body>`
 * sample green (band on top), bands in the stage sample magenta.
 *
 * The pop-over is one rung **above** the drawer even though `shell.js`
 * closes the drawer whenever a pop-over opens. The ladder is not a
 * statement of intent, it is the tie-break for whatever the JS misses: if
 * a drawer close is mid-flight, or a later code path lets both stand, the
 * more recently opened surface wins instead of being buried under a
 * full-width panel. One rung, not a hundred — enough to order them, not
 * enough that nothing can ever be raised above it again.
 */

/* The map is the full stage; base.html's article `main { max-width: 54rem }`
 * must not leak in here. `inset: 0` plus an explicit dvh height rather than
 * `inset: 0` alone: on mobile, `fixed` resolves against the *large* viewport
 * while the visual viewport shrinks under the URL bar, and dvh tracks the
 * visible part. The prototype used 100vh here and its map was permanently
 * taller than the screen on a phone. */
.asrm-stage {
  position: fixed;
  inset: 0;
  height: 100dvh;
  max-width: none;
  margin: 0;
  padding: 0;
}

.asrm-map {
  position: absolute;
  inset: 0;
  background: #0b0e13;
}

.asrm-shell {
  font-family: system-ui, -apple-system, sans-serif;
  color: var(--asrm-fg);
}

/* Swap hooks for the no-reload model (M-07 T2). `display: contents` means
 * the wrapper generates no box at all, so its children position against
 * .asrm-shell exactly as they did unwrapped — the hook cannot move the UI. */
.asrm-region {
  display: contents;
}

/* --- rails ---------------------------------------------------------- */
.asrm-rail {
  position: fixed;
  top: 0;
  bottom: var(--asrm-statusbar-h);
  z-index: 2000;
  background: var(--asrm-bar-bg);
  overflow: hidden;
}

.asrm-rail--left {
  left: 0;
  width: var(--asrm-rail-w);
  transition: width var(--asrm-rail-transition);
  border-right: 1px solid var(--asrm-line);
  /* Flex column rather than one scrolling box, so the icon strip stays at the
   * top, the open section scrolls in the middle, and the site nav is pinned
   * at the bottom. The nav sitting *above* the bottom of the rail — "the
   * other part is not bottom aligned" — was a consequence of it being the
   * last item in a single scroll flow: where it landed depended on how long
   * the list above it happened to be. */
  display: flex;
  flex-direction: column;
}

/* Hover / keyboard-focus reveal — **the JavaScript-off path only**.
 *
 * `html.asrm-js` is set by shell.js before the first paint it touches.
 * While it is absent these rules are the whole interaction, which is what
 * keeps the rail usable with scripting disabled: no handler, no state, just
 * CSS. Once JS *is* running, hover must stop opening the rail, because the
 * strip's buttons now own that decision — a rail that opens on hover and
 * also opens on click fights itself every time the pointer crosses a
 * section nobody asked for, and on a touchscreen hover is a lie anyway. */
html:not(.asrm-js) .asrm-rail--left:hover,
html:not(.asrm-js) .asrm-rail--left:focus-within {
  width: var(--asrm-rail-w-open);
}

/* The collapsed strip: one column of icons, always rendered, the only
 * thing visible at 44px. Labels are in the DOM and clipped by the rail's
 * width rather than `display:none`, so no-JS hover reveals them exactly as
 * it always did and the two paths cannot disagree about what is there. */
.asrm-rail__strip {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px;
  border-bottom: 1px solid var(--asrm-line);
}

.asrm-rail__tab {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  min-height: 36px;
  padding: 0 0.5rem;
  background: none;
  border: 0;
  border-radius: 4px;
  color: var(--asrm-fg-muted);
  font: inherit;
  font-size: var(--asrm-fs-rail);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  cursor: pointer;
}

.asrm-rail__tab:hover {
  background: var(--asrm-panel-bg-raised);
  color: var(--asrm-fg);
}

.asrm-rail__tab[aria-expanded="true"] {
  color: var(--asrm-accent);
  background: var(--asrm-panel-bg);
}

.asrm-rail__tab:focus-visible {
  outline: 2px solid var(--asrm-accent);
  outline-offset: 1px;
}

/* --- opened section (M-07-T4) ---------------------------------------- */
/* The open state is an attribute on `[data-region="left"]`, which is
 * **outside** the swapped subtree. `applyScope` replaces the region's
 * `innerHTML`, so a class on the rail, the panel or the button is gone
 * the moment the scope changes — and a rail that closes itself every time
 * you click an airline is a rail whose open button does nothing useful.
 * The region wrapper survives every swap, so the state does too. */
.asrm-js [data-region="left"][data-open-panel] .asrm-rail--left {
  width: var(--asrm-rail-w-open);
}

.asrm-js .asrm-rail--left .asrm-panel {
  display: none;
}

.asrm-js [data-region="left"][data-open-panel="world"] .asrm-panel[data-panel="world"],
.asrm-js [data-region="left"][data-open-panel="player"] .asrm-panel[data-panel="player"],
.asrm-js [data-region="left"][data-open-panel="airlines"] .asrm-panel[data-panel="airlines"],
.asrm-js [data-region="left"][data-open-panel="hub"] .asrm-panel[data-panel="hub"] {
  display: block;
}

/* **Collapsed means no glyphs at all** (owner, M-07-T5: "When the menu is
 * collapsed, half the first letter of the menu is still visible. Is it
 * possible to hide the text completely?").
 *
 * It was clip doing the hiding, not intent. At 44px the tab's padding and
 * the icon's own advance put the first letter's left edge at ~32px, so a
 * screenful of `W` `P` `A` stuck out past the clip — the bar reads as
 * noise rather than as three icons, and the letters tease toward a menu
 * that is not showing.
 *
 * `display:none`, not `visibility:hidden` or `opacity:0`. Those two keep
 * the box in the layout, so the label still occupies its width inside the
 * tab and the tab's own `overflow:hidden` has to keep doing the work —
 * i.e. they fix the symptom only as long as the clip stays narrower than
 * the text, which is a property of the label length rather than of the
 * design. `display:none` removes the question.
 *
 * **Why this is safe for screen readers:** the rule is only reachable with
 * `.asrm-js` on `<html>` *and* nothing open, and every button and link it
 * empties now carries its own explicit `aria-label` in
 * `_left_rail.html` / `base.html`. The visible string is decoration for
 * the sighted path; the name no longer depends on it. In the JavaScript-off
 * path the rule cannot match at all, so the no-JS rail still reveals every
 * label on hover exactly as before — the two paths cannot disagree,
 * because neither can reach the other's mechanism. */
.asrm-js [data-region="left"]:not([data-open-panel]) .asrm-rail__tablabel,
.asrm-js [data-region="left"]:not([data-open-panel]) .asrm-rail__navlabel {
  display: none;
}

.asrm-rail__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0.4rem 0;
  scrollbar-width: thin;
  scrollbar-color: var(--asrm-line) transparent;
}

.asrm-rail--right {
  right: var(--asrm-edge);
  top: var(--asrm-edge);
  bottom: auto;
  width: var(--asrm-rail-right-w);
  border: 1px solid var(--asrm-line);
  border-radius: 6px;
}

.asrm-rail__stack {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 2px;
}

/* ------------------------------------------------------------------ *
 * Refresh island (ADR-0052) — count / countdown / full screen, docked
 * below the layer rail on the right.
 *
 * **`display: none` until `shell.js` adds `.is-placed`, and that is not
 * a loading animation.** The island is registered in `MAP_COVERS`, so
 * its rectangle is subtracted from the area whose aircraft count as
 * "in view". A box that has not been positioned yet is a box in the
 * wrong place, and the wrong place here would silently hide real
 * aircraft from the count. The island therefore claims cover only after
 * it has been measured and moved to where it belongs. Without
 * JavaScript it never becomes visible: every value in it needs a fetch
 * and the full-screen button needs the API, so there is nothing to show.
 *
 * **Positioned by measurement, not by arithmetic on the rail's height.**
 * `.asrm-rail--right` is `top: var(--asrm-edge); bottom: auto`, so its
 * height is whatever its contents make it — today six controls, ~216 px.
 * `shell.js` reads that rail's `getBoundingClientRect().bottom` and
 * writes `top` from it, so adding or removing a layer button moves the
 * island with no CSS change and no stale magic number left behind. This
 * is the same "measure the box" rule that made the cover rects read
 * `getBoundingClientRect()` instead of trusting `--asrm-rail-right-w`.
 *
 * The *gap* stays here in CSS (`margin-top`), because the gap is a
 * styling choice a restyle should be able to change, while `top` is a
 * measured fact. JS contributes the fact; CSS contributes the taste.
 * ------------------------------------------------------------------ */
.asrm-refresh-island {
  display: none;
}

.asrm-refresh-island.is-placed {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  /* 3px, and the number is the whole reason: 34 (the full-screen button,
     the widest thing in here) + 3 + 3 padding + 1 + 1 border = 42, which
     is exactly `.asrm-rail--right`'s rendered outer width — 40px
     content-box plus its own 1px border on each side. With 4px here the
     island came out 44px against a 42px rail and stuck out 2px before any
     text was drawn; the count on top of that made it the ~4px the owner
     reported on 2026-10-02. `check-refresh-island.mjs` asserts that sum
     from the shipped CSS, so changing any one of the four numbers without
     the others fails a check rather than silently un-aligning the column. */
  padding: 3px;
  box-sizing: border-box;
  position: fixed;
  right: var(--asrm-edge);
  /* Overwritten by shell.js from the measured bottom of the layer rail.
     This value only applies in the instant before that write. */
  top: var(--asrm-edge);
  margin-top: var(--asrm-edge);
  width: auto;
  min-width: var(--asrm-rail-right-outer);
  /* Only a guard now, not the thing that sizes the card. The countdown
     face was a whole sentence ("update failed — retrying in 240s") until
     2026-10-01, and that string, not the aircraft count, was what made
     this island read as several times wider than the rail above it.
     What actually holds the card to the rail's width is not this
     `min-width` but the 34px full-screen button plus the 3px padding
     and 1px border around it — see the padding note above — and that
     sum is what `check-refresh-island.mjs` pins. `min-width` is the
     backstop for the case where that button goes away, and `max-width`
     here is only a viewport guard. */
  max-width: min(13rem, calc(100vw - 2 * var(--asrm-edge)));
  /* Same rung as the rails, not a new one: the island is chrome of the
     same class as a rail. Staying at 2000 keeps it under the tile-error
     notice (2500), the drawer (3000) and the pop-over (3100), all of
     which must still be able to cover it. */
  z-index: 2000;
  background: var(--asrm-bar-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 6px;
  color: var(--asrm-fg);
  font-size: var(--asrm-fs-caption);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.asrm-refresh-island__count {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  /* No horizontal padding: the 42px column leaves 34px inside the
     island's own padding and border, and that is the budget for the
     widest count. 4px on each side spent 8 of those 34 on nothing and
     pushed a six-figure `128K` through the edge of the card. */
  padding: 2px 0;
  cursor: help;
}

.asrm-refresh-island__num {
  /* `--asrm-fs-body`, not `--asrm-fs-value`. The owner's read on
     2026-10-02 was that this glyph was "a bit too big", and it is also
     the load-bearing number in the width sum above: at 15.2px a
     three-glyph count measures ~32px against a 34px budget, which only
     fits by luck and loses to any wider UI font; at 12.8px it is
     ~27px, leaving room on every font this was measured against. The
     count stays the loudest thing in the island because it is the only
     semibold text in it, not because it is the largest. */
  font-size: var(--asrm-fs-body);
  line-height: 1.1;
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* The countdown row. `min-height` holds the row at the height of one
 * line even while it reads only a dash, so the full-screen button below
 * does not jump when the text grows to "update failed — retrying in 240s"
 * wrapping differently. */
.asrm-refresh-island__countdown {
  padding: 2px 4px;
  min-height: calc(var(--asrm-fs-caption) * 1.35);
  text-align: center;
  color: var(--asrm-fg-muted);
  border-top: 1px solid var(--asrm-line);
  border-bottom: 1px solid var(--asrm-line);
}

.asrm-refresh-island__full {
  align-self: center;
}

/* ------------------------------------------------------------------ *
 * Corner chrome (M-07-T9, ADR-0056): the zoom island and the cursor
 * readout on the right, the scale-bar-and-legend column on the left.
 *
 * All four are `position: fixed` over the map and all four are hidden by
 * the `hidden` attribute until `shell.js` has both a map to read and a
 * measured place to put them — the same contract the refresh island
 * follows, for the same reason: an unplaced box in `MAP_COVERS` would
 * subtract map area that is plainly on screen.
 *
 * **The right-hand pair is placed by measurement, not by tokens.** The
 * zoom island docks above MapLibre's attribution summary button, whose
 * size and inset are MapLibre's, not ours; the cursor island docks left
 * of the zoom island, bottom-aligned. `positionMapIslands()` in shell.js
 * writes `right`/`bottom` from `getBoundingClientRect()`; CSS here only
 * supplies what measurement cannot — the look, and the width sum.
 * ------------------------------------------------------------------ */
.asrm-zoom-island {
  position: fixed;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* The refresh island's recipe, repeated: 34 (the button, the widest
   * thing in here) + 3 + 3 padding + 1 + 1 border = 42, which is exactly
   * the rendered outer width of the two islands on the top right and of
   * the layer rail they dock under. `check-refresh-island.mjs` asserts
   * this sum from the shipped CSS for both islands. */
  padding: 3px;
  box-sizing: border-box;
  width: auto;
  min-width: var(--asrm-rail-right-outer);
  background: var(--asrm-bar-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 6px;
  color: var(--asrm-fg);
  font-variant-numeric: tabular-nums;
}

.asrm-zoom-island[hidden],
.asrm-cursor-island[hidden],
.asrm-map-corner-left[hidden] {
  display: none;
}

.asrm-zoom-island__btn {
  width: 34px;
  height: 20px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 4px;
  color: var(--asrm-fg);
  font: inherit;
  font-size: var(--asrm-fs-body);
  line-height: 20px;
  text-align: center;
  cursor: pointer;
}

.asrm-zoom-island__btn:hover {
  background: var(--asrm-panel-bg-raised);
}

.asrm-zoom-island__btn:focus-visible {
  outline: 2px solid var(--asrm-accent);
  outline-offset: 1px;
}

/* The live zoom, one decimal, no word in front of it: the island *is* the
 * zoom readout, so `zoom 3.5` would label a label. Hairlines above and
 * below rather than gaps so the three rows read as one instrument. */
.asrm-zoom-island__readout {
  padding: 2px 0;
  min-height: calc(var(--asrm-fs-body) * 1.35);
  text-align: center;
  font-size: var(--asrm-fs-body);
  border-top: 1px solid var(--asrm-line);
  border-bottom: 1px solid var(--asrm-line);
}

/* Where the cursor is. Right-aligned because the three lines are of
 * different lengths and the owner's sketch aligns their hemispheres, not
 * their left edges; `tabular-nums` so a moving cursor does not make the
 * column shimmer. */
.asrm-cursor-island {
  position: fixed;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  padding: 4px 7px;
  background: var(--asrm-bar-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 6px;
  color: var(--asrm-fg);
  font-size: var(--asrm-fs-micro);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* Readout, not control: a cursor island that eats the drag under it
   * would make the map feel broken in the one place every pan ends. */
  pointer-events: none;
}

.asrm-cursor-island__line {
  line-height: 1.3;
}

/* No cursor, no readout (owner ruling of 2026-10-03: *"…and hide it on
 * touch devices"*). A touch screen has nothing that rests over a
 * coordinate — the box would freeze on the last tap point and keep
 * showing it as though it were live, which is a stale claim wearing a
 * readout's clothes. The whole island goes rather than only the decimal
 * line, because the degrees-and-minutes lines answer the same question a
 * finger cannot ask.
 *
 * `(hover: none)` asks about the **primary** pointer, so a touchscreen
 * laptop with a mouse attached keeps its readout; a phone or a tablet
 * does not.
 *
 * Source order is load-bearing: a media query adds no specificity, so
 * this must come after the base `.asrm-cursor-island` block above to win
 * the `display` cascade — `check-visible-region.mjs` pins the order, not
 * just the presence.
 *
 * The counting follows for free. `mapCoverRects()` in shell.js drops any
 * cover that measures zero and `display: none` measures zero, so on a
 * phone the island stops subtracting aircraft from the in-view count in
 * the same tick it stops being painted. */
@media (hover: none) {
  .asrm-cursor-island {
    display: none;
  }
}

/* The bottom-left column: scale bar above the status bar, clear of the
 * left rail by one edge unit, free-slot legend above it with a small gap.
 * One fixed box so the gap is a flex `gap` and the pair moves as one
 * column. It is **not itself** a `MAP_COVERS` entry — the two children are,
 * each separately, because only they paint. See `MAP_COVERS` in shell.js.
 *
 * **Docked to the rail, not to the screen.** First shipped at `left: 0` —
 * the "true corner" of the 2026-10-02 ruling — and the owner's first look
 * was that a box half-under the menu bar "obviously doesn't make sense":
 * flush means flush against the chrome it sits beside, not against a
 * screen edge another element owns. So `left` is the rail's width in both
 * rail states — closed and open (`data-left-open`, mirrored by
 * `setRailPanel`) — plus the 10px unit that keeps it off the rail's edge
 * the way the right-hand islands keep off theirs, with the rail's own
 * transition so the column rides the rail's animation instead of jumping
 * ahead of it. The rail's site-nav icons are uncovered again.
 *
 * z-index one rung above the rails (2000) because an open panel is opaque
 * and full-height: a column that lost the stacking fight would vanish
 * behind the very menu it docks to. */
.asrm-map-corner-left {
  position: fixed;
  /* Rail width plus one `--asrm-edge`, so the column keeps the same 10px
   * of clear air off the rail that every island on the right keeps off the
   * viewport edge and off the layer rail above it (`--asrm-edge` on
   * `.asrm-rail--right`, `margin-top: var(--asrm-edge)` on the refresh
   * island). Before this the column was hard against the rail at 0px while
   * its mirror side floated at 10px, which read as the box being attached
   * to the menu rather than parked beside it. */
  left: calc(var(--asrm-rail-w) + var(--asrm-edge));
  bottom: var(--asrm-statusbar-h);
  z-index: 2100;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  pointer-events: none;
  transition: left var(--asrm-rail-transition);
}

.asrm-shell[data-left-open] .asrm-map-corner-left {
  left: calc(var(--asrm-rail-w-open) + var(--asrm-edge));
}

/* The scale bar: white plate, black ink (owner's second look, 2026-10-05).
 *
 * Two looks in one day, and the second one is measurable rather than a
 * matter of taste. The first took the plate away entirely — `--asrm-bar-bg`
 * (#000) with white type became bare black ink on the map, "just the black
 * number and the distance line". That reads fine over pale land and the
 * light daytime ocean, which is why it looked fine in the screenshot it was
 * accepted from. It stops reading the moment the ground under it is dark,
 * and there are two reachable states where it is: the `#0b0e13` fallback
 * this element paints behind a basemap that has not arrived (the tile host
 * has no SLA, so that is not hypothetical), and any night band over it.
 *
 * Measured rather than eyeballed, over the fallback ground:
 *
 *   `#0b0e13` alone               → black ink **1.07 : 1**
 *   + the four night bands stacked → **1.14 : 1**  (0.595 composite alpha)
 *   WCAG floor for text this size  → 4.5 : 1
 *
 * One point one four is not low contrast, it is *invisible* — the ink and
 * the ground differ by about one digital step. So the plate returns
 * inverted: `#fff` behind, `#000` on top, **21 : 1** against anything the
 * basemap or the night layer puts under it, with no dependency on a tile
 * colour this repo does not own.
 *
 * White rather than the shell's own dark plates because this box has no
 * chrome to sit inside; it floats alone in a corner the status bar does not
 * reach. It now matches the white `i` pill diagonally across from it, which
 * makes the two boxes a first-time visitor is meant to notice at the bottom
 * of the screen the two white ones.
 *
 * The opacity is not purely cosmetic, because of one other list: this box
 * is in `MAP_COVERS` again, and was taken out of it on exactly the grounds
 * that it painted nothing. The plate coming back means the cover comes back
 * with it — an aircraft under this box is hidden and stops counting toward
 * the in-view total, as it did before v1.90. That is deliberate and it is
 * asserted; see the `.asrm-scalebar` entry in `MAP_COVERS` in shell.js.
 *
 * **22 px tall, the height of the `i` credit pill's button** (owner,
 * 2026-10-06: "make the distance island in the lower left corner the same
 * height as the `i` credit icon"). The two white plates at the bottom of
 * the screen now share one height, so the pair reads as one chrome family
 * rather than as two boxes that happened to land on the same row. The
 * height is declared rather than grown from padding: `box-sizing:
 * border-box` with `height: 22px` and `justify-content: center`, so the
 * label and the bracket keep their shipped sizes and the plate centres
 * them — a padding-derived height would move again the next time the
 * label's font metrics do. Paint is otherwise the 2026-10-05 second look:
 * white plate, black ink, no dependency on any tile colour. */
.asrm-scalebar {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  height: 22px;
  box-sizing: border-box;
  padding: 0 8px;
  background: #fff;
  color: #000;
  font-size: var(--asrm-fs-micro);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The drawn half of the scale: an open-topped bracket whose pixel width
 * `shell.js` sets to the round distance it names, so the line is the
 * measurement and the label is its name — never a fixed-width line with
 * a number that approximates it. Black with the label now, so the line
 * and its number are one drawing rather than two colours. */
.asrm-scalebar__line {
  height: 5px;
  min-width: 24px;
  border-left: 1px solid #000;
  border-right: 1px solid #000;
  border-bottom: 1px solid #000;
  box-sizing: border-box;
}

/* --- panels inside the left rail ------------------------------------ */
.asrm-panel {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--asrm-line);
}

.asrm-panel__title {
  margin: 0 0 0.4rem;
  font-size: var(--asrm-fs-title);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--asrm-fg-muted);
  font-weight: 600;
}

.asrm-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.asrm-list li + li {
  margin-top: 1px;
}

.asrm-list__link {
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
  padding: 0.18rem 0.3rem;
  color: var(--asrm-fg);
  text-decoration: none;
  font-size: var(--asrm-fs-rail);
  border-radius: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.asrm-list__link:hover {
  background: var(--asrm-panel-bg-raised);
}

.asrm-list__link.is-current {
  background: var(--asrm-panel-bg);
  color: var(--asrm-accent);
}

.asrm-code {
  font-variant-numeric: tabular-nums;
  color: var(--asrm-fg-muted);
  font-size: var(--asrm-fs-body);
  flex: none;
}

.asrm-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- tables --------------------------------------------------------- */
/* The **base** for every `.asrm-table` lives here, before its modifiers, so
 * that a `.asrm-table--foo` rule below actually wins. With the base declared
 * after them (which is where it used to be) the two selectors were equal at
 * (0,1,1) and source order gave the base the win: the airline table's own
 * `padding` and `font-weight: 400` were silently dead, and every consumer
 * got the drawer's metrics instead of its own. Base first, modifiers after. */
.asrm-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--asrm-fs-body);
}

.asrm-table th,
.asrm-table td {
  padding: 0.15rem 0.4rem 0.15rem 0;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--asrm-line);
}

.asrm-table th {
  width: 45%;
}

.asrm-table td {
  font-variant-numeric: tabular-nums;
}

/* --- airline table (M-07-T4) ----------------------------------------- */
/* The list this replaces was a `<ul>` of `display:flex` rows with
 * `gap: .4rem`, so the code column took *each row's own width* and the
 * names started at a different x in every row: `2L`, `RCN` and
 * `Aegean Dreamlines` gave three left edges in one list. A real table
 * sizes column one to the widest code once, for every row, which is the
 * only way the names line up without hand-tuned padding.
 *
 * `table-layout` is deliberately left **auto**. A fixed layout would need a
 * hard-coded code-column width, and the widest code is a property of the
 * data, not of the design — a new long code would overflow a fixed width
 * and break the alignment the whole change exists to create. */
.asrm-table--airlines {
  font-size: var(--asrm-fs-body);
}

/* The containing block for the row-wide link below.
 *
 * EVERY row-link table needs this, and the hub table shipped without it. That
 * was not a cosmetic miss: `.asrm-stretchedlink::after` is `position:
 * absolute; inset: 0`, so it sizes to the nearest **positioned** ancestor.
 * With no `relative` on the hub rows the nearest one was `.asrm-rail`
 * itself (`position: fixed`), which made every hub row's invisible overlay
 * cover the **entire rail** — ten overlays stacked on one box, last row on
 * top. Clicking the row that read "LWO" (rank 2) fired the link of rank 10
 * (AMS), and every later click fired that same link again, so the rail also
 * looked frozen: `applyScope` saw an unchanged scope and did nothing.
 *
 * A table that uses the stretched link without a positioned row is not a
 * slightly-off hit area — it is the whole rail becoming one link.
 * `test_every_stretched_link_row_has_a_containing_block` guards the pairing
 * so a third table cannot ship with the same defect. */
.asrm-table--airlines tr,
.asrm-table--hubs tr {
  position: relative;
}

.asrm-table--airlines th,
.asrm-table--airlines td {
  padding: 0.22rem 0.3rem;
  text-align: left;
  vertical-align: baseline;
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.asrm-table--airlines tr:hover {
  background: var(--asrm-panel-bg-raised);
  color: var(--asrm-accent);
}

.asrm-table--airlines tr.is-current {
  background: var(--asrm-panel-bg);
  color: var(--asrm-accent);
}

/* One anchor per row, stretched over all of it.
 *
 * The row is the thing you mean to click; the text inside it is where the
 * label happens to be. A `::after` covering the row makes the whole row a
 * target while the tab order stays **one stop per airline** — the obvious
 * alternative, a link in each cell, doubles the tabbing through a
 * 72-airline player, which is the opposite of what a rail is for.
 *
 * The anchor itself stays `position: static`, so the absolutely positioned
 * pseudo-element resolves against the `<tr>` rather than the text box,
 * which is what makes it cover the row rather than the glyphs. */
.asrm-stretchedlink {
  color: inherit;
  text-decoration: none;
}

.asrm-stretchedlink::after {
  content: "";
  position: absolute;
  inset: 0;
}

.asrm-stretchedlink:focus-visible {
  outline: 2px solid var(--asrm-accent);
  outline-offset: -2px;
}

.asrm-count,
.asrm-truncated,
.asrm-note {
  color: var(--asrm-fg-muted);
  font-size: var(--asrm-fs-caption);
}

.asrm-currentscope {
  margin: 0.4rem 0 0;
  font-size: var(--asrm-fs-rail);
}

.asrm-currentscope strong {
  color: var(--asrm-accent);
}

.asrm-clear {
  color: var(--asrm-fg-muted);
}

/* The same `clear` control is an `<a>` in the two scope lines and a
 * `<button>` in the airport lens, and only the second one was broken:
 * `.asrm-clear` ever set a *colour*, which is all an `<a>` needs, but a
 * browser gives a `<button>` a light `ButtonFace` fill and an outset
 * border of its own. `--asrm-fg-muted` is `#e8eaed`, near-white, so it
 * sat on a near-white face and the word vanished inside a white
 * rectangle — the owner's "the `clear` button is just a white
 * rectangle". Same failure class as `flightcard.css`'s note that the
 * default MapLibre close button is a black × on a dark panel, opposite
 * polarity, and the same lesson: in a dark theme a colour rule alone
 * never makes a `<button>` legible, its default chrome has to be
 * stripped first or it paints over your colour.
 *
 * Shaped to match `a.asrm-clear` rather than to look like a button,
 * because it sits inline in a sentence of chips next to anchors that
 * *are* links and must not outrank them. The underline is set
 * explicitly instead of being left to the UA default: once the fill and
 * border are gone, the underline is the only thing saying this is
 * actionable. */
button.asrm-clear {
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--asrm-fg-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

button.asrm-clear:hover,
button.asrm-clear:focus-visible {
  color: var(--asrm-fg);
}

/* Honest empty states: visible, muted, and phrased as a fact. Never a
 * blank space a reader has to interpret (R10). */
.asrm-empty {
  margin: 0;
  padding: 0.25rem 0.3rem;
  color: var(--asrm-fg-muted);
  font-size: var(--asrm-fs-body);
  font-style: italic;
}

/* --- type-ahead ----------------------------------------------------- */
.asrm-typeahead {
  position: relative;
}

.asrm-typeahead__input {
  width: 100%;
  box-sizing: border-box;
  padding: 0.3rem 0.4rem;
  background: var(--asrm-panel-bg);
  color: var(--asrm-fg);
  border: 1px solid var(--asrm-line);
  border-radius: 4px;
  font-size: var(--asrm-fs-rail);
}

.asrm-typeahead__input:focus {
  outline: 1px solid var(--asrm-accent);
  border-color: var(--asrm-accent);
}

.asrm-typeahead__results {
  list-style: none;
  position: absolute;
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  z-index: 2100;
  max-height: 14rem;
  overflow-y: auto;
  margin: 0;
  padding: 2px;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 4px;
}

.asrm-typeahead__results[hidden] {
  display: none;
}

.asrm-typeahead__item {
  display: flex;
  justify-content: space-between;
  gap: 0.4rem;
  padding: 0.22rem 0.35rem;
  font-size: var(--asrm-fs-rail);
  cursor: pointer;
  border-radius: 3px;
}

.asrm-typeahead__item:hover,
.asrm-typeahead__item.is-active {
  background: var(--asrm-panel-bg-raised);
  color: var(--asrm-accent);
}

/* --- left-rail nav (same NAV_ITEMS as the content header) ------------ */
.asrm-rail__nav {
  padding: 0.5rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 1px;
  /* Pinned to the bottom of the rail. `margin-top: auto` in the rail's
   * flex column pushes away whatever height the section above leaves —
   * which is the whole point: the nav's position must not depend on how
   * many airlines the player happens to run. In the collapsed rail it
   * reads as the site-links icon column it was always meant to be. */
  margin-top: auto;
  flex: 0 0 auto;
  border-top: 1px solid var(--asrm-line);
}

.asrm-rail__navlink {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.3rem;
  color: var(--asrm-fg-muted);
  text-decoration: none;
  font-size: var(--asrm-fs-rail);
  border-radius: 3px;
  white-space: nowrap;
}

.asrm-rail__navlink:hover,
.asrm-rail__navlink[aria-current="page"] {
  color: var(--asrm-accent);
  background: var(--asrm-panel-bg);
}

/* --- icon buttons (right rail) -------------------------------------- */
.asrm-iconbtn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: transparent;
  color: var(--asrm-fg-muted);
  border: 1px solid transparent;
  border-radius: 4px;
  cursor: pointer;
}

.asrm-iconbtn:hover {
  background: var(--asrm-panel-bg-raised);
  color: var(--asrm-fg);
}

.asrm-iconbtn.is-on {
  color: var(--asrm-accent);
  border-color: var(--asrm-accent);
  background: var(--asrm-panel-bg);
}

/* Present-but-unavailable is visually distinct from off, so a visitor does
 * not click repeatedly at a control that is simply not built yet. */
.asrm-iconbtn.is-unavailable,
.asrm-iconbtn:disabled {
  color: #4c4c4c;
  cursor: not-allowed;
  background: repeating-linear-gradient(
    135deg,
    transparent 0 4px,
    rgba(255, 255, 255, 0.03) 4px 8px
  );
}

.asrm-icon {
  width: var(--asrm-fs-icon);
  height: var(--asrm-fs-icon);
  flex: none;
}

/* --- the right-rail menus ------------------------------------------- */
/* The right rail is five icon buttons, each opening its own panel to its
 * left (ADR-0071). The opener is `position: relative` so its mouse-over
 * tip can hang off its left edge.
 *
 * The opener carries no border or fill of its own any more. Framing it was
 * how the single-menu rail told an opener apart from the layer toggles
 * stacked under it; with no toggles left on the rail there is nothing to
 * tell it apart from, and a framed box among four borderless ones reads as
 * the odd one out rather than as a menu. */
.asrm-rail__menu {
  position: relative;
}

/* The owner's ruling on how a menu announces that something inside it is
 * drawn: **the icon tints, and nothing else.** No dot badge — the old
 * `.asrm-iconbtn__dot` is deleted along with the framed button it hung
 * on. The tint is enough because the icons are the only thing on the rail,
 * so a coloured one cannot be missed, and it is one fewer thing to keep
 * in step with the layer state.
 *
 * This rule MUST stay below `.asrm-iconbtn:hover`. Both are single-class
 * specificity, so source order decides: declared before the hover rule,
 * hovering an already-drawn icon would wipe the very state the tint
 * exists to report, which reads as the hover *resetting* it. */
.asrm-rail__menu.is-drawn {
  color: var(--asrm-accent);
}

/* The mouse-over text (owner: the icons "should also feature mouse-over
 * explanations"), as a CSS tooltip rather than a native `title`.
 *
 * Three reasons the native attribute loses. It takes roughly half a
 * second to appear, which on a rail whose entire job is "click me and you
 * get something" is the difference between an affordance and a mystery.
 * It cannot be styled, so it would arrive as an OS-grey box beside a
 * black rail. And it is positioned by the OS, so it cannot be kept off the
 * thing it is describing.
 *
 * So: `aria-hidden` decoration in the DOM, `visibility` toggled by hover
 * and keyboard focus, `pointer-events: none` so the tip can never eat the
 * click aimed at the icon behind it. The button keeps a real `aria-label`
 * as well, because a control whose only visible text is `aria-hidden` and
 * whose only icon is `aria-hidden` has **no accessible name at all** —
 * a worse failure than a tooltip that never appears. The checker pins the
 * two strings against each other so they cannot drift apart.
 *
 * It hangs to the **left** for the same reason the panels do: the rail is
 * pinned 10px from the viewport edge, so there is nothing out there but
 * the edge. And it keeps that same `--asrm-edge` unit off the icon — the
 * one measurement this shell repeats everywhere for exactly this feeling
 * of a column of things floating beside the map. */
.asrm-tip {
  position: absolute;
  right: calc(100% + var(--asrm-edge));
  top: 50%;
  transform: translateY(-50%);
  padding: 3px 7px;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 4px;
  color: var(--asrm-fg);
  font-size: var(--asrm-fs-body);
  white-space: nowrap;
  visibility: hidden;
  opacity: 0;
  transition: opacity 120ms ease;
  pointer-events: none;
}

.asrm-iconbtn:hover .asrm-tip,
.asrm-iconbtn:focus-visible .asrm-tip {
  visibility: visible;
  opacity: 1;
}

/* While a panel is open the tip has to get out of the way: it hangs into
 * exactly the space the panel has just taken, covering the options the
 * visitor is about to read. `:has()` is used rather than a JS class
 * because the state is already in the DOM as `aria-expanded`, and a
 * second class encoding the same fact is a second thing that can disagree.
 * Where `:has()` is unsupported the tip simply stays visible while a panel
 * is open — cosmetic, and nothing depends on it. */
.asrm-rail:has(.asrm-rail__menu[aria-expanded="true"]) .asrm-tip {
  visibility: hidden;
  opacity: 0;
}

/* The rail's own `overflow: hidden` — which keeps the left rail's
 * scrolling airline list inside its box — would clip the tip at the rail
 * edge, so the right rail opts out. Safe here because nothing in the
 * right rail overflows its box: the menus and their tips are positioned,
 * not flowed, and the rail has no scrolling content. */
.asrm-rail--right {
  overflow: visible;
}

/* --- the menu panels ------------------------------------------------ */
/* One panel class, `.asrm-menu-panel`, is shared by all five panels.
 * That is deliberate: `MAP_COVERS` in `shell.js` then needs a single
 * entry for "an open menu is hiding part of the map" instead of five,
 * and a sixth menu joins the occlusion contract for free. */
.asrm-menu {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.asrm-menu__hint {
  margin: 0 0 0.1rem;
  font-size: var(--asrm-fs-rail);
  color: var(--asrm-fg-muted);
}

/* A row that flips a layer. It reuses `.asrm-iconbtn` so it lands in the
 * same delegated click handler as every other control — the class is the
 * wiring, and restyling a row by dropping it would leave a control that
 * looks interactive and does nothing. The fixed 34px square is what gets
 * overridden here, not the behaviour. */
.asrm-menu__toggle {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  width: 100%;
  height: auto;
  padding: 0.45rem 0.55rem;
  text-align: left;
  border-color: var(--asrm-line);
}

.asrm-menu__toggle.is-on {
  background: var(--asrm-panel-bg-raised);
}

/* A row that is a row and not a button: a sub-function that is not built
 * yet. Same shape as a toggle so the panel reads as one list, with the
 * unavailable state carried by a hatched fill and a not-allowed cursor
 * rather than by grey text alone — SPEC §28.5 will not let colour be the
 * only channel, and a hatch survives a greyscale print. */
.asrm-menu__row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  width: 100%;
  padding: 0.45rem 0.55rem;
  text-align: left;
  border: 1px solid transparent;
  border-radius: 4px;
}

.asrm-menu__row.is-unavailable {
  cursor: not-allowed;
  background: repeating-linear-gradient(
    135deg,
    transparent 0 4px,
    rgba(255, 255, 255, 0.03) 4px 8px
  );
}

.asrm-menu__row.is-unavailable .asrm-menu__name {
  color: #8b8b8b;
}

/* The colour key at the head of each row. Every swatch is a *miniature of
 * the thing it turns on*, not a flat chip of the layer's hue, because the
 * row's job is "recognise the view, then read the words" — a gold square
 * tells you nothing about what gold means on this map. `--asrm-pax` and
 * `--asrm-cargo` appear in the routes swatch as a three-band stack
 * because that is literally what the route layer draws: three dash
 * patterns in three colours.
 *
 * `--zones` deliberately has no background here. The zebra tones are
 * painted inline by `paintZoneSwatch()` from the same `ZEBRA_TONES` array
 * the overlay itself is built from, so the swatch and the map cannot
 * disagree about a boundary colour. A hard-coded gradient here would be a
 * second answer to a question the JS already answers. */
.asrm-menu__swatch {
  width: 14px;
  height: 14px;
  margin-top: 1px;
  flex: none;
  border: 1px solid var(--asrm-line);
  border-radius: 2px;
}

.asrm-menu__swatch--aircraft {
  background: #f2f2f2;
}

.asrm-menu__swatch--routes {
  background: linear-gradient(
    180deg,
    var(--asrm-pax) 0 33.3%,
    var(--asrm-cargo) 33.3% 66.6%,
    #9a9a9a 66.6% 100%
  );
}

.asrm-menu__swatch--dest {
  background: radial-gradient(circle, #f2f2f2 0 2.5px, transparent 2.5px);
}

.asrm-menu__swatch--slots {
  background: linear-gradient(
    90deg,
    var(--asrm-slot-full) 0 20%,
    var(--asrm-slot-tight) 20% 40%,
    var(--asrm-slot-narrow) 40% 60%,
    var(--asrm-slot-loose) 60% 80%,
    var(--asrm-slot-open) 80% 100%
  );
}

.asrm-menu__swatch--night {
  background: linear-gradient(90deg, #0b1026 0%, #23325a 55%, #e8b877 100%);
}

.asrm-menu__text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.asrm-menu__name {
  font-size: var(--asrm-fs-rail);
  color: var(--asrm-fg);
}

/* `white-space: normal` is the point on this one. The panel is 21rem wide
 * and the descriptions are sentences, not labels; without it the shell's
 * inherited `nowrap` would run a sentence straight out of the panel. */
.asrm-menu__desc {
  font-size: var(--asrm-fs-rail);
  white-space: normal;
  color: var(--asrm-fg-muted);
}

/* The panel's width. The base `.asrm-popup` only declares a *maximum*, so
 * with no width of its own a panel settles at whatever its widest line of
 * prose happens to be — which is exactly how the old layers panel came to
 * sit 91px off the icon when 10px was intended: `popup.js` measured the
 * box before the width class was applied. Setting it here, and passing the
 * class into `openPopup` before `place()` runs, fixes both ends.
 *
 * 21rem = 336px, which holds the size grid's two columns plus the
 * longest description without either wrapping into ribbons or forcing a
 * horizontal scroll on a 1024px screen. */
.asrm-menu-panel {
  width: min(21rem, calc(100vw - 24px));
}

.asrm-menu__section {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--asrm-line);
}

.asrm-menu__section .asrm-typeahead__input {
  width: 100%;
}

.asrm-menu__picked {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin: 0;
  font-size: var(--asrm-fs-caption);
  color: var(--asrm-fg-muted);
}

.asrm-menu__picked[hidden] {
  display: none;
}

/* Inside a panel the suggestion list cannot float. The popup clips
 * absolute overflow at its own box (`overflow-y: auto` on `.asrm-popup`),
 * and this panel's bottom edge is exactly the search field's row — so a
 * floating list landed in the scroll region below the visible panel: the
 * "type ahead window appears behind the search field" of the owner's
 * first look at the feature, invisible and unclickable. In flow, the list
 * grows the panel instead, and the popup's own max-height and scroll take
 * over only when the viewport is genuinely short. The left rail keeps the
 * floating overlay: that panel is tall enough to swallow it. */
.asrm-menu-panel .asrm-typeahead__results {
  position: static;
}

/* --- the airport service-level grid --------------------------------- */
/* A `<table>` because it is two columns that must line up across ten
 * rows, which is the same reason the left rail's airline list is one.
 * `font-variant-numeric: tabular-nums` keeps the level digits the same
 * width so the two columns do not shimmer against each other. */
.asrm-sizegrid {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.asrm-sizegrid th {
  padding: 0 0 3px;
  font-size: var(--asrm-fs-title);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  text-align: left;
  color: var(--asrm-fg-muted);
}

.asrm-sizegrid td {
  width: 50%;
  padding: 1px;
}

/* The whole cell is the label, so clicking the number toggles the box.
 * A 12px checkbox alone is a target most visitors miss on a dense grid. */
.asrm-sizegrid__cell {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 2px 6px;
  border: 1px solid var(--asrm-line);
  border-radius: 3px;
  font-size: var(--asrm-fs-body);
  color: var(--asrm-fg-muted);
  cursor: pointer;
}

.asrm-sizegrid__cell:hover {
  background: var(--asrm-panel-bg-raised);
  color: var(--asrm-fg);
}

.asrm-sizegrid__cell input {
  margin: 0;
  flex: none;
}

/* Each kind's checked cell wears that kind's own hue, so the grid reads as
 * two independent columns rather than one selection. Both kinds can be on
 * at once (owner ruling, ADR-0071), so the columns must not look like
 * they belong to one control — tinting them the same colour would suggest
 * exactly the exclusivity the grid exists to lift.
 *
 * These use `:has()`, and the degradation is deliberate: without it the
 * cell keeps its neutral border and the native checkbox still shows
 * checked. The grid still works; only the column tint is missing. Nothing
 * in the behaviour depends on `:has()`, which is the bar for using it
 * here. */
.asrm-sizegrid__cell:has(input:checked) {
  border-color: var(--asrm-pax);
  background: rgba(255, 210, 63, 0.13);
  color: var(--asrm-fg);
}

.asrm-sizegrid__cell--cargo:has(input:checked) {
  border-color: var(--asrm-cargo);
  background: rgba(47, 129, 247, 0.13);
  color: var(--asrm-fg);
}

.asrm-sizegrid__cell:has(input:focus-visible) {
  outline: 1px solid var(--asrm-accent);
  outline-offset: 1px;
}

.asrm-sizegrid__n {
  font-size: var(--asrm-fs-body);
  color: inherit;
}

/* --- status bar ----------------------------------------------------- */
.asrm-statusbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  gap: 1rem;
  height: var(--asrm-statusbar-h);
  padding: 0 0.6rem;
  box-sizing: border-box;
  background: var(--asrm-bar-bg);
  border-top: 1px solid var(--asrm-line);
  font-size: var(--asrm-fs-bar);
  white-space: nowrap;
}

.asrm-statusbar__seg {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
}

.asrm-statusbar__scope {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
}

.asrm-statusbar__world {
  color: var(--asrm-accent);
  font-weight: 600;
}

.asrm-statusbar__dot {
  color: var(--asrm-fg-muted);
}

.asrm-statusbar__server {
  flex: none;
  /* The `margin-left: auto` the credit segment used to claim (M-07-T7).
   * The credit left the bar on 2026-10-02 (M-07-T9), and with it the only
   * other claimant went, so the server handle — now the bar's last
   * right-hand group — takes the free space back. Exactly one segment may
   * hold an auto margin or the space splits and a hole opens mid-bar. */
  margin-left: auto;
}

/* The build version, back in the bar on the owner's second look of
 * 2026-10-02 (it rode in the credit segment until T9, and in the credit `i`
 * for the few hours between T9 and that look). Muted like the scaffolding
 * around it, tabular so a bump does not reflow the strip, and last before
 * the drawer icon — the position the old credit segment ended in. */
.asrm-statusbar__version {
  color: var(--asrm-fg-muted);
  font-variant-numeric: tabular-nums;
}

/* --- the scope readout (M-07-T8, was the scope *fields* of T6) ------ */
/* The "who you are looking at" part of the bar. In T6 each of these was a
 * **button** that opened a pop-over above itself; the owner reversed that
 * in T8 ("The status bar should be a purely informational area and not
 * hold clickable buttons... more logical for the users to have only one
 * place to make changes, instead of two"), so the template now emits
 * `<span>`s and this rule styles text that happens to carry an icon.
 *
 * What was deliberately **not** kept from the button rules, and why it
 * matters more than the element name:
 *
 *   - `cursor: pointer` — gone. A pointer cursor on a static span is a
 *     lie the browser tells about the element, and it is the one control
 *     signal that survives a `<button>` → `<span>` swap if you restyle by
 *     copy-paste. `cursor: default` is stated rather than left to inherit
 *     so the intent is greppable: this rule is where "the bar does not
 *     act" is enforced visually.
 *   - `:hover` background — gone. The hover wash was the affordance that
 *     said "click me". With no click it would be a highlight that leads
 *     nowhere, and a bar the user sweeps the cursor across repeatedly
 *     would flash for no reason.
 *   - `[aria-expanded="true"]` accent — gone with `aria-haspopup`/
 *     `aria-expanded`, which no longer belong on a span. There is no box
 *     to be open.
 *
 * `font: inherit` / `color: inherit` **are** kept, still load-bearing for
 * the same reason they were when this was a button reset: without them an
 * element can drift off the shell's type scale. Here they are cheap
 * insurance on spans that would otherwise inherit anyway — the rule that
 * actually needed them was the button, and deleting that must not take the
 * habit with it. The T6 note about six type sizes in the shell still holds;
 * the token work in `tokens.css` exists to keep exactly one of each. */
.asrm-statusbar__item {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin: 0;
  min-width: 0;
  cursor: default;
  color: inherit;
  font: inherit;
}

/* Long names truncate rather than shove. Airline names run to 30+ characters
 * and the bar's readout shares one 30px line with the layer chips, the zoom,
 * the credit and the server segment. `text-overflow` needs a width to
 * overflow *within*, and a flex item's minimum content size defeats the
 * parent's `overflow: hidden` — so the cap goes on the value span itself.
 * Truncating the name keeps the **hub** on screen, which is the least
 * guessable thing on the bar; truncating the hub to keep a long name full
 * would hide the more useful half. */
.asrm-statusbar__value {
  min-width: 0;
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Pure white, stated rather than inherited (owner, 2026-10-02: "the
   * grey is not legible"). The scope names and the two traffic counts are
   * the bar's payload; everything around them — dots, chips, icons — is
   * scaffolding and may sit a notch down, but the values themselves take
   * the same white the airport count reads as. */
  color: var(--asrm-fg);
}

/* Still deliberately **no hover rule that un-truncates**, for the reason
 * it always was: the field grows, everything to its right slides sideways
 * under the cursor, and a bar meant to be read at a glance ripples every
 * time the mouse crosses it. What changed in T8 is where the full value
 * lives — it used to be "one click away in the pop-over", and now it is a
 * visible row in the left rail. That is strictly better: the full name is
 * readable without first having to discover that hovering exists. */

.asrm-statusbar__count {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  /* White with the scope values since 2026-10-02 (M-07-T9): the owner's
   * read was that this count *was* the white the rest of the left part
   * should match, so the notch between them went instead of staying as a
   * hierarchy nobody saw. */
  color: var(--asrm-fg);
}

.asrm-statusbar__muted {
  color: var(--asrm-fg-muted);
  font-style: italic;
}

.asrm-layerchip {
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--asrm-line);
  border-radius: 999px;
  /* 0.62rem = 9.9px. This one chip is the smallest text the shell ever
   * shipped and the clearest proof that the "make it the same on all
   * menus" complaint was about size, not colour: it was already near-white
   * and still unreadable. Uppercase + letter-spacing makes a glyph *wider*
   * without making its x-height taller, which reads as blurrier at a small
   * size rather than as clearer. */
  font-size: var(--asrm-fs-caption);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--asrm-fg-muted);
}

.asrm-layerchip.is-on {
  color: var(--asrm-accent);
  border-color: var(--asrm-accent);
}

/* Tablet and below, the chips go (owner, 2026-10-02: "On smaller screens
 * like tablets and phones, hide the AIRCRAFT PAX, etc. labels"). Seven
 * pills are the bar's widest segment and the first thing a narrow viewport
 * runs out of; the layer state they mirror is still one tap away in the
 * right rail and the layers menu, both of which keep their pressed state.
 * 1100px is the breakpoint the drawer grid already uses for "this is a
 * tablet", so one width means one answer. */
@media (max-width: 1100px) {
  .asrm-layerchip {
    display: none;
  }
}

.asrm-warn {
  color: var(--asrm-warn);
}

.asrm-recent {
  color: var(--asrm-fg-muted);
}

.asrm-stale {
  color: var(--asrm-warn);
}

.asrm-critical,
.asrm-unknown {
  color: var(--asrm-danger);
}

/* --- drawer --------------------------------------------------------- */
/* <details> in the flex bar: the summary is the handle, the body floats
 * above the bar rather than pushing the layout. No JS is involved in
 * opening it — see the template's note on why that is the acceptance
 * criterion. */
.asrm-drawer {
  display: contents;
}

.asrm-drawer__handle {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0.45rem;
  margin-left: 0.3rem;
  list-style: none;
  cursor: pointer;
  color: var(--asrm-fg-muted);
  border: 1px solid var(--asrm-line);
  border-radius: 4px;
  font-size: var(--asrm-fs-bar);
}

.asrm-drawer__handle::-webkit-details-marker {
  display: none;
}

.asrm-drawer__handle:hover {
  color: var(--asrm-fg);
  background: var(--asrm-panel-bg);
}

.asrm-drawer[open] .asrm-drawer__handle {
  color: var(--asrm-accent);
  border-color: var(--asrm-accent);
}

/* The handle's icon *is* the freshness readout the bar's text line used to
 * be (M-07-T9), so it wears the level colour. Restated at the handle's own
 * specificity because the generic `.asrm-stale` / `.asrm-critical` rules
 * sit earlier in this file and would otherwise lose to the handle's own
 * `color` — a stale pipeline painting a calm grey icon is exactly the
 * masquerade R10 forbids. The `[open]` accent above outranks both, which
 * is right: an open drawer is showing you the sentence, so the handle can
 * stand down to the "you are here" colour. */
.asrm-drawer__handle.asrm-stale {
  color: var(--asrm-warn);
}

.asrm-drawer__handle.asrm-critical,
.asrm-drawer__handle.asrm-unknown {
  color: var(--asrm-danger);
}

.asrm-drawer__freshness {
  margin: 0 0 0.9rem;
  font-size: var(--asrm-fs-body);
}

.asrm-drawer__body {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--asrm-statusbar-h);
  z-index: 3000;
  max-height: var(--asrm-drawer-max-h);
  overflow-y: auto;
  padding: 0.9rem 1.5rem;
  background: var(--asrm-panel-bg);
  border-top: 1px solid var(--asrm-line);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.5);
  /* The bar it hangs off is `white-space: nowrap` on purpose — one line of
   * status text. That inherits in here and made every drawer cell non-wrapping,
   * which is what pushed the Pipeline heading across the Quarantine column and
   * made the panel scroll sideways. Reset at the body so no descendant has to
   * remember it. */
  white-space: normal;
}

.asrm-drawer__grid {
  display: grid;
  gap: 1.25rem 2rem;
  /* `minmax(0, 1fr)`, not `minmax(200px, 1fr)`: with a fixed minimum a long
   * unbreakable value sets a floor the column cannot go below, so the grid
   * overflows its container instead of wrapping inside it. Zero as the floor
   * is what makes sideways scrolling impossible rather than unlikely. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

@media (max-width: 1100px) {
  .asrm-drawer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 620px) {
  .asrm-drawer__grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

.asrm-drawer__h {
  margin: 0 0 0.4rem;
  font-size: var(--asrm-fs-title);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--asrm-fg-muted);
}

.asrm-drawer__col p {
  margin: 0.3rem 0;
  font-size: var(--asrm-fs-body);
}

/* Label → value rows, with the **value** as the prominent element.
 *
 * This is the fix for "the wrong parts are highlighted in the Server
 * status: the interesting parts (the numbers) are hard to read, while the
 * labels (Airports and Airlines) are white(r) and easy to read."
 *
 * Colour alone could not fix it. Since `--asrm-fg-muted` went near-white
 * both columns are equally light, so what was actually distinguishing a
 * label from a value was weight — and it was inverted: `th` at 500, `td`
 * at 400. The label was the heavier thing on the screen. Making the number
 * white is useless if it stays the lighter of the two; the emphasis has to
 * move to the value column. */
.asrm-drawer__col .asrm-table th {
  color: var(--asrm-fg-muted);
  font-weight: 400;
}

.asrm-drawer__col .asrm-table td {
  color: var(--asrm-fg);
  font-weight: 600;
}

.asrm-visuallyhidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --- pop-over anchored above the status bar (M-07-T6) -------------- */
/* The box that opens over the field you clicked. `shell.js` owns `top`/`left`
 * and the flip-below decision; everything about how it *looks* is here, and
 * no geometry is duplicated in JS beyond a gap and an edge margin.
 *
 * **`white-space: normal` is not optional.** The rule inherits from
 * `.asrm-statusbar` (`nowrap`, correct for a single line of status text) and
 * this box hangs off that bar. The drawer hit exactly this and shipped
 * broken: every cell refused to wrap, long quarantine notes set a width the
 * panel could not shrink below, and the whole drawer scrolled sideways. The
 * reset lives on the box, not on each child, so no descendant has to remember
 * it — including the four templates rendered into here, none of which carry a
 * single wrapping rule of their own.
 *
 * `max-height` uses `min()` rather than a bare `60vh` because the box is
 * *anchored*: with a tall listing and a short window, a fixed height can be
 * taller than the space above the bar, and the JS would flip it below — onto
 * the bar itself. Capping against the viewport keeps it inside the screen in
 * whichever direction it is placed, and `overflow-y: auto` makes the excess
 * scroll inside the box rather than push the page. */
.asrm-popover {
  position: fixed;
  z-index: 3100;
  min-width: 15rem;
  max-width: min(30rem, calc(100vw - 2 * var(--asrm-edge)));
  max-height: min(60vh, calc(100vh - var(--asrm-statusbar-h) - 2 * var(--asrm-edge)));
  overflow-y: auto;
  padding: 0.55rem 0.7rem 0.6rem;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 6px;
  box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.5);
  font-size: var(--asrm-fs-body);
  white-space: normal;
}

.asrm-popover__title {
  margin: 0 0 0.4rem;
  font-size: var(--asrm-fs-title);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--asrm-fg-muted);
}

/* Shown while the body is in flight. It has to occupy roughly the space the
 * body will take — a 1rem-high box that jumps to 20rem a moment later reads
 * as a glitch rather than a load, and the reader loses the place they were
 * about to look at. `min-height` gives the pending state a body. */
.asrm-popover__pending {
  margin: 0;
  min-height: 4rem;
  color: var(--asrm-fg-muted);
}

.asrm-popover__body {
  min-width: 0;
}

/* The hub ranking's count column.
 *
 * The descendant combinator is load-bearing here, not stylistic. `.asrm-num`
 * alone is (0,1,0) and the base table rule `.asrm-table th { text-align: left }`
 * is (0,1,1) — the base wins and the column stays left-aligned, with a
 * right-aligned-looking `td` under a left-aligned header. Same trap as the
 * airline table's padding, for the same reason: a utility class cannot beat a
 * two-part selector, so it has to be written as one. */
.asrm-table .asrm-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The base `.asrm-table th { width: 45% }` is sized for the drawer's
 * label→value pairs. This table has four columns, and 45% on the first one
 * would spend nearly half the box on "#". */
.asrm-table--hubs th {
  width: auto;
  white-space: nowrap;
}

.asrm-table--hubs td {
  white-space: nowrap;
}

.asrm-table--hubs td:nth-child(3) {
  white-space: normal;
  width: 100%;
}

/* --- the credit pill, flush above the status bar --------------------- */
/* M-07-T9, ADR-0056 Amendment 3: a native `<details>`
 * in the corner chrome, retired from MapLibre's AttributionControl after
 * that control shipped three owner-visible bugs in one day — an expanded
 * start, an open/closed state split across an `open` attribute and a
 * `compact-show` class, and an empty inner box that painted as a black
 * rectangle under the `i`.
 *
 * The closed pill is exactly its button: the body is hidden by an
 * explicit rule rather than by trusting the UA's `<details>` semantics
 * under a flex parent, which is the hole the black rectangle came
 * through. `row-reverse` opens the credit **left of the `i`**, on the
 * button's own row (Amendment 4, the owner's fourth look: "the credit is
 * not shown left of the i, but above it. Why change that suddenly?") —
 * the direction the vendored compact control always expanded in, which
 * my v1.75 `column-reverse` restyle overrode on my own call rather than
 * on a ruling. The button stays at the anchored right edge either way,
 * so the zoom island docked above it never moves while the credit reads.
 *
 * The button is the white rounded square the owner accepted on the third
 * look ("Before it was a white circle, although a rounded white square
 * with the `i` inside would also be fine"): the one light surface left on
 * the dark shell, on purpose, because it is the one control whose glyph
 * every visitor already knows from every other map on the internet.
 *
 * `right: var(--asrm-edge)` puts the pill in the same 10px column as the
 * two islands on the top right; the zoom island measures itself above
 * the summary button wherever that lands. */
.asrm-credit-pill {
  position: fixed;
  right: var(--asrm-edge);
  bottom: var(--asrm-statusbar-h);
  z-index: 2001;
  display: flex;
  flex-direction: row-reverse;
  align-items: flex-end;
  gap: 4px;
}

/* The author `display: flex` above outranks the UA's `[hidden]` rule, so
 * the attribute needs its own rule or the unplaced pill paints in the
 * wrong corner for one frame. */
.asrm-credit-pill[hidden] {
  display: none;
}

.asrm-credit-pill:not([open]) .asrm-credit-pill__body {
  display: none;
}

/* Shrunk 2026-10-05 at the owner's request: "The white box around the `i`
 * on the lower right should be smaller with less padding". The box is a
 * fixed square with the glyph centred, so the padding *is* the box minus
 * the glyph — both moved. 26px → 22px, radius 6px → 5px so the corners
 * stay proportionally as round rather than looking clipped on a smaller
 * square, and the glyph 0.9rem → 0.8rem so the `i` does not simply fill
 * the smaller plate and leave the padding where it was. That takes the
 * space around the glyph from ~5.8px a side to ~4.6px. White plate and
 * dark glyph are unchanged: this is the one control the owner has kept
 * bright on purpose, because every visitor knows the `i` from every other
 * map on the internet. */
.asrm-credit-pill__button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 0;
  border-radius: 5px;
  background: #fff;
  color: #17181d;
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 700;
  line-height: 1;
  list-style: none;
  cursor: pointer;
}

.asrm-credit-pill__button::-webkit-details-marker {
  display: none;
}

.asrm-credit-pill__body {
  margin: 0;
  padding: 4px 8px;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 6px;
  color: var(--asrm-fg);
  font-size: var(--asrm-fs-micro);
  /* The body grows leftward from the button now, so the viewport clamp
   * has to pay for the button and the gap or a narrow screen pushes the
   * line's start off the left edge. */
  max-width: min(24rem, calc(100vw - 2 * var(--asrm-edge) - 30px));
}

.asrm-credit-pill a {
  color: var(--asrm-fg-muted);
}

.asrm-credit-pill a:hover {
  color: var(--asrm-fg);
}

/* --- tile outage (SPEC §28.4) --------------------------------------- */
.tile-error {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2500;
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
  padding: 0.5rem 1rem;
  background: #7a1f1f;
  color: #fff3f3;
  font-size: var(--asrm-fs-body);
}

.tile-error[hidden] {
  display: none;
}

/* Idle-pause prompt (M-08 T2). A card over the map, not a bar across it:
 * the traffic keeps animating behind this and the visitor should still see
 * the whole map while deciding. Sits clear of the status bar for the same
 * reason .asrm-noscript does, and deliberately uses the accent rather
 * than the tile-error red — nothing is broken here, the visitor simply
 * walked away, and painting that as a failure would be shouting at them
 * for something they did on purpose.
 *
 * `[hidden]` is restated as `display: none` because this is a flex box;
 * without the restated rule the `hidden` attribute loses to
 * `display: flex` and the prompt never goes away. Same trap .tile-error
 * two blocks above already documents. */
.idle-prompt {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--asrm-statusbar-h) + 0.75rem);
  z-index: 2500;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-width: min(28rem, calc(100vw - 2rem));
  padding: 0.75rem 1rem;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-accent);
  border-radius: 8px;
  box-shadow: 0 4px 16px rgb(0 0 0 / 18%);
  font-size: var(--asrm-fs-body);
}

.idle-prompt[hidden] {
  display: none;
}

/* The map steps back while the question is up (owner, 2026-10-05): the
 * prompt is the foreground and the map the background it returns to.
 * The step-back is an overlay layer inside the map element, not a CSS
 * filter over it. The filter was this rule's first shape (2026-10-05), and
 * the owner reported on 2026-10-06 that the prompt came up over a map that
 * had not darkened: a filter on an element whose child is a
 * hardware-composited WebGL canvas is exactly the declaration a compositor
 * can drop, while a plain layer over the canvas composites on every
 * compositor. Half black, not black: the aircraft stay readable as
 * silhouettes of where the cached schedule was, which is exactly what the
 * prompt says they are, and the transition is what makes "as soon as the
 * user is back" a return rather than a flip. Chrome outside `.asrm-map`
 * (rails, status bar, corner islands) stays lit on purpose: the pause is
 * about the map, not about the shell. */
.asrm-idle-scrim {
  position: absolute;
  inset: 0;
  /* Over the canvas containers (z auto) and under MapLibre's control
   * container, so the attribution the map carries stays legible while the
   * pause lasts. `pointer-events: none` keeps every map gesture alive
   * through the scrim: an interaction is the confirmation that resumes, so
   * the scrim must never swallow one. */
  z-index: 1;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 400ms ease;
}

.asrm-shell.asrm-idle .asrm-idle-scrim {
  opacity: 0.5;
}

.idle-prompt__resume {
  align-self: flex-start;
  min-height: 32px;
  padding: 0.35rem 0.8rem;
  border: 1px solid var(--asrm-accent);
  border-radius: 5px;
  background: var(--asrm-accent);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.idle-prompt__resume:hover {
  filter: brightness(1.08);
}

.idle-prompt__resume:focus-visible {
  outline: 2px solid var(--asrm-accent);
  outline-offset: 2px;
}

/* Traffic readout (M-08 T2). `font-variant-numeric: tabular-nums` is not
 * cosmetic here: the countdown redraws every second, and with proportional
 * figures a changing digit changes the text width, which makes the whole
 * status bar twitch once a second along its entire length. */
.asrm-statusbar__traffic {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Refresh-island state colours (ADR-0052). These two rules used to hang
 * off `.asrm-statusbar__refresh`; the countdown they colour now lives in
 * the island, so the selectors moved with it and the class they target no
 * longer exists in any template.
 *
 * They also stop using the `--asrm-warn-fg` / `--asrm-err-fg` fallbacks.
 * Neither token is defined in `tokens.css`, so those hexes were what
 * actually rendered — `#8a6d1f` and `#7a1f1f`, both chosen for a light
 * surface, on a `--asrm-bar-bg` of `#000000`. The island is a larger,
 * darker surface than the 30 px strip ever was, which made the difference
 * impossible to miss. `--asrm-warn` and `--asrm-danger` *are* defined
 * (tokens.css:157, :164) and are legible on black, so the island uses
 * those and there is no undefined-token fallback left to rot. */
.asrm-refresh-island.is-paused {
  color: var(--asrm-warn);
}

.asrm-refresh-island.is-failed {
  color: var(--asrm-danger);
}

.asrm-refresh-island.is-paused .asrm-refresh-island__countdown,
.asrm-refresh-island.is-failed .asrm-refresh-island__countdown {
  color: inherit;
  font-weight: 600;
}

.asrm-noscript {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--asrm-statusbar-h) + 0.75rem);
  z-index: 2500;
  max-width: 32rem;
  margin: 0;
  padding: 0.6rem 0.9rem;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-accent);
  border-radius: 6px;
  color: var(--asrm-fg);
  font-size: var(--asrm-fs-body);
}

/* --- in-place update feedback (M-07 T2) ----------------------------- */
/* A scope change no longer reloads the document, so the reload's blank
 * screen — crude as it was — is gone, and with it the user's only signal
 * that anything happened. This replaces it, and it is driven by the
 * `is-asrm-busy` class set *before* the fetch, not after. */
.asrm-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 4000;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  background: rgba(255, 162, 0, 0.12);
  transition: opacity 120ms linear;
}

html.is-asrm-busy .asrm-progress {
  opacity: 1;
}

html.is-asrm-busy {
  cursor: progress;
}

.asrm-progress::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 35%;
  background: var(--asrm-accent);
  animation: asrm-sweep 1.1s ease-in-out infinite;
}

@keyframes asrm-sweep {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(380%);
  }
}

/* Reduced motion keeps the signal and drops the movement: a static bar is
 * still feedback, an absent bar is not. */
@media (prefers-reduced-motion: reduce) {
  .asrm-progress::after {
    width: 100%;
    animation: none;
  }
}

/* --- pop-up (M-07 T2) ----------------------------------------------- */
/* Anchored, not centred: the map is the thing you are looking at while you
 * pick a filter, so the panel opens beside its trigger and the map stays
 * visible behind it. `white-space: normal` because a pop-up opened from
 * the status bar would otherwise inherit the bar's nowrap rule — the exact
 * bug that broke the drawer. */
.asrm-popup {
  position: fixed;
  z-index: 3500;
  box-sizing: border-box;
  max-width: min(26rem, calc(100vw - 24px));
  max-height: calc(100vh - 24px);
  overflow-y: auto;
  padding: 0.6rem 0.75rem;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-accent);
  border-radius: 6px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55);
  white-space: normal;
  scrollbar-width: thin;
}

.asrm-popup__title {
  margin: 0 1.5rem 0.4rem 0;
  font-size: var(--asrm-fs-title);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--asrm-fg-muted);
}

.asrm-popup__close {
  position: absolute;
  top: 0.25rem;
  right: 0.3rem;
  width: 22px;
  height: 22px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  color: var(--asrm-fg-muted);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.asrm-popup__close:hover {
  color: var(--asrm-fg);
  background: var(--asrm-panel-bg-raised);
}

.asrm-popup__body {
  font-size: var(--asrm-fs-rail);
  color: var(--asrm-fg);
}

.asrm-popup__body:focus {
  outline: 1px solid var(--asrm-accent);
}

/* --- failure toast --------------------------------------------------- */
/* A failed in-place update must be loud and must not have touched the view.
 * Stays until dismissed when there is a retry, because a toast that fades
 * while the user is still reading it leaves them with a stale page and no
 * explanation. */
#asrm-toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--asrm-statusbar-h) + 0.75rem);
  z-index: 4100;
  display: flex;
  gap: 0.7rem;
  align-items: center;
  max-width: calc(100vw - 24px);
  padding: 0.5rem 0.75rem;
  background: #4a1f1f;
  border: 1px solid var(--asrm-danger);
  border-radius: 6px;
  color: #fff3f3;
  font-size: var(--asrm-fs-body);
}

#asrm-toast[hidden] {
  display: none;
}

.asrm-toast__retry {
  padding: 0.15rem 0.55rem;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-accent);
  border-radius: 4px;
  color: var(--asrm-accent);
  font-size: var(--asrm-fs-caption);
  cursor: pointer;
  flex: none;
}

.asrm-toast__retry:hover {
  background: var(--asrm-panel-bg-raised);
}

/* --- the first-visit gate (M-07 T3) --------------------------------- */
/* "Blank canvas, dark chrome": the owner's blank means *nothing drawn*,
 * not a second colour system. So the intro sits on --asrm-bar-bg and the
 * whole map chrome is simply absent rather than dimmed.
 *
 * Hiding the stage here is belt-and-braces, never the mechanism. Nothing
 * map-shaped is requested while the gate is closed because shell.js does
 * not create the map at all — cancelling a fetch would not have satisfied
 * the rule, since the request is the thing that must not happen. */
.asrm-shell[data-gate="closed"] .asrm-stage,
.asrm-shell[data-gate="closed"] .asrm-rail,
.asrm-shell[data-gate="closed"] .asrm-statusbar {
  display: none;
}

.asrm-intro {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.9rem;
  box-sizing: border-box;
  padding: 2rem 1.25rem;
  background: var(--asrm-bar-bg);
  color: var(--asrm-fg);
  text-align: center;
  overflow-y: auto;
}

/* Page one: three marks, nothing else. Colour is muted rather than accent
 * so the trio reads as the quiet identity it is and the hover lift is the
 * only thing that says "this is clickable". No text label under the marks:
 * the owner's design is an invitation, not a legend. */
.asrm-intro__cta {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--asrm-fg-muted);
  text-decoration: none;
  transition: color var(--asrm-rail-transition);
}

.asrm-intro__cta:hover,
.asrm-intro__cta:focus-visible {
  color: var(--asrm-accent);
}

.asrm-intro__trio {
  display: flex;
  align-items: center;
  gap: 2.4rem;
}

.asrm-intro__mark {
  width: 54px;
  height: 54px;
  display: block;
}

/* Page two hides the trio. Once the player is being asked for, the three
 * marks have done their job and leaving them up would compete with the one
 * line of text that matters. `:has()` is how "the pick is open" is known
 * from the ancestor; where `:has()` is unavailable the trio simply stays
 * above the field, which is the older look rather than a broken one. */
.asrm-intro:has(.asrm-intro__pick:target) .asrm-intro__cta {
  display: none;
}

/* Hidden until the fragment identifier points at it, which is what makes
 * "click the trio → choose your player" work with JavaScript completely
 * absent. `display:none` rather than opacity or visibility: a form that is
 * not shown must also be out of the tab order, not merely invisible. */
.asrm-intro__pick {
  display: none;
}

.asrm-intro__pick:target {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
  box-sizing: border-box;
  width: min(25rem, calc(100vw - 2rem));
  padding: 1.15rem 1.25rem 1.35rem;
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 8px;
  text-align: left;
}

/* The only words on the page, and the only ones this screen will ever
 * show. Deliberately not styled louder than the field under it: "Choose
 * your player" is a question, not a heading. */
.asrm-intro__label {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--asrm-fg);
}

.asrm-intro__typeahead {
  width: 100%;
}

/* No submit button exists on this form. The rules that follow are about
 * making the field itself look like the door, because it is the only one:
 * a visible focus ring is the whole affordance once there is no button to
 * press. `box-sizing` keeps the field inside the panel rather than
 * overflowing it by its own padding. */
.asrm-intro__pick .asrm-typeahead__input {
  box-sizing: border-box;
  width: 100%;
  font-size: 1rem;
}

.asrm-intro__pick .asrm-typeahead__input:focus {
  outline: 2px solid var(--asrm-accent);
  outline-offset: 1px;
}

/* --- reduced motion ------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .asrm-rail--left {
    transition: none;
  }

  .asrm-intro__cta {
    transition: none;
  }

  .asrm-drawer[open] .asrm-drawer__handle .asrm-icon {
    transform: none;
  }
}

/* Small screens: the rails stop being hover-dependent — but **only when there
 * is no script to click with**. Below ~640px a hover state is unreachable on
 * touch, and in the no-JS fallback hover is the only mechanism that opens the
 * rail, so the rail is simply always open there and the map loses width rather
 * than the UI losing usability.
 *
 * It used to apply unconditionally, which is what the owner hit on a phone:
 * *"The left menu bar is never collapsed when looking at the map in a mobile
 * phone in vertical position. It basically covers 70% of the map. When
 * looking at it horizontally, it collapses fine."* Both halves of that
 * report are this one rule. A portrait phone is ~390px wide, so the query
 * matched and pinned the rail to `--asrm-rail-w-open` = 300px = **77% of the
 * screen**; landscape is ~844px and fell outside the query, so the normal
 * click-driven collapse worked again. The rail was not failing to collapse —
 * CSS was forbidding it, and only while held vertically.
 *
 * With `.asrm-js` present the rule is both unnecessary and harmful: tap is a
 * click, outcome 4 of `wireRail` ("blank rail surface opens up") works
 * exactly as well under a finger as under a mouse, so the rail starts collapsed
 * at 44px and the visitor opens it deliberately. shell.js additionally
 * closes a panel that is open when the viewport *becomes* narrow
 * (`wireNarrowRail`), so rotating a phone to portrait while a section is open
 * returns the map instead of keeping it covered.
 *
 * Owner ruling 2026-09-26 was explicitly "auto-collapse only": the 300px open
 * width is kept as-is — an opened rail on a portrait phone still covers most
 * of the screen, by choice — and the fix is that it is never open by default.
 * The full bottom-sheet treatment remains deferred (card open question); this
 * is the floor, not the design. */
@media (max-width: 640px) {
  html:not(.asrm-js) .asrm-rail--left {
    width: var(--asrm-rail-w-open);
  }

  .asrm-intro__trio {
    gap: 1.5rem;
  }

  .asrm-intro__mark {
    width: 38px;
    height: 38px;
  }
}

/* --- map feature hover: the one-liner -------------------------------
 *
 * Lives in shell.css rather than its own stylesheet because the only place
 * CSS reaches the page is the `<link>` list in
 * `backend/apps/map/templates/map/shell.html`, and this slice is not
 * permitted to edit `backend/`. A new `hover.css` would ship and never be
 * loaded, which is the worst kind of dead file. If a later slice opens
 * that template up, this block is meant to move out intact.
 *
 * The tooltip is the **light** half of the owner's split — "hover = light
 * tooltip, click = full card". It is one line, it follows the cursor, and
 * it must never grow into a paragraph: `nowrap` plus `max-width: min(90vw,
 * 420px)` with an ellipsis means an unexpectedly long airport name gets
 * truncated rather than wrapped into a box that covers the map.
 *
 * `pointer-events: none` is load-bearing, not cosmetic. The tooltip is a
 * child of the map container, so if it could receive the pointer it would
 * swallow the `mousemove` that produced it; the resolver would stop
 * running and the white highlight would stay stuck under a cursor that is
 * no longer over the feature. That is the flicker/stick bug this whole
 * interaction is arranged to avoid, arriving from the DOM side. */
.asrm-hover-tip {
  position: absolute;
  /* 2200: above the rails and every corner island (see the ladder at the
   * top of this file), below the tile-error pane, the drawer and the
   * pop-over. At its old 20 the tooltip painted under the lat/lon readout,
   * the scale bar and the right-hand rail — every island in front of the
   * one thing the cursor was actually asking about (owner, 2026-10-06). */
  z-index: 2200;
  max-width: min(90vw, 420px);
  padding: 3px 8px;
  border: 1px solid var(--asrm-line);
  border-radius: 4px;
  background: rgb(24 24 24 / 92%);
  color: var(--asrm-fg);
  font-size: var(--asrm-fs-body);
  line-height: 1.35;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  opacity: 0;
  transform: translateY(2px);
  transition:
    opacity 90ms ease-out,
    transform 90ms ease-out;
}

/* `hidden` is toggled in JS so the element is out of the a11y tree while
   idle; the opacity transition only ever runs on the visible one. */
.asrm-hover-tip:not([hidden]) {
  opacity: 1;
  transform: translateY(0);
}

/* --- map feature click: the full card -------------------------------
 *
 * Deliberately reuses `.asrm-fc-popup` so the aircraft card and the
 * airport/route card cannot drift apart in chrome — same panel, same close
 * button, same tip colour — and `.asrm-hover-card` only carries what is
 * specific to these two object types. Re-declaring the panel background
 * here would be the second copy that forgets to change. */
.asrm-hover-card .asrm-hover-card__head {
  padding: 8px 10px 6px;
  border-bottom: 1px solid var(--asrm-line);
}

.asrm-hover-card__title {
  font-weight: 600;
  font-size: var(--asrm-fs-value);
  color: var(--asrm-fg);
}

/* The title of a card **inside a popup** is the drag handle, shared with
 * the aircraft card through `popupdrag.js`, which delegates on the popup
 * container with exactly this selector. Scoped to `.asrm-fc-popup` so the
 * hover one-liner — same class, not draggable, gone on the next mousemove —
 * never promises a grab it cannot honour.
 *
 * `touch-action: none` is functional, not cosmetic: without it a touch or
 * pen drag is claimed by the browser's own pan/scroll gesture and the
 * `pointermove` events never reach the handler — a card that drags fine
 * with a mouse and is completely stuck on a phone. `user-select: none`
 * stops the drag selecting the title text under the pointer. This rule is
 * the reason the affordance survives a content repaint: an inline cursor
 * set on the old title node dies with it, a stylesheet rule does not. */
.asrm-fc-popup .asrm-hover-card__title {
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.asrm-fc-popup .asrm-hover-card__title:active {
  cursor: grabbing;
}

.asrm-hover-card__sub {
  margin-top: 1px;
  color: var(--asrm-fg-muted);
  font-size: 0.75rem;
}

.asrm-hover-card__body {
  padding: 6px 10px 8px;
}

.asrm-hover-card__row {
  display: flex;
  gap: 8px;
  align-items: baseline;
  padding: 1px 0;
}

.asrm-hover-card__k {
  flex: 0 0 88px;
  color: var(--asrm-fg-muted);
  font-size: var(--asrm-fs-title);
  text-transform: lowercase;
}

.asrm-hover-card__v {
  flex: 1 1 auto;
  color: var(--asrm-fg);
  overflow-wrap: anywhere;
}

/* --- the 4-column pair grid (the airport card) ----------------------
 *
 * The card used to be one label/value pair per line, which meant a click
 * on an airport opened a 14-line column tall enough to hit the body's
 * scroll cap on a laptop. Two pairs per line makes the same facts eight
 * lines and they fit without scrolling, which is the difference between
 * reading a card and operating a scrollbar.
 *
 * `auto minmax(0, 1fr) auto minmax(0, 1fr)` gives four *real* columns:
 * both label columns size to the widest label that lands in them, so
 * every value in a column starts at the same x. `minmax(0, 1fr)` rather
 * than `1fr` because `1fr` has a min-content floor — one long value
 * ("Nadiestown - Lord Howe Island") would widen the whole column and
 * push its neighbours out. With a zero floor it wraps inside its own cell
 * instead, which `overflow-wrap: anywhere` above already handles.
 *
 * The `column-gap` is the **pair** rhythm: 16px between one pair and the
 * next. The space between a label and its own value is not a gap at all —
 * see `padding-right` on `.asrm-ad-fact .asrm-hover-card__k` below — and
 * the reason it isn't is recorded there.
 *
 * **The two-value form that used to sit here was never valid.** It read
 * `column-gap: 5px 16px`, which reads like the two-value `gap` shorthand
 * but `column-gap` itself takes exactly one value. Chromium drops the
 * declaration, so the *effective* column gap for the whole life of this
 * grid was `normal`, i.e. zero, and the 5px/16px rhythm described in this
 * comment is the most convincing kind of bug: documented, intended, and
 * absent. Measured before the fix, the widest label in each label column
 * sat **0.0px** from its value, which is why the card read
 * `Timezone:UTC+02:00` — and why a narrower label looked fine, because
 * the `auto` track's slack faked the spacing for it.
 *
 * `display: contents` on the row is load-bearing, not a shortcut: it
 * removes the row's box so its two spans become the grid items and land
 * in adjacent columns. A row that kept its own box would be a single grid
 * item and the card would render as two columns of independently-sized
 * pairs — the squashed version of this same mistake. Inherited
 * properties (colour, size) still pass through the box-less element,
 * which is all the row-level classes here carry; nothing in this card
 * paints a background or border on a row, so there is nothing for the
 * removed box to have been drawing. */
.asrm-hover-card__pairs {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
  /* Deliberately **zero**, with both horizontal rhythms supplied by
   * padding on the cells below. A grid has one `column-gap` for every
   * boundary, and this card needs the two boundaries to differ in
   * opposite directions: tight between a label and its own value, wider
   * between one pair and the next. No single `column-gap` value can do
   * that — whatever is set applies to both — so the gap is zero and each
   * boundary is owned by whichever cell can express it. */
  column-gap: 0;
  row-gap: 3px;
  align-items: baseline;
}

/* Label-to-value space on the fact rows: 10px.
 *
 * The label column is an `auto` track, so it sizes to the widest label in
 * it and every narrower label gets slack for free — which is exactly why
 * the absence of any spacing went unnoticed on some rows. `Night ban`
 * and `Timezone` *are* the widest label in their column, so they got
 * none, and ran straight into the value. Measured before this fix, with
 * the labels still written with colons: **0.0px** colon to value on both.
 *
 * The space is `padding-right` on the label rather than a bigger
 * `column-gap`, because the gap would move both boundaries at once. See
 * the `.asrm-hover-card__pairs` rule above.
 *
 * Scoped to `.asrm-ad-fact`: the hero owns its own spacing (its column
 * gap is zero by its own rule and the `Size` label carries its own
 * `padding-right`), so the hero is left exactly as it was. */
.asrm-hover-card__pairs .asrm-ad-fact .asrm-hover-card__k {
  padding-right: 10px;
}

/* Pair-to-pair space on the fact rows: a 16px **floor**, not a rhythm.
 *
 * What this rule actually buys is narrower than it first looks, and the
 * measurement is the honest version: on a card with short values the
 * visible gap between one pair and the next is 90–100px, because the
 * value column is `minmax(0, 1fr)` and its slack dwarfs anything set
 * here. The padding only becomes the whole story when a value actually
 * fills its track — and that is precisely the case worth protecting,
 * because with a zero floor a long value would butt its next pair's label
 * with nothing between them.
 *
 * So: 16px is a minimum guaranteed between pairs, invisible in the
 * normal case, load-bearing in the worst one. Padding rather than a
 * `column-gap` for the reason in the rule above — the gap would move the
 * label boundary too.
 *
 * Padding, not margin: under `display: contents` these cells are the grid
 * items and a margin on a `minmax(0, 1fr)` item fights the track sizing
 * that keeps the two label columns aligned. */
.asrm-hover-card__pairs .asrm-ad-fact .asrm-hover-card__v {
  padding-right: 16px;
}

.asrm-hover-card__pairs .asrm-hover-card__row {
  display: contents;
}

.asrm-hover-card__pairs .asrm-hover-card__k {
  flex: none;
  white-space: nowrap;
}

/* In the grid the value carries the emphasis: the owner's reading is
 * "the numbers are the card", and at 15.2px against an 11.5px label a
 * value is found by shape alone without scanning the label first. Only
 * the grid gets this — the route card's list layout keeps body-sized
 * values, because a single pair per line has no label/value contrast to
 * manage and would just get a bigger font. */
.asrm-hover-card__pairs .asrm-hover-card__v {
  font-size: var(--asrm-fs-value);
  /* 1fr under `display: contents` is meaningless; the grid column owns
   * the width. `min-width: 0` is what lets a long value wrap inside the
   * column instead of straining against it. */
  min-width: 0;
}

/* --- card sections -------------------------------------------------
 *
 * The airport card has two: `Airport` (the static facts the airport table
 * carries) and `Statistics` (statistics about the airport). Every other
 * card this app renders is one section with no heading, which is why the
 * heading is optional here rather than a special case — one renderer, one
 * shape, nothing to keep in sync.
 *
 * The separator between sections is a rule, not only spacing, because the
 * two halves of the airport card answer different questions and a visitor
 * scanning it should be able to see the boundary without reading the
 * headings. It is a `+` selector rather than a rule on every section so a
 * lone section gets no stray line above it.
 *
 * The heading takes `--asrm-fs-title`, the size reserved for uppercase,
 * letter-spaced labels — the same treatment the drawer and panel headings
 * get, and the backend's type-scale test insists the smaller size is only
 * ever earned that way. */
.asrm-hover-card__section-title {
  margin: 0 0 3px;
  font-size: var(--asrm-fs-title);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--asrm-fg-muted);
}

.asrm-hover-card__section + .asrm-hover-card__section {
  margin-top: 7px;
  padding-top: 6px;
  border-top: 1px solid var(--asrm-line);
}

/* --- the airport detail card (the `/api/airports/<id>` enrichment) ---
 *
 * Same arrangement as the route card: every class here arrives as the
 * **third element** of a row from `airportdetail.js`, which decides what
 * the row means and says it in words. Nothing in this block changes what
 * is said — delete it wholesale and the card still reads correctly, which
 * is the property worth having when the content is entirely data.
 *
 * That is also why the status line is not expressed in colour alone
 * (SPEC §28.5). The colour says how bad it is; the text — `statistics
 * unavailable`, `that request was not accepted`, `no statistics we know
 * for this airport` — says what happened, and it is the text
 * `tools/check-airport-detail.mjs` asserts, not this colour. A stylesheet
 * rewrite can take the hue; it cannot take the word.
 *
 * The same reason makes the runway row unconditional in words: `No runway`
 * is carried by the text, never by an absent cell or a greyed one, so a
 * seaplane base cannot be rendered as a gap in the data. */
.asrm-ad-note {
  font-style: italic;
  font-size: 0.72rem;
}

.asrm-ad-note.is-ad-loading {
  color: var(--asrm-fg-muted);
}

.asrm-ad-note.is-ad-unknown {
  color: var(--asrm-fg-muted);
}

/* A 400 means we asked wrongly, so it is louder than the neutral states
 * and distinct from a 404's quiet "we do not know this one" — the two
 * need different follow-up from whoever reads the card. */
.asrm-ad-note.is-ad-badreq {
  color: var(--asrm-warn);
}

.asrm-ad-note.is-ad-failed {
  color: var(--asrm-danger);
}

/* Statistics are numbers, and they sit in a narrow right-hand column next
 * to labels. Tabular figures keep `1 842 per week` over `94.3 %` aligned
 * instead of jittering, which is the difference between scanning a column
 * and reading it one row at a time. */
.asrm-ad-num {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* A unit riding inside a value cell — `/5min` after a slot count.
 *
 * The owner asked for it smaller than the number it qualifies, which is
 * the right call: `8` is the fact and `/5min` is the scale, and at the
 * same size the two read as equal-weight claims. It is sized and coloured
 * like the **label**, so the card resolves into "label + number, with the
 * scale quiet" instead of a value that merely got longer.
 *
 * `font-size: var(--asrm-fs-title)` and `color: var(--asrm-fg-muted)` are
 * the whole rule. Deliberately absent is `font-weight`: the label renders
 * at 600 by inheriting it from the row's `.asrm-ad-num`, and measuring
 * that is what killed an earlier draft of this rule, which set `400` on
 * the theory that a bold unit would sit closer to the number than to the
 * label. The label is already bold, so 400 made the unit the lightest text
 * on the card and it stopped looking like its own label. Inheriting matches
 * it exactly — 11.52px / 600 / `--asrm-fg-muted`, same as `.asrm-hover-card__k`.
 *
 * Nothing outside the airport card emits a unit today, so this is inert on
 * every other card by construction rather than by a scoping selector. */
.asrm-hover-card__unit {
  margin-left: 0.35em;
  color: var(--asrm-fg-muted);
  font-size: var(--asrm-fs-title);
  white-space: nowrap;
}

/* --- the card footer link (M-11 T8, ADR-0044) ----------------------
 *
 * The one thing in a card that leaves the map. It sits below every
 * section, separated by a rule so it cannot be mistaken for one more
 * row of the section above it — with rows this tightly packed, anything
 * without a separator reads as the last pair.
 *
 * `text-align: right` because the link is an exit, not content: it
 * should not compete with the values above it for the eye's starting
 * position, and a left-aligned link lines up under the label column and
 * reads like a label whose value never arrived. */
.asrm-hover-card__foot {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--asrm-line);
  text-align: right;
}

.asrm-hover-card__link {
  color: var(--asrm-fg);
  font-size: var(--asrm-fs-title);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.asrm-hover-card__link:hover {
  color: var(--asrm-accent);
}

/* --- the M-08-T4 airport card: labels as written --------------------
 *
 * `.asrm-hover-card__k` lowercases every label because the cards that
 * first shipped wrote them as sentence-case nouns and the shell wanted
 * them quiet. The airport card writes its labels **sentence-cased, as
 * words** — `Slots`, `Runway`, `Departures` — so this card's own rows opt
 * out and render the string exactly as `airportdetail.js` wrote it. (They
 * carried colons until the owner removed them on 2026-09-30; the opt-out
 * is needed just as much for the capitals that remain, since lowercasing
 * `Night ban` to `night ban` is not what this card asks for.) The rule is
 * on the row class rather than on the label class so the other cards'
 * labels keep their existing treatment: nothing outside `.asrm-ad-*`
 * changes. */
.asrm-ad-hero .asrm-hover-card__k,
.asrm-ad-fact .asrm-hover-card__k,
.asrm-ad-stat .asrm-hover-card__k {
  text-transform: none;
}

/* The one value that is a word rather than a number. `nightBanText()` is
 * tested returning lower-case `no` / `yes` / `unknown` — the *word* is
 * the tested semantic, and capitalising it in JS would change the value
 * every formatter test pins for a reason that belongs to typography. So
 * the capital comes from here: `no` renders as `No`, `unknown` renders
 * as `Unknown`, and the string on the wire and in the test is untouched. */
.asrm-ad-cap .asrm-hover-card__v {
  text-transform: capitalize;
}

/* --- the hero: the codes and the size, on one line ------------------
 *
 * `ATH - LGAV                 Size 9/4`
 *
 * The four runs are two rows, flowed through this row's own grid. It has
 * to be its own container: grid tracks are sized per
 * `.asrm-hover-card__pairs`, so if the hero shared the grid with the
 * fact block, the 1.9rem codes would become the `auto` label column's
 * content and push `Slots`, `Runway` and `Night ban` about a hundred
 * pixels to the right. Owning the tracks is the whole reason
 * `buildAirportDetailCard()` puts the hero in a section of its own.
 *
 * **Five tracks, with the slack in track 2** (owner, 2026-09-30: "Move
 * the `Size` to the right just in front of the size numbers"). Track 1 is
 * the codes, track 2 is the empty middle, track 3 is a zero-width spacer,
 * tracks 4 and 5 are the two level digits. The label cell *spans* tracks
 * 2 and 3 and is right-aligned, which parks `Size` flush against the
 * start of the pax digit.
 *
 * It needs the explicit `grid-column` placements rather than relying on
 * auto-placement because the two rows are not the same shape: row 1 is
 * `[codes, "Size"]` and row 2 is `[pax, cargo]`. Auto-placed, those would
 * land one-after-another into 1,2 then 3,4 — putting the label between the
 * codes and the digits in the wrong place, and dropping the digits into
 * tracks sized by the label. Placement is declared, so the label can never
 * drift back to the left half when something else changes.
 *
 * `:has()` scopes these tracks to the grid that actually contains a
 * hero, leaving every other pair grid alone. Without `:has()` the hero
 * still renders — as a plain two-line list with the codes at label size
 * — so the degradation is "less emphatic", never "broken". The same
 * trade is already struck for `.asrm-intro:has(...)` above.
 */
.asrm-hover-card__pairs:has(.asrm-ad-hero) {
  grid-template-columns: auto minmax(0, 1fr) auto auto auto;
  column-gap: 0;
  align-items: baseline;
}

.asrm-ad-hero-codes .asrm-hover-card__k {
  grid-column: 1;
  /* 1.9rem is the size the aircraft card already spends on an airport
   * code (`.asrm-fc-end-code`), so "a big airport code is this big" is
   * one number in the system rather than two that can drift apart.
   * `calc()` off `--asrm-fs-value` rather than a bare `rem` so the card
   * keeps scaling with the token. tokens.css is not this card's file, so
   * there is no `--asrm-fs-hero` to take — see the note in the task
   * report. */
  font-size: calc(var(--asrm-fs-value) * 2);
  line-height: 1.15;
  letter-spacing: 0.01em;
  color: var(--asrm-fg);
  white-space: nowrap;
}

.asrm-ad-hero-codes .asrm-hover-card__v {
  /* The word `Size` — small and muted on purpose. Under §28.5 this
   * string is doing real accessibility work: it is what tells a reader
   * who cannot separate the red from the green that the two digits are
   * the airport's size, and where in the pair to look.
   *
   * It spans the empty middle (tracks 2-3) and reads right, so the word
   * lands immediately before the digits it belongs to instead of trailing
   * behind the codes it does not. The `padding-right` is the gap between
   * the label and the pax digit — not a `margin-left`, which would push
   * the label away from the numbers rather than holding it at them. */
  grid-column: 2 / 4;
  text-align: right;
  padding-right: 6px;
  color: var(--asrm-fg-muted);
  font-size: var(--asrm-fs-title);
  white-space: nowrap;
}

.asrm-ad-hero-levels .asrm-hover-card__k {
  /* Pax level, first, red. Sized **identical to the codes** above
   * (`calc(--asrm-fs-value * 2)`, 30.4px), per the owner's 2026-09-29
   * instruction to "increase the size of the two size numbers to match
   * the IATA code". It started at 1.15rem (18.4px) for fit reasons, and
   * it still fits: the level pair rides tracks 4 and 5, both `auto`, and
   * the slack lives in track 2 (`minmax(0,1fr)`, the empty middle behind
   * the `Size` label), which shrinks before the digits do. Measured with
   * the real stylesheet rather than assumed — see the render check in the
   * task report.
   *
   * `line-height` is repeated rather than inherited because the codes get
   * theirs from a different selector; two independently-specified 1.15s
   * on the same baseline is what keeps `9/4` sitting on the codes' line
   * instead of drifting below it. */
  grid-column: 4;
  font-size: calc(var(--asrm-fs-value) * 2);
  line-height: 1.15;
  color: var(--asrm-pax);
}

.asrm-ad-hero-levels .asrm-hover-card__v {
  /* Cargo level, second, green. Same size as pax: neither half of the
   * pair is subordinate to the other, and a size difference would read
   * as a second fact rather than the other half of the first one. */
  grid-column: 5;
  font-size: calc(var(--asrm-fs-value) * 2);
  line-height: 1.15;
  color: var(--asrm-cargo);
}

/* The slash is generated content on the pax digit, because
 * `renderSection()` gives a row exactly two spans and a third run is not
 * available to me. It is deliberately **neutral** (`--asrm-fg`, neither
 * red nor green), matching `.asrm-fc-size-sep` on the aircraft card: a
 * coloured slash would read as a third category.
 *
 * The cost of generated content is that `textContent` does not contain
 * it, so `check-airport-detail.mjs` asserts the pair as two numbers with
 * two classes rather than as the string `9/4`. That is the honest
 * trade-off — the meaning is carried by `Size` and by pax-first
 * position, both of which are in the DOM, not by the slash. */
.asrm-ad-hero-levels .asrm-hover-card__k::after {
  content: "/";
  /* em, not px: the digits are now 30.4px instead of 18.4px, and a 1px
   * margin that looked fine at the old size reads as welded-on at the new
   * one. Scaling with the type keeps the slash's own air proportional. */
  margin: 0 0.08em;
  color: var(--asrm-fg);
}

/* The mock-up draws **one** separator on this card, between the facts
 * and the statistics. The shell's own rule puts one above every section
 * after the first, which would add a line between the hero and the facts
 * and cut the card into three blocks where the mock-up shows two. The
 * hero and the facts are one block; the rule above them is suppressed,
 * and the rule above `Statistics` is left exactly as the shell draws it. */
.asrm-hover-card__section:has(.asrm-ad-hero) + .asrm-hover-card__section {
  border-top: 0;
  margin-top: 2px;
  padding-top: 0;
}

/* The statistic sentences. Full-width list rows, not grid cells: `dep
 * 182 · arr 180/week by 9 airlines` inside a quarter-width track wraps
 * into fragments. The label column is the shell's 88 px, which every
 * label here (`Departures`, `Destinations`, `Free slots`) fits. */
.asrm-ad-stat .asrm-hover-card__v {
  overflow-wrap: break-word;
}

/* --- the section heading's right-aligned exit -----------------------
 *
 * The airport card's `Explain` used to ride inside the Free slots value
 * cell (owner, 2026-09-30: "can be on the same line as the Free slots so
 * the pop-up can be a bit smaller"). It has moved up into the section
 * heading (owner, 2026-10-04: "Move that up next to the `STATISTICS`
 * title, and right align it") because the anchor inside that row made the
 * row's minimum width exceed the card's: the popup grew a horizontal
 * scrollbar with one word on it. Inside the heading the link cannot widen
 * a value column, because the heading has no value column.
 *
 * `margin-left: auto` on the link is what right-aligns it against the
 * section name — the same mechanism the row version used, and the reason
 * no width is pinned anywhere. The heading keeps its own typography; only
 * its layout becomes a row, and only when a link is actually present.
 *
 * The Free slots row that used to host this link is now an ordinary value
 * row: the `display: flex` it needed to push the anchor right is gone with
 * the anchor, so its value and unit lay out inline like every other row's.
 */
.asrm-hover-card__section-title--with-link {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.asrm-hover-card__link--head {
  margin-left: auto;
  /* The link must never break across lines to fit beside a long heading.
   * The heading's own `letter-spacing` would otherwise be inherited into
   * the anchor and change its width under a font change. */
  white-space: nowrap;
}

/* A capped list inside a popup the visitor did not ask to scroll: without
 * this, 40 occurrence rows run off the bottom of a laptop viewport and the
 * rows below the fold might as well not exist. The airport card has four
 * rows and is unaffected. The scrollbar is styled by the browser; what
 * matters is that the content is reachable, and that the truncation line
 * above stays inside the scrollable area so it is seen with the rows it
 * qualifies. */
.asrm-hover-card__body {
  max-height: min(55vh, 420px);
  overflow-y: auto;
}

/* No transition on the card: it is opened by a click and closing it is a
 * deliberate act, and an animation on both reads as theatre around a
 * popup the visitor just asked to dismiss. The tooltip's 90 ms fade is
 * the only hover-related motion in this slice, and it is switched off
 * below. */
@media (prefers-reduced-motion: reduce) {
  .asrm-hover-tip {
    transition: none;
    transform: none;
  }

  .asrm-hover-tip:not([hidden]) {
    transform: none;
  }

  /* The airport-detail card is the same story: it replaces itself in
   * place, `loading…` becomes the answered sections, and animating that
   * would move text under the eyes of someone already reading it. Listed
   * in **this** block rather than a later one because the last
   * `prefers-reduced-motion` block is the operative one — a new block
   * added further down would leave these two classes covered only by
   * accident. */
  .asrm-ad-note,
  .asrm-ad-num {
    transition: none;
    animation: none;
  }
}

/* --- the free-slot legend (ADR-0050) ---------------------------------
 *
 * A map key, not a caption. It exists because the free-slot ramp encodes
 * an ordered quantity in colour, and SPEC §28.5 forbids colour as the
 * only channel — so the threshold is printed beside every swatch. A
 * reader who cannot tell the five fills apart still reads "0 %", "< 5 %",
 * "≥ 50 %" and knows exactly what the map is saying.
 *
 * Small on purpose, and set at `--asrm-fs-caption` (the body size) rather
 * than a smaller legend size: this is the most load-bearing text on the
 * map when the slot view is on, and the shell's whole type history here is
 * a warning about legends shrunk below 11 px.
 *
 * Opaque panel background rather than translucent: it sits over the map,
 * and a see-through legend over a dense network of gold lines is unreadable
 * in exactly the place it is needed.
 */
.asrm-slot-legend {
  background: var(--asrm-panel-bg);
  border: 1px solid var(--asrm-line);
  border-radius: 3px;
  padding: 8px 10px;
  color: var(--asrm-fg-muted);
  font-size: var(--asrm-fs-caption);
  line-height: 1.45;
  /* The legend must never eat a map drag. MapLibre's control container is
   * above the canvas, and a legend that swallows pointer events blocks
   * panning the map underneath a corner of it. */
  pointer-events: none;
  max-width: 170px;
}

.asrm-slot-legend__head {
  /* Caps + letter-spacing is the shell's label treatment, used here so the
   * legend reads as chrome belonging to the map rather than as data. */
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: var(--asrm-fs-title);
  color: var(--asrm-fg);
  margin-bottom: 6px;
}

.asrm-slot-legend__row {
  display: grid;
  grid-template-columns: 12px auto 1fr;
  align-items: center;
  gap: 7px;
}

.asrm-slot-legend__sw {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  /* The same white ring the slot circles carry on the map, so the swatch
   * and the thing it stands for are drawn the same way. Two ways of
   * drawing one symbol is how legends start lying about maps. */
  border: 1px solid #ffffff;
  box-sizing: border-box;
}

.asrm-slot-legend__label {
  color: var(--asrm-fg);
  white-space: nowrap;
}

.asrm-slot-legend__note {
  color: var(--asrm-fg-muted);
  white-space: nowrap;
}

/* The offset ruler bands (M-07-T11, third pass).
 *
 * Two full-viewport-width fixed bands: the top one carries the UTC offsets,
 * the bottom one the live local clock at each offset. They replace the old
 * `.asrm-zone-legend` chip box in the corner column, and the reason they
 * cannot live there is the whole point of this pass: a chip's horizontal
 * position **is** its offset's longitude on the map, so the band has to
 * span the map in the same coordinate space `map.project()` returns. In the
 * corner column, offset by the rail, every chip would need the rail's
 * current width subtracted — mid-transition, on every frame. Full-width at
 * `left: 0` means the projected x is the chip's x, with nothing to subtract
 * and nothing to re-measure when the rail animates. The rails simply paint
 * over the two ends.
 *
 * `z-index: 1500` — above the map canvas, below the rails' 2000 and the
 * corner chrome's 2100, so the bands tuck under the chrome that covers them
 * rather than fighting it, which is what keeps "the rails hide the ends"
 * true without the bands having to know the rails exist.
 *
 * **No `display` on the root.** Same pin that keeps `.asrm-slot-legend`
 * hideable: the bands are toggled with the `hidden` attribute from
 * `map.js::applyLayerState`, and a `display` rule here would beat the UA's
 * `[hidden] { display: none }` and strand two full-width bands on screen
 * with the layer off.
 *
 * The band is **drawn**, not just laid out — a translucent panel across the
 * top and bottom of the map, not a row of floating chips. That matters
 * because both bands are entries in `MAP_COVERS`, which excludes map area
 * from the in-view aircraft count: what a cover entry excludes must be what
 * is actually painted. Transparent bands claiming a covered rectangle would
 * under-count nothing and mis-report everything. */
.asrm-tzband {
  position: fixed;
  left: 0;
  right: 0;
  z-index: 1500;
  /* The band itself must never eat a map drag; the chips re-enable pointer
   * events for themselves, because those are the point of the band. */
  pointer-events: none;
  background: var(--asrm-panel-bg);
  color: var(--asrm-fg-muted);
  /* 11 px, not the 9–10 px these chips were drafted at. The shell enforces a
   * readability floor (`MIN_RENDERED_PX`, backend/tests
   * /test_rail_order_and_autoselect.py) because the status bar's *contrast*
   * was raised twice while its chips sat at 9.9 px — colour cannot rescue a
   * 10 px x-height. The ruler is the one element that must be read at a
   * glance mid-drag, so it starts at the floor rather than under it. */
  font-size: 11px;
  line-height: 1.15;
  border-top: 1px solid var(--asrm-line);
  border-bottom: 1px solid var(--asrm-line);
}

.asrm-tzband--top {
  top: 0;
}

/* Sits **above** the status bar rather than at the viewport bottom: the bar
 * is opaque chrome with its own cover entry, and a band laid over it would
 * hide the layer toggle's own readout while claiming to be over the map. */
.asrm-tzband--bottom {
  bottom: var(--asrm-statusbar-h);
}

/* When chips cannot be placed without overlapping — the world zoomed so far
 * out that an hour is narrower than a label — the band removes itself
 * entirely rather than show one lonely chip. `display: none` on this class
 * is deliberate, not a `visibility` toggle: the band's rectangle is in
 * `MAP_COVERS`, and a band that has nothing to show must stop excluding map
 * area in the same tick it stops painting. */
.asrm-tzband.is-too-tight {
  display: none;
}

/* Two lines of 11 px at line-height 1.15 = 25.3 px, plus the chip's own 2 px
 * border and 2 px vertical padding = 29.3. 30 px is the tightest row that
 * does not clip the second line, and the band is therefore
 * `border 1 + 30 + 30 + border 1 = 62 px` — the number
 * `check-visible-region.mjs` has to agree with. */
.asrm-tzband__row {
  position: relative;
  height: 30px;
}

/* The boundary geometry the ruler draws through its own strip.
 *
 * `z-index: 0` with the rows at `1` is not decoration. Both the canvas
 * and the rows are positioned, so DOM order alone would put the rows on
 * top — which is what is wanted — but a future rule giving a chip a
 * background or a `filter` would silently start hiding the lines. Naming
 * the two levels makes the intent the thing that breaks if it is broken.
 *
 * `pointer-events: none` because the band's chips are the interactive
 * thing; a 1 px line that swallows a chip click two pixels away is the
 * worst possible answer to a request about visibility.
 *
 * `inset: 0` rather than a set height so the canvas tracks the band
 * exactly if the row heights ever change; the drawing reads the rect and
 * sizes its backing store from it, so there is no second copy of the
 * height to keep in sync here. */
.asrm-tzband__lines {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.asrm-tzband__row {
  z-index: 1;
}

.asrm-tzchip {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: auto;
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  min-width: 2.4em;
  padding: 1px 3px;
  border: 1px solid var(--asrm-line);
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--asrm-fg-muted);
  font: inherit;
  /* Tabular figures, and this is not cosmetic: the bottom band's clock
   * digits change every minute, and proportional ones would make each chip
   * jitter in width as it re-renders. */
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  line-height: 1.15;
}

.asrm-tzchip__a {
  font-size: 11px;
  letter-spacing: 0.02em;
  min-height: 11px;
  color: var(--asrm-fg-muted);
}

.asrm-tzchip__b {
  font-size: 11px;
  color: var(--asrm-fg);
}

.asrm-tzchip:hover,
.asrm-tzchip:focus-visible {
  border-color: var(--asrm-accent);
  color: var(--asrm-fg);
  z-index: 2;
}

.asrm-tzchip.is-preview {
  border-color: var(--asrm-accent);
  color: var(--asrm-fg);
  background: rgba(255, 162, 0, 0.12);
}

.asrm-tzchip.is-on {
  background: var(--asrm-accent);
  border-color: var(--asrm-accent);
  color: #141414;
  font-weight: 700;
  z-index: 3;
}

.asrm-tzchip.is-on .asrm-tzchip__a,
.asrm-tzchip.is-on .asrm-tzchip__b {
  color: #141414;
}
