: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;
}

/* Theme is chosen in Settings ('auto' | 'dark' | 'light') and applied by
   applyTheme() in js/config.js, which resolves 'auto' against the device's
   prefers-color-scheme and stamps the winner on <html data-theme>. Colors key
   off that attribute rather than a media query directly, so an explicit choice
   can override the device. Dark is the default above; light only overrides what
   actually differs. An inline script in app.html's <head> stamps the attribute
   before first paint, so there's no flash of the wrong theme. */
:root[data-theme='light'] {
  --bg: #f4f6fb;
  --panel-bg: #ffffff;
  --panel-border: #dde3ee;
  --text: #1a2233;
  --text-dim: #5b6980;
}

* {
  box-sizing: border-box;
}

/* Design rule: anything clickable shows a pointer cursor. New interactive
   elements should match one of these selectors (or add [role="button"]/
   [onclick] to a custom element) rather than relying on a per-component rule. */
a[href],
button:not(:disabled),
input[type="button"]:not(:disabled),
input[type="submit"]:not(:disabled),
input[type="reset"]:not(:disabled),
select,
label[for],
summary,
[role="button"],
[onclick] {
  cursor: pointer;
}

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;
}

/* Transparent, full-height flex slot. The visible grey panel is .aircraft-body
   inside it, centred here — so a width-limited readout (a narrow card) no longer
   leaves big empty bands of panel above and below it: the body shrinks to hug
   its content. The card keeps a definite full height, which fitAircraftReadout
   uses as the height bound, so sizing never feedback-loops off the body's own
   (content-driven) height. */
.aircraft-card {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

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

.aircraft-body {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  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;
}

/* "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;
  padding: 0;
  max-height: none; /* override the portrait 60dvh cap — fullscreen fills the viewport */
}

/* Full-bleed in fullscreen: the body fills the fixed card rather than hugging. */
body.fullscreen-overhead .aircraft-body {
  flex: 1;
  max-height: none;
  border-radius: 0;
  border: none;
}

.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);
  white-space: normal; /* let the "From → To" route wrap if it's too long for one line */
}

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

/* Registration (tail number) beside the model on the subrow — same dim grey
   as the model text (inherited), just tabular figures for the digits. */
.flight-table tbody tr.flight-subrow .flight-reg {
  font-variant-numeric: tabular-nums;
}

/* Fix the flight-table body text to one size on tablet and desktop (>=701px) so
   the in-view panel reads identically on iPad and a PC browser — the base rules
   above scale it with vw, which made it noticeably smaller on the narrower iPad
   viewport. (Phone, <701px, keeps the vw clamps so the table still fits.) */
@media (min-width: 701px) {
  .flight-table th,
  .flight-table td {
    font-size: 1rem;
  }

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

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

.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 left of the carrier name + flight number (DOM order: the img
     comes first in .readout-head). The tall/centered variant stacks it on top
     instead; see below. */
  order: 0;
}

.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;
}

/* Header flight-number line: always shown, so the flight number stays visible
   regardless of whether a route is known (see renderAircraftCard in ui.js). */
.readout-flight-number {
  display: block;
  font-size: 1.15em;
  font-weight: 600;
  color: var(--text-dim);
  white-space: nowrap;
}

/* From/to airport codes appended to the flight-number line (e.g. "QF96D
   (MEL→SYD)"). Accent colour so the route still reads as its own thing now that
   it no longer has a dedicated line. Sized in em, so it scales with the readout
   like everything else. */
.readout-route-abbr {
  color: var(--accent);
}

.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: a single "Depart → Dest" line, sized close to the
   header text so it reads as its own block rather than fine print. Kept to one
   nowrap line (like every other readout line) so it stays intact and
   fitAircraftReadout shrinks the whole readout to fit it, rather than wrapping.
   It's the longest text in the card, so it's often what caps the readout scale. */
/* No divider and no gap above the route line — the negative margin cancels the
   readout's row gap so the from→to route sits directly beneath the aircraft
   model, reading as one block. */
.readout-route-full {
  margin-top: -0.55em;
  padding-top: 0;
}

.readout-route-full-line {
  font-size: 1em;
  color: var(--text);
  white-space: nowrap;
  /* Capped in em (rather than left to grow with the text) so a long pair of
     full airport names can't drive fitAircraftReadout's width-fit search
     (see ui.js) — without this cap, that search reads this line's full
     scrollWidth and shrinks the whole readout to make it fit. Ellipsis here
     instead, so only this line truncates and everything else stays at the
     size the rest of the card's content actually needs. */
  max-width: 24em;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 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.2em; /* same size as the aircraft model (.readout-model) */
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1.1;
}

.readout-stat-label {
  font-size: 1em; /* same size as the unit (<small>) beside it — the label used to be larger */
  color: var(--text); /* full-strength text, not the dim grey */
  margin-top: 0.2em;
  white-space: nowrap;
  line-height: 1.1;
}

/* Unit of measure (km / m / km/h) reads at the same size as its label rather
   than the browser's default smaller <small>. */
.readout-stat-label small {
  font-size: inherit;
}

/* 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;
}

/* 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;
}

/* Landscape only: the radar is otherwise sized purely from the row height, so a
   tall (4:3, e.g. iPad) landscape makes it a giant square that fills the whole
   height with zero slack — any browser toolbar then pushes its bottom off
   screen, and it squeezes the aircraft panel to a sliver. Cap its size so it
   also can't exceed the viewport width minus ~24rem reserved for that panel
   (plus the gap), and centre it vertically so the leftover height becomes even
   top/bottom breathing room. The cap stays wider than a 16:9 desktop radar, so
   that layout is unchanged and only tall landscapes are reined in. Scoped to
   landscape because in portrait the same calc would go negative on a phone.
   The aircraft panel gets the same cap + centering as the radar so the two
   stay the same height and their top/bottom edges stay level — without this,
   the (uncapped) aircraft panel stretches to the full row height while the
   (capped) radar sits shorter and centered beside it, leaving mismatched gaps
   above and below the radar relative to the card/table panel next to it. */
@media (orientation: landscape) {
  .radar-panel,
  .aircraft-panel {
    align-self: center;
    height: min(100%, calc(100vw - 27.75rem));
  }
}

/* Phone landscape (short viewport): lay every panel out as a full-height column
   in one horizontally-scrolling row, instead of stacking the overhead card and
   the planes-in-view table vertically inside .aircraft-panel. Order follows the
   DOM — overhead (or empty-state), planes-in-view, radar — and any panel that's
   hidden just collapses out of the row. Scoped to max-height:560px so tablet /
   desktop landscape (taller) keep the side-by-side card+radar layout. 560 (not
   500) because an iPhone added to the home screen ("standalone" display mode)
   has no Safari toolbar to ever show, so its landscape viewport reports several
   tens of pixels taller than the same phone in a regular Safari tab — enough to
   cross a 500px cutoff and fall through to the tablet/desktop branch below,
   losing the widened panel. 560 stays far under any real tablet's landscape
   height (iPad mini: 768) so tablets are unaffected. */
@media (orientation: landscape) and (max-height: 560px) {
  .content {
    overflow-x: auto;
    overflow-y: hidden;
    align-items: stretch;
  }

  /* Dissolve the wrapper so the overhead card and the planes-in-view table
     become full-height horizontal siblings of the radar rather than a stacked
     column. */
  .aircraft-panel {
    display: contents;
  }

  /* The table's flight column is squeezed tight in this half-width layout —
     drop the "(OOL→SYD)" abbreviation that rides beside the flight number
     (the subrow below already spells the route out in full) so the DIST/ALT/
     SPD columns beside it have room to render without horizontal scroll. */
  .flight-route-abbr {
    display: none;
  }

  /* Even with the abbreviation gone, the base padding/font-size (tuned for a
     full-width phone or tablet column) leaves the 4-column header row sized
     right at the edge of this half-width panel — measured exactly flush in
     one browser engine, which means an engine with slightly wider glyph
     metrics (this targets iOS Safari/WKWebView) clips it. Trim both for
     real slack instead of a pixel-perfect fit. */
  .flight-table th,
  .flight-table td {
    padding: 0.4rem 0.35rem;
    font-size: 0.78rem;
  }

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

  .flight-table th:first-child,
  .flight-table td:first-child {
    padding-left: 0.5rem;
  }

  .flight-table th:last-child,
  .flight-table td:last-child {
    padding-right: 0.5rem;
  }

  /* Each of these fills the width left beside the square radar, so the visible
     [panel][radar] pair fills the viewport exactly — the table is as wide as it
     can be while the radar stays fully on screen. Width = content width minus the
     radar (a square, so its width == the content height) minus the row gap; the
     app-main padding cancels out, leaving 100vw - 100dvh - gap. Card and table
     share this width so the radar doesn't shift when the overhead card appears. */
  .aircraft-card,
  .empty-state,
  .flight-table-panel {
    flex: 0 0 auto;
    width: calc(100vw - 100dvh - 1.25rem);
    height: 100%;
    min-height: 0;
  }

  /* Overhead grey panel fills its full-height slot here, matching the solid
     table/radar panels beside it (rather than hugging its content). */
  .aircraft-body {
    flex: 1;
    max-height: none;
  }

  .radar-panel {
    flex: 0 0 auto;
    align-self: stretch;
    height: 100%;
    width: auto;
    margin-inline: 0;
  }

  /* When an aircraft is overhead, keep the radar exactly where it sat in the
     no-overhead [in-view][radar] view and push the in-view table off to the far
     right instead: order the row [overhead][radar][in-view]. Because the
     overhead card and the in-view table are the same width, the radar's on-screen
     position doesn't move when the card appears — overhead + radar stay visible,
     the table scrolls off the side. (:has on .content; the card is a flex item of
     .content via the display:contents wrapper above.) */
  .content:has(#aircraft-card:not([hidden])) #aircraft-card {
    order: 0;
  }

  .content:has(#aircraft-card:not([hidden])) .radar-panel {
    order: 1;
  }

  .content:has(#aircraft-card:not([hidden])) #flight-table-panel {
    order: 2;
  }

  /* Weather showing (no aircraft nearby): same "cover everything" fullscreen
     trick as the tablet/desktop version further below, but this breakpoint
     needs its own copy because the row-based sizing just above (.empty-state
     gets width: calc(100vw - 100dvh - 1.25rem), sized to sit beside the
     radar) would otherwise win the cascade over a plain position: fixed —
     width/height must be explicitly reset here to cancel that out. */
  .empty-state:has(#weather-panel:not([hidden])) {
    position: fixed;
    inset: 0;
    z-index: 4;
    width: auto;
    height: auto;
    padding: 2rem;
    border-radius: 0;
    border: none;
  }
}

/* Tablet / desktop landscape with the planes-in-view table showing: the overhead
   card hugs its content and floats to the top of the left column, so the in-view
   table fills the rest of that column (bigger) rather than the two splitting it
   50/50 and leaving empty space above and below the compact card.
   fitAircraftReadout uses a 0.5vh cap as its height bound here (not the card's
   own hugged height), so it can't feedback-loop. min-height:561px keeps phone
   landscape (its own horizontal-scroll layout, including standalone iPhone
   home-screen apps) out. */
@media (orientation: landscape) and (min-height: 561px) {
  body.has-flight-table .aircraft-card {
    flex: 0 0 auto;
    height: auto;
    max-height: 60dvh;
  }
}

/* Tablet / desktop landscape, weather showing (no aircraft nearby): let the
   empty state take over the whole viewport instead of sharing the row with
   the radar/table, the same "cover everything" trick as
   body.fullscreen-overhead above — fitClock/fitWeatherPanel (ui.js) refit
   against the now-fullscreen box via the existing resize/orientationchange
   listeners, no extra JS needed. :has() scopes this to when the weather panel
   is actually visible, so the plain clock-only empty state (weather off or
   still loading) keeps sharing the row as before. z-index matches
   fullscreen-overhead (below .settings-toggle-btn's 5) so the gear stays
   reachable. */
@media (orientation: landscape) and (min-height: 561px) {
  .empty-state:has(#weather-panel:not([hidden])) {
    position: fixed;
    inset: 0;
    z-index: 4;
    padding: 2rem;
    border-radius: 0;
    border: 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.8em;
}

/* Matches .flight-table-heading so the two panel titles always read as a pair.
   Overlaid on the panel's top-left corner (outside the circular radar disc)
   rather than in flow, so the svg below gets the full panel box and the radar
   circle floats dead-centre in it. */
.radar-panel h2 {
  position: absolute;
  top: 1rem;
  left: 1rem;
  z-index: 1;
  margin: 0;
  font-size: clamp(1rem, 1.6vw, 1.35rem);
  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);
}

/* radarMap.js fetches CARTO's dark- or light-styled tile set to match the
   theme directly (see its comment) rather than faking one out of the other
   with a CSS filter, so no per-theme override is needed here. */
.radar-map-tile {
  opacity: 0.7;
}

.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-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.settings-header h2 {
  margin-bottom: 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;
  justify-content: space-between;
  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;
}

/* Overlays .app-main's bottom padding band rather than taking its own flex row,
   so the panels above keep the same 1.25rem clearance below as above and the
   credits sit inside that gap. */
.app-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  color: var(--text-dim);
}

.app-footer p {
  margin: 0;
}

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

/* Vertical space is scarce in phone landscape (height <= 560px) — the data
   attribution footer isn't worth the row it costs here. Placed after the base
   .app-footer rules above (same specificity) so it wins the cascade instead of
   being overridden by them. */
@media (orientation: landscape) and (max-height: 560px) {
  .app-footer {
    display: none;
  }
}

/* ---- Portrait (any form factor) ----------------------------------------
   The one orientation rule: stack instead of sitting side by side, radar on
   top, everything else (card, table, empty state) below. Every panel spans the
   full viewport width. The radar is the exception: its box is full width too,
   but its height is capped to half the viewport so it can't dominate the stack.
   Its circular SVG (a square viewBox) self-centres via the default
   preserveAspectRatio, so on a wide (tablet) portrait the capped square sits
   centred with white space to its left and right inside the full-width box; a
   phone is narrow enough that the square fills the width with none.

   Portrait also unlocks vertical page scrolling: instead of the landscape
   one-screen shell (body: 100dvh + overflow hidden), the stack is allowed to run
   taller than the viewport and be reached by scrolling up/down. The fit-to-box
   panels (card, empty state) still need a definite height to scale their text
   into — an auto height would feedback-loop (see .empty-state / fitBoxToContent)
   — so they get a generous viewport-based slot rather than a flex share, while
   the flight table (not fit-to-box) grows to its natural full-rows height and
   scrolls with the page instead of inside its own box. */
@media (orientation: portrait) {
  body {
    height: auto;
    min-height: 100dvh;
    overflow: visible;
  }

  /* Take the height of the actual stack, not the whole viewport: as a flex:1
     child of the body it would otherwise fill 100dvh and, via align-items:
     stretch, pull .content taller than its panels — leaving dead space between
     the last panel and the credits when the stack is shorter than the screen. */
  .app-main {
    flex: 0 0 auto;
  }

  .content {
    flex-direction: column;
    height: auto;
  }

  .radar-panel {
    order: -1;
    flex: 0 0 auto;
    width: 100%;
    aspect-ratio: auto;
    height: min(50dvh, calc(100vw - 2.5rem));
  }

  .aircraft-panel {
    flex: 0 0 auto;
  }

  /* Empty state keeps a definite 60dvh slot: its clock/weather are fit-to-box
     against their own height, so an auto height would feedback-loop. */
  .empty-state {
    flex: 0 0 auto;
    height: 60dvh;
  }

  /* The overhead card hugs its content instead (capped at 60dvh) so it sits
     snug below the radar with no reserved dead space above/below it.
     fitAircraftReadout uses a viewport fraction as its height bound here rather
     than the card's own (now content-driven) height, so it can't feedback-loop. */
  .aircraft-card {
    flex: 0 0 auto;
    height: auto;
    max-height: 60dvh;
  }

  .flight-table-panel {
    flex: 0 0 auto;
  }

  .flight-table-scroll {
    overflow: visible;
  }

  .app-footer {
    position: static;
    height: auto;
    padding: 0.75rem 1.25rem;
  }
}

/* iPad / tablet portrait only: the overhead card is width-limited here with
   vertical room to spare (and now hugs its content), so tightening the internal
   spacing genuinely shrinks the panel rather than just letting the text grow.
   Trim the space around both divider lines (the readout row gap above them, the
   padding below them) and the space around the stat values. Scoped to
   min-width:701px so phone portrait keeps its roomier spacing. */
@media (orientation: portrait) and (min-width: 701px) {
  .aircraft-readout {
    gap: 0.3em; /* was 0.55em — space above each section, incl. the divider lines */
  }

  .readout-details,
  .readout-stats {
    padding-top: 0.22em; /* was 0.45em — space below each divider line */
  }

  /* Match the reduced row gap so the route still sits flush under the model. */
  .readout-route-full {
    margin-top: -0.3em;
  }

  .readout-stat-label {
    margin-top: 0.05em; /* was 0.2em — tighten the gap between value and label */
  }
}
