/* ═══════════════════════════════════════════════════════════════════════════
   assets/site.css — the WiFi Odds design system. ONE file, every page.

   THE FORECAST. The reference is a weather forecast delivered by a colleague who
   flies every week: warm paper, two voices in type, and colour that only ever
   means something. It replaced The Plate, whose reference was an approach plate
   — a document written for a trained, calm, paid reader, which ours is not. The
   spec is public/wifiodds-fable/ARCHETYPES.md in the websites repo; where this
   file and that file disagree, that file wins.

   THE COLOUR RULE. Colour has two owners and no third.
   -----------------------------------------------------
   THE BANDS own every number. Green 60-100, amber 40-59, clay 1-39, grey for
   zero and for every projection. Scores, odds, badges and score chips wear a
   band, through --band, which the .sc-* classes set.

   THE SKY owns the chrome. Links, buttons, kickers, the rule at the top of every
   page, the homepage banner and the orbit mark. One blue at two altitudes —
   stratosphere-deep over the paper, pale night-sky over the dark ground — and
   always flat. No gradients, no glows, no starfields. The old site had a
   starfield canvas and a gradient wordmark and both were deleted for cause.

   Nothing that encodes a number may take the sky. Nothing decorative may take a
   band. Provenance lines stay muted ink, because they sit inside the data's
   screen space. The first chart line that borrows green is the day the score
   stops meaning anything, and the first score that turns blue is the day links
   stop meaning anything.

   The projected score is the standing exception and it is a NEGATIVE one: it
   never takes --band. Grey outline, always. See .proj near the end of this file.

   TWO THEMES, ONE TOKEN SET
   -------------------------
   LIGHT IS THE DEFAULT (:root) and it is the register decision, not a taste one:
   cockpits are dark because they fly at night, and the reader booking a Tuesday
   flight is not. Dark is the same room with the lamp on — a warm brown-black
   ground and cream ink, never a cold inversion of the light theme.

   The dark values appear twice because CSS cannot alias a declaration block:
   once under the OS preference, so pages are correct with JavaScript off, and
   once under the manual .dark class the masthead switch sets. IF YOU CHANGE A
   DARK VALUE, CHANGE IT IN BOTH PLACES.

   NOTHING IS STORED. No cookie, no localStorage, no fingerprint. The switch
   holds its state in a class on <html> until you reload, and that is the ceiling
   for cleverness here. The old build wrote localStorage.woTheme; that key is
   gone, and so is the boot script that read it.

   TYPE — two voices, one webfont, 41 KB
   -------------------------------------
   THE SERIF SPEAKS TO THE READER: headings, the say-sentence, the wordmark, and
   every large figure. Source Serif 4 (Adobe, SIL OFL), self-hosted, subset, two
   real weights. Self-hosted rather than a system stack because Charter/Georgia/
   Noto Serif is three different faces for one voice depending on who is reading.

   THE SANS REPORTS: tables, labels, provenance, footnotes, micro-caps. That one
   stays on system-ui and costs zero bytes. It is the right call rather than a
   budget one — this voice lives at 12 to 14px, where the reader's own OS face is
   already hinted and rasterised for their screen, and where an unhinted webfont
   is measurably worse. A table set in the platform's own UI face also reads as
   the system reporting, which is the register the spec asks for.

   Source Serif's figures are tabular by default (all ten digits measure 529
   units), so a numeral does not change width when it changes value. That was the
   whole job B612 Mono was doing, and it no longer needs a fourth font file.
   --mono is a system stack now and is reserved for what is literally code: the
   API paths, the formula blocks.

   Regenerate the faces with `python3 build/make-brand.py fonts`.
   Licence: assets/SourceSerif4-OFL.txt.

   ANIMATION IS A PRIVILEGE, NOT A DEFAULT. Every animated rule is scoped under
   html.anim, a class site.js adds only when prefers-reduced-motion is
   no-preference. No JS / reduced motion => the page is already finished.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Source Serif 4, self-hosted ──────────────────────────────────────────
   Subset with fontTools to Latin plus the punctuation and symbols the pages
   actually use (checked by counting the non-ASCII characters in the built HTML,
   not by guessing). font-display:swap, so a slow font never blocks the text —
   and the fallback stack below is metric-adjacent enough that the swap does not
   reflow a heading into a second line. */
@font-face{font-family:"Source Serif 4";font-style:normal;font-weight:400;font-display:swap;
  src:url(/assets/serif-400.woff2) format("woff2")}
@font-face{font-family:"Source Serif 4";font-style:normal;font-weight:700;font-display:swap;
  src:url(/assets/serif-700.woff2) format("woff2")}

:root{
  /* ── the two voices, plus code ── */
  /* ONE FACE, 28 Jul 2026. The approved interior system (Codex, round 16) sets
     Inter on <body> and overrides nothing but .mono, and the V5 homepage does the
     same. Interior routes were still rendering headings in Source Serif 4 while
     the homepage rendered Inter, so a visitor crossing from / to /united/fleet/
     changed typeface mid-session. Both tokens now resolve to the same stack.
     The names are kept because ~40 rules reference them; --serif now means
     "display" and --sans means "UI", not two different faces. */
  --serif:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --sans:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --mono:ui-monospace,"SF Mono",Menlo,"DejaVu Sans Mono",Consolas,monospace;

  color-scheme:light;
  /* ── surfaces and ink. Warm paper, warm near-black. ── */
  --paper:#fbf8f2; --panel:#f4eee2; --card:#ffffff; --field:#ffffff;
  --ink:#29241c; --ink-soft:#443d32; --muted:#6e6557; --muted-strong:#59503f;
  --line:#e3daca; --line-soft:#ece5d6;
  /* --field-border is SCOPED to form-control resting borders. --line itself
     measures 1.20-1.43:1 against --field in both themes — fine for a card
     edge or a divider, not enough for a control boundary, which needs 3:1
     (WCAG 1.4.11). Reusing --sky here would fix the ratio but make a field's
     resting and focused borders identical, trading this failure for a
     2.4.7 one, so this is a new token rather than a --line retune or a
     --sky reuse. Verified 3.60-4.24:1 against paper/panel/card/field. */
  --field-border:#91794d;

  /* ── the bands. Every number on the site, and nothing else. ── */
  --good:#1e7a46; --mixed:#975e00; --long:#a84b2f; --zero:#716a62;
  --proj:#6e675c; --proj-bg:#efe9dc;

  /* ── the sky. Every piece of chrome, and nothing else. ── */
  --sky:#2d5a7d; --sky-deep:#22455f; --sky-fg:#e9f1f7;
  --link:var(--sky); --placeholder:#9b917f;
  --btn-bg:var(--sky); --btn-fg:#f5f9fc;
  --sheen:none;

  /* --band is what every accented surface reads. Un-scored chrome inherits the
     muted ink, which is the correct answer for chrome. */
  --band:var(--muted-strong);

  /* ── the six-stop score arc ──────────────────────────────────────────────
     build/lib/pages.js bands at 85/60/35/20/5 and emits six .sc-* classes. The
     spec has four bands. Six map onto four, so a score cannot invent a fifth
     hue: exc and good are both green, long and rare are both clay. The one real
     seam is at 35-39, which pages.js calls `mix` (amber) and the spec calls clay
     — moving that threshold is a data-layer change and does not belong here. */
  --s-exc:var(--good);
  --s-good:var(--good);
  --s-mix:var(--mixed);
  --s-long:var(--long);
  --s-rare:var(--long);
  --s-no:var(--zero);

  /* ── compatibility aliases ───────────────────────────────────────────────
     build/lib/render.js, build/lib/viz.js, build/lib/pages.js and the four page
     templates reference these names inline (var(--faint) in a span style,
     var(--r-md) in a border-radius, and so on). They are kept as aliases onto
     the tokens above so that a re-skin of this file cannot silently blank out an
     SVG or a caption in a file it does not own. New rules should use the tokens
     above. --gold/--green/--blue/--red are only ever read inside .plc and .rlc,
     both of which redeclare them; they are aliased here so that nothing goes
     undefined if that ever stops being true. */
  --bg:var(--paper);
  --bg-panel:var(--panel);
  --bg-inset:var(--panel);
  --ink-2:var(--ink-soft);
  --ink-3:var(--muted);
  --rule:var(--line-soft);
  --rule-2:var(--line);
  --low:var(--long);
  --none:var(--zero);
  --surface:var(--panel);
  --edge:var(--line);
  --edge-strong:var(--line);
  --body:var(--ink-soft);
  --dim:var(--ink-soft);
  --faint:var(--muted);
  --accent:var(--ink);
  --brand:var(--sky);
  --brand-deep:var(--sky-deep);
  --brand-lift:var(--sky);
  --sky-bright:var(--sky);
  --gold:var(--mixed);  --green:var(--good);  --blue:var(--sky);
  --amber:var(--mixed); --red:var(--long);
  /* Interior-system-v1 port (28 Jul 2026): the masthead CTA pill and the
     page-head glow want the same cyan/violet pair the dark palette already
     carries under --sky. Light mode has no --cyan/--violet of its own, so
     these are new tokens rather than a --sky retune — the pill still reads
     as an accent rather than a link in both themes. Dark values are set
     alongside --sky in :root.dark and its prefers-color-scheme twin. */
  --cyan:var(--sky); --violet:#5b4a99;
  --field-bg:var(--field);
  --shadow:none;
  --bg-glow-a:transparent; --bg-glow-b:transparent;

  /* ── chart semantics — used by every visualization, no exceptions ────────
     A chart line is not a score, so lines are INK, told apart by weight and dash
     pattern and labelled at the line's end. The waffle's lit cells are the one
     exception and they earn it: a lit cell is a count of equipped aircraft, so
     it takes the green it would take as a number. */
  --viz-equipped:var(--good);
  --viz-empty:var(--panel);
  --viz-mainline:var(--ink);
  --viz-express:var(--muted);
  --sys-starlink:var(--ink); --sys-leo:var(--muted); --sys-viasat:var(--muted-strong);
  --sys-legacy:var(--muted);  --sys-none:transparent;

  /* radii. The Forecast has soft corners; the Plate had none. Kept as tokens
     because render.js asks for var(--r-md) in an inline style. */
  --r-sm:6px; --r-md:8px; --r-lg:10px; --r-xl:12px;
  /* type scale. Body never drops below 16px — that is a spec line, not a taste. */
  --fs-body:16px; --fs-small:14px; --fs-micro:12px;
}

:root.dark{
  color-scheme:dark;
  /* V5 THEME PORT (28 Jul 2026). Approved palette: design-competition/theme-v5.css.
     Ground goes near-black, ink goes pure white, the chrome accent becomes the
     cyan/violet pair, and the score bands get slightly brighter siblings. See
     the --line note just below for the one value this port did NOT take
     literally, and why. */
  --paper:#050505; --panel:#0d0d0f; --panel2:#121216; --card:#121216; --field:#0d0d0f;
  --ink:#ffffff; --ink-soft:#d3d3d8; --muted:#a6a6ad; --muted-strong:#c0c0c7;
  /* The approved palette gives --line the hex #29292f. Measured against
     --panel/--card that is ~1.3:1 — the exact invisible-border shape Option A
     fixed on 27 Jul 2026 (raised from ~1.2:1 to ~3.2:1 for the card/board
     frame, WCAG 1.4.11). Taking #29292f literally as --line would undo that
     fix, so the approved hex is kept here as --line-soft instead (internal
     row hairlines, no 3:1 floor), and --line gets a lighter neutral, #65656d,
     which measures ~3.0-3.5:1 against panel/card/paper. --field-border
     matches --line for the same reason (control boundary, not a hairline).
     This token exists in TWO places (this block and the
     @media(prefers-color-scheme:dark) twin below) — build/apitest.js asserts
     they match, so a change here without its twin fails the build. */
  --line:#65656d; --line-soft:#29292f;
  --field-border:#65656d;
  --good:#54f09b; --mixed:#ffd45a; --long:#ff698d; --zero:#87878f; --unknown:#85858e;
  --proj:#c0c0c7; --proj-bg:#121216;
  --sky:#29d8ff; --sky-deep:#0b2e38; --sky-fg:#c9f0fa;
  --link:var(--sky); --placeholder:#6d6d75;
  --cyan:#29d8ff; --violet:#926cff;
  --btn-bg:#29d8ff; --btn-fg:#052029;
  --sheen:inset 0 1px 0 rgba(255,255,255,.05);
  --viz-empty:var(--panel);
}

@media (prefers-color-scheme:dark){
  :root:not(.light){
    color-scheme:dark;
    --paper:#050505; --panel:#0d0d0f; --panel2:#121216; --card:#121216; --field:#0d0d0f;
    --ink:#ffffff; --ink-soft:#d3d3d8; --muted:#a6a6ad; --muted-strong:#c0c0c7;
    /* twin of :root.dark's --line/--line-soft/--field-border above; keep them
       identical (build/apitest.js checks it). See that block for why --line
       is #65656d rather than the approved palette's literal #29292f. */
    --line:#65656d; --line-soft:#29292f;
    --field-border:#65656d;
    --good:#54f09b; --mixed:#ffd45a; --long:#ff698d; --zero:#87878f; --unknown:#85858e;
    --proj:#c0c0c7; --proj-bg:#121216;
    --sky:#29d8ff; --sky-deep:#0b2e38; --sky-fg:#c9f0fa;
    --link:var(--sky); --placeholder:#6d6d75;
    --cyan:#29d8ff; --violet:#926cff;
    --btn-bg:#29d8ff; --btn-fg:#052029;
    --sheen:inset 0 1px 0 rgba(255,255,255,.05);
    --viz-empty:var(--panel);
  }
}

/* ── the band map ─────────────────────────────────────────────────────────
   Put a .sc-* class on any element and its whole subtree accents to that score.
   [data-band] is the same switch under the name the older markup used. */
.sc-exc {--band:var(--s-exc)}
.sc-good{--band:var(--s-good)}
.sc-mix {--band:var(--s-mix)}
.sc-long{--band:var(--s-long)}
.sc-rare{--band:var(--s-rare)}
.sc-no  {--band:var(--s-no)}
[data-band="good"] {--band:var(--good)}
[data-band="mixed"]{--band:var(--mixed)}
[data-band="low"]  {--band:var(--long)}
[data-band="none"] {--band:var(--zero)}
.s-good{color:var(--good)} .s-mixed{color:var(--mixed)}
.s-long{color:var(--long)} .s-zero{color:var(--zero)}

/* ── base ─────────────────────────────────────────────────────────────── */
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{
  background:var(--paper);
  color:var(--ink);
  font:400 var(--fs-body)/1.55 var(--sans);
  -webkit-font-smoothing:antialiased;min-height:100vh;
  -webkit-tap-highlight-color:transparent;
  /* Every page opens on a thin rule of sky at its top edge. On the homepage the
     extension banner sits directly under it in the same colour, so the rule
     reads as the banner's top edge there. */
  border-top:4px solid var(--sky-deep);
}
/* Links are the sky and they are underlined. An underline that thickens on
   hover is the whole interaction; there is no colour change, because the colour
   already means "this is chrome you can press". */
a{color:var(--link);text-decoration:underline;text-underline-offset:2px;
  text-decoration-thickness:1px}
a:hover{text-decoration-thickness:2px}
/* Tracking is per-size, not one value. Inter needs far tighter tracking than
   Source Serif 4 did at display sizes and roughly the same at body sizes, so a
   single -.012em would have left every heading looking loose after the face
   change. The steps below match the approved system: -.055em at 36-60px,
   -.035em at 22px, -.03em at 19px. */
h1,h2,h3,h4{font-family:var(--serif);color:var(--ink);font-weight:750;
  letter-spacing:-.03em;line-height:1.14}
h1{letter-spacing:-.045em}
h2{letter-spacing:-.038em}
/* THE SPEAKING VOICE. Anything that sounds like a colleague talking takes the
   serif; anything that reports takes the sans. Read the block aloud if unsure. */
.say,.lede,.sec-lede,.callout p,.moves b,dl.loop dt{font-family:var(--serif)}
.lede,.sec-lede{font-size:1.06rem;color:var(--ink-soft);max-width:44rem}
.lede b,.sec-lede b{color:var(--ink);font-weight:700}
.say{font-size:1.18rem;line-height:1.5;margin:.35rem 0 .6rem;max-width:38rem}
/* THE FIGURES. Serif, tabular by default in this face, so a digit does not
   change width when it changes value. If you add a class that holds a large
   number, add it here rather than setting font-family locally. */
.n,.sco,.big,.v,.fa-big,.chip .n,.stat .n,.kv .v,.scorenum .n,.proj .pv,
.usrow .sco{
  font-family:var(--serif);font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1}
/* THE REPORTING NUMERALS. Sans, tabular. Table cells, ranks, counts, captions. */
.num,.ct,.dn,.rank,.tblcap,.fa-as,td.num,th.num,input{
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
/* Literal code: API paths, formula blocks. The only monospace on the site. */
.mono,.code{font-family:var(--mono)}
/* The column is a column, not a ruled margin. The Plate drew hairlines and
   ticks down both edges of .wrap; that was the plate idiom and it left with it. */
/* ONE right edge (round seven, note 2). The wrap narrows to 58rem and prose,
   small graphics and both boards all fill it: at 1280 that is one 888px line
   box for everything in main. The boards still fit — their scroll floor is
   52rem — and single-sentence ledes carry the width; anything longer than two
   lines is structured (board footers, release states) rather than set as a
   raw paragraph. The max-width:none group below is the other half of the same
   note. */
/* 1240px since 28 Jul 2026, was 58rem/928px. The homepage shipped at 1240 and
   the interiors did not, so the two halves of the site had visibly different
   canvases while sharing every token. Round 17 measured it: 1240 vs 928 shell,
   888px main.
   Widening the SHELL is not the same as widening the TEXT, and the group below
   is what keeps them apart: data (tables, boards, grids, charts) gets the full
   1240, prose gets a measure. Without that second half this rule would set
   120-character lines, which is worse than the narrow canvas it replaced. */
.wrap{max-width:var(--max,77.5rem);margin:0 auto;position:relative;
  padding:0 max(1.25rem,env(safe-area-inset-right)) 0 max(1.25rem,env(safe-area-inset-left))}
main .lede,main .sec-lede,main .footnote,main .skycap,main .src,
main .prov,main ul.vstate,main .board-note,main .pointer,main .extfine,
.rankb .sortcap p,.sysrows,.herocall{max-width:none}
/* THE READING MEASURE, and it is the other half of the 1240px wrap above.
   `ch` not `rem`, so the cap follows the type rather than the root size. 74ch
   lands around 66-70 characters of running text in Inter, which is the top of
   the comfortable band; going wider is what makes a wide canvas feel like a
   regression rather than an improvement.
   Scoped to the direct text children of a section, so it reaches paragraphs and
   ledes and NOT the things that legitimately want 1240: tables, boards, grids,
   charts, card rows, the rollout curve. Those are data, and data reads better
   wide. Anything that needs to opt out can set `max-width:none` locally, and
   the group directly above is the existing list of things that already do. */
main p,main .lede,main .sec-lede,main .say,main dl.loop dd,main .callout p{max-width:74ch}
/* The hero used to end on a 96px chart and a caption, so the standard section
   rhythm below it (51px margin + 37px padding) read as normal. It now ends on
   the bordered extension card, which already separates it from what follows, so
   the same rhythm plus the section's own rule left 100px of dead air between
   the card and "THE BIG 4". Scoped to the hero's next sibling: every other
   section on every page keeps the site rhythm. */
header.hero+.blk{margin-top:1.4rem;padding-top:1.6rem}
.prose{max-width:44rem}
:focus-visible{outline:2px solid var(--sky);outline-offset:2px}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
/* The skip link. Hidden exactly like .vh until it takes focus, so it costs the
   page nothing at rest; it gets its own class rather than reusing .vh because
   .vh is also used for labels that must NEVER become visible. Colours are the
   button pair, which measures 6.9:1 in both themes. */
.skip{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.skip:focus{position:fixed;top:.5rem;left:.5rem;width:auto;height:auto;overflow:visible;
  clip:auto;z-index:60;padding:.45rem .8rem;border-radius:var(--r-md);
  background:var(--btn-bg);color:var(--btn-fg);text-decoration:none}
/* (0,2,1) on purpose. The bare `.needs-js{display:none}` this replaced was
   (0,1,0), so a rule on the wrapped element such as `.filters{display:flex}`
   outranked it and left a JS-only control showing with script off.

   There was an `html.js .needs-js{display:revert}` partner here until 27 Jul
   2026. It was redundant: `html:not(.js)` and `html.js` can never both match,
   so with script on nothing hides the element and it already keeps whatever
   display its own rules give it. It was also harmful. At (0,2,1) it outranked
   `.filters{display:flex}` and `.rlc-nav{display:flex}` at (0,1,0), and
   `revert` drops a div to the UA default `block`, which collapsed the Rank-by
   rows and the reel arrows for every reader running JavaScript, and made their
   `gap` inert. Do not add an enable rule back. Put layout on the element and
   let the disable rule lapse. `build/apitest.js` asserts this rule is absent. */
html:not(.js) .needs-js{display:none}
html.js .no-js-only{display:none}

/* ── the masthead ─────────────────────────────────────────────────────────
   Orbit mark, serif wordmark, nav, the theme switch and the datechip. Every page
   carries it, and the date in it comes from united/data.json by way of
   H.page({updated}). Nothing here is hard-coded; a stale date is worse than
   no date. */
header.site{border-bottom:1px solid var(--line);background:var(--paper)}
/* ── the header stays with you (round seven, note 18) ─────────────────────
   position:sticky is pure CSS, so the sticky part works with script off. The
   shrink is a JS enhancement: site.js adds html.hdr-c past 170px of scroll and
   removes it under 40px — a hysteresis gap ~3x the height the header gives up,
   so the change can never re-cross its own trigger and flicker. */
header.site{position:sticky;top:0;z-index:40}
/* ── ONE header-height token (Phase 1c) ──────────────────────────────────
   The masthead consumes it as a min-height and every sticky table heading
   consumes it as its offset, so the two read the same number and cannot
   drift; anchor targets clear both through the scroll-margin-top on [id]
   above. Values are the measured tallest masthead per width band on the
   built pages (wordmark row + datechip row + border), with the compact
   html.hdr-c state — the state a scrolled reader is actually in — getting
   its own smaller value so a stuck heading does not float mid-air. */
:root{--sticky-header-height:6.6rem}
@media(max-width:880px){:root{--sticky-header-height:7rem}}
@media(max-width:600px){:root{--sticky-header-height:7.75rem}}
html.hdr-c{--sticky-header-height:4.4rem}
@media(max-width:880px){html.hdr-c{--sticky-header-height:4.7rem}}
.masthead{min-height:calc(var(--sticky-header-height) - 1px)}
.masthead{transition:padding .22s ease}
.wordmark{transition:font-size .22s ease}
.datechip{max-height:2.6rem;overflow:hidden;transition:max-height .22s ease,opacity .18s ease}
html.hdr-c .masthead{padding-top:.5rem;padding-bottom:.5rem}
html.hdr-c .wordmark{font-size:1.08rem}
html.hdr-c .datechip{max-height:0;opacity:0}
@media(prefers-reduced-motion:reduce){.masthead,.wordmark,.datechip{transition:none}}
/* anchors land clear of the sticky header */
html{scroll-padding-top:calc(var(--sticky-header-height) + .5rem)}
[id]{scroll-margin-top:calc(var(--sticky-header-height) + .5rem)}
/* ── the phone menu is a checkbox and a label, so it opens script-free.
   Its media rules live in the 600px block of the RESPONSIVE section, AFTER the
   base `.masthead nav` rule they override — order is what wins at equal
   specificity, and an earlier `display:none` would lose to the later flex. */
.nav-cb{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.nav-btn{display:none}
/* padding-top/bottom, never the shorthand. .masthead is also a .wrap, and
   `padding:1rem 0` silently zeroed the horizontal padding .wrap provides, so
   the wordmark, nav and datechip sat flush to the screen edge while the body
   stayed inset. Below 431px a later .wrap rule happened to restore it, which
   is exactly why it hid: every narrow width I tested looked correct. */
.masthead{display:flex;align-items:baseline;gap:.6rem 1.25rem;flex-wrap:wrap;
  padding-top:1rem;padding-bottom:1rem}
.wordmark{font-family:var(--sans);font-size:1.3rem;font-weight:850;letter-spacing:-.03em;
  color:var(--ink);text-decoration:none;white-space:nowrap}
.wordmark:hover{text-decoration:none}
/* The orbit mark. Inline SVG drawn in currentColor; this sets the colour, and
   the colour is the sky, because the mark is chrome. It is inline in the
   document so the custom property resolves — inside an <img> it would silently
   fall back to black. */
.wordmark .mk{color:var(--sky);vertical-align:-.12em;margin-right:.35rem}
/* The 26 Jul pivot: underlined default-blue nav read as nobody decided this.
   Small-caps wordmarks now, hover fill, no underline. The Extension link is the
   one FILLED element because it is the only nav item that is an action rather
   than a place — still one pitch sitewide, just dressed as what it is. */
.masthead nav{margin-left:auto;display:flex;gap:.2rem;font-size:.92rem;flex-wrap:wrap;
  align-items:center}
/* 12px floor (Phase 1c), and the box stays a single line box: the layout
   assert reads bounding-box height against line-height to catch words split
   across lines, so a one-line label carries its padding inside an explicit
   tall line-height rather than around a short one. */
.masthead nav a{color:var(--muted-strong);text-decoration:none;font-size:.75rem;
  font-weight:650;letter-spacing:.11em;text-transform:uppercase;padding:.35rem .65rem;
  line-height:2;white-space:nowrap;border-radius:8px}
.masthead nav a:hover{color:var(--ink);background:var(--bg-inset);text-decoration:none}
.masthead nav a[aria-current="page"]{color:var(--ink)}
/* THE CTA PILL — ported from the approved interior system
   (design-competition/interior-system-v1.html). One filled pill in the nav,
   gradient cyan → violet, matching the homepage's own `.pill.primary`. It is
   the only nav item styled as an action; its href always comes from H.EXT,
   never a literal store URL, so a store-side redirect only has to be fixed
   in one constant. */
/* `.masthead .mast-cta`, not `.masthead nav .pill`: the pill moved OUT of <nav>
   on 28 Jul so the hamburger could not take it down with the menu. The selector
   had to follow it, and it must not go back to a `nav`-descendant form. */
.masthead .mast-cta{display:inline-flex;align-items:center;justify-content:center;
  min-height:2.75rem;padding:0 1.15rem;margin-left:.3rem;border-radius:99px;
  font-size:.78rem;font-weight:800;letter-spacing:.02em;text-transform:none;
  line-height:1;color:#050505;background:linear-gradient(100deg,var(--cyan),var(--violet));
  border:0;white-space:nowrap;text-decoration:none;align-self:center}
.masthead .mast-cta:hover{background:linear-gradient(100deg,#82eaff,#c2b0ff);color:#050505;
  text-decoration:none}
.masthead .mast-cta:focus-visible{outline:3px solid var(--sky);outline-offset:3px}
.datechip{font-size:.78rem;color:var(--muted);letter-spacing:.02em;width:100%;
  padding-bottom:.15rem}
.datechip b{color:var(--muted-strong);font-weight:600}
/* The switch is hidden until the inline script reveals it, so a page without
   JavaScript never shows a control that cannot work. Its title text is
   deliberate down to the clause about storage; do not shorten it. */
/* Prominent since the 26 Jul pivot: dark is the default now, and the switch
   label is a full sentence of intent ("Switch to light") so a casual reader
   knows what clicking does without decoding a state word. */
.themetoggle{font:inherit;font-size:.8rem;letter-spacing:.02em;padding:.42rem .95rem;
  border:1.5px solid var(--muted);border-radius:99px;background:transparent;
  color:var(--ink);cursor:pointer;font-weight:600}
.themetoggle u{text-decoration:none}
.themetoggle:hover{border-color:var(--sky);color:var(--sky)}

/* ── crumbs ─────────────────────────────────────────────────────────────── */
.crumb{font-size:.82rem;color:var(--muted);padding:1rem 0 0}
.crumb a{color:var(--link)}
.crumb .sep{padding:0 .45rem;color:var(--muted)}
.crumb [aria-current="page"]{color:var(--muted-strong)}
/* The `.asof` chip — interior-system-v1.html's page-head date pill, riding
   beside the breadcrumb rather than inside the masthead's own datechip
   (which keeps its refresh-vs-retained language; this one is a plain
   freshness stamp for the page's own content). `.ph-top` is the flex row
   that holds both; pages that skip H.page({crumb}) simply never render it. */
.ph-top{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.6rem}
.ph-top .crumb{padding-top:1rem}
.asof{display:inline-flex;align-items:center;gap:.4rem;margin-top:1rem;padding:.35rem .7rem;
  border:1px solid var(--line);border-radius:99px;background:var(--panel);color:var(--muted-strong);
  font:700 .68rem var(--mono);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap}
.asof i{width:.4rem;height:.4rem;border-radius:50%;background:var(--good)}
@media(max-width:600px){.ph-top{align-items:flex-start}}

/* ── airline sub-nav: the SECOND header row ───────────────────────────────
   The masthead nav says where you are on the SITE and is identical on every
   page. .subnav says where you are inside ONE airline. Anything airline-specific
   belongs here — with 18 airlines it can never live in the masthead. */
.subnav{display:flex;align-items:baseline;flex-wrap:wrap;gap:.35rem 1rem;
  padding:.7rem 0;border-bottom:1px solid var(--line-soft);font-size:.9rem}
.subnav .sn-air{font-family:var(--serif);font-weight:700;color:var(--ink);
  white-space:nowrap}
.subnav a{white-space:nowrap;padding:.3rem .6rem;border-radius:8px;text-decoration:none}
.subnav a:hover{background:var(--bg-inset)}
.subnav a[aria-current="page"]{color:var(--ink);text-decoration:none;background:var(--bg-inset);
  font-weight:700}
.subnav .sn-back{margin-left:auto;color:var(--muted)}
.subnav.lone .sn-back{margin-left:0}
.subnav.lone .sn-air{margin-left:auto}
@media(max-width:560px){.subnav .sn-back{margin-left:0}.subnav.lone .sn-air{margin-left:0}}

/* ── type ─────────────────────────────────────────────────────────────── */
/* The page-head glow — ported from interior-system-v1.html's `.page-head`.
   A quiet radial cyan/violet wash in the top-right corner, the same accent
   the masthead pill carries, so a reader who scrolls from the nav into a
   page's own header sees one continuous idea rather than two chrome
   languages. Purely decorative: pointer-events:none, no content, no data. */
/* overflow:hidden clips the glow below to the hero's own box. Without it the
   glow's negative right offset added to its width pokes past `.wrap` on
   narrow viewports and forces a real horizontal scrollbar — caught by
   ~/websites/scripts/responsive/check.mjs ("layout N > viewport N" on
   /united/fleet/ at every width below 1024), not by curl and not by eye on a
   desktop browser, which is exactly the class of bug that script exists for. */
.hero{position:relative;padding:2.4rem 0 .4rem;overflow:hidden}
/* right:0, not a negative offset: `overflow:hidden` above stops this from
   creating a real scrollbar either way, but a negative right still inflates
   .hero's OWN scrollWidth (measurable even while clipped from view), which
   re-triggers the responsive checker's per-element check with no visible
   fault behind it. Flush to the box's own right edge, it cannot exceed
   .hero's content width at all, in either measurement. */
.hero:before{content:"";position:absolute;width:18rem;height:14rem;right:0;top:-4rem;
  background:radial-gradient(circle,rgba(41,216,255,.16),rgba(146,108,255,.08) 46%,transparent 70%);
  filter:blur(10px);pointer-events:none}
/* Display scale, 28 Jul 2026. Was clamp(1.9rem,5.2vw,3rem) = 38.4px at 1440 and
   27.2px at 390, against the homepage's 121px and 51px. Round 17 named the gap
   as the real content of "the interior pages don't match": same tokens, same
   face, a third of the size. The approved interior system sets 94px desktop and
   54.6px phone, which is what these two clamps land on.
   The vw term carries the middle of the range, so the floor and ceiling are the
   two ends that matter: 3.4rem keeps the phone at 54.4px (6.5vw would only give
   25px there) and 5.875rem caps the desktop at 94px before the viewport runs
   away with it. */
.hero h1{position:relative;font-size:clamp(3.4rem,6.5vw,5.875rem);line-height:.94;
  letter-spacing:-.055em;max-width:16ch}
/* `h1.ph` is the PAGE-HEADER heading and it is what almost every interior route
   actually renders — `.hero h1` is the homepage-style hero, used on far fewer
   pages. Measuring only `.hero h1` after the first scale change is why the
   interiors came back at 68px instead of 94px: the rule that ran was this one.
   Same clamp as `.hero h1` deliberately. Two display scales one step apart read
   as an accident when the pages sit next to each other in a nav. */
h1.ph{font-size:clamp(3.4rem,6.5vw,5.875rem);line-height:.94;letter-spacing:-.055em;
  margin:.5rem 0 0;max-width:18ch}
/* the second line of the hero, one step down the scale */
.hero h1 .tag{display:block;font-size:clamp(1.05rem,2.4vw,1.4rem);font-weight:400;
  letter-spacing:-.004em;margin-top:.6rem;color:var(--ink-soft)}
.lede{margin-top:.9rem}
/* THE KICKER IS SKY. It is the one all-caps label that is chrome rather than
   provenance: it names the section the way a forecast names a panel. Provenance
   labels stay muted ink — see .cls and .src. */
.kicker{display:block;font-family:var(--sans);font-size:.75rem;font-weight:600;
  letter-spacing:.14em;text-transform:uppercase;color:var(--sky);margin:0 0 .6rem}
.kicker .dot{display:inline-block;width:.44rem;height:.44rem;border-radius:50%;
  background:var(--band);margin-right:.5rem;vertical-align:.06em}

/* Sections divide with a hairline and a sky kicker. There are no cards around
   sections and no headings floating in space. */
/* Breathing room, sitewide (round seven): sections sit further apart and
   ledes clear their headings. */
section.blk{position:relative;border-top:1px solid var(--line-soft);
  margin:3.2rem 0 0;padding-top:2.3rem}
.sec-h{display:flex;align-items:baseline;gap:.5rem 1rem;margin-bottom:.6rem;flex-wrap:wrap}
.sec-h h2{font-size:clamp(1.35rem,3.4vw,1.8rem);line-height:1.2}
.sec-h .sub{font-size:.75rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
  color:var(--sky)}
section.blk>.sec-h>.sub{display:block;width:100%;margin-bottom:.1rem}
.sec-h .more{margin-left:auto;font-size:.85rem}
.sec-lede{margin-bottom:1.2rem}
main .micro{margin-top:.35rem}
/* the pointer paragraph after a board — outside .rankb since round seven */
.pointer{font-size:.9rem;color:var(--ink-soft);margin-top:1.1rem}
/* .micro is the reporting label: quiet ink, never sky, because it sits inside
   the data's screen space. */
.micro{font-size:.75rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
  color:var(--muted)}
.note{font-size:.875rem;color:var(--ink-soft)}
.footnote{font-size:.875rem;color:var(--muted);max-width:44rem}

/* ── boxes ────────────────────────────────────────────────────────────── */
.card,.panel{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:1.1rem 1.2rem;box-shadow:var(--sheen);color:inherit}
a.card{text-decoration:none;display:flex;flex-direction:column;gap:.5rem}
a.card:hover{border-color:var(--muted);text-decoration:none}
.card h3{font-size:1.05rem}
.card p{font-size:var(--fs-small);color:var(--ink-soft)}
.card .go{margin-top:auto;font-size:.78rem;font-weight:600;letter-spacing:.1em;
  text-transform:uppercase;color:var(--link)}
a.card:hover .go{text-decoration:underline;text-decoration-thickness:2px}
/* minmax(0,1fr), never a bare 1fr. A grid track sized 1fr will not shrink below
   its content's min-content width, so one long unbreakable string inside a card
   pushes the whole track wider than its share and the page scrolls sideways.
   /race/ measured 407px of document scrollWidth in a 390px viewport in both
   engines, with script on and off, because four projection cards carrying long
   source citations each rendered 392.2px inside a 390px column. minmax(0,1fr)
   lets the track go narrower than its content and the text wraps instead.
   Found by an external audit on 27 Jul 2026; the project's own responsive sweep
   never flagged it because /race/ was not in the assertion's route list. */
.grid2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:1rem}
.grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}
@media(max-width:860px){.grid3{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}}
@media(max-width:640px){.grid2,.grid3{grid-template-columns:minmax(0,1fr)}}

.chips{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1rem;margin:1.4rem 0 .5rem}
@media(max-width:860px){.chips{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}}
@media(max-width:420px){.chips{grid-template-columns:1fr}}
.chip{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--sheen);padding:.9rem 1rem}
.chip .n{font-size:1.7rem;font-weight:700;line-height:1.05;color:var(--ink);
  letter-spacing:-.02em}
.chip .n small{font-size:.9rem;color:var(--muted);font-weight:400}
.chip .l{font-size:.75rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
  color:var(--muted);margin-top:.5rem}
.chip .d{font-size:.8rem;color:var(--ink-soft);margin-top:.3rem}
.chip .d .up{color:var(--good);font-weight:700}
/* .glow is a name from the old system. It means "this number wears its band",
   and there is no glow: it is flat colour, like everything else. */
.chip.glow .n{color:var(--band)}
.chip.hero-kpi .n{font-size:2.6rem}

/* ── US airlines at a glance (homepage only) ───────────────────────────────
   Seven fixed carriers (build/lib/pages.js US_MAJORS), four across, so the last
   row is three. The set never grows with the data, which is why the column count
   is a literal 4 and not an auto-fit: a row that reflows on every data pull is a
   layout nobody has actually looked at.
   The score reuses the leaderboard's `.sco` NAME but not its rule — that one is
   scoped `table.tbl .sco`. Do not "dedupe" the two by unscoping the table rule;
   it would resize every leaderboard cell on /airlines/. */
.usrow{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1rem;margin:1.2rem 0 .6rem}
@media(max-width:980px){.usrow{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:720px){.usrow{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:430px){.usrow{grid-template-columns:1fr}}
.usrow .card{padding:.9rem 1rem;gap:.35rem}
.ush{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}
.ush h3{margin-right:auto;font-size:1rem}
.usrow .sco{font-size:1.5rem;font-weight:700;color:var(--band);line-height:1;
  letter-spacing:-.02em}
/* the three stacked lines: next-gen headline, today's tier, and the ConnectScore
   footer that lets a reader reconcile this row's order with /airlines/. See the
   comment above usGlance() in build/lib/pages.js for why there are three. */
.usng{font-weight:700;color:var(--ink-soft);font-size:.92rem}
.usnow{color:var(--ink-soft);font-size:.92rem}
.uscs,.ustat{margin-top:auto;padding-top:.5rem;border-top:1px solid var(--line-soft);
  font-size:.75rem;letter-spacing:.04em;color:var(--muted)}

/* ── buttons, badges, pills ───────────────────────────────────────────────
   One button and it is the sky, because a button is chrome. The ghost is its
   outline. Nothing here ever takes a band. */
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.72rem 1.15rem;
  border-radius:var(--r-md);font:inherit;font-weight:600;font-size:.95rem;
  background:var(--btn-bg);color:var(--btn-fg);text-decoration:none;
  border:1px solid var(--btn-bg);cursor:pointer}
.btn:hover{text-decoration:none;filter:saturate(1.1)}
/* Buttons carry a border at rest (round seven, note 3). --field-border is the
   token reserved for control boundaries (3.6-4.2:1 on every ground); hover
   still moves it to the sky, so the hover state stays a change rather than
   the only signal. */
.btn.ghost{background:transparent;color:var(--link);border-color:var(--field-border)}
.btn.ghost:hover{border-color:var(--sky);filter:none}
/* inline links promoted to buttons, round six */
.btn.mini{padding:.34rem .7rem;font-size:.8rem;border-radius:8px;gap:.35rem}
.btnrow{display:flex;flex-wrap:wrap;gap:8px;margin:.75rem 0 .35rem}
.cta-row{display:flex;gap:.7rem;flex-wrap:wrap;align-items:center;margin-top:1.3rem}
.microlinks{display:flex;gap:1.1rem;flex-wrap:wrap;margin-top:.9rem;font-size:.85rem}

/* Badges. A badge that carries a NUMBER wears its band; a badge that carries a
   CATEGORY stays ink and tells itself apart by border style. */
/* Pills are sentence case at 12px since Phase 1c: the 11.5px letterspaced
   caps were under the audit's 12px floor, and wide tracking below 13px is
   reserved for section eyebrows. ::first-letter capitalises the baked
   lowercase band words without changing the textContent the weld and
   round-trip checks read. */
.badge{display:inline-block;font-size:.75rem;font-weight:700;letter-spacing:.02em;
  padding:.14rem .5rem;border-radius:99px;
  border:1px solid var(--line);color:var(--muted);white-space:nowrap}
.badge::first-letter,.band::first-letter{text-transform:uppercase}
.badge.good{color:var(--good);border-color:currentColor}
.badge.mixed{color:var(--mixed);border-color:currentColor}
.badge.long,.badge.low{color:var(--long);border-color:currentColor}
.badge.zero,.badge.none{color:var(--zero);border-color:currentColor}
.badge.live{color:var(--good);border-color:currentColor}
.badge.cachetag,.badge.offline{color:var(--muted-strong);border-color:var(--line)}
/* mainline vs express is a category, not a score, so it stays monochrome and
   separates itself with the border style instead of a hue. */
.badge.mainline{color:var(--muted-strong);border-color:var(--muted)}
.badge.express{color:var(--muted);border-style:dashed}
.pill{display:inline-block;font-size:.8rem;padding:.14rem .5rem;border-radius:99px;
  margin:.12rem .35rem .12rem 0;background:transparent;border:1px solid var(--line);
  color:var(--ink-soft)}
.pill.add{color:var(--good);border-color:currentColor}
.pill.tail{color:var(--ink-soft)}
.pill.soon{color:var(--muted-strong);border-style:dashed;font-size:.75rem;margin:0}
.pill.live{color:var(--good);border-color:currentColor;font-size:.75rem;margin:0}
a.pill{text-decoration:none}
a.pill:hover{border-color:var(--muted)}
/* The score band chip. This one IS allowed the colour: it is the score. The
   word is the non-colour signal for the band, so it survives everywhere the
   score goes — sentence case now, 12px floor, see the .badge note above. */
.band{display:inline-block;font-size:.75rem;font-weight:700;letter-spacing:.02em;
  padding:.14rem .5rem;border-radius:99px;white-space:nowrap;
  color:var(--band);border:1px solid currentColor}
.band.good{color:var(--good)} .band.mixed{color:var(--mixed)}
.band.long{color:var(--long)} .band.zero{color:var(--zero)}
.legend{display:flex;gap:.5rem 1.1rem;flex-wrap:wrap;margin:.5rem 0 .8rem;
  font-size:.75rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted)}
.legend span{display:inline-flex;align-items:center;gap:.45rem}
.legend i{width:.6rem;height:.6rem;display:inline-block;flex:none;background:var(--line)}
.legend i.eq{background:var(--viz-equipped)}
.legend i.no{background:var(--viz-empty);border:1px solid var(--line)}
.legend i.ml{background:var(--viz-mainline)}
.legend i.ex{background:var(--viz-express)}
/* System dots key a CATEGORY, not a score, so they are ink graded by weight —
   no legend on this site resolves by colour alone. */
.sysdot{width:.5rem;height:.5rem;display:inline-block;flex:none;margin-right:.45rem;
  border-radius:50%;background:var(--sys-legacy)}
.sysdot.starlink{background:var(--sys-starlink)}
.sysdot.leo{background:var(--sys-leo);box-shadow:inset 0 0 0 1px var(--muted)}
.sysdot.viasat,.sysdot.ku,.sysdot.intelsat{background:var(--sys-viasat)}
.sysdot.panasonic,.sysdot.thales,.sysdot.inmarsat,.sysdot.anuvu{background:var(--sys-legacy)}
.meta-pill{display:inline-block;font-size:.75rem;color:var(--muted);
  border:1px solid var(--line);border-radius:99px;padding:.1rem .5rem}

/* ── progress tapes ───────────────────────────────────────────────────────
   A ruled inset with a solid band fill. The colour is the band, so a bar next to
   a score always agrees with it.
   COLLISION NOTE: /united/ has its own .track/.fill pair (inline widths set by
   1400 lines of live-tested app JS). These rules must not outrank it, which is
   why the fill selector is wrapped in :where() — that keeps it at .fill-level
   specificity, so the optimizer's own later rules win on that page and these win
   everywhere else. Do not "simplify" the :where() away.
   Width comes from a baked --pct custom property, not an inline width, so the
   html.anim rules further down can start it at 0 and grow it. No JS => --pct is
   used directly and the bar is already correct. */
.track{display:block;height:.5rem;border-radius:99px;background:var(--proj-bg);
  border:1px solid var(--line);overflow:hidden}
.chip .track,.stat .track,.hpanel .track,.scorebox .track{margin-top:.55rem}
:where(.track) .fill{display:block;height:100%;border-radius:99px;width:var(--pct,0%);
  background:var(--band);transition:width 1s ease-out}
:where(.track) .fill.ml{background:var(--viz-mainline)}
:where(.track) .fill.ex{background:var(--viz-express)}
.track.mini{height:.4rem;width:5.75rem;margin:0}

/* ── tables. Real tables, ruled rows, no shell. ─────────────────────────── */
/* THE SHELL IS NOT A SCROLLER ABOVE 700px ANY MORE (Phase 1c). An ancestor
   with overflow-x:auto becomes the containing scrollport for position:sticky,
   and that wrapper never scrolls vertically, so every "sticky" table heading
   simply scrolled away — measured at top -299/-357/-838 with the table body
   still on screen. Above 700px every table fits its column (the boards render
   as cards below 880px and never scroll at all), so the wrapper gives up the
   scroll and the viewport becomes the scrollport: the headings finally stick,
   just under the masthead, at --sticky-header-height. Below 700px the dense
   tables still need the sideways scroll frame, sticky physically cannot work
   inside it, and a sticky declaration that does nothing is the defect the
   layout assert exists to catch — so the headings are static there. */
/* EVERY table shell scrolls, at every width, and the headings are honestly
   static. This reverses a trade made on 27 Jul 2026 that was wrong.
   To make `position:sticky` work on thead th, the shell gave up overflow-x
   above 700px, because an ancestor with overflow-x becomes the sticky
   scrollport. That holds on the homepage boards, which measure 888/888 and
   fit. It does not hold on /race/, whose table wants 960px: an external audit
   measured the DOCUMENT at 980px in a 768px viewport and 1028px at 1024px, so
   a tablet reader got page-level horizontal scrolling.
   There is no CSS that gives both a sticky heading and a shell that scrolls,
   so this is a harm ordering, not a preference. A page you cannot read without
   dragging it sideways is worse than losing column headers part-way down a long
   table. The sticky declaration is gone rather than left in place doing
   nothing, which is the defect build/layout-assert.mjs exists to catch. */
.tbl-shell,.tscroll{background:transparent;border:0;
  overflow-x:auto;-webkit-overflow-scrolling:touch}
table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}
table.tbl{min-width:40rem}
/* Cell boxes hold a single line box inside their line-height (line-height 1.7
   with .4rem vertical padding) rather than around a 1.55 one with .5rem: the
   layout assert reads box height against line-height to catch mid-word wraps,
   and a padded one-line cell must not measure like a two-line one. */
table.tbl th,table.tbl td{text-align:left;padding:.4rem .6rem .4rem 0;font-size:.92rem;
  line-height:1.7;border-bottom:1px solid var(--line-soft);vertical-align:middle}
/* Column headings are sentence case at 12px now, not 9.6px letterspaced caps:
   the audit floor is 12px rendered, and wide tracking below 13px is reserved
   for section eyebrows — the all-caps microcopy was exactly the text that
   wrapped worst. */
/* NOT sticky, at any width. Every shell is a horizontal scrollport now (see
   the .tbl-shell rule above and the harm ordering recorded there), and sticky
   resolves against the nearest scrollport, which never scrolls vertically. A
   sticky declaration here would sit in the stylesheet doing nothing, which is
   precisely the defect build/layout-assert.mjs was written to find. */
table.tbl thead th{font-size:.75rem;font-weight:650;letter-spacing:.02em;
  color:var(--muted);white-space:nowrap;position:static;
  z-index:2;line-height:2;padding-top:.3rem;
  background:var(--paper);border-bottom:1px solid var(--ink);padding-bottom:.4rem;
  vertical-align:bottom}
table.tbl tbody tr:last-child td{border-bottom:1px solid var(--line)}
table.tbl tbody tr:hover{background:var(--panel)}
/* Data cells may WRAP since round seven (note 2): nowrap on every .num cell
   put the Big 4 table's min-content at 1422px, 400px of it hidden behind an
   invisible horizontal scroll at every width. Figures still hold together
   because spaces, not digits, are the only break points; the pieces that must
   never break mid-figure (.fitpct, the projected chip's date phrase) carry
   their own nowrap below. Headers stay nowrap. */
table.tbl td.mono{white-space:nowrap}
th.num,td.num{text-align:right}
table.tbl td b{color:var(--ink);font-weight:700}
table.tbl .rank{color:var(--muted);font-size:.8rem;width:2.6rem}
table.tbl .aname{font-weight:700}
table.tbl .code{font-size:.75rem;font-weight:600;color:var(--muted);margin-left:.45rem;
  letter-spacing:.1em}
table.tbl .sco{font-size:1.4rem;font-weight:700;color:var(--band);line-height:1;
  letter-spacing:-.02em}
/* the instrumented carriers get a tick in the margin, the way a forecast marks
   the row it wants you to read. It is ink: instrumentation is a provenance
   class, not a score. */
table.tbl tr.instr{box-shadow:inset 2px 0 0 var(--muted-strong)}
table.tbl tr.instr td:first-child{padding-left:.6rem}
html.js table.tbl th[data-k]{cursor:pointer;user-select:none}
html.js table.tbl th[data-k]:after{content:"\2195";opacity:.45;margin-left:.3rem;font-size:.75rem}
html.js table.tbl th[data-k][aria-sort="ascending"]:after{content:"\2191";opacity:1}
html.js table.tbl th[data-k][aria-sort="descending"]:after{content:"\2193";opacity:1}
@media(max-width:720px){.hide-sm{display:none}}
.tblcap{font-size:.875rem;letter-spacing:.02em;color:var(--muted);margin-top:.7rem}
.caveat{margin-top:.9rem;font-size:.875rem;color:var(--ink-soft);
  border:1px solid var(--line);border-left:3px solid var(--line);
  background:var(--panel);border-radius:var(--r-md);padding:.8rem 1rem}
/* .875rem: empty states are sentences of copy, under the 14px floor */
.empty-state{font-size:.875rem;color:var(--muted);border:1px dashed var(--line);
  border-radius:var(--r-md);padding:.8rem 1rem}

/* ── filter chips + search (JS-only affordances) ──────────────────────── */
/* ── Option A leaderboard: colour follows the ranked column ───────────────
   Ported from the reviewed Big 4 mockup (Version A, approved 26 Jul 2026). In a
   board with four score-ish columns, only the column the table is currently
   ranked on wears band colour and its band chip; everything else stays ink.
   That is how four columns share a row without becoming a rainbow (rule 8).
   The --band custom property already exists per .sc-good/.sc-mix/.sc-long/.sc-no
   (below); this block only decides which cell gets to read it. */
.rankb .segctrl{display:flex;align-items:baseline;gap:.6rem;flex-wrap:wrap;margin-bottom:.2rem}
.rankb .segctrl .lbl{font-size:.75rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--muted-strong)}
.rankb .sortcap p{margin:.15rem 0 .8rem;font-size:.92rem;color:var(--ink-soft);max-width:44rem}
.rankb .sortcap p a{color:inherit}
.rankb .pointer{font-size:.9rem;color:var(--ink-soft);margin:.6rem 0 0}
.rankb table.tbl{min-width:52rem}
/* Big 4 board fix (approved mockup, 27 Jul 2026): the mainline/regional split
   cells were narrow enough that "split not published" and "no regional
   fleet · ..." wrapped to 2-3 lines inside their dashed empty-state box, on
   both boards that render this pair. Widen the cells and stop the text from
   wrapping so the placeholder reads as one line; #home-big4-board and
   #big4-board below also drop the table's own min-width so this doesn't force
   a sideways scroll on the Big 4 board specifically (the full 18-airline
   board may keep its scroll). */
.rankb td[data-col="mainline"],.rankb td[data-col="regional"]{min-width:7.4rem}
.rankb .empty-state.cell{white-space:nowrap}
/* The Big 4 board specifically must not scroll sideways (both places it
   renders: the homepage's #home-big4-board and /airlines/'s own #big4-board,
   same id rankBoard() falls back to). Drop the table's 52rem floor so it can
   size to its own four rows' content instead of a width picked for the wider
   18-row board; the 18-row #full-board-rankb keeps min-width:52rem and its
   scroll unchanged. */
#home-big4-board table.tbl,#big4-board table.tbl{min-width:0}
/* The one-line empty-state fix above makes the mainline/regional columns'
   own min-content their full unwrapped text width, and once ANY column on
   the Big 4 board has a hard floor, a `width:100%` auto-layout table can
   render past its container rather than shrink it (that is the actual
   mechanism `.tbl-shell{overflow-x:auto}` was quietly forgiving). The
   "Signed, not flying" column already wraps onto more than one line at
   normal widths (its own .proj is flex-wrap:wrap) so it is the one column
   that can safely give the ~125px back; capping it lets the table settle at
   the container width instead of past it. Scoped to the two Big 4 ids so
   the 18-row board's own last-child rule (`.rankb th:last-child{width:30%}`
   below) is untouched. */
#home-big4-board th:last-child,#big4-board th:last-child,
#home-big4-board td:last-child,#big4-board td:last-child{width:auto;max-width:11.5rem}
/* Southwest's longer projection phrase ("300+ by end-2026, installs running")
   would not wrap inside the capped column above and spilled past it instead.
   Two things were holding it to one line: `.proj>*{white-space:nowrap}`
   (site-wide, so a short chip phrase never breaks mid-word) and a flex
   item's automatic minimum size, which defaults to its content's max-content
   width and blocks it from shrinking even where wrapping IS allowed. Site-
   wide every other projection phrase is short enough that nowrap never
   costs a line; Southwest's is not, once the column is capped to fit. Both
   are undone here, scoped to the two Big 4 ids only — /airlines/southwest/
   and the 18-row board keep the site-wide one-line chip look. */
#home-big4-board .proj>*,#big4-board .proj>*{white-space:normal;min-width:0}
.rankb .lab{display:block;font-size:.75rem;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);margin-bottom:.12rem;font-weight:600}
.rankb .sco.pct{font-size:1.02rem}
.rankb th[data-rc][aria-sort="descending"]:after{content:"\2193";opacity:.55;margin-left:.3rem;
  font-size:.75rem}
.rankb td[data-col] .sco{color:var(--ink)}
.rankb td[data-col] .band{display:none}
.rankb table[data-active="score"]    td[data-col="score"] .sco,
.rankb table[data-active="nextgen"]  td[data-col="nextgen"] .sco,
.rankb table[data-active="mainline"] td[data-col="mainline"] .sco,
.rankb table[data-active="regional"] td[data-col="regional"] .sco{color:var(--band,var(--ink))}
.rankb table[data-active="score"]    td[data-col="score"] .band,
.rankb table[data-active="nextgen"]  td[data-col="nextgen"] .band,
.rankb table[data-active="mainline"] td[data-col="mainline"] .band,
.rankb table[data-active="regional"] td[data-col="regional"] .band{display:inline-block}

.filters{display:flex;flex-wrap:wrap;gap:.4rem;margin:.3rem 0 .9rem}
/* resting borders on the chips too (round seven, note 3) */
.filters button{font:inherit;font-size:.82rem;color:var(--ink-soft);cursor:pointer;
  padding:.3rem .7rem;border:1px solid var(--field-border);border-radius:99px;background:transparent}
.filters button:hover{border-color:var(--sky)}
.filters button[aria-pressed="true"]{color:var(--btn-fg);background:var(--btn-bg);
  border-color:var(--btn-bg)}
.srch{display:inline-flex;align-items:center;gap:.5rem;margin:0 0 .9rem}
.srch input{font:inherit;padding:.5rem .7rem;border-radius:var(--r-md);font-size:.9rem;
  background:var(--field);color:var(--ink);border:1px solid var(--field-border);min-width:11rem}
.srch input::placeholder{color:var(--placeholder)}
/* No outline:none here. This selector is more specific than the sitewide
   :focus-visible rule, so suppressing the outline left the /airlines/ search
   box with only a 1px border-colour change as its focus indicator, on a border
   that measures 1.2-1.4:1 against its own background. */
.srch input:focus{border-color:var(--sky)}

/* ── Hangar Floor waffle ──────────────────────────────────────────────────
   Dim cells are the fleet, lit cells are what is flying. A lit cell is a count,
   so it takes the band's green; the dim ones are ink at low opacity. */
.hangar{display:grid;gap:1rem}
.hpanel{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--sheen);padding:1rem 1.1rem}
.hpanel-h{display:flex;align-items:baseline;gap:.6rem;flex-wrap:wrap;margin-bottom:.25rem}
.hpanel-h .ty{font-family:var(--serif);font-size:1.02rem;font-weight:700;color:var(--ink)}
.hpanel-h .ct{font-size:.82rem;color:var(--muted);margin-left:auto}
.hpanel-h .ct b{color:var(--ink);font-weight:700}
.waffle{display:grid;grid-template-columns:repeat(auto-fill,minmax(10px,1fr));gap:2px;
  margin-top:.7rem}
.waffle i{display:block;aspect-ratio:1;border-radius:1px;background:var(--muted);opacity:.28}
.waffle i.eq{background:var(--viz-equipped);opacity:1}
.waffle i.eq:hover{transform:scale(1.6);position:relative;z-index:2}
.hpanel .cap{font-size:.78rem;color:var(--muted);margin-top:.55rem}
.mini-waffle{display:grid;grid-template-columns:repeat(auto-fill,minmax(9px,1fr));gap:2px;
  margin:.25rem 0 .15rem}
.mini-waffle i{display:block;aspect-ratio:1;border-radius:1px;background:var(--muted);opacity:.28}
.mini-waffle i.eq{background:var(--viz-equipped);opacity:1}

/* ── visualizations (baked SVG) ───────────────────────────────────────────
   Lines are ink, told apart by weight and dash pattern, labelled at each line's
   end. No chart line borrows a band. */
.viz{width:100%;height:auto;display:block;overflow:visible}
.viz .gl{stroke:var(--line-soft);stroke-width:1}
.viz .ax{fill:var(--muted);font-family:var(--sans);font-size:10px}
.viz .lbl{fill:var(--ink);font-family:var(--sans);font-size:12px;font-weight:700}
.viz .a-ml{fill:var(--viz-mainline);fill-opacity:.10}
.viz .a-ex{fill:var(--viz-express);fill-opacity:.10}
.viz .s-ml{stroke:var(--viz-mainline);stroke-width:1.8;fill:none}
.viz .s-ex{stroke:var(--viz-express);stroke-width:1.4;fill:none;stroke-dasharray:4 3}
.viz .dot-ml{fill:var(--viz-mainline)}
.viz .dot-ex{fill:var(--viz-express)}
/* THE PACE BARS ARE A COUNT, SO THEY WEAR THE BAND, FLAT.
   build/lib/viz.js still emits a <linearGradient id="pacegrad"> for these — that
   file is not this agent's to edit and the gradient is now unreferenced. Do not
   "restore" fill:url(#pacegrad): the sky does not paint numbers, and the site
   has no gradients at all. */
.viz .bar{fill:var(--band)}
.viz .bar.partial{opacity:.45}
.viz .ring-bg{stroke:var(--proj-bg);fill:none}
.viz .ring-fg{stroke:var(--band);fill:none;stroke-linecap:butt}
.viz .ring-n{fill:var(--ink);font-family:var(--serif);font-weight:700}
.spark{width:100%;height:auto;display:block}
.spark path.s{stroke:var(--viz-mainline);stroke-width:1.6;fill:none}
.spark path.f{fill:var(--viz-mainline);fill-opacity:.10;stroke:none}
.spark-note{font-size:.78rem;color:var(--muted);margin-top:.35rem}

/* ── roadmap vertical timeline ────────────────────────────────────────────
   EVERY rule is scoped under .rm. /united/ has its own .steps/.step playbook
   component with a different geometry, and an unscoped .step:before here painted
   a rogue circle on top of it. Keep the .rm scope. */
.steps.rm{display:grid;gap:0;position:relative;padding-left:1.6rem}
.steps.rm:before{content:"";position:absolute;left:.35rem;top:0;bottom:0;width:1px;
  background:var(--line)}
.rm .step{position:relative;display:block;padding:1rem 0;border-bottom:1px solid var(--line-soft)}
.rm .step:before{content:"";position:absolute;left:-1.6rem;top:1.3rem;width:.8rem;height:.8rem;
  border-radius:50%;background:var(--paper);border:1px solid var(--line)}
.rm .step.shipped:before{border-color:var(--good);background:var(--good)}
.rm .step.building:before{border-color:var(--sky);background:var(--paper)}
.rm .step.planned:before{border-color:var(--muted)}
.rm .step .sh{display:flex;align-items:baseline;gap:.6rem;flex-wrap:wrap}
.rm .step h3{font-size:1.05rem}
/* SHIPPED / BUILDING / PLANNED is a state, not a score. Shipped earns the green
   because it is a fact about the world; planned stays grey and dashed, which is
   the same fence the projected score wears. */
.rm .step .st{font-size:.75rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  border:1px solid;border-radius:99px;padding:.1rem .45rem}
.rm .step.shipped .st{color:var(--good);border-color:currentColor}
.rm .step.building .st{color:var(--muted-strong);border-color:var(--line)}
.rm .step.planned .st{color:var(--proj);border-color:var(--line);border-style:dashed}
.rm .step p{font-size:var(--fs-small);color:var(--ink-soft);margin-top:.35rem;max-width:44rem}
/* .tgts, not .targets — /united/ already owns a .targets list and an unscoped
   :before here gave its items a second bullet. */
.tgts{list-style:none;margin-top:.7rem;display:grid;gap:0}
.tgts li{font-size:.86rem;color:var(--ink-soft);padding:.45rem 0 .45rem 1.3rem;
  position:relative;border-top:1px solid var(--line-soft)}
.tgts li:before{content:"";position:absolute;left:.1rem;top:.85rem;width:.4rem;height:.4rem;
  border-radius:50%;background:var(--muted)}

/* ── credit strip (credits are architecture, not a footnote) ───────────── */
.credit{display:flex;gap:.8rem;align-items:flex-start;flex-wrap:wrap;margin-top:1.1rem;
  padding:.8rem 1rem;text-decoration:none;background:var(--panel);
  border:1px solid var(--line);border-left:3px solid var(--line);border-radius:var(--r-md);
  color:var(--ink-soft);font-size:.86rem}
.credit .cb{flex:none;font-size:.75rem;font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;color:var(--muted);border:1px solid var(--line);
  border-radius:99px;padding:.14rem .5rem;background:transparent;margin-top:.1rem}
.credit .cbody{flex:1;min-width:14rem}
.credit b{color:var(--ink);font-weight:700}

/* ── footer ───────────────────────────────────────────────────────────── */
footer.site{margin-top:2.8rem;border-top:1px solid var(--line);padding:1.6rem 0 2.2rem;
  color:var(--muted);font-size:.875rem}
/* The footer brand row — interior-system-v1.html's `.footer-top`, the same
   mark and wordmark as the masthead, linking home. It sits above the existing
   link row rather than replacing it: every href/label build/apitest.js checks
   (Site source, Extension source, API, Privacy, …) stays exactly where it was. */
footer.site .ftop{display:flex;align-items:center;gap:.55rem;margin-bottom:1rem;
  font-family:var(--sans);font-weight:850;letter-spacing:-.03em;font-size:1.05rem;
  color:var(--ink);text-decoration:none;min-height:44px}
footer.site .ftop .mk{color:var(--sky)}
footer.site .ftop:hover{text-decoration:none}
footer.site .frow{margin-top:.6rem;max-width:52rem}
footer.site .flinks{display:flex;gap:.5rem 1.1rem;flex-wrap:wrap;margin-bottom:.9rem;
  font-size:.9rem}
footer.site .flinks a{display:inline-flex;align-items:center;min-width:44px;min-height:44px}
footer.site b{color:var(--muted-strong)}

/* ── faq ──────────────────────────────────────────────────────────────── */
.faq{display:grid;gap:0;border-top:1px solid var(--line-soft)}
.faq .q{background:transparent;border:0;border-bottom:1px solid var(--line-soft);
  padding:.9rem 0}
.faq .q h3{font-size:1.02rem}
.faq .q p{font-size:var(--fs-small);color:var(--ink-soft);margin-top:.35rem;max-width:48rem}

/* ── score hero (airline pages) ───────────────────────────────────────── */
.scorebox{display:flex;gap:1.4rem;align-items:center;flex-wrap:wrap;margin-top:1.4rem;
  background:var(--panel);border:1px solid var(--line);border-radius:var(--r-xl);
  padding:1.3rem 1.4rem;box-shadow:var(--sheen)}
/* THE RING IS 120px. It arrives from V.scoreRing() as <svg class="viz" width="120">,
   and `.viz{width:100%}` — which is right for a full-width chart — turned it into
   a 966px circle that filled the screen on all 18 airline pages. It shipped that
   way, because at that size it reads as a design choice rather than a bug. Give
   it its intrinsic size and take it out of the flex maths. */
.scorebox .viz{flex:0 0 120px;width:120px;height:120px}
/* Until the page markup carries a .sc-* class on the box itself, the ring has no
   score to take its colour from. Ink is the honest default for "a number with no
   band applied yet"; the moment a .sc-* lands on .scorebox this rule stops
   matching and the band takes over. */
.scorebox:not([class*="sc-"]){--band:var(--ink)}
.scorebox .sbmid{flex:1;min-width:15rem}
.scorebox .sbmid .t{font-size:.75rem;font-weight:600;letter-spacing:.12em;
  text-transform:uppercase;color:var(--muted)}
.scorebox .sbmid .m{font-size:.86rem;color:var(--ink-soft);margin-top:.35rem}
.scorebox .sbscore{font-family:var(--serif);font-weight:700;font-size:3.4rem;line-height:1;
  color:var(--band);font-variant-numeric:tabular-nums}
/* the scorehead: the number, its band word, the rank, then the say-sentence */
.scorenum{text-align:center}
.scorenum .n{font-size:4.2rem;font-weight:700;line-height:1;color:var(--band)}
.scorenum .b{font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
  color:var(--band)}
.scorenum .scale{font-size:.75rem;color:var(--muted);margin-top:.2rem}

.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem}
@media(max-width:860px){.stats{grid-template-columns:repeat(2,1fr)}}
@media(max-width:440px){.stats{grid-template-columns:1fr}}
.stat{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--sheen);padding:.9rem 1rem}
.stat .n{font-size:1.7rem;font-weight:700;color:var(--ink);line-height:1.1;letter-spacing:-.02em}
.stat .n small{font-size:.9rem;color:var(--muted);font-weight:400}
.stat .l{font-size:.75rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
  color:var(--muted);margin-top:.5rem}
.stat .d,.stat .k{font-size:.8rem;color:var(--ink-soft);margin-top:.3rem}
.stat.done{box-shadow:var(--sheen),inset 3px 0 0 var(--good)}
.stat.early{box-shadow:var(--sheen),inset 3px 0 0 var(--mixed)}
.callout{margin-top:.6rem;background:var(--panel);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:1.1rem 1.2rem}
.callout h3{font-size:1.15rem}
.callout p{font-size:1.02rem;color:var(--ink-soft);margin-top:.5rem;max-width:44rem}
.callout ul{margin:.6rem 0 0 1.1rem;font-size:.92rem;color:var(--ink-soft)}
.callout li{margin:.25rem 0}
.movers{display:grid;gap:0;border-top:1px solid var(--line-soft)}
.mover{display:flex;gap:.7rem;align-items:baseline;flex-wrap:wrap;font-size:.9rem;
  padding:.55rem 0;background:transparent;border:0;border-bottom:1px solid var(--line-soft)}
.mover .fn{font-weight:700;color:var(--ink)}
.mover .dn{margin-left:auto}
.mover .dn.up{color:var(--good)}
.mover .dn.dn2{color:var(--long)}
.steady{color:var(--muted);font-size:.86rem;padding:.8rem 1rem;border:1px dashed var(--line);
  border-radius:var(--r-md)}
/* the four playbook moves, numbered by their own tag rather than a counter */
.moves{list-style:none;margin:1.2rem 0 0;padding:0;max-width:46rem}
.moves li{position:relative;padding:.9rem 0 .9rem 4.4rem;border-top:1px solid var(--line-soft)}
.moves .tag{position:absolute;left:0;top:1rem;font-size:.75rem;font-weight:700;
  letter-spacing:.1em;color:var(--muted-strong);border:1px solid var(--line);
  border-radius:99px;padding:.1rem .4rem;background:var(--card);min-width:2.9rem;
  text-align:center}
.moves b{font-size:1.02rem}
.moves p{margin:.2rem 0 0;font-size:.92rem;color:var(--ink-soft)}

/* ── tooltip (pointer users only; the title attribute is the no-JS truth) ── */
.wtip{position:fixed;z-index:50;font-size:.75rem;pointer-events:none;
  background:var(--card);color:var(--ink);border:1px solid var(--line);
  border-radius:var(--r-sm);padding:.25rem .5rem;box-shadow:0 2px 8px rgb(0 0 0 / .12);
  white-space:nowrap}

/* ═══ THE FLIGHT CHECK IS GONE — round seven, 27 Jul 2026. Its styles left
   with assets/flightcheck.js and the .fchk markup; the hero answers with the
   skyline now. .heroq survives only as a name some old links may remember —
   nothing emits it. ═══════════════════════════════════════════════════════ */
.heroq{padding:2.6rem 0 0}

/* the answer card. The one surface on the site that breathes, and the tier tag
   is not decoration: no number appears on this card without the method that
   produced it sitting next to it. */
.fa{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-xl);
  padding:1.3rem 1.4rem 1.1rem;box-shadow:var(--sheen);max-width:44rem}
.fa-h{display:flex;align-items:center;gap:1rem;flex-wrap:wrap}
.fa-big{font-weight:700;line-height:1;display:flex;align-items:baseline;gap:1px;
  color:var(--band)}
.fa-big b{font-size:clamp(2.4rem,7vw,3.1rem);letter-spacing:-.03em;color:inherit}
.fa-big small{font-size:1.5rem;color:inherit;font-weight:400}
.fa-t{flex:1 1 12rem;min-width:0}
.fa-t h3{font-size:1.15rem}
.fa-air{color:var(--ink-soft);font-weight:700}
.fa-what{font-size:.78rem;color:var(--muted);margin-top:.15rem}
.fa-band{margin-left:auto}
/* the provenance block: the tier word, then the source and the date. Muted ink,
   never sky and never a band, because it sits inside the data's screen space. */
.fa-tier,.prov{font-size:.78rem;color:var(--muted);margin-top:.8rem;
  border-top:1px solid var(--line);padding-top:.7rem}
.prov{letter-spacing:.05em;text-transform:uppercase;max-width:44rem}
.prov b{font-weight:700;color:var(--muted-strong)}
.fa-tag,.cls{display:inline-block;font-size:.75rem;font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;padding:.12rem .45rem;border-radius:99px;border:1px solid var(--line);
  color:var(--muted);white-space:nowrap;margin-right:.5rem}
/* tier tags are PROVENANCE, not score. Monochrome, and they separate by border
   style. A tag competing with the bands would break the colour rule. */
.fa-tag.tv,.cls-m{color:var(--muted-strong);border-color:var(--muted)}
.fa-tag.tf{color:var(--muted);border-color:var(--line)}
.fa-tag.tc,.cls-f{color:var(--muted);border-color:var(--line);border-style:dashed}
.fa-fleet{font-size:.86rem;color:var(--ink-soft);margin-top:.55rem}
.fa-links{display:flex;gap:1.1rem;flex-wrap:wrap;margin-top:.9rem;font-size:.85rem}
.fa-ext{font-size:.86rem;color:var(--ink-soft);margin-top:.9rem;
  border-top:1px solid var(--line-soft);padding-top:.75rem}
.fa-as{font-size:.75rem;color:var(--muted);margin-top:.5rem;letter-spacing:.04em}
/* the umbrella: what to do when the answer is bad. It never leaves the card. */
.umbrella{font-size:.95rem;margin:.2rem 0 .8rem;color:var(--ink-soft)}
.umbrella b{color:var(--ink)}

/* the compact above-the-fold extension line — a signpost to #extension, not the
   pitch. One line by design; the one pitch is the companion half further down. */
.extplug{display:flex;align-items:baseline;gap:.6rem;flex-wrap:wrap;margin-top:1.1rem;
  font-size:.86rem;color:var(--ink-soft)}
.extplug b{color:var(--ink)}
.extplug-go,.extplug-alt{font-size:.86rem}

/* ═══ HOMEPAGE EXTENSION SECTION ═══════════════════════════════════════════
   .extdemo is a DARK STAGE in BOTH themes, deliberately. Inside it sits the
   <section class="rlc"> emitted by build/lib/reel.js — the four-caption sequence
   over the two REAL product screenshots. That block declares its own
   light-on-dark palette (--rlc-ink and friends) rather than inheriting --ink,
   because a screenshot of a white airline website needs a dark surround to read
   as a screenshot; on the light theme, inheriting would give white on white.
   Every `.rlc*` rule lives in that block's scoped <style>; nothing here reaches
   inside it. The stage is the dark theme's own warm ground, so the two themes
   agree about what dark means on this site.
   ═════════════════════════════════════════════════════════════════════════ */
.extdemo{background:#1b1713;border:1px solid #3a332a;border-radius:var(--r-xl);
  padding:clamp(.9rem,3vw,1.6rem);overflow:hidden}
.extfeat{margin-top:1.1rem}
.extfeat .card h3{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}
.extwhere{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap;margin-top:.9rem}
.extwhere .micro{flex:0 0 6rem}
@media(max-width:520px){.extwhere .micro{flex:1 0 100%}}
.extwhere .pill{margin:0}
.extwhere .pill em{font-style:normal;color:var(--muted);font-size:.75rem}
.extwhy{margin-top:.6rem;max-width:48rem}
.extfine{margin-top:1rem;font-size:.8rem;color:var(--muted);max-width:52rem;
  border-top:1px solid var(--line-soft);padding-top:.9rem}
.extfine b{color:var(--muted-strong)}
/* the release-state list: in the store, submitted, built but unreleased. State
   grammar, never a promise — the horizons live on /roadmap/, dated and fenced. */
ul.vstate{list-style:none;margin:1.2rem 0 0;padding:0;max-width:44rem}
ul.vstate li{padding:.6rem 0;border-top:1px solid var(--line-soft);font-size:.92rem}
ul.vstate .st{display:inline-block;min-width:10rem;font-size:.75rem;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;color:var(--muted-strong)}

/* ═══ SHARED PRIMITIVES ════════════════════════════════════════════════════
   The pieces every archetype reuses. Renaming one of these breaks a page
   function in build/lib/, so extend rather than rename.
   ═════════════════════════════════════════════════════════════════════════ */
/* a ruled box, with optional corner ticks */
.plate-box{border:1px solid var(--line);background:var(--panel);border-radius:var(--r-lg);
  position:relative}
.ticked::before,.ticked::after{content:"";position:absolute;width:8px;height:8px;
  pointer-events:none;border-color:var(--line);border-style:solid}
.ticked::before{top:-1px;left:-1px;border-width:1px 0 0 1px}
.ticked::after{bottom:-1px;right:-1px;border-width:0 1px 1px 0}
/* the source line under a figure. Muted ink, and it always carries a date. */
.src{font-size:.78rem;letter-spacing:.02em;color:var(--muted);margin-top:.5rem}
/* THE PROJECTED SCORE. Grey, always. It never sorts a table, it carries its
   horizon date and a confidence word everywhere it goes, and a passed horizon
   flips to SLIPPED while keeping the date it missed. All five fence rules are
   asserted by build/prerender.js; this rule is the visual half of them. */
/* `white-space:nowrap` made this chip an unbreakable 358px box, which set a
   390px floor on the whole document: /race/ measured 390px wide at 320, 360 and
   375px viewports in both engines. The chip may wrap; what it may not do is
   split its value from its horizon date, so the parts stay whole and the box
   breaks between them. */
.proj{display:inline-flex;align-items:baseline;gap:.45rem;color:var(--proj);
  background:var(--proj-bg);border:1px solid var(--line);border-radius:var(--r-sm);
  padding:.12rem .5rem;font-size:.8rem;white-space:normal;flex-wrap:wrap;
  row-gap:.1rem;max-width:100%}
.proj>*{white-space:nowrap}
.proj b,.proj .pv{font-size:.95rem;font-weight:700;letter-spacing:-.01em}
.proj .ph{font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;color:var(--proj)}
.proj.slipped{border-color:var(--muted);color:var(--muted-strong);
  background:repeating-linear-gradient(135deg,transparent 0 3px,var(--proj-bg) 3px 6px)}
.conf{font-size:.75rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--proj)}
.conf.slip{color:var(--muted-strong)}
.dash{color:var(--muted)}
/* the tape: a score drawn as a length rather than a number */
.tape{position:relative;height:.5rem;background:var(--proj-bg);border:1px solid var(--line);
  border-radius:99px;margin:.7rem 0 .5rem;overflow:hidden}
.tape i{position:absolute;left:0;top:0;bottom:0;background:var(--band);display:block}
.tape .tk{position:absolute;top:0;bottom:0;width:1px;background:var(--line)}
.tapescale{display:flex;justify-content:space-between}
/* key-value rows */
.kv{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem}
.kv>div{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:var(--sheen);padding:.9rem 1rem}
.kv.kv-2{grid-template-columns:1fr 1fr}
.kv .v{font-size:1.5rem;font-weight:700;letter-spacing:-.01em;display:block;margin-top:.4rem}
.kv .v small{font-size:.75rem;letter-spacing:.1em;color:var(--muted);display:block;
  margin-top:.35rem;font-weight:600;text-transform:uppercase}
@media(max-width:900px){.kv,.kv.kv-2{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.kv,.kv.kv-2{grid-template-columns:1fr 1fr}}

/* ═══ RESPONSIVE ═══════════════════════════════════════════════════════════
   Down to 390px, where the first full screen must contain the page's answer.
   DENSE REFERENCE TABLES stay tables and get a scroll frame under their own
   heading — a schedule that reflows into pseudo-rows stops being readable as
   a schedule. The two RANK BOARDS are the standing exception since Phase 1b:
   their information model never survived 390-440px as a table, so below 880px
   they render as one .crd record per airline (see THE BOARDS BELOW 880px ARE
   CARDS further down). Body text never drops below 16px.
   ═══════════════════════════════════════════════════════════════════════════ */
@media(max-width:700px){
  .tablescroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
  .tablescroll table{min-width:34rem}
}
/* ── target sizes (Phase 1c). Standalone controls meet 24x24 CSS px
   everywhere; at touch widths they meet the 44px preferred size. The list is
   the controls the audit measured under: the mini buttons (90x33), the filter
   chips (~30px), the theme switch (~35px), the column-guide disclosure
   (~39px). Inline links inside sentences are exempt under WCAG 2.5.8 and stay
   as they are. */
.btn.mini,.filters button,.themetoggle,.colguide summary{min-height:24px}
@media(max-width:700px){
  .btn.mini,.filters button,.themetoggle,.colguide summary{min-height:44px}
  /* line-height 2.2 keeps the 44px box reading as the single line it is:
     the layout assert compares box height to line-height to catch words
     split across lines, and a 44px chip around a 20px line measures like a
     two-line wrap */
  .filters button{padding-left:.85rem;padding-right:.85rem;line-height:2.2}
}
@media(max-width:600px){
  /* Vertical only. The shorthand `padding:.85rem 0` used to sit here and it
     zeroed the horizontal padding .masthead inherits as a .wrap, so between
     431px and 600px the wordmark, the nav and the datechip ran flush to the
     screen edge while the body content stayed inset. Below 431px a later rule
     restored .wrap's padding and hid the fault, which is why every width I
     tested — 390, 393, 398, 402, 430, then 480 — missed it. Jeremy found it on
     a 440px iPhone and guessed the cause from the symptom: "it may just be the
     menu." Never set the shorthand on an element that is also a .wrap. */
  .masthead{gap:.5rem .9rem;padding-top:.85rem;padding-bottom:.85rem}
  .themetoggle{margin-left:0}
  .hero,.heroq{padding-top:2rem}
}
/* ── the hamburger (round seven, note 18; cut moved 600 → 880 in Phase 1c):
   the nav hides behind the checkbox label and opens as a stacked column,
   script-free. Between 601 and 880px the full nav row used to WRAP, putting
   the masthead at 147-155px on some pages and 102 on others — a height no
   single --sticky-header-height token could honestly describe, which matters
   now that the sticky table headings hang off that token. Behind the menu
   button the header is one stable height per width band. 880 also matches
   the boards' card cut, so the compressed chrome starts at one width. This
   block sits AFTER the base `.masthead nav{display:flex}` rule on purpose —
   order decides at equal specificity. */
@media(max-width:880px){
  /* 44x44 (Phase 1c): the menu button measured 40x36 against the 44px floor */
  .nav-btn{display:inline-flex;align-items:center;justify-content:center;margin-left:auto;
    width:2.75rem;height:2.75rem;border:1px solid var(--field-border);border-radius:8px;
    color:var(--ink);cursor:pointer;font-size:1.05rem;line-height:1;background:var(--card)}
  .nav-cb:focus-visible+.nav-btn{outline:2px solid var(--sky);outline-offset:2px}
  .nav-btn .x{display:none}
  .nav-cb:checked+.nav-btn .x{display:inline}
  .nav-cb:checked+.nav-btn .h{display:none}
  .masthead nav{display:none}
  .nav-cb:checked~nav{display:flex;flex-direction:column;align-items:stretch;gap:.1rem;
    margin-left:0;order:4;flex:1 0 100%;
    border-top:1px solid var(--line-soft);padding-top:.4rem;margin-top:.2rem}
  /* Explicit order, because DOM order puts the burger BEFORE the pill and the
     burger carries `margin-left:auto`. Left to itself that pushes the burger to
     the right edge and strands the pill past it. The pill takes the auto margin
     instead and both sit at the right, CTA first, in the reading order a thumb
     expects. `order` on the menu is 4 so the opened column still drops below
     the whole row rather than between the two controls. */
  /* EVERY child gets an explicit order here, because leaving any of them at the
     default 0 is what put the theme toggle on row one. First attempt set order
     only on the pill and the burger; the toggle kept order 0, sorted ahead of
     both, and took the space beside the wordmark — so the masthead went to three
     rows and ~360px of chrome before any content on a 390px screen.
     Row 1 is identity and action: wordmark, CTA, menu. The datechip is
     width:100% so it always breaks the line, and everything after it is
     secondary. */
  .masthead .wordmark{order:1}
  .masthead .mast-cta{order:2;margin-left:auto;margin-right:.5rem}
  .nav-btn{order:3;margin-left:0}
  .masthead .datechip{order:5}
  .masthead .themetoggle{order:6;margin-left:0}
  /* THE HEADER STOPS STICKING BELOW 881px (28 Jul 2026). It stuck at every width
     before, and measured 179px tall on /united/fleet/ at 390px — a fifth of an
     844px viewport permanently spent on chrome, on the surface with the least of
     it to spare. Desktop keeps the sticky header; the phone gets its screen back.

     --sticky-header-height MUST go to zero with it, and this is the coupling that
     makes the change non-trivial. Three things read that token: the sticky
     <thead> in the 700-880px band, html{scroll-padding-top}, and
     [id]{scroll-margin-top}. Leave it at 7rem while the header no longer occupies
     that space and the table headings hang 112px below the viewport top with
     nothing above them, and every in-page anchor lands 112px short of its target.
     Both selectors are repeated because html.hdr-c (0,1,1) outranks :root (0,1,0)
     and the collapsed-header override earlier in this file would otherwise win. */
  header.site{position:static}
  :root,html.hdr-c{--sticky-header-height:0rem}
}
@media(max-width:430px){
  /* At the narrowest widths the wordmark plus a full-width pill plus the burger
     will not sit on one line, and wrapping the masthead to three rows costs more
     than the label does. The pill keeps its 44px target and loses only padding. */
  .masthead .mast-cta{padding:0 .8rem;font-size:.72rem;margin-right:.35rem}
}
@media(max-width:430px){
  .wrap{padding-left:max(.9rem,env(safe-area-inset-left));
    padding-right:max(.9rem,env(safe-area-inset-right))}
  .wordmark{font-size:1.2rem}
  .sec-h h2{font-size:1.25rem}
  .card,.panel{padding:.9rem 1rem}
  .fa{padding:1rem 1rem .9rem}
  .scorenum .n{font-size:3.2rem}
  ul.vstate .st{display:block;margin-bottom:.15rem}
}

/* ═══ ANIMATION — every rule below is scoped under html.anim ═══════════════
   site.js adds .anim to <html> only when matchMedia('(prefers-reduced-motion:
   no-preference)') matches. No JS, or reduced motion => none of this exists and
   the page is already in its finished state. ═══════════════════════════════ */
/* ── THERE IS NO TRANSITION ON <body>, AND THAT IS THE FIX FOR A REAL BUG ──
   This file used to carry `body{transition:background .2s,color .2s}` so the
   theme switch cross-faded. It does not cross-fade. It STICKS: when the new
   value arrives because a custom property changed (--paper and --ink change when
   a class lands on <html>), Chrome never starts the transition and the property
   holds the OLD theme's value indefinitely. Flipping the switch recoloured the
   links, the rules, the mark and every card, and left the page's ground and its
   body text in the theme you just left.

   Measured on a built page, not guessed. 1,500 ms after the class change, with a
   200 ms transition declared:

     getComputedStyle(body).backgroundColor  rgb(27,23,19)   ← the dark ground
     getComputedStyle(body).color            rgb(242,233,219)← the dark ink
     getComputedStyle(body)['--paper']       #fbf8f2         ← the LIGHT ground
     getComputedStyle(body)['--ink']         #29241c         ← the LIGHT ink

   Setting transition:none unfroze both in the same tick, with no theme change.
   The old stylesheet had the same declaration against the same var swap, so the
   live site has been doing this the whole time — a green build, a 200, and the
   wrong bytes on screen, which is this project's recurring failure mode.

   A theme cross-fade is decoration, and decoration does not get to break the
   thing it decorates. If you want it back, transition a wrapper element that
   does not own the vars, and verify it by reading the COMPUTED value in a
   browser rather than by watching it once. */
html.anim .rv{opacity:0;transform:translateY(8px);
  transition:opacity .3s ease-out,transform .3s ease-out}
html.anim .rv.in{opacity:1;transform:none}
html.anim .rv[data-d="1"]{transition-delay:60ms}
html.anim .rv[data-d="2"]{transition-delay:120ms}
html.anim .rv[data-d="3"]{transition-delay:180ms}
html.anim .rv[data-d="4"]{transition-delay:240ms}
html.anim .rv[data-d="5"]{transition-delay:300ms}
html.anim .rv[data-d="6"]{transition-delay:360ms}

html.anim .viz .s-ml,html.anim .viz .s-ex{stroke-dasharray:4000;stroke-dashoffset:4000}
html.anim .viz.in .s-ml,html.anim .viz.in .s-ex{stroke-dashoffset:0;
  transition:stroke-dashoffset .9s ease-in-out}
html.anim .viz .a-ml,html.anim .viz .a-ex{opacity:0}
html.anim .viz.in .a-ml,html.anim .viz.in .a-ex{opacity:1;transition:opacity .6s ease-out .3s}
html.anim .viz .bar{transform:scaleY(0);transform-origin:center bottom;
  transition:transform .7s ease-out}
html.anim .viz.in .bar{transform:scaleY(1)}
html.anim .viz .bar:nth-child(2){transition-delay:40ms}
html.anim .viz .bar:nth-child(3){transition-delay:80ms}
html.anim .viz .bar:nth-child(4){transition-delay:120ms}
html.anim .viz .bar:nth-child(5){transition-delay:160ms}
html.anim .viz .bar:nth-child(6){transition-delay:200ms}
html.anim .viz .bar:nth-child(7){transition-delay:240ms}
html.anim .viz .bar:nth-child(8){transition-delay:280ms}
html.anim .viz .bar:nth-child(9){transition-delay:320ms}
html.anim .viz .bar:nth-child(10){transition-delay:360ms}

/* waffle cells fill in install order — 20 delay buckets baked per panel, so
   1807 cells cost 20 CSS rules instead of 1807 inline styles */
html.anim .waffle i.eq{opacity:.15;transition:opacity .25s ease-out}
html.anim .waffle.in i.eq{opacity:1}
html.anim .waffle i.d1{transition-delay:60ms}   html.anim .waffle i.d2{transition-delay:120ms}
html.anim .waffle i.d3{transition-delay:180ms}  html.anim .waffle i.d4{transition-delay:240ms}
html.anim .waffle i.d5{transition-delay:300ms}  html.anim .waffle i.d6{transition-delay:360ms}
html.anim .waffle i.d7{transition-delay:420ms}  html.anim .waffle i.d8{transition-delay:480ms}
html.anim .waffle i.d9{transition-delay:540ms}  html.anim .waffle i.d10{transition-delay:600ms}
html.anim .waffle i.d11{transition-delay:660ms} html.anim .waffle i.d12{transition-delay:720ms}
html.anim .waffle i.d13{transition-delay:780ms} html.anim .waffle i.d14{transition-delay:840ms}
html.anim .waffle i.d15{transition-delay:900ms} html.anim .waffle i.d16{transition-delay:960ms}
html.anim .waffle i.d17{transition-delay:1020ms} html.anim .waffle i.d18{transition-delay:1080ms}
html.anim .waffle i.d19{transition-delay:1140ms}
html.anim .waffle i.eq{transition-property:opacity,transform}
html.anim .waffle i.eq:hover{transform:scale(1.6);transition-duration:.15s;transition-delay:0s}

html.anim .track .fill{width:0;transition:width 1s ease-out}
html.anim .track.in .fill{width:var(--pct)}

@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *{animation:none!important;transition:none!important}
}

/* ── THE REPORT FORM, AND A HONEYPOT THAT WAS ADVERTISING ITSELF ───────────
 * Found on the branch before it ever reached a preview: `.hp` had no rule at
 * all, so the anti-spam honeypot rendered as a visible field labelled "Leave
 * this empty" on all 23 pages carrying the report block. A honeypot a human can
 * read is not a honeypot, and it is also the single most broken-looking element
 * we could have put under a form that asks for trust.
 *
 * It is off-screen rather than `display:none`, because some bots skip inputs
 * that are display-none and fill everything else. `aria-hidden` and
 * `tabindex="-1"` are already on the markup, so no screen reader or keyboard
 * user ever reaches it.
 *
 * The rest of these were the same omission with milder symptoms: the form
 * rendered in browser defaults on every page it appears. */
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
/* the honeypot's label has NO box at all: it is not a control, and a 1px rect
   still reads as an undersized target to anything measuring rects. Bots read
   the DOM, not the layout, so this hides nothing from them. */
.hp label{display:block;width:0;height:0;overflow:hidden}

.frm{margin:18px 0 0}
.ffgrid{display:grid;gap:14px 16px;grid-template-columns:1fr}
@media (min-width:620px){.ffgrid{grid-template-columns:1fr 1fr}}
.ff{display:flex;flex-direction:column;gap:5px;min-width:0}
.ff.wide{grid-column:1/-1}
/* THE LABEL WRAPS ITS FIELD since Phase 1c. A 17px-tall label above a field
   is a click target that focuses the input, and it measured 291x17 against
   the 24/44px floors. Wrapping the control puts the whole field inside the
   label's box — the honest fix, not a padded phantom — while the for=""
   attribute stays for explicitness. The visible caption is the .lt span. */
.ff label{display:flex;flex-direction:column;gap:5px;min-width:0}
.ff .lt,.fh{font:600 12px/1.4 var(--sans);letter-spacing:.04em;
  text-transform:uppercase;color:var(--ink-3)}
.fh{margin:20px 0 2px;grid-column:1/-1}
.ff input,.ff select,.ff textarea{
  font:400 15px/1.45 var(--sans);color:var(--ink);background:var(--field-bg);
  border:1px solid var(--field-border);border-radius:3px;padding:9px 11px;width:100%;
  min-width:0;-webkit-appearance:none;appearance:none;
  /* the wrapping label is uppercase microtype; what you TYPE is not */
  text-transform:none;letter-spacing:normal}
.ff select{background-image:none}
.ff textarea{min-height:82px;resize:vertical}
.ff input:focus-visible,.ff select:focus-visible,.ff textarea:focus-visible{
  outline:2px solid var(--sky);outline-offset:1px;border-color:var(--sky)}
.ff input::placeholder,.ff textarea::placeholder{color:var(--ink-3);opacity:.75}

/* An error only occupies space once the endpoint has put a sentence in it. */
.ferr{margin:0;font:400 13px/1.4 var(--sans);color:var(--s-long)}
.ferr:empty{display:none}
.ferr.frm-top{margin-bottom:10px}
.frm-ft{margin:16px 0 0;display:flex;flex-wrap:wrap;gap:12px;align-items:baseline}
.frm-ft .note{margin:0;flex:1 1 16rem;min-width:0}

/* ── the split-homepage seam is GONE — round seven, 27 Jul 2026. The halfmark
 * strips and the .sm-off lede spans left with the two-named-halves layout;
 * nothing emits those classes any more. */

/* ── the 26 Jul pivot: the board drawn, the race strip, the loop bar ──────
 * Every number drawn before it is written. The bar's colour is the band and
 * ONLY the band; the chip word beside the number is the non-colour signal. */
.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap}
.tbl.board td{padding-top:.62rem;padding-bottom:.62rem}
.tbl.board .barcell{width:26%;min-width:120px}
.scobar{display:block;height:9px;border-radius:99px;background:var(--bg-inset);
  overflow:hidden}
.scobar .fill{display:block;height:100%;border-radius:99px}
.scobar .fill.sc-good{background:var(--good)}
.scobar .fill.sc-mix{background:var(--mixed)}
.scobar .fill.sc-long{background:var(--long)}
.scobar .fill.sc-no{background:var(--zero)}
/* ═══ THE BOARDS BELOW 880px ARE CARDS, NOT TABLES (Phase 1b) ═════════════
   The shed-and-squeeze approach that stood here died of its own information
   model: rank, airline, code, score, band, tier, fitted share, next-gen odds,
   publication state and sort state in one table row cannot be tuned into a
   390-440px column, and `overflow-wrap:anywhere` — the rule that kept it from
   overflowing — was rendering "America / n" and 9.6px headers while every
   overflow sweep reported clean. That rule is gone from the boards entirely.

   Both boards now bake TWO renderings of the SAME data model: the real table
   (desktop earns cross-row comparison) and one .crd record per airline. CSS
   picks exactly one. The cut is 880px, not 700: the seven-column rank board is
   832px of table at minimum, so anywhere under ~880px it could only survive
   inside a sideways scroll frame, which is the trap the audit called the
   site's worst surface — and which also broke every sticky heading. Above
   880px the boards fit the wrap with no scroll frame at all.

   The record, per the approved plan: rank + name (never breaks) + code and
   the score with its band word on the first line; next-gen odds labelled in
   words, carrying its per cent sign; ONE supporting line that states its own
   denominator; a 44px link to the airline page. Sorting stays as the same
   segmented control — there are no column headers here to point at. */
.board-note{display:none;font-size:.875rem;color:var(--muted);margin:.5rem 0 0}
.cardsb{display:none;list-style:none;margin:.4rem 0 0;padding:0}
.crd{border:1px solid var(--line);border-radius:var(--r-lg);background:var(--card);
  box-shadow:var(--sheen);padding:.8rem .95rem .35rem;margin-top:.6rem}
.crd-top{display:flex;align-items:baseline;gap:.3rem .55rem;flex-wrap:wrap}
.crd-rank{color:var(--muted);font-size:.8rem;font-variant-numeric:tabular-nums;
  min-width:1.6rem}
.crd-top .aname{font-weight:700;white-space:nowrap;font-size:1.02rem}
.crd-top .code{font-size:.75rem;font-weight:600;color:var(--muted);letter-spacing:.1em}
.crd-sco{margin-left:auto;display:flex;align-items:baseline;gap:.45rem;white-space:nowrap}
.crd-sco .sco{font-family:var(--serif);font-size:1.35rem;font-weight:700;
  color:var(--band);line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.crd-ng{display:flex;align-items:baseline;justify-content:space-between;gap:.7rem;
  margin:.45rem 0 0;font-size:.92rem;color:var(--ink-soft)}
.crd-ng b{color:var(--ink);font-size:1.02rem;font-variant-numeric:tabular-nums;
  text-align:right}
.crd-sub{margin:.15rem 0 0;font-size:.875rem;color:var(--muted)}
/* the 44px link row: WCAG 2.5.8 preferred size on touch widths, and the card's
   one obvious door */
.crd-go{display:flex;align-items:center;min-height:44px;font-size:.9rem;
  margin-top:.15rem;border-top:1px solid var(--line-soft)}
/* the 18-row boards keep the same record at a denser rhythm */
.cardsb.dense .crd{padding:.55rem .8rem .15rem;margin-top:.45rem}
.cardsb.dense .crd-ng{margin-top:.25rem}
.cardsb.dense .crd-go{min-height:44px}
@media(max-width:880px){
  .rankb .tbl-shell,.board-shell{display:none}
  .cardsb{display:block}
  .board-note{display:block}
  /* no split columns in the record, so no control may point at them */
  .rankb .filters button[data-sort="mainline"],
  .rankb .filters button[data-sort="regional"]{display:none}
}

/* Provenance lines carry bare domains — unitedstarlinktracker.com is one
   unbreakable 25-character word — and at a raised phone text size a single one
   of them was forcing the document to 466px in a 398px screen. Phase 1b took
   `anywhere` out sitewide with the board tables that needed it; what stays
   here is `break-word`, which breaks NOTHING at any tested width (the layout
   assert proves no word renders across lines) but still lets that one
   unbreakable domain give way at 200% text instead of forcing a sideways
   page. The difference from `anywhere` is min-content sizing: this rule can
   no longer squeeze a column narrow enough to shatter ordinary words. */
.prov,.src,.footnote,.credit .cbody{overflow-wrap:break-word}

/* Chart lines own a third small palette, by Jeremy's call on 26 Jul 2026: the
 * bands still own scores and the sky still owns chrome, but a time-series line
 * encodes IDENTITY, not a score, so it may not borrow a band colour and no
 * longer has to be bare ink. Three hues, none of them a band's. */
:root{--chart-1:#2d5a7d;--chart-2:#7d5aa6;--chart-3:#2e7d8a}
html.dark{--chart-1:#8fc0e0;--chart-2:#b79fe0;--chart-3:#7ecfdd}
.racestrip{display:grid;grid-template-columns:1.3fr 1fr;gap:26px;align-items:center;
  margin-top:.6rem}
.racestrip .spark path.s{stroke:var(--chart-1);stroke-width:2.2}
.racestrip .spark path.f{fill:var(--chart-1);fill-opacity:.12}
.rs-counts{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem}
.rs-counts li{font-size:.92rem;color:var(--muted-strong);display:flex;align-items:baseline;
  gap:.55rem}
.rs-counts b{font-family:var(--serif);font-size:1.25rem;color:var(--ink);
  font-variant-numeric:tabular-nums}
.rs-counts .dot{width:10px;height:10px;border-radius:99px;flex:0 0 auto;align-self:center}
.rs-counts .dot.c1{background:var(--chart-1)}
.rs-counts .dot.c2{background:var(--chart-2)}
.rs-counts .dot.c3{background:var(--chart-3)}
@media(max-width:700px){.racestrip{grid-template-columns:1fr}}

.loopbar{display:flex;gap:1rem 1.4rem;align-items:baseline;flex-wrap:wrap;
  background:var(--panel);border:1px solid var(--line);border-radius:var(--r-md);
  padding:1rem 1.3rem}
.loopbar p{margin:0;font-family:var(--serif);font-size:1.02rem;flex:1 1 26ch}


/* ═══ ROUND SEVEN (27 Jul 2026) — the approved homepage, generated ═════════
   Ported from websites/public/wifiodds-b4/ after Jeremy's three review rounds.
   Everything below is shared chrome: the boards, the badge, the skyline and
   the systems chart appear wherever the generators emit them. ═══════════════ */

/* ── figures that must never break mid-figure ── */
.fitpct{white-space:nowrap}

/* ── the hero call: extension surface 1 of the sitewide 3 ── */
.herocall{display:flex;flex-wrap:wrap;align-items:center;gap:.7rem 1.6rem;margin:1.7rem 0 .3rem;
  padding:.95rem 1.15rem;border-radius:12px;
  /* Google Chrome Blue banner, approved by Jeremy 27 Jul 2026 — hardcoded,
     not a theme token, so it reads the same in light and dark mode rather
     than guessing at a second, unapproved light-mode treatment. */
  background:#1a73e8;border:1px solid #1a73e8}
.herocall .hc-t{flex:1 1 17rem;min-width:0}
.herocall .hc-h{margin:0;font-size:.98rem;color:#fff}
.herocall .hc-h b{font-family:var(--serif);font-size:1.04rem;color:#fff}
.herocall .hc-s{margin:.15rem 0 0;font-size:.85rem;line-height:1.5;color:#fff}
/* the button now lives in its own row under .hc-s, not inside its sentence */
.herocall .hc-btnrow{margin-top:.55rem}
/* white/filled, blue text — legible on the blue ground, in place of the
   ghost look .btn.ghost.mini has everywhere else. Scoped to .herocall so no
   other ghost/mini button on the site changes. */
.herocall .btn.ghost.mini{background:#fff;color:#1a73e8;border-color:#fff}
.herocall .btn.ghost.mini:hover{background:#fff;color:#1a73e8;border-color:#fff;filter:saturate(1.1)}
/* P2-04: .cws-ver's sitewide rule below sets color:var(--muted) (#a4988a),
   which measures 1.595:1 against this banner's #1a73e8 — nowhere near the
   4.5:1 AA floor the rest of this component holds (.hc-h/.hc-s/.hc-h b all
   measure 4.505:1 at #fff). Scoped to .herocall so the sitewide muted
   .cws-ver used elsewhere (on light/dark page backgrounds, not this blue
   banner) is untouched. #fff is the ceiling here, not an arbitrary pick:
   background luminance is fixed, so no color lighter than white exists, and
   white is exactly what the rest of this banner's text already uses —
   measures 4.505:1, same as the heading/description/button. */
.herocall .cws-ver{color:#fff}
@media(max-width:480px){
  .herocall{padding:.6rem .8rem;gap:.5rem .8rem;margin:1.15rem 0 .1rem}
  .herocall .hc-s{display:none}
  .herocall .hc-h{font-size:.9rem}
  .herocall .hc-h b{font-size:.95rem}
}

/* ── Google's store badge, self-hosted, resized only (496:150 held by
   width:auto), always a link to the listing. Bordered art on the dark ground,
   plain on light, switched exactly the way the themes switch, so it is
   correct with script off too. The version sits on its own line beneath,
   because Google's rules forbid putting it inside the badge. ── */
.cwswrap{display:flex;flex-direction:column;align-items:flex-start;gap:.3rem;flex:0 0 auto}
.cwsbadge{display:inline-block;line-height:0}
.cwsbadge img{height:52px;width:auto;display:block}
.cws-ver{font-size:.75rem;font-weight:600;letter-spacing:.09em;text-transform:uppercase;
  color:var(--muted)}
.cws-d{display:none}
@media(prefers-color-scheme:dark){
  html:not(.light) .cws-d{display:block}
  html:not(.light) .cws-l{display:none}
}
html.dark .cws-d{display:block}
html.dark .cws-l{display:none}
html.light .cws-d{display:none}
html.light .cws-l{display:block}
.extblk .cwsbadge img{height:58px}
@media(max-width:480px){.cwsbadge img,.extblk .cwsbadge img{height:44px}}

/* The skyline was removed on 27 Jul 2026 along with its rules. Eighteen
   unlabelled bars, no scale, and touch targets measured at 11.5px wide at
   320px. See build/lib/render.js for the reasoning. */
/* ── the column guide reads as a control: a bordered, hoverable disclosure
   with a chevron that flips. details/summary, so it opens with script off. ── */
.colguide{margin:1.5rem 0 0}
.colguide summary{display:inline-flex;align-items:center;gap:.55rem;padding:.5rem .95rem;
  border:1px solid var(--field-border);border-radius:8px;background:var(--card);
  color:var(--ink);font-size:.84rem;font-weight:600;cursor:pointer;list-style:none;
  width:auto;user-select:none}
.colguide summary::-webkit-details-marker{display:none}
.colguide summary:after{content:"\25BE";font-size:.8em;color:var(--muted);transition:transform .15s}
.colguide[open] summary:after{transform:rotate(180deg)}
.colguide summary:hover{border-color:var(--sky);color:var(--sky)}
.cg-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:10px;
  margin:.8rem 0 0}
.cg{background:var(--card);border:1px solid var(--line);border-radius:10px;
  padding:.6rem .8rem .65rem;font-size:.79rem;line-height:1.45;color:var(--ink-soft)}
.cg b{display:block;font-size:.78rem;color:var(--ink)}
.cg .ar{font-family:var(--mono);color:var(--muted);font-size:.75rem}

/* ── the Rank-by control row. The single-class .segctrl selector is
   load-bearing: build/apitest.js reads it to prove .needs-js still sits on an
   element with its own non-block display. ── */
.segctrl{display:flex;align-items:baseline;gap:.7rem;flex-wrap:wrap;margin:1.4rem 0 .5rem}
.segctrl .lbl{font-size:.75rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--muted-strong)}
.segctrl .filters{margin:0}

/* ── clickable sortable headers on the rank boards ── */
html.js .rankb th[data-rc]:not([data-rc="rank"]){cursor:pointer;user-select:none}
html.js .rankb th[data-rc]:not([data-rc="rank"]):not([aria-sort]):after{content:"\2195";
  opacity:.45;margin-left:.3rem;font-size:.75rem}
.rankb th[data-rc][aria-sort="ascending"]:after{content:"\2191";opacity:.75;margin-left:.3rem;
  font-size:.75rem}

/* ── everything below a board is ONE box: hairline-ruled rows inside a single
   border, each row under a small-caps label, spanning the table width. ── */
.bfoot{margin-top:1.4rem;border:1px solid var(--line);border-radius:var(--r-lg);
  background:var(--card);box-shadow:var(--sheen)}
.bfoot>div{padding:.85rem 1.1rem;border-top:1px solid var(--line-soft)}
.bfoot>div:first-child{border-top:0}
.bfoot .bf-l{display:block;font-size:.75rem;font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;color:var(--muted);margin-bottom:.35rem}
.bfoot p{margin:0;font-size:.875rem;color:var(--ink-soft);line-height:1.55}
.bf-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem 1.6rem}
.bfoot .btnrow{margin:.1rem 0 0}
.bfoot .src,.bfoot .board-note{margin:0;max-width:none}
.ngunpub{font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted)}

/* ── the Big 4 rows, tightened (the one fix on top of the mockup) ──
   b4 let every cell wrap freely and auto layout squeezed the last column
   until the projected chip broke onto three lines and its citation onto three
   more. The chip and its date phrase stay one unit; the citation drops to its
   own line; the column is wide enough that only genuinely long phrases wrap. */
.rankb th:last-child{width:30%;white-space:normal}
/* the two-word column names may break — "Mainline next-gen" on one nowrap
   line cost 162px of column that the projected chip needed. A header on two
   short lines is cheaper than a data row on six. */
.rankb th[data-rc="nextgen"],.rankb th[data-rc="mainline"],
.rankb th[data-rc="regional"]{white-space:normal}
.rankb .proj{white-space:normal;flex-wrap:wrap;row-gap:.1rem}
.rankb td .proj+.micro{display:block;margin-top:.3rem}
/* the 18-board's projected cells get the same treatment: the chip may wrap at
   its own gaps, the header may wrap, and neither may widen the table past the
   shared right edge. width is a PREFERENCE under auto table layout, never a
   min-content floor — a min-width here put the Big 4 table at 942px inside an
   888px wrap, which is exactly the hidden scroll this block exists to kill. */
.tbl.board .proj{white-space:normal;flex-wrap:wrap;row-gap:.1rem}
.tbl.board th:last-child{white-space:normal}

/* ── the rank boards on a phone: the shed-and-squeeze block that stood here
   is gone (Phase 1b). Under 880px the .rankb table is display:none and the
   .cardsb records render instead — see THE BOARDS BELOW 880px ARE CARDS
   above. No `overflow-wrap:anywhere`, no 9.6px headers, no hidden scroll. */

/* ── the systems chart, hardware-identity palette (round seven, notes 12-14).
   A FOURTH colour owner: hardware identity, beside the bands (numbers), the
   sky (chrome) and --chart-1/2/3 (time series). Drawn from the sky family —
   blues, teals, violets — so a quality weight can never be misread as a
   score band. ── */
:root{--hw-leo:#2d5a7d;--hw-mgeo:#1f7a86;--hw-lgeo:#6c55a3;--hw-atg:#5d6f80}
:root.dark{--hw-leo:#8fc0e0;--hw-mgeo:#6fd0d8;--hw-lgeo:#b49fe0;--hw-atg:#93a5b3}
@media(prefers-color-scheme:dark){
  :root:not(.light){--hw-leo:#8fc0e0;--hw-mgeo:#6fd0d8;--hw-lgeo:#b49fe0;--hw-atg:#93a5b3}
}
.hw-leo{--hw:var(--hw-leo)}.hw-mgeo{--hw:var(--hw-mgeo)}
.hw-lgeo{--hw:var(--hw-lgeo)}.hw-atg{--hw:var(--hw-atg)}
.sysrows{margin:1.7rem 0 .6rem}
.sysrow{display:grid;grid-template-columns:minmax(11rem,16rem) 1fr 3.4rem;gap:.3rem 1.1rem;
  align-items:center;margin:1.2rem 0}
.sysrow .nm{font-weight:700;font-size:.95rem;line-height:1.25}
.sysrow .nm small{display:block;font-weight:400;color:var(--muted);font-size:.76rem;
  margin-left:1rem}
.hwdot{display:inline-block;width:.55rem;height:.55rem;border-radius:2px;background:var(--hw);
  margin-right:.45rem}
.sysrow .scobar{height:14px;border:1px solid var(--line-soft)}
.rngb{position:relative;overflow:visible}
.rngb .fill{position:absolute;top:0;bottom:0;background:var(--hw,var(--ink));min-width:4px;
  border-radius:99px}
.rngb .p10{position:absolute;top:-6px;bottom:-6px;left:28.9%;width:0;
  border-left:2px dashed var(--muted)}
.sysrow .wt{font-family:var(--mono);font-weight:700;font-size:.95rem;text-align:right;
  color:var(--ink)}
.sysrow .ms{grid-column:2/4;margin:0;font-size:.875rem;color:var(--muted)}
.sysaxis{display:grid;grid-template-columns:minmax(11rem,16rem) 1fr 3.4rem;gap:0 1.1rem;
  margin:.2rem 0 0}
.sysaxis .ax{grid-column:2;display:flex;justify-content:space-between;
  font:700 .75rem/1.4 var(--mono);color:var(--muted)}
.sysaxis .wl{grid-column:3;font:700 .75rem/1.4 var(--mono);color:var(--muted);text-align:right}
@media(max-width:560px){
  .sysrow,.sysaxis{grid-template-columns:1fr 3.4rem}
  .sysrow .nm{grid-column:1/3}
  .sysrow .ms{grid-column:1/3}
  .sysaxis .ax{grid-column:1}
  .sysaxis .wl{grid-column:2}
}

/* ── the release states as a labelled grid, not three paragraphs behind an
   inline-block label (round seven, note 16). Overrides the base ul.vstate
   rules above; later at equal-or-higher specificity, so these win. ── */
main ul.vstate{margin:1.4rem 0 0}
ul.vstate li{display:grid;grid-template-columns:10.5rem 1fr;gap:.35rem 1.2rem;
  align-items:start;padding:.85rem 0}
ul.vstate .st{min-width:0;justify-self:start;margin-top:.1rem;border:1px solid var(--line);
  border-radius:99px;padding:.16rem .6rem}
ul.vstate .st.live{color:var(--good);border-color:currentColor}
ul.vstate .st.blt{border-style:dashed}
ul.vstate p{margin:0}
@media(max-width:560px){ul.vstate li{grid-template-columns:1fr;gap:.3rem}}
