/* FlyBy web — fullscreen dot-matrix LED panel.
   Visually mirrors a warm-amber HUB75 display: big Jersey-10 pixel
   text, a subtle dot-grid "unlit LED" background, and a soft
   text-shadow bloom. Line 1 takes an airline brand color; lines
   2/3 are amber. */

:root {
  --bg-deep: #0a0604;
  --led-off: rgba(255, 170, 0, 0.14);
  --led-amber: #ffaa00;
  --led-amber-soft: #ffc044;
  --led-glow: rgba(255, 170, 0, 0.55);
  --ui-border: rgba(255, 170, 0, 0.28);
  --ui-muted: rgba(255, 190, 80, 0.55);
  --ui-panel-bg: rgba(12, 8, 6, 0.92);
  --font-display: 'Jersey 10', 'Courier New', monospace;
  --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  min-height: 100%;
  background: var(--bg-deep);
  color: var(--led-amber);
  font-family: var(--font-ui);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Hide horizontal overflow from the oversized LED text on narrow
     viewports, but allow vertical scrolling so a tall 4-line panel
     stays reachable when the window is short. */
  overflow-x: hidden;
  overflow-y: auto;
}

body {
  /* The unlit LED pixel grid: a dim radial dot, tiled across the
     whole viewport. The "on" pixels (text) sit on top and outshine
     the grid with the text-shadow bloom. The 10px grid with a
     clearly-visible dot sells the dot-matrix aesthetic without
     competing with the lit characters. */
  background-image: radial-gradient(
    circle at center,
    var(--led-off) 38%,
    transparent 40%
  );
  background-size: 10px 10px;
}

/* ───── Poll countdown bar ─────
   A thin amber bar pinned to the top of the viewport that
   shrinks right→left over the poll interval. JS sets
   --poll-duration and toggles .countdown-active to restart. */
.countdown-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 6px;
  background: #664400;
  z-index: 200;
}

.countdown-bar::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--led-amber);
  opacity: 0.8;
  transform-origin: left;
}

.countdown-bar.countdown-active::after {
  animation: countdown-shrink var(--poll-duration, 30s) linear forwards;
}

@keyframes countdown-shrink {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

.page {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  position: relative;
}

.stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* `safe center` falls back to flex-start when content is taller
     than the stage, so the top of the panel stays reachable and
     scrolling reveals what's below. Plain `center` would clip the
     top. */
  justify-content: safe center;
  padding: 1rem clamp(0.75rem, 3vw, 2rem);
  min-height: 0;
}

/* The plane-card wrapper is what the 30-s poll replaces via
   innerHTML. It can contain a <panel> plus an optional map link.
   gap 0 keeps the Track/Settings row glued to the panel; the row
   has its own padding. align-items: flex-start keeps the panel
   and its links left-aligned together. */
.plane-card {
  width: fit-content;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
}

/* ───── LED panel ───── */

.panel {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(0.9rem, 2.5vw, 2rem);
  padding: 1rem 0 1rem;
  position: relative;
  width: fit-content;
  max-width: 100%;
  overflow: hidden;
}

/* Intensity breathing: the whole panel (text + scanlines +
   stale banner) subtly brightens and dims on a 6.5 s sine wave.
   Variation is small enough (±3%) that you can't "see" it
   pulsing, but your eye picks up the low-frequency rhythm.
   Feels like a slightly loose power supply. */

/* CRT scanline overlay — sells the "this is a physical display"
   illusion that the dot-grid background started. A repeating
   2px-transparent / 1px-dark stripe pattern layered above the
   panel content, with a very subtle slow drift so it feels
   alive. Kept at ~18% opacity so it never competes with the
   text. prefers-reduced-motion disables the drift animation. */
.panel::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 10;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0,
    rgba(0, 0, 0, 0) 2px,
    rgba(0, 0, 0, 0.18) 2px,
    rgba(0, 0, 0, 0.18) 3px
  );
}

@media (prefers-reduced-motion: no-preference) {
  .panel::before {
    animation: scanline-drift 6s linear infinite;
  }
}

@keyframes scanline-drift {
  from {
    background-position-y: 0;
  }
  to {
    background-position-y: 3px;
  }
}

/* ───── First-load entrance ─────
   Panel lines start hidden via .panel-line on [data-fly-state="ok"]
   panels, then JS adds .panel-entered to reveal them with a
   staggered slide-up. If JS fails or reduced-motion is on, the
   .panel-ready class (added by JS immediately) makes them visible
   without animation. */
@media (prefers-reduced-motion: no-preference) {
  /* .panel-entering is added by JS on first load only.
     Lines start hidden, then .panel-entered reveals each one. */
  .panel-entering .panel-line {
    opacity: 0;
    transform: translateY(20px);
  }

  .panel-entering .panel-entered {
    animation: panel-enter 250ms ease-out both;
  }
}

@keyframes panel-enter {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ───── Text glow breathing ─────
   The text-shadow bloom gently pulses — the outer halo grows
   and shrinks on a 7 s cycle, simulating slight voltage ripple
   in the LED driver. Offset from the panel-breathe (6.5 s) so
   the two rhythms don't sync, which would look mechanical. */
.panel-line {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 0.9;
  color: var(--led-amber);
  text-shadow:
    0 0 18px var(--led-glow),
    0 0 6px var(--led-glow),
    0 0 2px var(--led-glow);
  white-space: nowrap;
  letter-spacing: 0.04em;
  user-select: none;
  text-align: left;
}

@media (prefers-reduced-motion: no-preference) {
  .panel-line {
    animation: glow-breathe 7s ease-in-out infinite;
  }
}

@keyframes glow-breathe {
  0%, 100% {
    text-shadow:
      0 0 18px var(--led-glow),
      0 0 6px var(--led-glow),
      0 0 2px var(--led-glow);
  }
  50% {
    text-shadow:
      0 0 24px var(--led-glow),
      0 0 10px var(--led-glow),
      0 0 3px rgba(255, 200, 80, 0.7);
  }
}

.panel-line-1 {
  font-size: clamp(4rem, 15vw, 13rem);
}

.panel-line-2 {
  font-size: clamp(2.75rem, 10vw, 9rem);
  color: var(--led-amber-soft);
  /* Pull line 2 slightly up into the panel's gap so the airline
     name on line 1 sits visually closer to the FLIGHT/ROUTE row
     than to the ALT/SPD/DIST stats row below. Only affects the
     line 1 → line 2 transition; later gaps stay as the panel gap
     defines. */
  margin-top: -0.6rem;
}

.panel-line-3,
.panel-line-type {
  font-size: clamp(2.25rem, 8vw, 7rem);
  color: var(--led-amber-soft);
  opacity: 0.92;
}

/* ───── Value-fresh roll-in ─────
   When a stat value changes on poll, JS adds .value-fresh.
   The text rolls up from below (split-flap / Solari style)
   and blooms warm-white at the start, settling back to amber.
   The parent .stat has overflow:hidden so the slide clips
   cleanly at the stat's boundary. */
.value-fresh {
  animation: value-roll-in 500ms cubic-bezier(0.22, 0.9, 0.36, 1);
  /* backwards: stay at keyframe 0% (hidden, translated down)
     during the stagger delay so values don't flash in place
     before their turn to roll. */
  animation-fill-mode: backwards;
}

@keyframes value-roll-in {
  0% {
    transform: translateY(80%);
    opacity: 0;
  }
  30% {
    opacity: 1;
    color: #fff4d6;
    text-shadow:
      0 0 24px rgba(255, 220, 120, 0.9),
      0 0 10px rgba(255, 200, 80, 0.8),
      0 0 3px #ffe8aa;
  }
  100% {
    transform: translateY(0);
    color: inherit;
    text-shadow: inherit;
  }
}

/* Panel stats rows (lines 2, 3, 4): labelled stats in a row.
   Labels sit ABOVE the values, tiny and dim; values big and bright.
   - justify-content: flex-start keeps stats hugged to the panel's
     left edge — user preference over centered.
   - align-items: flex-start keeps the labels visually in line even
     when one value (e.g. the route with its pixel arrow) renders
     at a slightly different height from the others.
   - flex-wrap: wrap lets a stat drop to a new row on narrow
     viewports instead of overflowing and getting clipped by the
     body's overflow:hidden. */
.panel-stats {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: flex-start;
  column-gap: clamp(1.5rem, 5vw, 4rem);
  row-gap: clamp(0.8rem, 2vw, 1.5rem);
  opacity: 1;
  width: 100%;
}

/* Line 2 stat values are larger than line 3's — line 2 is the
   main ID (callsign + route), line 3 is the secondary numeric.
   Two stats (FLIGHT + ROUTE) should fit side-by-side on typical
   desktop widths; on phones they wrap (flex-wrap on .panel-stats)
   instead of getting clipped by the body's overflow:hidden. */
.panel-line-2 .stat-value {
  font-size: clamp(2.75rem, 10vw, 9rem);
}


.stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 0.9;
  text-align: left;
  /* Clip the roll-in: value slides up from below this box. */
  overflow: hidden;
}

.stat-label {
  font-family: var(--font-display);
  font-size: clamp(1rem, 2.6vw, 2.25rem);
  color: var(--ui-muted);
  letter-spacing: 0.15em;
  text-shadow: 0 0 6px rgba(255, 170, 0, 0.25);
  text-transform: uppercase;
  line-height: 0.75;
}

.stat-value {
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 8vw, 7rem);
  color: var(--led-amber-soft);
  line-height: 0.85;
  /* Expressed in the value's own em (the large one), so the gap
     stays minimal regardless of font-size scaling. */
  margin-top: -0.12em;
}


.panel[data-fly-state='locating'] .panel-line-1 {
  animation: blink 1.4s ease-in-out infinite;
}

.panel[data-fly-state='locating'] {
  gap: clamp(0.3rem, 1vw, 0.8rem);
}

.panel-stale-banner {
  position: absolute;
  bottom: 0.6rem;
  right: 1rem;
  max-width: 60%;
  font-family: var(--font-ui);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--ui-muted);
  text-shadow: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Pixel arrow for the FROM→TO route — a 9×5 dot-matrix SVG that
   uses currentColor and inherits the amber glow via drop-shadow.
   crispEdges keeps the pixels hard. Aspect 9:5; kept narrow so it
   reads as a separator, not a dominant glyph.
   - `vertical-align: 0.15em` sits slightly above the text midline;
     plain `middle` read too low next to Jersey 10's tall caps.
   - margin asymmetric: the left side is ~1 pixel-unit (0.048em,
     one dot in the 9-wide SVG grid) tighter than the right, so
     the arrow hugs the "origin" airport on the left more than the
     "destination" on the right. */
.pixel-arrow {
  display: inline-block;
  width: 0.48em;
  height: 0.27em;
  vertical-align: 0.15em;
  margin: 0 0.12em 0 0.07em;
  fill: currentColor;
  shape-rendering: crispEdges;
  filter: drop-shadow(0 0 8px var(--led-glow)) drop-shadow(0 0 2px var(--led-glow));
}

/* Vertical-rate arrow sits next to the altitude value on line 3.
   It's a 5×7 portrait-aspect SVG (override the horizontal arrow's
   dimensions) with the same glow + crispEdges styling. Sized a
   bit smaller than the Jersey 10 cap-height so it never appears
   larger than the digits it sits next to, and nudged just above
   the baseline so it visually fits within the character bounds
   rather than hanging below them. */
.pixel-arrow-vert {
  width: 0.32em;
  height: 0.45em;
  vertical-align: 0.05em;
  margin: 0 0.05em 0 0.15em;
}

/* Pixel degree glyph — a 5×5 hollow ring that replaces the `°`
   character on the LOOK stat. Sits as a high-shoulder annotation
   next to the preceding digit (not floating above the cap-height)
   and small enough to read as an indicator mark rather than a
   digit. Minimal left margin so it sits tight. */
.pixel-degree {
  width: 0.2em;
  height: 0.2em;
  vertical-align: 0.4em;
  margin-left: 0.02em;
}

/* Map link + inline Settings link row, sitting immediately under
   the panel inside .plane-card (which has gap: 0). Left-aligned
   with the panel's content. */
.panel-links {
  font-family: var(--font-ui);
  font-size: 1rem;
  color: var(--ui-muted);
  letter-spacing: 0.04em;
  display: flex;
  gap: 0;
  align-items: center;
  justify-content: flex-start;
  padding: 0.6rem 0 0;
  margin: 0;
}

.panel-link {
  color: inherit;
  text-decoration: none;
  border: none;
  border-bottom: 1px dotted var(--ui-border);
  padding: 0 0 2px;
  cursor: pointer;
  background: transparent;
  font: inherit;
  line-height: inherit;
}

.panel-link:hover {
  color: var(--led-amber);
  border-bottom-color: var(--led-amber);
}

.panel-link-sep {
  margin: 0 0.75rem;
  color: var(--ui-border);
  user-select: none;
}

.settings-inline-link {
  user-select: none;
}

/* ───── Denied banner ───── */

.banner {
  position: fixed;
  left: 50%;
  bottom: 1.25rem;
  transform: translateX(-50%);
  max-width: calc(100vw - 2rem);
  padding: 0.75rem 1.25rem;
  border-radius: 6px;
  font-family: var(--font-ui);
  font-size: 0.85rem;
  z-index: 10;
  backdrop-filter: blur(6px);
}

.banner-warn {
  background: rgba(50, 24, 0, 0.9);
  border: 1px solid var(--ui-border);
  color: var(--ui-muted);
}

.banner a {
  color: var(--led-amber);
  text-decoration: underline;
}

/* ───── Locating page ───── */

.locating-choices {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  align-items: flex-start;
  margin-top: 1rem;
}

.btn-primary,
.btn-secondary {
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding: 0.9rem 1.75rem;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: none;
  transition:
    background 120ms ease,
    border-color 120ms ease,
    color 120ms ease;
}

.btn-primary {
  background: var(--led-amber);
  color: #120803;
  border: 1px solid var(--led-amber);
  box-shadow: 0 0 24px rgba(255, 170, 0, 0.35);
}

.btn-primary:hover {
  background: var(--led-amber-soft);
  border-color: var(--led-amber-soft);
}

.btn-primary[disabled] {
  opacity: 0.6;
  cursor: default;
}

.btn-secondary {
  background: transparent;
  color: var(--led-amber);
  border: 1px solid var(--ui-border);
}

.btn-secondary:hover {
  border-color: var(--led-amber);
}

.locating-help {
  margin-top: 1.5rem;
  font-family: var(--font-ui);
  font-size: 0.85rem;
  color: var(--ui-muted);
  text-align: left;
  max-width: 28rem;
}

/* ───── Settings modal (centered overlay) ─────
   A hidden checkbox toggles a full-screen backdrop + a centered
   panel. Opened by the inline "Settings" label inside the
   plane-card. Closed by clicking the backdrop or the × label
   inside the panel. Both are <label for="settings-toggle">
   elements that uncheck the checkbox. Pure CSS, no JS.
   The checkbox + backdrop + panel live OUTSIDE #plane-card so
   they survive the 30 s innerHTML poll. */

.settings-toggle-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 1px;
  height: 1px;
}

.settings-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(4, 2, 0, 0.72);
  backdrop-filter: blur(3px);
  z-index: 30;
  cursor: pointer;
}

.settings-panel {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(24rem, calc(100vw - 2rem));
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  padding: 1.5rem 1.5rem 1.25rem;
  background: var(--ui-panel-bg);
  border: 1px solid var(--ui-border);
  border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.75);
  font-family: var(--font-ui);
  z-index: 40;
}

.settings-toggle-input:checked ~ .settings-backdrop,
.settings-toggle-input:checked ~ .settings-panel {
  display: block;
}

.settings-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1rem;
}

.settings-close {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  background: transparent;
  border: 1px solid var(--ui-border);
  border-radius: 4px;
  color: var(--ui-muted);
  cursor: pointer;
  font-size: 1.25rem;
  line-height: 1;
  user-select: none;
  transition:
    color 120ms ease,
    border-color 120ms ease;
}

.settings-close:hover {
  color: var(--led-amber);
  border-color: var(--led-amber);
}

.settings-heading {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--ui-muted);
  font-weight: 500;
  margin: 0;
}

.settings-form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.6rem;
}

.settings-field {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.settings-field label {
  display: block;
  font-size: 0.7rem;
  color: var(--ui-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 0.2rem;
}

.settings-form input,
.settings-form select {
  /* width:100% + min-width:0 + box-sizing inherited from `*`
     keeps number inputs and selects inside their column instead
     of falling back to the browser's default content-driven width. */
  width: 100%;
  min-width: 0;
  padding: 0.45rem 0.55rem;
  background: #000;
  border: 1px solid rgba(255, 170, 0, 0.2);
  color: var(--led-amber-soft);
  font-family: inherit;
  font-size: 0.9rem;
  border-radius: 4px;
  outline: none;
}

.settings-form input:focus,
.settings-form select:focus {
  border-color: var(--led-amber);
  box-shadow: 0 0 0 2px rgba(255, 170, 0, 0.15);
}

.settings-apply {
  grid-column: 1 / -1;
  padding: 0.55rem;
  background: var(--led-amber);
  color: #120803;
  border: none;
  border-radius: 4px;
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  cursor: pointer;
  margin-top: 0.25rem;
}

.settings-apply:hover {
  background: #ffc044;
}

.settings-secondary {
  grid-column: 1 / -1;
  width: 100%;
  padding: 0.5rem;
  background: transparent;
  border: 1px solid var(--ui-border);
  color: var(--ui-muted);
  font-family: inherit;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-radius: 4px;
  cursor: pointer;
}

.settings-secondary:hover {
  color: var(--led-amber);
  border-color: var(--led-amber);
}

.settings-secondary[disabled] {
  opacity: 0.6;
  cursor: default;
}

.settings-meta {
  margin-top: 0.9rem;
  padding-top: 0.75rem;
  border-top: 1px solid rgba(255, 170, 0, 0.12);
  font-size: 0.75rem;
  color: var(--ui-muted);
  line-height: 1.6;
}

.settings-meta a {
  color: var(--ui-muted);
  text-decoration: none;
  border-bottom: 1px dotted var(--ui-border);
}

.settings-meta a:hover {
  color: var(--led-amber);
}

/* ───── Animations ───── */

@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

@media (prefers-reduced-motion: reduce) {
  .panel[data-fly-state='locating'] .panel-line-1 {
    animation: none;
  }
}

/* ───── Narrow viewports ───── */

@media (max-width: 560px) {
  /* Uniform vertical rhythm on mobile: every panel line gets
     the same gap. Remove the line-2 negative margin that
     creates uneven spacing on small screens. */
  .panel {
    padding: 0.75rem 0.75rem;
    gap: 0.6rem;
  }

  .panel-line-2 {
    margin-top: 0;
  }

  /* Tighter stat gaps so 4 stats pair into 2×2. */
  .panel-stats {
    column-gap: clamp(1rem, 4vw, 2rem);
    row-gap: 0.4rem;
  }

  /* Force 2 stats per row on line 3. */
  .panel-line-3 .stat {
    min-width: calc(50% - 1rem);
    flex: 0 0 auto;
  }

  /* Footer: match the panel's left padding so it aligns
     with the stat text above. */
  .panel-links {
    font-size: 0.85rem;
    padding: 0.3rem 0 0 0.75rem;
  }

  /* Locating page: align buttons and help text with
     the panel's padded content. */
  .locating-choices {
    padding-left: 0.75rem;
  }

  .locating-help {
    padding-left: 0.75rem;
  }

  .settings-panel {
    width: calc(100vw - 1.5rem);
  }

  .settings-form {
    grid-template-columns: 1fr 1fr;
  }
}
