:root {
  --page: #f9f9f7;
  --surface: #fcfcfb;
  --ink: #0b0b0b;
  --ink-secondary: #52514e;
  --ink-muted: #898781;
  --border: #e1e0d9;
  --border-strong: #c3c2b7;
  --brand: #2554ff;
  --status-good: #0ca30c;
  --status-good-text: #006300;
  --status-warning: #fab219;
  --status-serious: #ec835a;
  --status-critical: #d03b3b;
  --radius: 6px;
  color-scheme: light;
}

/* Auto dark mode — same tokens, just repointed, so every component that's
   already built on these custom properties (buttons, tooltips, badges,
   chart fills via color-mix) adapts without extra dark-specific rules. */
@media (prefers-color-scheme: dark) {
  :root {
    --page: #0d0d0d;
    --surface: #1a1a19;
    --ink: #ffffff;
    --ink-secondary: #c3c2b7;
    --ink-muted: #898781;
    --border: #2c2c2a;
    --border-strong: #383835;
    --brand: #5c82ff;
    --status-good-text: #0ca30c;
    color-scheme: dark;
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

a { color: inherit; }

.muted { color: var(--ink-muted); }

/* Header */

.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.header-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 14px 20px;
  display: flex;
  align-items: center;
  gap: 24px;
  flex-wrap: wrap;
}
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 1.05rem;
  text-decoration: none;
  letter-spacing: -0.01em;
  white-space: nowrap;
  color: var(--brand);
}
.wordmark-icon {
  display: block;
  flex: none;
  width: 28px;
  height: 28px;
}

.search-form {
  display: flex;
  gap: 6px;
  margin-left: auto;
}
.search-carrier, .search-number {
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  padding: 8px 10px;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
  background: var(--surface);
  color: var(--ink);
}
.search-carrier { width: 60px; }
.search-number { width: 80px; }
.search-carrier:focus, .search-number:focus {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
}
.search-submit {
  border: 1px solid var(--brand);
  background: var(--brand);
  color: #fff;
  border-radius: 4px;
  padding: 8px 16px;
  font-size: 0.95rem;
  font-weight: 500;
  cursor: pointer;
}
.search-submit:hover { opacity: 0.85; }

/* Page shell */

.page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 32px 20px 64px;
}

.hidden { display: none !important; }

/* Home view — centered hero search, same card/border language as the data
   page (stat-row, sections) so the two views read as one product. */

.home-view {
  min-height: 56vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: 20px 0;
}

.home-hero {
  width: 100%;
  max-width: 520px;
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: var(--radius);
  padding: 40px 36px;
  text-align: center;
}

.home-tagline {
  margin: 0 0 24px;
  color: var(--ink-secondary);
  font-size: 1rem;
  line-height: 1.5;
}

.home-search-form {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 8px;
}
.home-search-carrier, .home-search-number, .home-search-date {
  padding: 12px 14px;
  font-size: 1.1rem;
  height: 48px;
}
.home-search-carrier { width: 76px; }
.home-search-number { width: 100px; }
/* min-width: 0 (overriding the flex-item default that pins shrinking to
   the native control's internal-UI width) fixed the field collapsing to
   a broken size on iOS. Separate, also-documented iOS Safari behavior:
   an *empty* date input shows no placeholder text at all (unlike
   desktop, which renders "mm/dd/yyyy" for an empty value) — sizing and
   emptiness turned out to be two different bugs, not one. Rather than
   keep working around that by prefilling a real value (today), this
   builds our own placeholder instead of depending on iOS's broken native
   one: .home-search-date-wrap is the actual flex item now (min-width: 0
   lives here instead of on the input, since it's the direct flex child),
   giving .home-search-date-overlay — a plain <span>, not a native form
   control, so it can't hit the same shadow-DOM rendering bug — something
   to position against. app.js toggles the overlay's visibility based on
   whether the input actually has a value. */
.home-search-date-wrap {
  position: relative;
  display: inline-block;
  width: 135px;
  height: 48px;
  min-width: 0;
  max-width: 100%;
}
.home-search-date {
  width: 100%;
  text-transform: none;
  line-height: 1.25;
  overflow: hidden;
  -webkit-appearance: none;
  appearance: none;
}
/* pointer-events: none lets a tap pass through to the real input
   underneath (still opens the native date picker) rather than the
   overlay text intercepting it — opacity is irrelevant to that, so the
   solid background below doesn't block interaction. That solid
   background matters for a different reason: on browsers that DO render
   their own native placeholder for an empty date input (desktop Chrome
   shows "mm/dd/yyyy"; iOS Safari shows nothing, which is the whole
   reason this overlay exists), a transparent overlay let both texts
   show through at once, garbled together. Matching the input's own
   background fully masks whatever's on the native control underneath,
   so only our text ever shows, consistently across browsers. */
.home-search-date-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  padding: 12px 14px;
  font-size: 1.1rem;
  color: var(--ink-muted);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
}
.home-search-submit { padding: 12px 22px; font-size: 1rem; }
.home-date-hint { max-width: 460px; margin: 10px auto 0; font-size: 0.78rem; text-align: center; }

.section-heading {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  font-weight: 600;
  margin: 0 0 10px;
}

.recent-searches { margin-top: 28px; padding-top: 24px; border-top: 1px solid var(--border); }
.recent-list { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }

.home-footer {
  display: block;
  margin-top: 24px;
  font-size: 0.78rem;
  color: var(--ink-muted);
  text-decoration: none;
}
.home-footer:hover { color: var(--ink); text-decoration: underline; }

/* Fun facts (home page, below the search card) — see loadFunFacts()/
   renderFunFacts() in app.js. Cards reuse the same bordered-tile visual
   language as the flight page's top stat row (label above value), just
   clickable — each one is a live example pulled from the real ingested
   data, not placeholder copy, so clicking it searches that exact
   flight/route. */
.fun-facts { width: 100%; max-width: 760px; text-align: center; }
/* flex + justify-content:center, not CSS grid — grid's auto-fit picks a
   fixed column count for the whole container and left-aligns whatever's
   left in a short final row (items sit in the first N columns, leaving a
   bare gap on the right). flex-wrap centers each row's own leftover
   space instead, so a row of 1-3 cards (now that there are up to 9 facts,
   not evenly divisible by any likely column count) sits centered like the
   full rows above it rather than stranded on the left. flex-grow: 0 keeps
   every card the same width regardless of row fullness — the alternative
   (letting cards grow to fill a short row) would make a lone leftover card
   balloon to the full row width, visually inconsistent with every other
   card on the page. */
.fun-facts-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: 10px;
  text-align: left;
}
.fun-fact-card {
  display: flex;
  flex: 0 1 180px;
  flex-direction: column;
  gap: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 14px 16px;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: left;
}
/* (hover: hover) scopes this to devices with a real persistent pointer —
   touch devices apply :hover on tap and have no "mouse left" event to ever
   clear it, so without this guard a tapped card stays visually
   highlighted until some other element is tapped, including across a
   back-navigation that reveals the home view again. app.js also blurs the
   button on click as a second guard, since a tapped <button> also picks up
   the default focus outline, which persists independently of :hover. */
@media (hover: hover) {
  .fun-fact-card:hover { border-color: var(--ink); }
  .fun-fact-card-info:hover { border-color: var(--border); }
}
.fun-fact-card-info { cursor: default; }
.fun-fact-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  font-weight: 600;
}
.fun-fact-value {
  font-size: 1.3rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.fun-fact-sub {
  font-size: 0.8rem;
  color: var(--ink-secondary);
}

.recent-chip, .example-chip {
  border: 1px solid var(--border-strong);
  background: var(--surface);
  border-radius: 4px;
  padding: 6px 12px;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  color: var(--ink);
}
.recent-chip:hover, .example-chip:hover { border-color: var(--ink); }

.example-row {
  margin-top: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}

@media (max-width: 560px) {
  .wordmark-icon { width: 34px; height: 34px; }
  .home-date-hint { display: none; }
  .home-hero { padding: 32px 20px; }
  .home-search-carrier { width: 64px; }
  .home-search-number { width: 84px; }
  .home-search-form { flex-wrap: wrap; row-gap: 10px; }
  /* Wide enough to fit iOS Safari's spelled-out date rendering ("Aug 20,
     2026", not the compact "08/20/2026" desktop shows) on one line at
     the 1.1rem font-size shared with carrier/number. Still leaves room
     for carrier/number/date on one row within a ~375px viewport; Search
     wraps to its own row via flex-wrap either way. Targets the wrapper
     now, not the input directly — see the wrap's own rule above for why. */
  .home-search-date-wrap { width: 160px; order: 3; }
  .home-search-submit { order: 4; }
}

/* Result view */

.result-error {
  border: 1px solid var(--status-critical);
  background: color-mix(in srgb, var(--status-critical) 8%, var(--surface));
  color: var(--ink);
  border-radius: var(--radius);
  padding: 14px 16px;
  font-size: 0.92rem;
}

/* Heuristics banner — one data-driven takeaway near the top of a flight or
   route page, only rendered when a rule in app.js's computeHeuristic()
   schema actually fires. Same bordered/tinted-background language as
   .result-error, just severity-parameterized. */
.heuristic-banner {
  border-radius: var(--radius);
  padding: 12px 16px;
  font-size: 0.92rem;
  margin: 4px 0 20px;
  border: 1px solid var(--border-strong);
}
.heuristic-banner.good {
  border-color: var(--status-good);
  background: color-mix(in srgb, var(--status-good) 8%, var(--surface));
}
.heuristic-banner.warning {
  border-color: var(--status-warning);
  background: color-mix(in srgb, var(--status-warning) 12%, var(--surface));
}
.heuristic-banner.critical {
  border-color: var(--status-critical);
  background: color-mix(in srgb, var(--status-critical) 8%, var(--surface));
}

.flight-title {
  display: flex;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}
.flight-number {
  font-size: 1.9rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.flight-route {
  font-size: 1.3rem;
  color: var(--ink-secondary);
  font-variant-numeric: tabular-nums;
}
.flight-cities {
  color: var(--ink-muted);
  font-size: 0.95rem;
  margin: 0;
}
.flight-operator {
  color: var(--ink-muted);
  font-size: 0.95rem;
  margin: 2px 0 24px;
}
.flight-operator strong { color: var(--ink-secondary); }

/* Stat row */

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  margin-bottom: 32px;
}
.stat-tile {
  padding: 16px 18px;
  border-right: 1px solid var(--border);
}
.stat-tile:last-child { border-right: none; }
.stat-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
  margin-bottom: 6px;
}
.stat-value {
  font-size: 1.6rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.stat-value.good { color: var(--status-good-text); }
.stat-value.warning { color: color-mix(in srgb, var(--status-warning) 70%, black); }
.stat-value.serious { color: color-mix(in srgb, var(--status-serious) 65%, black); }
.stat-value.critical { color: var(--status-critical); }

/* Route picker (flight numbers that cover more than one city-pair) */

.route-picker, .time-bucket-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 4px 0 6px;
}
.route-pill {
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--ink);
  border-radius: 4px;
  padding: 6px 12px;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.route-pill:hover { border-color: var(--ink); }
.route-pill.active {
  border-color: var(--brand);
  background: var(--brand);
  color: #fff;
}
.route-pill-count { color: var(--ink-muted); }
.route-pill.active .route-pill-count { color: color-mix(in srgb, var(--surface) 70%, transparent); }
.route-picker-note {
  font-size: 0.82rem;
  color: var(--ink-muted);
  margin: 0 0 24px;
  max-width: 640px;
}

/* Route map (mainland + Alaska/Hawaii insets, see reference/us_map.json) */

.route-map-wrap {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 12px 16px;
  margin: 24px 0 32px;
}
.us-map { display: block; width: 100%; height: auto; }
.us-map-land {
  fill: color-mix(in srgb, var(--ink) 6%, var(--surface));
  stroke: var(--border-strong);
  stroke-width: 1;
  stroke-linejoin: round;
}
/* Internal state lines, mainland only (Alaska/Hawaii insets are already a
   single state, so their coastline doubles as the state border) — faint on
   purpose, well below the coastline's contrast, just enough to give the
   route line geographic context without competing with it. */
.us-map-state-border {
  fill: none;
  stroke: var(--border);
  stroke-width: 0.6;
  stroke-linejoin: round;
  opacity: 0.8;
}
.us-map-route {
  fill: none;
  stroke: var(--ink);
  stroke-linecap: round;
}
.us-map-arrow { fill: var(--ink); }
/* Hollow ring for the departure airport, solid fill for the arrival airport
   — a common flight-map convention that makes which end is which legible
   from the dot shapes alone, reinforced by the arrowhead on the route line. */
.us-map-dot { stroke: var(--ink); }
.us-map-dot-origin { fill: var(--surface); }
.us-map-dot-destination { fill: var(--ink); }
.us-map-label {
  fill: var(--ink-secondary);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-anchor: middle;
}

/* Route search — fallback for a flight number with no history of its own
   (or none matching the route you want); mounted independently on the home
   view, the no-data page, and below a multi-route picker. */

.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--ink-secondary);
  font-size: 0.88rem;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.link-button:hover { color: var(--ink); }

.route-search-mount { margin-top: 20px; text-align: left; }
.home-hero .route-search-mount { text-align: center; margin-top: 16px; }

/* Live schedule check — same collapsed-reveal-button family as route
   search above, kept visually distinct only by its own content once
   revealed (a schedule result, not a form). Results reuse .data-table (the
   same table the "Recent flights" section uses) rather than a flexbox list
   of pill buttons — direct feedback that the pill-per-row version read as
   cluttered; aligned columns read as more of a piece with the rest of the
   page's data-dense tables. */
.live-schedule-mount { margin-top: 12px; text-align: left; }
.live-schedule-result { font-size: 0.88rem; margin: 8px 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.live-schedule-table { margin: 8px 0 4px; }
.live-schedule-table td:last-child { text-align: right; }
.live-schedule-date-mount { margin-top: 10px; }
.live-schedule-date-form { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.live-schedule-date-input {
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  background: var(--surface);
  color: var(--ink);
  padding: 6px 10px;
  font-size: 0.85rem;
  font-family: inherit;
}

.route-search-body {
  margin-top: 12px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.route-search-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.route-search-airport-field { position: relative; flex: 1 1 160px; min-width: 140px; }
.route-search-carrier, .route-search-origin, .route-search-dest, .route-search-time {
  width: 100%;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  padding: 8px 10px;
  font-size: 0.95rem;
  background: var(--surface);
  color: var(--ink);
}
.route-search-time { width: 130px; font-variant-numeric: tabular-nums; }
.route-search-arrow { color: var(--ink-muted); }
.route-search-suggest {
  position: absolute;
  z-index: 4;
  top: calc(100% + 4px);
  left: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 4px 12px color-mix(in srgb, var(--ink) 12%, transparent);
  max-width: 320px;
}
.airport-suggest-pill { font-size: 0.82rem; }
.route-search-error { margin-top: 10px; }

/* Section */

.section {
  margin-bottom: 32px;
  padding-top: 24px;
  border-top: 1px solid var(--border);
}
.section-title {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 4px;
}
.section-sub {
  font-size: 0.85rem;
  color: var(--ink-muted);
  margin: 0 0 14px;
}

/* Chart chrome (shared by histogram / day-of-week / month bar charts) */

.chart-wrap { width: 100%; overflow-x: auto; }
.bar-chart { font-family: inherit; }
.bar-chart .axis-label {
  fill: var(--ink-muted);
  font-size: 11px;
}
.bar-chart .value-label {
  fill: var(--ink-secondary);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.bar-chart .value-label-muted {
  fill: var(--ink-muted);
  font-size: 10px;
  font-style: italic;
}
.bar-chart .gridline { stroke: var(--border); stroke-width: 1; }
.bar-chart .baseline { stroke: var(--border-strong); stroke-width: 1; }
.bar-rect { cursor: pointer; }

.status-legend {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 0.8rem;
  color: var(--ink-secondary);
}
.status-legend-item { display: flex; align-items: center; gap: 6px; }
.status-swatch {
  width: 10px; height: 10px; border-radius: 2px; display: inline-block;
}

/* Time-breakdown bar (ground vs. air time composition) — neutral ink
   shades, not the status green/amber/red palette, since this isn't a
   good/bad signal. Air time (the dominant segment) gets the strongest
   tone; the two taxi segments are lighter, giving an intuitive
   light-to-dark-to-light "gate, runway, air, runway, gate" read. */
.time-breakdown-bar { display: block; width: 100%; height: 18px; border-radius: 3px; overflow: hidden; }
.time-breakdown-segment { cursor: pointer; }
.time-breakdown-taxi-out { fill: color-mix(in srgb, var(--ink) 40%, var(--surface)); background: color-mix(in srgb, var(--ink) 40%, var(--surface)); }
.time-breakdown-air { fill: var(--ink); background: var(--ink); }
.time-breakdown-taxi-in { fill: color-mix(in srgb, var(--ink) 22%, var(--surface)); background: color-mix(in srgb, var(--ink) 22%, var(--surface)); }
.time-breakdown-legend-value { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }

/* Delay-cause donut (see renderDelayCauseBreakdown() in app.js). Tried a
   neutral ink-shade ramp first (matching the time-breakdown bar), but
   grayscale wedges in a ring are genuinely harder to tell apart at a
   glance than grayscale segments in a row — a bar's position and size
   still disambiguate it, a ring slice mostly has color to go on. These are
   5 distinct, deliberately muted hues instead — desaturated enough to
   stay in the site's calm palette rather than reading as a bright startup
   chart, but clearly NOT the green/amber/orange/red status palette used
   for delay severity elsewhere: a delay *cause* has no inherent ranking,
   and reusing those colors here would wrongly suggest one slice is "the
   severe one." */
.delay-donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.delay-donut { display: block; }
.delay-donut-seg { cursor: pointer; transition: opacity 0.12s ease; }
.delay-donut-seg:hover { opacity: 0.75; }
.delay-donut-center-value { font-size: 22px; font-weight: 700; fill: var(--ink); font-variant-numeric: tabular-nums; }
.delay-donut-center-label { font-size: 10px; fill: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.05em; }
.delay-donut-carrier { stroke: #6b84a8; background: #6b84a8; }
.delay-donut-weather { stroke: #5fa89c; background: #5fa89c; }
.delay-donut-nas { stroke: #9178a8; background: #9178a8; }
.delay-donut-security { stroke: #a8926b; background: #a8926b; }
.delay-donut-lateAircraft { stroke: #b06e83; background: #b06e83; }

.chart-tooltip {
  position: absolute;
  background: var(--ink);
  color: var(--surface);
  font-size: 0.78rem;
  padding: 6px 9px;
  border-radius: 4px;
  pointer-events: none;
  white-space: nowrap;
  transform: translate(-50%, -100%);
  z-index: 5;
  opacity: 0;
  transition: opacity 0.08s ease;
}
.chart-tooltip.visible { opacity: 1; }
.chart-tooltip-container { position: relative; }

/* Tables */

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}
.data-table th {
  text-align: left;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  font-weight: 600;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border-strong);
}
.data-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.data-table tbody tr:hover { background: color-mix(in srgb, var(--ink) 3%, var(--surface)); }
.flight-row { cursor: pointer; }
.carrier-compare-row:not(.carrier-compare-current) { cursor: pointer; }
.carrier-compare-current { background: color-mix(in srgb, var(--ink) 3%, var(--surface)); font-weight: 600; }
.recent-flights-actions { display: flex; align-items: center; gap: 16px; margin: 14px 0 0; }

/* Below the mobile breakpoint, the recent-flights table switches from a
   wide row-of-columns layout (which needed real horizontal scrolling to
   see Departure/Arrival — direct feedback that this was a bad experience
   on a phone) to one stacked card per flight, each cell's column header
   shown as a small label above its value via the data-label attribute
   renderFlightRows() already sets on every <td>. Same markup either way —
   this is a pure CSS re-layout, no separate mobile template.
   Departure/Arrival pair up alongside Date/Route in a 2-column grid
   instead of every field stacking full-width — direct feedback that the
   first version of this (everything in one column) made each card too
   tall, leaving a lot of unused white space next to the short Date/Route
   lines that Departure/Arrival could sit in instead. The route-search
   view's extra Flight cell (:has() below) gets its own row rather than
   forcing a 3-across layout, since bare flight numbers don't pair as
   naturally with anything else. */
@media (max-width: 560px) {
  .data-table thead { display: none; }
  .data-table, .data-table tbody, .data-table tr, .data-table td { display: block; width: 100%; }
  .data-table tr.flight-row {
    position: relative;
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 12px;
    row-gap: 6px;
    align-items: start;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px 40px 10px 12px;
    margin-bottom: 8px;
  }
  .data-table tr.flight-row td {
    display: block;
    width: auto;
    padding: 0;
    border: none;
    white-space: normal;
  }
  .data-table tr.flight-row td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--ink-muted);
  }
  .data-table tr.flight-row td[data-label="Date"] { grid-column: 1; grid-row: 1; }
  .data-table tr.flight-row td[data-label="Departure"] { grid-column: 2; grid-row: 1; }
  .data-table tr.flight-row td[data-label="Route"] { grid-column: 1; grid-row: 2; }
  .data-table tr.flight-row td[data-label="Arrival"] { grid-column: 2; grid-row: 2; }
  .data-table tr.flight-row:has(td[data-label="Flight"]) td[data-label="Date"] { grid-column: 1; grid-row: 1; }
  .data-table tr.flight-row:has(td[data-label="Flight"]) td[data-label="Flight"] { grid-column: 2; grid-row: 1; }
  .data-table tr.flight-row:has(td[data-label="Flight"]) td[data-label="Route"] { grid-column: 1 / -1; grid-row: 2; }
  .data-table tr.flight-row:has(td[data-label="Flight"]) td[data-label="Departure"] { grid-column: 1; grid-row: 3; }
  .data-table tr.flight-row:has(td[data-label="Flight"]) td[data-label="Arrival"] { grid-column: 2; grid-row: 3; }
  /* The toggle button is the row's only unlabeled cell — pin it to the
     top-right corner instead of leaving it in the grid flow, and give it
     a round chip background so it reads as a distinct tappable icon
     rather than a stray character floating in the corner (real feedback:
     the plain chevron wasn't obviously an expand control on mobile). The
     whole card is already clickable (see wireFlightRow() in app.js) — this
     is purely about the affordance being legible at a glance. */
  .data-table tr.flight-row td:first-child { position: absolute; top: 8px; right: 8px; padding: 0; }
  .data-table tr.flight-row .flight-detail-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--border);
    color: var(--ink-secondary);
    font-size: 1.05rem;
  }
  .data-table tr.flight-detail-row {
    margin: -4px 0 8px;
  }
  .data-table tr.flight-detail-row td {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px;
    white-space: normal;
  }

  /* Same card-per-row treatment as .flight-row above (border, labeled
     cells via data-label + ::before) — the carrier-comparison table fell
     through to the generic .data-table mobile rule instead (thead hidden,
     every td just stacked full-width with no label and no card boundary),
     which read as an unlabeled wall of numbers with no way to tell which
     value belonged to which carrier or which stat. Carrier name spans the
     full card as a header row; the four stats pair up 2-across below it,
     same reasoning as Date/Route pairing with Departure/Arrival above —
     no expand toggle here, so no absolute-positioned corner element needed. */
  .data-table tr.carrier-compare-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 12px;
    row-gap: 6px;
    align-items: start;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px 12px;
    margin-bottom: 8px;
  }
  .data-table tr.carrier-compare-row td {
    display: block;
    width: auto;
    padding: 0;
    border: none;
    white-space: normal;
  }
  .data-table tr.carrier-compare-row td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--ink-muted);
  }
  .data-table tr.carrier-compare-row td[data-label="Carrier"] { grid-column: 1 / -1; grid-row: 1; }
  .data-table tr.carrier-compare-row td[data-label="On-time"] { grid-column: 1; grid-row: 2; }
  .data-table tr.carrier-compare-row td[data-label="Cancellation"] { grid-column: 2; grid-row: 2; }
  .data-table tr.carrier-compare-row td[data-label="Median delay"] { grid-column: 1; grid-row: 3; }
  .data-table tr.carrier-compare-row td[data-label="Flights"] { grid-column: 2; grid-row: 3; }

  /* Same card-per-row treatment as flight-row/carrier-compare-row above —
     the live-schedule mismatch table and the home-search multi-leg picker
     (same markup, both use .live-schedule-row) fell through to the
     generic .data-table mobile rule otherwise: route/times/link all
     stacked with no label and no card boundary, reading as disconnected
     lines rather than one row per leg. Only two labeled fields here (not
     four), so no 2-column pairing needed — the "See performance" link
     just sits below them as a clear tappable line. */
  .data-table tr.live-schedule-row {
    display: block;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px;
    margin-bottom: 8px;
  }
  .data-table tr.live-schedule-row td {
    display: block;
    width: auto;
    padding: 0;
    border: none;
    white-space: normal;
  }
  .data-table tr.live-schedule-row td + td { margin-top: 8px; }
  .data-table tr.live-schedule-row td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--ink-muted);
  }
}
.delay-cell.good { color: var(--status-good-text); }
.delay-cell.warning { color: color-mix(in srgb, var(--status-warning) 65%, black); }
.delay-cell.serious { color: color-mix(in srgb, var(--status-serious) 60%, black); }
.delay-cell.critical { color: var(--status-critical); font-weight: 600; }

/* Per-row expandable detail (ground-timeline / delay-cause breakdown) —
   see renderFlightDetail()/wireFlightDetailToggles() in app.js. */
.flight-detail-toggle {
  background: none;
  border: none;
  padding: 0;
  width: 100%;
  color: var(--ink-muted);
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
  transition: transform 0.12s ease;
}
.flight-detail-toggle:hover { color: var(--ink); }
.flight-detail-toggle[aria-expanded="true"] { transform: rotate(90deg); }
.flight-detail-row td {
  white-space: normal;
  background: color-mix(in srgb, var(--ink) 2%, var(--surface));
  padding: 14px 16px;
}
.flight-detail { display: flex; flex-direction: column; gap: 12px; }
.flight-detail-stats { display: flex; flex-wrap: wrap; gap: 14px 28px; }
.flight-detail-stat { display: flex; flex-direction: column; gap: 3px; }
.flight-detail-stat-label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}
.flight-detail-stat-value {
  font-size: 0.9rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.flight-detail-causes {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.flight-detail-causes.no-border { padding-top: 0; border-top: none; }
.flight-detail-cause-list { display: flex; flex-wrap: wrap; gap: 6px; }
.flight-detail-cause {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: 0.8rem;
  padding: 2px 8px;
  border-radius: 3px;
  background: var(--border);
  color: var(--ink-secondary);
}
.flight-detail-cause-min { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }

/* Live / extras */

.live-panel {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 18px 20px;
}

.live-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
@media (max-width: 640px) {
  .live-grid { grid-template-columns: 1fr; }
  .header-inner { flex-direction: column; align-items: stretch; }
  .search-form { margin-left: 0; }
}
.live-airport-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
}
.live-airport-code { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; }
.live-line { font-size: 0.85rem; color: var(--ink-secondary); margin: 3px 0; }
.live-note { color: var(--ink-muted); font-size: 0.88rem; }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.78rem;
  padding: 2px 8px;
  border-radius: 3px;
  font-weight: 600;
}
.badge.good { background: color-mix(in srgb, var(--status-good) 14%, var(--surface)); color: var(--status-good-text); }
.badge.warning { background: color-mix(in srgb, var(--status-warning) 20%, var(--surface)); color: color-mix(in srgb, var(--status-warning) 70%, black); }
.badge.serious { background: color-mix(in srgb, var(--status-serious) 18%, var(--surface)); color: color-mix(in srgb, var(--status-serious) 60%, black); }
.badge.critical { background: color-mix(in srgb, var(--status-critical) 14%, var(--surface)); color: var(--status-critical); }
.badge.neutral { background: var(--border); color: var(--ink-secondary); }

.loading-line { color: var(--ink-muted); font-size: 0.88rem; padding: 8px 0; }

/* Loading animation — plane bobbing through two parallax cloud layers, see
   loadingHtml() in app.js. Cloud shapes repeat every 300 viewBox units, so
   animating exactly -300 loops seamlessly. Front layer sits above the
   plane in paint order (drawn last) so clouds occasionally drift across
   it, back layer is smaller/fainter/slower for depth. */
.loading-plane { display: flex; flex-direction: column; align-items: center; padding: 40px 0 20px; }
.loading-plane-svg { width: 100%; max-width: 320px; height: 110px; display: block; }
.loading-plane .loading-line { padding-top: 4px; }
/* Live schedule check's inline loading state — a small "scanning" sweep
   bar, not a shrunk copy of the plane animation above (looked cramped at
   icon size). Plain ink-toned motion, no glow/gradient, matching DESIGN.md's
   restrained visual language. Text and bar sit side by side on one line —
   stacked (text above, bar below) read as two disconnected pieces. */
.live-schedule-loading { display: flex; align-items: center; gap: 10px; padding: 10px 0 4px; }
.live-schedule-loading .loading-line { font-size: 0.85rem; color: var(--ink-secondary); white-space: nowrap; }
.live-schedule-loading-track {
  flex: 0 0 auto;
  width: 90px;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
  position: relative;
}
.live-schedule-loading-sweep {
  position: absolute;
  top: 0;
  height: 100%;
  width: 35%;
  border-radius: 2px;
  background: var(--ink);
  animation: live-schedule-sweep 1.3s ease-in-out infinite;
}
@keyframes live-schedule-sweep {
  0% { left: -35%; }
  100% { left: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .live-schedule-loading-sweep { animation: none; left: 0; width: 100%; }
}
.cloud-layer .cloud ellipse { fill: color-mix(in srgb, var(--ink) 10%, var(--surface)); }
.cloud-layer-front .cloud ellipse { fill: color-mix(in srgb, var(--ink) 16%, var(--surface)); }
.cloud-layer-back { animation: cloud-drift 11s linear infinite; }
.cloud-layer-front { animation: cloud-drift 6s linear infinite; }
@keyframes cloud-drift {
  from { transform: translateX(0); }
  to { transform: translateX(-300px); }
}
.plane-icon {
  transform-origin: 0px 0px;
  animation: plane-bob 2.2s ease-in-out infinite;
}
.plane-icon path { fill: var(--ink); }
@keyframes plane-bob {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50% { transform: translateY(-5px) rotate(4deg); }
}
@media (prefers-reduced-motion: reduce) {
  .cloud-layer, .plane-icon { animation: none; }
}
