/* ASRouteMap 2.0 — design tokens (M-07 T1).
 *
 * The prototype had NO custom properties: every colour and every pixel value
 * was repeated inline across sidebars.css / map.html / the JS. That is why
 * its bars drifted — changing one width meant remembering four places, and
 * the height chain (#map 100% <- calc(100% - 30px) <- .statusbar{30px})
 * broke silently when any link was missed. Everything dimensional the shell
 * depends on is here once, and shell.css reads only tokens.
 *
 * Values are measured from the prototype (SPEC §13.2/§13.3/§13.4) where the
 * prototype was right, and deliberately different where it was not. Each
 * deviation carries its reason inline — these are reviewable choices, not
 * drift.
 */

:root {
  /* --- geometry ------------------------------------------------------ */
  /* Left rail: 35px collapsed, 200px on hover, CSS-only. The prototype's
   * trick is worth keeping: labels are always in the DOM and the bar's width
   * reveals them, so there is no JS toggle that can fail. 44px rather than
   * 35px so the collapsed strip is still a touch target (WCAG 2.5.5); the
   * prototype's 35px was below the minimum on a phone. */
  --asrm-rail-w: 44px;
  /**
   * Open width: 300px, clamped to 78vw.
   *
   * 200px was the original prototype value and it truncates real content.
   * The rail ellipsises airline names and hub detail lines
   * (`text-overflow: ellipsis` in shell.css), and the airline names in the
   * pilot data are simply longer than 200px renders — the owner reported
   * names and hub details being cut off, which is the ellipsis doing its
   * job correctly inside a box that is too small for the data.
   *
   * The `min()` is not decoration. A fixed 300px on a 375px phone is 80%
   * of the screen and leaves the map as an unreadable strip, which is a
   * worse complaint than the one being fixed. 78vw keeps the rail dominant
   * without ever hiding the thing it is drawn over.
   */
  --asrm-rail-w-open: min(300px, 78vw);

  /* Right rail: 40px, floating rather than full-height. Matches the
   * prototype (map.html:23-29) and leaves the map bleed to the edge. */
  --asrm-rail-right-w: 40px;
  /* What that rail actually occupies on screen: the 40px content box plus
   * its own 1px border on each side. `.asrm-rail` sets no `box-sizing`,
   * so 40px is the *inner* width and 42px is the outer one.
   *
   * Anything docked under the rail that is meant to share its edge must
   * match THIS token, not `--asrm-rail-right-w` — comparing a border-box
   * island's 40px min-width against a content-box rail's 42px outer is a
   * 2px gap nobody can see in the token list, and it is exactly how the
   * refresh island came to sit 4px off the rail it hangs below. */
  --asrm-rail-right-outer: calc(var(--asrm-rail-right-w) + 2px);
  --asrm-edge: 10px;

  /* Status bar: 30px full-width. */
  --asrm-statusbar-h: 30px;
  --asrm-drawer-max-h: 80vh;

  /* --- type --------------------------------------------------------- */
  /* **One body size for the whole shell.** Before this there were six: the
   * bar at 0.7rem, the rail at 0.78, drawer tables at 0.74, section
   * titles at 0.68, counts at 0.66 and the layer chips at 0.62 — 9.9px at
   * the small end, which is the actual reason the owner kept saying the
   * text was unreadable after two colour fixes. The colour was never the
   * whole problem; a second reader cannot fix a 10px x-height by making it
   * whiter.
   *
   * The owner put two requests in one sentence — "The text is still
   * impossible to read. Let's make it the same on all menus" — and they
   * turn out to be the same request. Six sizes is why it was unreadable;
   * one size is what "the same" means. Every readable string in the shell
   * is now `--asrm-fs-body`, and a test asserts no live font-size in the
   * stylesheets falls below 11px, so a future 0.6rem cannot slide back in
   * unnoticed.
   *
   * Two sizes remain and both are earned only on **uppercase,
   * letter-spaced labels** (the panel headings, "LAST INGEST PER SOURCE"),
   * where caps' taller glyphs hold their shape 1.3px smaller than
   * lowercase would. `--asrm-fs-caption` keeps its own name because a
   * count and a section heading are different things even at equal size,
   * but it is now defined *at* the body size: "4,866 airports" on the
   * status bar is the most-read number on the page and had no business
   * being 10.6px. A test pins that chrome resolves to body-or-title and
   * to nothing else.
   *
   * The prototype's status bar was `font-size: 0.5rem !important` — 8px,
   * with !important so nothing could override it. The bar stays short
   * because its content moved to the drawer, not because the text shrank. */
  --asrm-fs-body: 0.8rem; /* 12.8px — bar, rails, drawer, pop-ups */
  --asrm-fs-title: 0.72rem; /* 11.5px — uppercase section titles only */
  --asrm-fs-caption: var(--asrm-fs-body); /* counts, chips, footnotes */
  /* The dense-numeric size (M-07-T9): the cursor island's three lines and
   * the scale bar's label, where the owner asked for "small font" over the
   * map. Not `--asrm-fs-title` even though it is half a pixel larger: that
   * token is earned by uppercase letter-spaced labels, and reusing it for
   * mixed-case coordinates would put a second meaning under one name.
   * 11.2px, one notch above the 11px readable floor the chrome test
   * enforces — small on purpose, never smaller than legible. */
  --asrm-fs-micro: 0.7rem; /* 11.2px — cursor island, scale bar label */
  /**
   * The route card's flight-table size — 9px, and the **only** exception to
   * the 11px readable floor anywhere in the site.
   *
   * The owner asked for 8px ("The font size of the flights table has to be
   * reduced to fit more information on the same space. Possibly the font
   * size should be 8px", 2026-10-04) and ruled 9px on the follow-up, so
   * that the one sub-floor size on the page is a number somebody chose
   * rather than the smallest one available. The next day's route-popup
   * pass ("Increase the font size on the route pop-up by one notch. It is
   * currently too small") moved that chosen number one notch to 10px —
   * still the single sub-floor size on the site, still a ruling rather
   * than a minimum.
   *
   * It is a real token and it is **declared inside the test that enforces
   * the floor** —
   * `backend/tests/test_rail_order_and_autoselect.py::SUBFLOOR_EXCEPTIONS`
   * — and that placement is the entire point. `routecard.css` used to be
   * outside that test's scan altogether, so `calc(var(--asrm-fs-body) *
   * 0.703)` = 9.0px there would have passed every check in this repo
   * without a single line of test noticing, which is an unpoliced loophole
   * and not an exception. Declaring it where the rule lives means: there is
   * exactly one sub-floor size, it has a name, its exact pixel value is
   * pinned, and widening it to a second file or shrinking it to 8px turns
   * the suite red rather than shipping quietly.
   *
   * Legitimate because the table is seven columns of codes and clock times
   * that a visitor reads by column position, not by prose — the same
   * dense-numeric justification as `--asrm-fs-micro`, pushed one step
   * further because a flight list that scrolls twice for what fits once is
   * the complaint this answers.
   */
  --asrm-fs-table: 0.625rem; /* 10px — the route card's flight grid ONLY */
  /* The emphasis size a card was already using for its title before this
   * token existed. It is a token now because the airport card's 4-column
   * grid needs the *same* emphasis on its value cells, and a second
   * literal 0.95rem in shell.css would be a second copy free to drift.
   * Three literals became one name; this does not add a third step to the
   * scale — 0.95rem was already rendered by `.asrm-hover-card__title`. */
  --asrm-fs-value: 0.95rem; /* 15.2px — card title, card value cells */
  /* Deprecated aliases kept so any straggler rule resolves to the body
   * size rather than to nothing. New rules must not use these. */
  --asrm-fs-bar: var(--asrm-fs-body);
  --asrm-fs-rail: var(--asrm-fs-body);
  --asrm-fs-icon: 16px;

  /* --- palette ------------------------------------------------------ */
  /* Measured from the prototype. Named by role, not by hue, so a palette
   * change is this file only. */
  --asrm-bar-bg: #000000;
  --asrm-panel-bg: #222222;
  --asrm-panel-bg-raised: #2c2c2c;
  --asrm-fg: #ffffff;
  /* Near-white, not grey (owner, M-07-T4: "the grey font color provides
   * too little contrast and makes it difficult to read the text,
   * especially on the status bar"). Measured rather than eyeballed:
   *   #9aa0a6  8.0:1 on #000   6.0:1 on #222   <- what it was
   *   #e8eaed 17.4:1 on #000  13.2:1 on #222   <- now
   *   #ffffff 21.0:1 on #000  15.9:1 on #222
   * Pure white was offered and declined: with `--asrm-fg` already white,
   * making the muted token white too leaves no notch between "this is the
   * value" and "this is the label under it", and the rails turn into one
   * flat wall of equal-weight text. 13.2:1 is still above AAA (7:1) at
   * every size used here, with the hierarchy intact. A test asserts the
   * token is not #ffffff so it cannot be "fixed" flat later. */
  --asrm-fg-muted: #e8eaed;
  --asrm-accent: #ffa200;
  --asrm-line: #3a3a3a;

  /* Traffic kind: **gold = passenger, blue = cargo** (ADR-0050).
   *
   * The prototype's red=pax / green=cargo was swapped out because red and
   * green are wanted for a second, unrelated encoding — the free-slot ramp
   * on airport circles (SPEC §28.5 amendment, ADR-0050). Two encodings
   * cannot share one colour pair: an airport drawn "green" would mean both
   * "cargo airport" and "wide open", and neither reading survives.
   *
   * Gold and blue are not a preference, they are the pair that survives the
   * constraints that were measured for this change:
   *   - **Blue vs yellow is the safest pairing under colour-blindness**,
   *     which is the whole point of §28.5. Red/green was the worst pair.
   *   - pax gold `#ffd23f` sits **ΔL 0.206 above** the brand accent
   *     `#ffa200`, so the traffic colour and the brand colour cannot be
   *     confused by lightness alone.
   *   - The airframes went neutral (ADR-0050), which is what freed the
   *     gold in the first place: the gold moved off the plane and onto the
   *     network the plane is flying.
   *
   * Kind still does NOT rest on hue. Dash carries it — pax **solid**,
   * cargo **dashed [2,2]**, unknown **dotted [0.5,2]** — and a highlight
   * keeps its dash pattern (§28.5, ADR-0039). The colour is a shortcut,
   * the dash is the channel. */
  --asrm-pax: #ffd23f;
  --asrm-cargo: #2f81f7;
  /* Third traffic class, for a route whose kind was never determined. It
   * must read as *absent information*, not as a paler pax or a sadder
   * cargo — hence a neutral grey rather than a tint of either. The value
   * is the one `map.js::ROUTE_STYLE.unknown.color` draws the line with,
   * and `check-route-detail.mjs` asserts the two stay equal so the dot on
   * the map and the dot on the card cannot drift apart. */
  --asrm-unknown: #9a9a9a;
  /* Warning and brand amber were two near-identical ambers (#ffc53d and
   * #ffa200, ΔL 0.144) sitting in the same hue neighbourhood as the new
   * pax gold. Three indistinguishable ambers on one page is one too many,
   * so `warn` collapses onto the brand amber rather than competing with
   * it. This is a deliberate merge, not a loss: a warning that has to be
   * a slightly different yellow than the brand has never been a warning. */
  --asrm-warn: var(--asrm-accent);
  /* Status colours stay red/green. They now sit on a different object from
   * any traffic colour — they mark *states* (a failing job, a stale
   * record) in chrome, never a line or an airport circle — so the pair is
   * not ambiguous with the slot ramp either, which reads "red = full"
   * exactly as these read "red = bad". Freeing red/green from *traffic
   * kind* is what made them safe to keep here. */
  --asrm-danger: #e5484d;
  --asrm-ok: #46a758;

  /* --- free-slot ramp ----------------------------------------------- */
  /* Five absolute bands over `Airport.slots_free_computed` — the share of
   * an airport's weekly departure capacity still unused (ADR-0049). Red =
   * full, green = free.
   *
   * **Absolute thresholds, not quantiles, and that was the decisive find**
   * (ADR-0050). Equal-population quintiles over the live data put the
   * "most constrained" band at free 0.000..0.934 — because 95 % of
   * airports are ≥50 % free, so forcing equal populations means that
   * band is 517 airports (world A) / 496 (world B) that are ≥80 %
   * free. A quantile ramp would paint wide-open airports scarce, which is
   * the exact opposite of what the ramp is for.
   *
   * The absolute bands instead split the population where the data
   * actually varies, and every band is populated in BOTH pilot worlds
   * (measured 2026-10-05 over the recomputed `slots_free`, so the legend
   * never shows a dead swatch):
   *
   *                    world A (4,846 computed)  world B (4,887)
   *   < 5 %   full      24  (0.5 %)       59  (1.2 %)
   *   5-20 %  tight     84  (1.7 %)       64  (1.3 %)
   *   20-50 % narrow   126  (2.6 %)      135  (2.8 %)
   *   50-70 % loose    107  (2.2 %)      107  (2.2 %)
   *   >=70 %  free   4,505 (93.0 %)    4,522 (92.5 %)
   *
   * The same day's second pass (owner) dropped the separate `0 %` band —
   * an airport with nothing left is `< 5 % full` like every other
   * exhausted one — and took every colour down one level, because over
   * the pale basemap the old ramp read pastel. The thresholds moved with
   * the labels; the hue order did not move at all.
   *
   * **Colour is not the only channel here, and it had to be that way by
   * text rather than by lightness.** A red→green ramp cannot be ordered by
   * luminance — yellow is the lightest hue, so the ramp bulges in the
   * middle and the middle pair runs backwards (measured neighbour ΔL:
   * +0.166 / +0.124 / -0.048 / -0.132). A greyscale reader therefore
   * cannot rank the bands, and no claim to the contrary is made. §28.5 is
   * satisfied by two *text* channels instead: the legend prints the exact
   * threshold of every band, and the airport card prints the exact
   * percentage. Circle radius is a third channel (traffic), independent of
   * the ramp.
   *
   * What the ramp IS ordered by is hue, against a floor: every neighbour
   * pair must differ by >= 15 deg (measured 21.0 / 16.3 / 46.5 / 58.4 on
   * the darkened ramp; `check-slot-ramp` enforces the floor). The white
   * stroke on slot circles stays load-bearing, not polish — a darkened
   * fill on a dim basemap needs its edge more, not less — and every band
   * keeps >= 98 of RGB chroma against the canvas's 6, which is why the
   * hues read at all. */
  --asrm-slot-full: #a3221a;   /* < 5 % free — full                */
  --asrm-slot-tight: #c25a12;  /* 5 % .. 20 % free — tight         */
  --asrm-slot-narrow: #b5851f;  /* 20 % .. 50 % free — narrow      */
  --asrm-slot-loose: #679431;  /* 50 % .. 70 % free — loose       */
  --asrm-slot-open: #187a42;   /* >= 70 % free — free              */
  /* Slot data absent (`slots_free_computed` is null) draws the same
   * neutral grey as an undetermined route kind, for the same reason: "we
   * have not computed this" must not read as either full or open. A real
   * 0 % and a missing value are different facts and must not collide on
   * the map (R10 — never let an absent value masquerade as a value). */
  --asrm-slot-unknown: var(--asrm-unknown);

  /* --- motion ------------------------------------------------------- */
  /* Rail expansion is instant on hover; 120ms keeps it from feeling glued
   * without being a animation the user waits on. Reduced-motion is honoured
   * in shell.css, not here. */
  --asrm-rail-transition: 120ms ease-out;
}
