:root {
  --bg: #0f1420;
  --panel-bg: #171e2e;
  --panel-border: #2a3448;
  --text: #e7ecf5;
  --text-dim: #9aa7bd;
  --accent: #4da3ff;
  --danger: #e05a4d;
  --radar-highlight: #ff5f7e;
  --airport-marker: #2dd4bf;
  --radius: 12px;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f4f6fb;
    --panel-bg: #ffffff;
    --panel-border: #dde3ee;
    --text: #1a2233;
    --text-dim: #5b6980;
  }
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.icon-button {
  background: none;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  color: var(--text);
  font-size: 1.1rem;
  padding: 0.35rem 0.6rem;
  cursor: pointer;
}

.icon-button:hover {
  border-color: var(--accent);
}

.app-main {
  flex: 1;
  min-height: 0;
  padding: 1.25rem;
  display: flex;
  justify-content: center;
  align-items: stretch;
}

.location-gate {
  margin: auto;
  text-align: center;
  max-width: 24rem;
}

.location-gate p {
  color: var(--text-dim);
}

.content {
  width: 100%;
  max-width: 100rem;
  height: 100%;
  min-height: 0;
  display: flex;
  gap: 1.25rem;
  align-items: stretch;
}

.aircraft-panel {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-height: 0;
}

.empty-state {
  flex: 1;
  /* min-height:0 + overflow:hidden keep this panel's height defined by its flex
     slot (the viewport/radar-bounded row or column it sits in) rather than by
     its own content. The fit-to-box clock and weather scale their font up until
     the content fills that slot, so without this the box would instead grow to
     fit the content — an unstable feedback loop that never settles and overflows
     (most visible in weather-only mode, where no radar sibling bounds the row). */
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 1.75rem 1.5rem;
  text-align: center;
  color: var(--text-dim);
}

.empty-state[hidden] {
  display: none;
}

.empty-top {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  width: 100%;
}

/* Like .weather-panel, the clock is a flex:1 box within .empty-top, and its
   inner .clock-content is scaled by fitClock (ui.js) to fill that box — so the
   time grows to use the available space instead of being pinned to a
   viewport-width clamp that ignored how much room the panel actually had.
   Making the clock flex:1 (rather than auto height) also gives the sibling
   weather-panel a content-independent box to be fit against. */
.empty-clock {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* em-based children scaled by fitClock's binary search against .empty-clock's
   box (single lines via white-space: nowrap so scroll size = true natural size). */
.clock-content {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  font-size: 16px; /* overridden by fitClock's binary search */
}

.clock-time {
  font-size: 1em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  line-height: 1.1;
  white-space: nowrap;
}

.clock-date {
  font-size: 0.3em;
  color: var(--text-dim);
  margin-top: 0.5em;
  white-space: nowrap;
}

.empty-message {
  margin: 1rem 0 0;
  font-size: 0.78rem;
  color: var(--text-dim);
  opacity: 0.75;
}

/* Fills whatever room empty-top has left after the clock (flex: 1 on a
   flex-column parent), so weather-content below always has a fixed box to be
   fit against rather than sizing itself off viewport width like the old
   clamp()-based rules did — those left the icons/text the same size
   regardless of how much bigger the panel actually was. */
.weather-panel {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 0.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--panel-border);
}

/* display:flex above overrides the [hidden] UA rule, so hide it explicitly when
   weather is off/unavailable — otherwise the empty (flex:1) panel would still
   claim half of .empty-top and starve the clock of the room it should fill. */
.weather-panel[hidden] {
  display: none;
}

/* Every size below is in em against this element's own font-size, which
   fitWeatherPanel (ui.js) binary-searches against the actual rendered box of
   .weather-panel (all text kept to single lines via white-space: nowrap so
   scrollWidth/scrollHeight reflect true natural size) — same pattern as
   .aircraft-readout/fitAircraftReadout. */
.weather-content {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9em;
  text-align: center;
  font-size: 16px; /* overridden by fitWeatherPanel's binary search */
}

/* Short-and-wide box (e.g. the desktop/tablet empty state): lay the hero
   beside the forecast so the content uses the box's width instead of stacking
   into a tall, narrow column that exhausts the height while most of the width
   sits empty. fitWeatherPanel picks column vs row by whichever yields the
   larger fitted font. */
.weather-content--row {
  flex-direction: row;
  align-items: center;
  gap: 1.5em;
}

.weather-hero {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1em;
}

.weather-icon {
  font-size: 3.2em;
  line-height: 1;
}

.weather-hero-details {
  text-align: left;
}

.weather-temp {
  font-size: 1.8em;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

.weather-desc {
  color: var(--text-dim);
  font-size: 0.85em;
  white-space: nowrap;
}

.weather-range {
  color: var(--text-dim);
  font-size: 0.7em;
  margin-top: 0.15em;
  white-space: nowrap;
}

.weather-forecast {
  display: flex;
  justify-content: center;
  gap: 1.2em;
}

.forecast-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25em;
  font-size: 0.65em;
  color: var(--text-dim);
  min-width: 2.5em;
}

.forecast-day-icon {
  font-size: 1.5em;
}

.forecast-day-range {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.aircraft-card {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 0.75rem;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: clamp(1rem, 2vw, 1.75rem);
  overflow: auto;
}

.aircraft-card[hidden] {
  display: none;
}

/* "Plane overhead full screen" setting: let the card grow past its normal
   panel bounds to cover the whole viewport. The readout inside (see
   .aircraft-readout below) is always auto-fit-sized by fitAircraftReadout
   (ui.js) regardless of this setting — this class only changes how much
   space the card itself gets to grow into. z-index sits below
   .settings-toggle-btn (5) so the gear stays reachable to turn this back off. */
body.fullscreen-overhead .aircraft-card:not([hidden]) {
  position: fixed;
  inset: 0;
  z-index: 4;
  border-radius: 0;
  border: none;
  padding: 3vmin;
}

.flight-table-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: clamp(1.25rem, 2.5vw, 2rem) clamp(0.5rem, 1vw, 0.85rem);
  min-height: 0;
}

.flight-table-panel[hidden] {
  display: none;
}

.flight-table-heading {
  margin: 0;
  padding: 0 clamp(0.5rem, 1.2vw, 0.9rem);
  font-size: clamp(1rem, 1.6vw, 1.35rem);
  color: var(--text-dim);
  font-weight: 600;
}

.flight-table-scroll {
  flex: 1;
  overflow: auto;
  min-height: 0;
}

.flight-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.flight-table th,
.flight-table td {
  text-align: left;
  padding: 0.5rem 0.6rem;
  white-space: nowrap;
  font-size: clamp(0.8rem, 1.1vw, 1rem);
}

.flight-table th:first-child,
.flight-table td:first-child {
  padding-left: clamp(0.5rem, 1.2vw, 0.9rem);
}

.flight-table th:last-child,
.flight-table td:last-child {
  padding-right: clamp(0.5rem, 1.2vw, 0.9rem);
}

.flight-table th {
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: clamp(0.7rem, 0.9vw, 0.85rem);
  border-bottom: 1px solid var(--panel-border);
  position: sticky;
  top: 0;
  background: var(--panel-bg);
  vertical-align: top;
}

.flight-table th small {
  display: block;
  text-transform: none;
  font-weight: 400;
  opacity: 0.7;
}

/* Each flight is two rows (flight/distance/altitude/speed, then the aircraft
   type spanning underneath) so the table never needs 5 nowrap columns side
   by side — that's what was forcing horizontal scroll in the landscape
   half-width layout. Group the pair visually: no rule between them, a rule
   after the subrow to separate one flight from the next. */
.flight-table tbody tr.flight-row td {
  padding-bottom: 0.15rem;
}

.flight-table tbody tr.flight-subrow td {
  padding-top: 0;
  color: var(--text-dim);
  font-size: clamp(0.7rem, 0.95vw, 0.85rem);
}

.flight-table tbody tr.flight-subrow:not(:last-child) td {
  border-bottom: 1px solid var(--panel-border);
}

.aircraft-main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(0.4rem, 0.8vw, 0.75rem);
  width: 100%;
}

/* The whole detail readout (logo, carrier, route, stats) is sized by
   fitAircraftReadout (ui.js), which binary-searches .aircraft-readout's own
   font-size against its actual rendered box every time it's shown — normal
   panel-sized or, with "Plane overhead full screen" on, the whole viewport
   (see body.fullscreen-overhead .aircraft-card above). Every fs-* rule below
   is sized in em against that one font-size so the whole block scales
   together. Text is kept to single lines (white-space: nowrap) so
   scrollWidth/scrollHeight reflect true natural size during that search. */
.aircraft-readout {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.55em;
  text-align: left;
  font-size: 16px; /* overridden by fitAircraftReadout's binary search */
}

/* Header row: logo left, carrier name + route stacked to its right. */
.readout-head {
  display: flex;
  align-items: center;
  gap: 0.55em;
}

/* Height tracks the head column via em so the whole block scales together,
   but fitAircraftReadout also sets an inline max-height (20% of the card's
   content box) so the logo never dominates the pane no matter how large the
   binary search drives the font. width: auto + object-fit preserve the
   image's own aspect ratio from whichever of the two limits wins. */
.readout-logo {
  height: 2.9em;
  width: auto;
  flex-shrink: 0;
  object-fit: contain;
  background: #fff;
  border-radius: 0.18em;
  padding: 0.06em;
  /* Sit to the right of the carrier name/route rather than left of it, so the
     name shares the same flush-left edge as the model, reg, and stats below —
     otherwise the logo indents only the header and the card gets two ragged
     left margins. (The tall/centered variant keeps the logo on top; see below.) */
  order: 2;
}

.readout-head-col {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}

.readout-carrier {
  font-weight: 700;
  font-size: 1.7em;
  line-height: 1.1;
  white-space: nowrap;
}

.readout-route {
  font-size: 1.15em;
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.readout-details {
  border-top: 1px solid var(--panel-border);
  padding-top: 0.45em;
}

.readout-model {
  font-size: 1.2em;
  color: var(--text);
  white-space: nowrap;
}

.readout-reg {
  font-size: 0.78em;
  color: var(--text-dim);
  opacity: 0.75;
  white-space: nowrap;
  margin-top: 0.18em;
}

/* Full depart/dest readout: one line each, sized close to the header text so
   it reads as its own block rather than fine print, and left to fill the
   card's full width. Unlike every other readout line these may wrap: they're
   the longest text in the card by far, and as nowrap lines they'd become the
   width limit fitAircraftReadout scales everything against. Wrapped, the
   binary search just accounts for the extra line height instead. */
.readout-route-full {
  border-top: 1px solid var(--panel-border);
  padding-top: 0.45em;
}

.readout-route-full-line {
  font-size: 1em;
  color: var(--text);
  white-space: normal;
}

.readout-route-full-line + .readout-route-full-line {
  margin-top: 0.25em;
}

/* Stats: 3-across strip with hairline dividers when the panel is wider than
   tall; fitAircraftReadout switches to stacked label/value rows (class
   stats-stacked) when the panel is taller than wide, where a wide strip
   would cap the font-size search on width long before height matters. */
.readout-stats {
  display: flex;
  border-top: 1px solid var(--panel-border);
  padding-top: 0.45em;
}

.readout-stat {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 0.6em;
}

.readout-stat + .readout-stat {
  border-left: 1px solid var(--panel-border);
}

.readout-stat-value {
  font-size: 1.55em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1.1;
}

.readout-stat-label {
  font-size: 1.55em; /* match .readout-stat-value so label and number read at the same size */
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin-top: 0.2em;
  white-space: nowrap;
  line-height: 1.1;
}

/* Tall card (taller than wide): the 3-across strip would be the widest line
   and cap the fit on width, so lay the stats out vertically instead. Label
   left, value right (row-reverse keeps the value first in the DOM, as the
   strip wants), spread apart. */
.aircraft-readout.stats-stacked .readout-stats {
  flex-direction: column;
  gap: 0.5em;
}

.aircraft-readout.stats-stacked .readout-stat {
  flex-direction: row-reverse;
  justify-content: space-between;
  align-items: baseline;
  gap: 1.2em;
  padding: 0;
}

.aircraft-readout.stats-stacked .readout-stat + .readout-stat {
  border-left: none;
}

/* Strongly portrait (mainly the fullscreen-overhead phone card): height to
   spare, width is the limit. Centre everything and stack the header — logo
   above the carrier name+route rather than beside it — so the long "logo +
   airline name" line stops being the width limit and every element grows into
   the vertical space. */
.aircraft-readout.readout-tall {
  align-items: center;
  text-align: center;
}

.aircraft-readout.readout-tall .readout-head {
  flex-direction: column;
  gap: 0.35em;
}

/* Centered masthead: logo back on top of the (centered) name, not below it. */
.aircraft-readout.readout-tall .readout-logo {
  order: 0;
}

.aircraft-readout.readout-tall .readout-head-col {
  align-items: center;
}

/* Let a long airline name wrap here (it's nowrap everywhere else). Stacked
   above the name, it's the single widest line in the card, so as one nowrap
   line it caps the whole readout's scale and leaves the tall card half empty;
   wrapped to two lines it's ~half as wide and the fit grows into the height. */
.aircraft-readout.readout-tall .readout-carrier {
  white-space: normal;
}

.aircraft-readout.readout-tall .readout-details {
  text-align: center;
}

/* Stats as a vertical list of centred value-over-label blocks (same shape as
   one cell of the 3-across strip, just stacked) — narrower than the spread-out
   label-left/value-right row, so the width-limited fit can scale them larger. */
.aircraft-readout.readout-tall .readout-stat {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1em;
  padding: 0.2em 0;
}

.aircraft-readout.readout-tall .readout-stat + .readout-stat {
  border-top: 1px solid var(--panel-border);
}

/* Square, derived from height not width: in the default row layout, height
   comes from flex stretch (definite, matches .aircraft-panel's column
   exactly), and aspect-ratio fills in width from that — flex-grow is off
   (0 1 auto) so it doesn't get distributed extra width like a plain flex:1
   item would, which left it wider than tall (measured ~707x688, not square)
   when this was tried with flex:1 + align-self:center + max-height instead. */
.radar-panel {
  position: relative;
  flex: 0 1 auto;
  min-width: 0;
  aspect-ratio: 1;
  max-width: 100%;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 1rem;
}

.radar-panel[hidden] {
  display: none;
}

.settings-toggle-btn {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 5;
  background: var(--panel-bg);
}

.radar-facing {
  font-weight: 400;
  font-size: 0.8rem;
}

.radar-panel h2 {
  margin: 0 0 0.75rem;
  font-size: 0.95rem;
  color: var(--text-dim);
  font-weight: 600;
}

.radar-svg {
  width: 100%;
  height: 100%;
  display: block;
  flex: 1;
  min-height: 0;
}

.radar-ring {
  fill: none;
  stroke: var(--panel-border);
  stroke-width: 1;
}

.radar-nearby-ring {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1;
  stroke-dasharray: 4 3;
}

.radar-ring-label {
  fill: var(--text-dim);
  font-size: 9px;
}

/* N/E/S/W now sit inside the busy part of the circle (map, rings, blips), so
   they need real contrast: bold, a size bump, and a halo (stroke painted
   behind the fill) that keeps them legible over any background underneath. */
.radar-dir-label {
  fill: var(--text);
  font-size: 11px;
  font-weight: 700;
  paint-order: stroke;
  stroke: var(--panel-bg);
  stroke-width: 3px;
  stroke-linejoin: round;
}

.radar-me {
  fill: var(--accent);
}

.radar-map-tile {
  opacity: 0.7;
  filter: invert(0.9) hue-rotate(180deg) brightness(0.85) contrast(0.9);
}

@media (prefers-color-scheme: light) {
  .radar-map-tile {
    filter: none;
    opacity: 0.6;
  }
}

.radar-flight-path {
  fill: none;
  stroke: var(--text-dim);
  stroke-width: 1;
  stroke-dasharray: 3 2;
  stroke-linecap: round;
  opacity: 0.5;
}

.radar-flight-path-nearby {
  stroke: var(--radar-highlight);
  stroke-width: 1.5;
  opacity: 0.85;
}

.radar-blip-plane {
  fill: var(--text-dim);
  stroke: var(--panel-bg);
  stroke-width: 0.5;
}

.radar-blip-label {
  fill: var(--text-dim);
  font-size: 8px;
}

.radar-airport-pin {
  fill: var(--airport-marker);
  stroke: var(--panel-bg);
  stroke-width: 0.75;
}

.radar-airport-label {
  fill: var(--airport-marker);
  font-size: 8px;
  font-weight: 600;
}

.radar-blip-nearby .radar-blip-plane {
  fill: var(--radar-highlight);
  stroke: var(--radar-highlight);
}

.radar-blip-nearby .radar-blip-label {
  fill: var(--radar-highlight);
  font-weight: 600;
}

.settings-drawer {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: flex-end;
  z-index: 10;
}

.settings-drawer[hidden] {
  display: none;
}

.settings-panel {
  width: min(22rem, 100%);
  height: 100%;
  background: var(--panel-bg);
  border-left: 1px solid var(--panel-border);
  padding: 1.5rem;
  overflow-y: auto;
}

.settings-panel h2 {
  margin-top: 0;
}

.settings-field {
  display: block;
  margin-bottom: 1.1rem;
}

.settings-field[hidden] {
  display: none;
}

.settings-field-label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 0.3rem;
}

.settings-field input[type='number'],
.settings-field select {
  width: 100%;
  padding: 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--panel-border);
  background: var(--bg);
  color: var(--text);
  font-size: 1rem;
}

.manual-location-row {
  display: flex;
  gap: 0.5rem;
}

.manual-location-row input {
  flex: 1 1 auto;
  min-width: 0;
}

.manual-location-row .secondary-button {
  flex: 0 0 auto;
  white-space: nowrap;
}

.settings-field-hint {
  display: block;
  font-size: 0.75rem;
  color: var(--text-dim);
  margin-top: 0.25rem;
}

.checkbox-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.3rem;
}

.checkbox-field input {
  width: auto;
}

.location-display {
  font-size: 0.85rem;
  color: var(--text-dim);
  margin-bottom: 0.5rem;
}

.settings-actions {
  display: flex;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

.primary-button,
.secondary-button {
  border-radius: 8px;
  padding: 0.55rem 1.1rem;
  font-size: 0.9rem;
  cursor: pointer;
  border: 1px solid transparent;
}

.primary-button {
  background: var(--accent);
  color: #06121f;
  font-weight: 600;
}

.secondary-button {
  background: none;
  border-color: var(--panel-border);
  color: var(--text);
}

.error-text {
  color: var(--danger);
  font-size: 0.85rem;
}

.app-footer {
  flex-shrink: 0;
  padding: 1rem 1.25rem;
  text-align: center;
  font-size: 0.75rem;
  color: var(--text-dim);
}

.app-footer a {
  color: var(--text-dim);
}

/* Stack the panels (main panel on top, radar below) whenever the viewport is
   portrait-shaped — this covers phones in portrait AND tablets like an iPad
   in portrait, regardless of their absolute width. */
@media (orientation: portrait) {
  .content {
    flex-direction: column;
  }

  .aircraft-panel,
  .radar-panel {
    flex: none;
    width: 100%;
  }

  /* .aircraft-panel is flex:none (sized to its own content) above — normally
     fine, since the radar panel below it is what actually claims the rest of
     the column and .empty-state (flex:1 inside .aircraft-panel) never has to
     size itself. With the radar hidden (see setRadarVisible in ui.js), there's
     no such box, and .empty-state's flex:1 has nothing definite to resolve
     against — its fit-to-box clock/weather sizing (fitClock/fitWeatherPanel in
     ui.js) then grows a little more each call with nothing to stop it. A fixed
     viewport-relative height breaks that: it's definite regardless of any
     ancestor or sibling, so the fit-to-box search has a stable box to
     converge against. */
  body.radar-hidden .empty-state {
    flex: none;
    height: min(55vh, 480px);
  }
}

/* Phone portrait with "always show table of planes" on: the list can grow
   arbitrarily long and there's no single bounded panel to scroll it within
   (unlike tablet-portrait's 50/50 split or landscape's fixed-height columns),
   so let the whole page grow and scroll instead. Scoped to has-flight-table
   only: without the table, phone portrait now gets the same locked one-screen
   shell as phone landscape (see :not(.has-flight-table) .aircraft-panel
   below). */
@media (orientation: portrait) and (max-width: 700px) {
  body.has-flight-table {
    height: auto;
    min-height: 100dvh;
    overflow: visible;
  }

  body.has-flight-table .app-main {
    min-height: auto;
  }

  body.has-flight-table .content {
    height: auto;
    min-height: auto;
  }

  /* Match phone landscape's principle for the main plane panel: a definite
     height the readout's fit-to-box search can converge against, instead of
     content-based sizing (which made the card's height jump around with each
     aircraft's text). Landscape locks to 100% of its side-by-side row;
     portrait stacks the panels in a column, so the panel takes whatever the
     locked one-screen column has left after the full-width square radar
     below it. The previous fixed min(55vh, 480px) ignored the radar's
     height, so card + radar overran 100dvh on every phone — the radar's
     bottom ran under the footer and was clipped with no way to scroll.
     min-height keeps the card usable on unusually square screens (foldables)
     where the leftover approaches zero; the radar clips instead there. */
  body:not(.has-flight-table) .aircraft-panel {
    flex: 1 1 auto;
    min-height: 8rem;
    overflow: hidden;
  }

  /* With the detail card up, the panel hugs the card's fitted content — only
     as tall as the readout needs, with the radar directly beneath — instead
     of stretching the card over the whole leftover column. Scoped via :has()
     to the card specifically: the empty state must keep the flex-filled box
     above, because its clock/weather fit-to-box sizing needs a definite
     height that isn't derived from its own content. fitAircraftReadout
     doesn't measure this content-sized box (that would be self-referential);
     it fits against the column's leftover-height budget explicitly, so the
     hugged card never grows past what the fill layout would have allowed.
     Browsers without :has() simply keep the filled-column behavior. */
  body:not(.has-flight-table) .aircraft-panel:has(> .aircraft-card:not([hidden])) {
    flex: 0 1 auto;
    min-height: 0;
  }

  /* On shorter viewports (small phones, or browser chrome showing) a
     full-width radar square plus footer would leave the card only a sliver
     of the leftover height — cap the square at half the viewport height so
     the split stays balanced. Tall modern phones still get the full-width
     circle (content width < 50dvh there); margin-inline: auto (base rule)
     keeps a capped one centred. Both dimensions are set to the same length
     rather than trusting aspect-ratio: with only width set, flexbox resolves
     the flex-basis height from the stretched (pre-cap) cross size and the
     panel comes out taller than wide. 1.2rem = .app-main's phone padding
     (0.6rem per side, set in the shared phone block above). */
  body:not(.has-flight-table) .radar-panel {
    width: min(100dvw - 1.2rem, 50dvh);
    height: min(100dvw - 1.2rem, 50dvh);
  }
}

/* Phone-sized screens in either orientation — narrow width catches portrait
   phones, short height catches landscape phones — get trimmed text and
   tighter spacing so both panels fit without scrolling. Tablets don't match
   either condition, so they keep full text/spacing even when stacked above. */
@media (max-width: 700px), (max-height: 500px) {
  .app-main {
    padding: 0.6rem;
  }

  .content {
    gap: 0.6rem;
  }

  .aircraft-card,
  .empty-state,
  .flight-table-panel {
    padding: 0.75rem;
  }

  .aircraft-main {
    gap: 0.6rem;
  }

  .radar-panel {
    padding: 0.6rem;
  }

  /* The attribution footer wraps to ~4 lines at phone widths and was costing
     ~100px of a locked one-screen column — height that comes straight out of
     the aircraft card's fit-to-box budget. Keep it legally present but small. */
  .app-footer {
    padding: 0.4rem 0.75rem;
    font-size: 0.62rem;
  }

  .app-footer p {
    margin: 0.25em 0;
  }

  /* The radar SVG renders noticeably smaller on phones than on tablets (it's
     capped by the smaller screen either way it's oriented), so an 8-unit
     label that reads fine on an iPad shrinks to ~7-9px on a phone — too small
     to read. Bump it (and bold it) specifically here where the render is small. */
  .radar-blip-label {
    font-size: 14px;
    font-weight: 600;
  }

  .weather-panel {
    gap: 0.5rem;
    padding-top: 0.75rem;
  }

  .empty-top {
    gap: 0.75rem;
  }

  /* .clock-time/.clock-date sizes are not overridden here — the clock is
     scaled to fit its box by fitClock, same as the weather panel, so a fixed
     rem size would fight the binary search. */

  /* Nowrap columns are what force the horizontal scrollbar on narrow phones —
     a flight label like "QFA1234 (Melbourne→Sydney)" plus three more nowrap
     columns is wider than the screen. Let cells wrap instead; the smaller
     font and tighter padding keep the extra wrapped lines from costing much
     vertical space. */
  .flight-table th,
  .flight-table td {
    white-space: normal;
    word-break: break-word;
    font-size: 0.88rem;
    padding: 0.4rem 0.45rem;
  }

  /* Dist./Alt./Spd. are always short numbers — they never need to wrap, so
     don't let auto table layout hand them slack width "just in case". That
     width is what the flight column (long "FLIGHT1234 (AAA→BBB)" labels)
     actually needs, especially now that its font is bigger/bold (see below);
     without this, that column would wrap onto two lines far more often. */
  .flight-table td:not(:first-child) {
    white-space: nowrap;
  }

  /* Redundant here now that the flight-row font is bigger/bolder: the subrow
     right below already spells out the full departure/destination names, so
     drop the "(AAA→BBB)" abbreviation to keep the flight number on one line. */
  .flight-route-abbr {
    display: none;
  }

  /* Subrow (aircraft type + route lines) floors out at the base rule's 0.7rem
     clamp minimum on phone-width viewports — bump it up for readability. */
  .flight-table tbody tr.flight-subrow td {
    font-size: 0.95rem;
  }

  /* Match the subrow's size (rather than the smaller 0.88rem every other
     cell gets) and bold it so the flight/dist/alt/spd line still reads as
     the primary line despite being the same size as the type/route line
     below it. */
  .flight-table tbody tr.flight-row td {
    font-size: 0.95rem;
    font-weight: 700;
  }

  /* Phones don't get the tablet's fixed 100vh/one-screen shell, so there's no
     bounded height for the table to scroll within — min-height:0 on an
     unbounded flex chain was instead squeezing it to an arbitrary small box
     and creating a stray vertical scrollbar. Let the card and table size to
     their natural content height and grow the page instead. */
  body.has-flight-table .aircraft-card,
  body.has-flight-table .empty-state,
  body.has-flight-table .flight-table-panel {
    flex: none;
  }

  body.has-flight-table .flight-table-scroll {
    flex: none;
    overflow: visible;
  }
}

/* Phone portrait with "always show table of planes" on: put the radar
   between the card and the table (card, then radar, then table) instead of
   the table sitting directly under the card. The card and table are DOM
   children of .aircraft-panel while the radar is a sibling of that wrapper,
   so reordering them needs the wrapper to stop generating its own box
   (display: contents) — that promotes its children to flex items of
   #content alongside the radar panel, where `order` can interleave them. */
@media (orientation: portrait) and (max-width: 700px) {
  body.has-flight-table .aircraft-panel {
    display: contents;
  }

  body.has-flight-table .aircraft-card,
  body.has-flight-table .empty-state {
    order: 1;
  }

  body.has-flight-table .radar-panel {
    order: 2;
  }

  body.has-flight-table .flight-table-panel {
    order: 3;
  }

  /* Content-sized like the no-table card above — the page scrolls in this
     mode (card flex:none via the shared phone block), so the card just hugs
     its fitted readout. fitAircraftReadout never measures this content-sized
     box (self-referential); it fits against an explicit min(55vh, 480px)
     budget in phone portrait, which this max-height mirrors as the clamp for
     anything the fit search couldn't shrink (its 8px floor). */
  body.has-flight-table .aircraft-card {
    max-height: min(55vh, 480px);
    overflow: hidden;
  }
}

/* Tablet portrait (e.g. iPad): the radar panel is sized by JS (fitRadarSquare
   in ui.js) to a square spanning the full content width, so the circle takes
   up most of the screen instead of being squeezed down to whatever height is
   left over above it. That square is usually taller than half the screen, so
   (unlike every other breakpoint) this one doesn't fit in one locked
   screen — un-lock the body/content height so the page scrolls instead of
   clipping the radar. Scoped to :not(.has-flight-table): with the table on,
   the 50/50 split below still fits one screen and stays locked. */
@media (orientation: portrait) and (min-width: 701px) {
  body:not(.has-flight-table) {
    height: auto;
    min-height: 100dvh;
    overflow: visible;
  }

  body:not(.has-flight-table) .app-main {
    min-height: auto;
  }

  body:not(.has-flight-table) .content {
    height: auto;
    min-height: auto;
  }

  /* The vw-based clamp() sizes on .flight-table th/td (see base rules) top out
     around 1rem at desktop widths, but an iPad portrait's viewport is narrow
     enough in CSS px that the clamp floors out at its *minimum* instead —
     text ends up smaller than the panel's physical size warrants. Override
     with fixed, larger sizes for this breakpoint specifically. */
  .flight-table th,
  .flight-table td {
    font-size: 1.05rem;
  }

  .flight-table th {
    font-size: 0.95rem;
  }

  .flight-table tbody tr.flight-subrow td {
    font-size: 0.95rem;
  }

  /* With "always show table of planes" on, split the screen evenly: the top
     half is the card (natural height) + flight table (fills whatever's left
     of that half, scrolling internally), the bottom half is the radar —
     fitRadarSquare (ui.js) skips its square-fit sizing in this case and
     leaves the 50/50 split to flex below. */
  /* Explicit calc rather than equal flex-grow: flex-grow alone gave the two
     panels an uneven split here (the radar's replaced/SVG content seems to
     feed into the browser's automatic flex-basis sizing despite min-height:0),
     so pin the height directly instead of trusting distribution. 0.625rem is
     half of .content's 1.25rem gap. */
  body.has-flight-table .aircraft-panel,
  body.has-flight-table .radar-panel {
    flex: 0 0 auto;
    height: calc(50% - 0.625rem);
    min-height: 0;
    width: 100%;
  }

  body.has-flight-table .aircraft-card,
  body.has-flight-table .empty-state {
    flex: none;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
  }

  body.has-flight-table .aircraft-main {
    justify-content: flex-start;
    gap: 0.4rem;
  }

  body.has-flight-table .flight-table-panel {
    flex: 1 1 auto;
    min-height: 0;
    padding-top: 0.6rem;
    padding-bottom: 0.6rem;
  }
}

/* Short landscape phones: the radar's circle is width-driven, so left at 50%
   width it can end up taller than the available height and force scrolling.
   Cap its width to roughly what the viewport height allows instead. */
@media (max-height: 500px) and (orientation: landscape) {
  /* Keep both columns an even 50/50 split (the default .aircraft-panel/.radar-panel
     flex:1 rules already do this), but stop them stretching to match each
     other's height — the weather panel's natural height and the radar's fixed
     circle size have no reason to agree, and stretch was inflating whichever
     one was shorter to match the taller one. */
  .content {
    align-items: flex-start;
  }

  /* Keep the radar panel's card chrome (background/border/padding) — same
     white box as every other breakpoint — but give it a definite height so
     the circle inside can size itself to fill it. The heading stays hidden
     to leave the circle as much room as possible. */
  .radar-panel {
    align-items: center;
    height: 100%;
  }

  .radar-panel h2 {
    display: none;
  }

  /* The main panel should always match the radar box's height exactly — same
     fixed height, not just a cap — so a short aircraft card doesn't leave it
     shorter than the radar, and a tall one doesn't push the page taller. */
  .aircraft-panel {
    height: 100%;
    overflow: hidden;
  }

  /* With height this tight, stacking the clock above the weather (as every
     other breakpoint does) doesn't fit without scrolling. Landscape phones
     have width to spare, so put them side by side instead — same content,
     roughly half the vertical space. align-items: stretch overrides the base
     rule's `center`, which (now that the main axis is horizontal, not
     vertical) stopped stretching the clock/weather boxes to the panel's full
     height and left each sized to its own natural content height instead —
     a ~16px clock font and a squeezed-or-clipped weather panel, since
     .aircraft-panel is overflow: hidden at this breakpoint. */
  .empty-top {
    flex-direction: row;
    align-items: stretch;
    gap: 1.25rem;
  }

  /* min-width: 0 is the row-direction twin of the min-height: 0 the base
     (column) rules already carry: without it, min-width:auto lets the nowrap
     clock/weather content set each box's width, so the 50/50 flex split
     tracks whatever font size the content happened to have when measured —
     and the fit-to-box sizing (fitClock/fitWeatherPanel) then refits to that
     moving box, so both panels jump on every clock tick / weather rerender. */
  .empty-clock,
  .weather-panel {
    min-width: 0;
  }

  .weather-panel {
    border-top: none;
    padding-top: 0;
    margin-top: 0;
  }

}

/* Phone card readout rebalance — BOTH phone orientations (short-height
   landscape, narrow-width portrait; tablets match neither condition, so they
   keep the shared base sizes). These rules used to live only in the landscape
   block above, which is exactly why the portrait card looked nothing like the
   landscape one: portrait fell back to the base sizes, inverting the visual
   hierarchy (1.55em stats dominating, 1em depart/dest, reg fine print back).
   Depart/dest is the primary content of the card, so it gets the biggest
   share of the readout's uniform font-size search (1.2x .readout-model, the
   headline); stats (distance/altitude/speed) are secondary and give up space
   in return; the registration/callsign fine print is dropped entirely so the
   fit spends the card's limited space on content that matters. Tight
   line-height/margin/padding on the depart/dest block keeps its two lines
   from costing more height than necessary despite the bigger font. */
@media (max-height: 500px) and (orientation: landscape), (orientation: portrait) and (max-width: 700px) {
  .readout-reg {
    display: none;
  }

  .readout-route-full {
    border-top: none;
    padding-top: 0.15em;
  }

  .readout-route-full-line {
    font-size: 1.44em;
    line-height: 1.15;
  }

  .readout-route-full-line + .readout-route-full-line {
    margin-top: 0.1em;
  }

  .readout-stat-value,
  .readout-stat-label {
    font-size: 0.85em;
  }
}

/* Phone landscape (short viewport height, same threshold as the block above)
   with "always show table of planes" on: put the table to the right of the
   radar (card, radar, table) instead of stacking it under the card in a
   squeezed half-width column. Scoped to short-height landscape specifically
   so it doesn't touch iPad/tablet landscape, which already fits its own
   half-width table layout without needing this. As in the phone-portrait
   case, the card and table are DOM children of .aircraft-panel while the
   radar is a sibling of that wrapper — display: contents promotes the
   wrapper's children to flex items of #content, alongside the radar, so
   `order` can interleave all three. None of the three panels are allowed to
   shrink below a usable width; if they don't all fit, the screen scrolls
   horizontally instead of cramping any one of them. */
@media (max-height: 500px) and (orientation: landscape) {
  body.has-flight-table .content {
    overflow-x: auto;
  }

  body.has-flight-table .aircraft-panel {
    display: contents;
  }

  body.has-flight-table .aircraft-card,
  body.has-flight-table .empty-state {
    order: 1;
    flex: 1 1 22rem;
    min-width: 22rem;
    height: 100%;
    overflow: hidden;
  }

  body.has-flight-table .radar-panel {
    order: 2;
    flex: 1 1 22rem;
    min-width: 22rem;
  }

  /* Same fixed height as the card and radar so a long list of planes never
     stretches this panel (and the other two with it) taller than the screen —
     it scrolls internally via .flight-table-scroll instead. */
  body.has-flight-table .flight-table-panel {
    order: 3;
    flex: 2 1 20rem;
    min-width: 20rem;
    height: 100%;
    min-height: 0;
  }

  /* The shared max-width:700px/max-height:500px block above sets this to
     flex:none + overflow:visible for the portrait case, where the panel has
     no bounded height and is meant to grow with its content. Here the panel
     *is* bounded (fixed height, just above), so undo that and let this
     scroll internally instead of spilling out and pushing the page taller. */
  body.has-flight-table .flight-table-scroll {
    flex: 1;
    overflow: auto;
  }
}
