/* ============================================================
   Weather ASCII - Stylesheet
   Dark atmospheric weather dashboard with ASCII art hero
   ============================================================ */

/* --- Custom Properties (theming) --- */
:root {
  /* Base palette */
  --color-bg: #0b0f1a;
  --color-bg-secondary: #111827;
  --color-surface: rgba(255, 255, 255, 0.04);
  --color-surface-hover: rgba(255, 255, 255, 0.07);
  --color-border: rgba(255, 255, 255, 0.08);
  --color-text-primary: #f1f5f9;
  --color-text-secondary: #94a3b8;
  --color-text-muted: #64748b;

  /* Accent colours per weather state */
  --accent-sunny: #fbbf24;
  --accent-partly-cloudy: #a3c4f3;
  --accent-cloudy: #94a3b8;
  --accent-rain: #60a5fa;
  --accent-storm: #a78bfa;
  --accent-night: #818cf8;

  /* Typography */
  --font-sans: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", Consolas, monospace;

  /* Spacing */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;

  /* Radius */
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;

  /* Transitions */
  --transition-fast: 150ms ease;
  --transition-base: 250ms ease;
}

/* --- Reset & Base --- */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-sans);
  background-color: var(--color-bg);
  color: var(--color-text-primary);
  line-height: 1.6;
  min-height: 100vh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Atmospheric gradient background */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  background:
    radial-gradient(ellipse 80% 60% at 50% 0%, rgba(30, 41, 70, 0.6) 0%, transparent 70%),
    radial-gradient(ellipse 60% 50% at 80% 100%, rgba(20, 30, 60, 0.4) 0%, transparent 60%),
    radial-gradient(ellipse 50% 40% at 10% 80%, rgba(25, 20, 50, 0.3) 0%, transparent 60%);
  pointer-events: none;
  z-index: -1;
  transition: background 1s ease;
}

/* --- Accessibility Utilities --- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--accent-sunny);
  color: #0b0f1a;
  font-weight: 600;
  border-radius: var(--radius-sm);
  z-index: 9999;
  transition: top var(--transition-fast);
}

.skip-link:focus {
  top: var(--space-md);
}

/* --- Focus States --- */
:focus-visible {
  outline: 2px solid var(--accent-rain);
  outline-offset: 2px;
}

button:focus-visible,
select:focus-visible {
  outline: 2px solid var(--accent-sunny);
  outline-offset: 2px;
}

/* --- App Container --- */
#app {
  max-width: 1100px;
  margin: 0 auto;
  padding: var(--space-lg) var(--space-md);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* --- Header --- */
.site-header {
  text-align: center;
  padding: var(--space-xl) 0 var(--space-lg);
}

/* The title stays centred, with the display controls balanced on the
   right. The empty first column mirrors the controls' width so the
   title remains optically centred rather than pushed left. */
.header-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-md);
}

.header-brand {
  grid-column: 2;
  text-align: center;
}

.header-controls {
  grid-column: 3;
  justify-self: end;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  flex-wrap: wrap;
}

.header-controls .control-btn {
  padding: var(--space-xs) var(--space-sm);
  font-size: 0.78rem;
}

.header-controls .control-btn-icon {
  font-size: 0.9rem;
}

/* The highlighted half of a compact toggle names the active option. */
.control-state {
  opacity: 0.55;
}

.control-state.is-current {
  opacity: 1;
  font-weight: 600;
  color: var(--color-text-primary);
}

.control-sep {
  opacity: 0.45;
}

.site-title {
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  background: linear-gradient(135deg, var(--color-text-primary) 0%, var(--accent-rain) 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.site-subtitle {
  font-size: 0.9rem;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* --- Location Bar --- */
.location-bar {
  margin-bottom: var(--space-xl);
}

.location-bar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.location-select {
  appearance: none;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-primary);
  padding: var(--space-sm) var(--space-lg) var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  font-size: 0.95rem;
  font-family: var(--font-sans);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast);
  min-width: 180px;
}

.location-select:hover {
  background: var(--color-surface-hover);
}

.location-select option {
  background: var(--color-bg-secondary);
  color: var(--color-text-primary);
}

.local-time {
  font-size: 0.9rem;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

/* --- Hero Section --- */
.hero-section {
  margin-bottom: var(--space-xl);
}

.hero-inner {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
  align-items: center;
}

/* --- ASCII Art Area --- */
.ascii-art-area {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-xl);
  text-align: center;
  position: relative;
  overflow: hidden;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Atmospheric glow behind ASCII art */
.ascii-art-area::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
  opacity: 0.15;
  transition: background 1s ease;
  pointer-events: none;
}

.ascii-art-area[data-weather="sunny"]::before {
  background: radial-gradient(ellipse at center, var(--accent-sunny), transparent 70%);
}

.ascii-art-area[data-weather="partly-cloudy"]::before {
  background: radial-gradient(ellipse at center, var(--accent-partly-cloudy), transparent 70%);
}

.ascii-art-area[data-weather="cloudy"]::before {
  background: radial-gradient(ellipse at center, var(--accent-cloudy), transparent 70%);
}

.ascii-art-area[data-weather="rain"]::before {
  background: radial-gradient(ellipse at center, var(--accent-rain), transparent 70%);
}

.ascii-art-area[data-weather="storm"]::before {
  background: radial-gradient(ellipse at center, var(--accent-storm), transparent 70%);
}

.ascii-art-area[data-weather="night"]::before {
  background: radial-gradient(ellipse at center, var(--accent-night), transparent 70%);
}

.ascii-art-area[data-weather="clear-night"]::before {
  background: radial-gradient(ellipse at center, var(--accent-night), transparent 70%);
}

.ascii-art {
  font-family: var(--font-mono);
  font-size: clamp(0.65rem, 1.8vw, 1.1rem);
  line-height: 1.25;
  color: var(--color-text-primary);
  white-space: pre;
  margin: 0;
  position: relative;
  z-index: 1;
  transition: opacity 0.4s ease, transform 0.4s ease;
}

.ascii-description {
  display: none; /* Screen-reader only */
}

/* --- Hero Info --- */
.hero-info {
  text-align: center;
}

.temperature-display {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-xs);
  margin-bottom: var(--space-sm);
}

.temperature-value {
  font-size: clamp(3.5rem, 10vw, 6rem);
  font-weight: 200;
  letter-spacing: -0.04em;
  line-height: 1;
  transition: color var(--transition-base);
}

.temperature-value[data-weather="sunny"] { color: var(--accent-sunny); }
.temperature-value[data-weather="partly-cloudy"] { color: var(--accent-partly-cloudy); }
.temperature-value[data-weather="cloudy"] { color: var(--accent-cloudy); }
.temperature-value[data-weather="rain"] { color: var(--accent-rain); }
.temperature-value[data-weather="storm"] { color: var(--accent-storm); }
.temperature-value[data-weather="night"] { color: var(--accent-night); }
.temperature-value[data-weather="clear-night"] { color: var(--accent-night); }

.temperature-unit {
  font-size: clamp(1.5rem, 4vw, 2.5rem);
  font-weight: 300;
  color: var(--color-text-secondary);
}

.condition-display {
  font-size: clamp(1.1rem, 3vw, 1.5rem);
  font-weight: 500;
  color: var(--color-text-secondary);
  letter-spacing: 0.02em;
}

/* UV beside the temperature in the hero. The chip takes its colour from
   the same category the timeline dots and the legend use, so one reading
   is coloured consistently everywhere it appears. */
.hero-uv {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
  padding: 2px var(--space-sm);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: 0.8rem;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.hero-uv-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--uv-color, var(--color-text-muted));
}

.hero-uv-label {
  color: var(--color-text-muted);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hero-uv-value {
  color: inherit;
  font-weight: 500;
}

/* Nothing was observed, so say so quietly rather than showing a zero. */
.hero-uv.is-unknown {
  color: var(--color-text-muted);
}

/* --- Details Grid --- */
.details-section {
  margin-bottom: var(--space-xl);
}

.details-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-md);
}

.detail-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-md) var(--space-lg);
  transition: background var(--transition-fast), transform var(--transition-fast);
}

.detail-card:hover {
  background: var(--color-surface-hover);
}

.detail-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-xs);
}

.detail-value {
  font-size: 1.2rem;
  font-weight: 500;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

/* --- Forecast Section --- */
.forecast-section {
  margin-bottom: var(--space-xl);
}

.section-title {
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-md);
  letter-spacing: 0.03em;
}

.forecast-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-sm);
}

.forecast-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  text-align: center;
  transition: background var(--transition-fast), transform var(--transition-fast);
}

.forecast-card:hover {
  background: var(--color-surface-hover);
}

.forecast-period {
  font-size: 0.8rem;
  color: var(--color-text-muted);
  margin-bottom: var(--space-xs);
  font-weight: 500;
}

.forecast-icon {
  font-family: var(--font-mono);
  font-size: 1.4rem;
  margin-bottom: var(--space-xs);
  display: block;
  line-height: 1.2;
}

.forecast-condition {
  font-size: 0.75rem;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-xs);
}

.forecast-temp {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

/* --- Controls Section --- */
.controls-section {
  margin-top: auto;
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border);
}

.controls-grid {
  display: flex;
  justify-content: center;
  gap: var(--space-md);
  flex-wrap: wrap;
  margin-bottom: var(--space-sm);
}

.control-btn {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-md);
  font-size: 0.85rem;
  font-family: var(--font-sans);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.control-btn:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
  border-color: rgba(255, 255, 255, 0.15);
}

.control-btn[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.2);
  color: var(--color-text-primary);
}

.control-btn-icon {
  font-size: 1rem;
}

.refresh-btn.spinning .refresh-icon {
  animation: spin 0.8s ease;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.last-updated {
  text-align: center;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

/* --- Weather Forecast (next 24 hours) ---
   Lives inside the hero, directly beneath the ASCII window, so the
   controls read as part of the display they animate. */
.movie-section {
  margin-top: var(--space-lg);
  padding: var(--space-md) var(--space-lg) var(--space-lg);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}

.movie-section.is-playing {
  border-color: rgba(255, 255, 255, 0.16);
}

/* While the forecast runs, the hero is showing a projection rather than
   the observation, so it reads slightly dimmer than the live view. */
.ascii-art-area[data-mode="movie"] .ascii-art {
  opacity: 0.92;
}

.ascii-art-area[data-mode="movie"]::after {
  content: "forecast playback";
  position: absolute;
  top: var(--space-sm);
  left: var(--space-sm);
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  pointer-events: none;
}

.movie-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

.movie-heading {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--color-text-primary);
}

.movie-heading-icon {
  font-size: 0.95rem;
  line-height: 1;
}

/* State badge. Replaces the old playback-speed readout with a plain
   word, since a multiplier is meaningless to most visitors. */
.movie-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-sunny);
  background: rgba(251, 191, 36, 0.12);
  border: 1px solid rgba(251, 191, 36, 0.3);
  border-radius: 999px;
  padding: 2px var(--space-sm);
}

.movie-badge-icon {
  font-size: 0.55rem;
  line-height: 1;
}

.movie-section.is-playing .movie-badge-icon {
  animation: movie-badge-pulse 1.4s ease-in-out infinite;
}

.movie-badge.is-paused {
  color: var(--color-text-muted);
  background: var(--color-surface);
  border-color: var(--color-border);
}

@keyframes movie-badge-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

.movie-summary {
  font-size: 0.82rem;
  color: var(--color-text-secondary);
  margin-top: var(--space-xs);
}

/* The forecast window, derived from the loaded hourly data. */
.movie-range {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  color: var(--accent-sunny);
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
  min-height: 1.2em;
}

.movie-transport {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  margin-top: var(--space-md);
}

.movie-btn {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-md);
  font-size: 0.85rem;
  font-family: var(--font-sans);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  transition: background var(--transition-fast), border-color var(--transition-fast),
              color var(--transition-fast), opacity var(--transition-fast);
}

.movie-btn:hover:not(:disabled) {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
  border-color: rgba(255, 255, 255, 0.15);
}

.movie-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.movie-btn-primary {
  min-width: 6.5rem;
  justify-content: center;
}

.movie-btn[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.22);
  color: var(--color-text-primary);
}

.movie-btn-icon {
  font-size: 0.75rem;
  line-height: 1;
}

.movie-readout {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin-left: auto;
}

.movie-clock {
  font-family: var(--font-mono);
  font-size: 1.05rem;
  letter-spacing: 0.06em;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

.movie-date {
  font-size: 0.72rem;
  color: var(--color-text-muted);
}

/* --- Timeline: markers, tracker, temperature strip ---
   Markers, the range input and the strip all occupy the same box and
   share its width, which is what keeps a marker aligned with the
   colour beneath it and with the thumb. */
.movie-timeline {
  margin-top: var(--space-md);
}

.movie-markers {
  position: relative;
  height: 1.1rem;
  margin-bottom: 2px;
  /* Markers sit behind the thumb so a drag cannot be obstructed. */
  pointer-events: none;
}

.movie-marker {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-size: 0.72rem;
  line-height: 1.1rem;
  color: var(--color-text-muted);
  opacity: 0.55;
  transition: opacity var(--transition-fast), color var(--transition-fast);
}

.movie-marker.is-active {
  opacity: 1;
  color: var(--color-text-primary);
}

.movie-scrubber-row {
  margin: 0;
}

.movie-scrubber {
  display: block;
  width: 100%;
  margin: 0;
  accent-color: var(--accent-sunny);
  background: transparent;
  cursor: pointer;
}

.movie-scrubber:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The UV and temperature rows are labelled above their bar rather than
   beside it. That keeps the bar full width and identical to the marker
   row and the scrubber, so all four share one horizontal axis exactly. */
.movie-strip-label {
  display: block;
  font-size: 0.58rem;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  margin-bottom: 3px;
}

.movie-temp-wrap,
.movie-uv-wrap {
  margin-top: var(--space-sm);
}

.movie-temp-strip,
.movie-uv-row {
  width: 100%;
  min-width: 0;
}

/* The number beside the row label names the UV at the selected hour, so
   the dots never have to be hovered to be read. */
.movie-uv-readout {
  margin-left: var(--space-sm);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

/* With a reading present the chip repeats the dot's own category colour,
   which ties the number to the row without colouring the text itself. */
.movie-uv-readout.has-value {
  color: var(--color-text-secondary);
}

.movie-uv-readout.has-value::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  margin-right: 4px;
  vertical-align: middle;
  background: var(--uv-color, var(--color-text-muted));
}

/* UV row: one dot per forecast hour, aligned to the same axis. */
.movie-uv-row {
  position: relative;
  height: 10px;
  pointer-events: none;
}

.movie-uv-dot {
  position: absolute;
  top: 50%;
  width: 5px;
  height: 5px;
  margin-left: -2.5px;
  margin-top: -2.5px;
  border-radius: 50%;
  background: var(--color-text-muted);
  opacity: 0.85;
  transition: box-shadow var(--transition-fast), transform var(--transition-fast);
}

/* A zero UV hour is genuinely zero, so it recedes rather than shouting
   a bright dot at three in the morning. */
.movie-uv-dot.is-night {
  opacity: 0.22;
}

/* No reading at all is not the same as a low reading. These dots stay
   neutral grey and never take a category colour. */
.movie-uv-dot.is-unknown {
  opacity: 0.4;
  background: var(--color-text-muted);
}

/* The dot at the selected forecast hour is ringed, matching the weather
   marker above it. Same position arithmetic, so the ring, the marker and
   the scrubber thumb always name one hour. */
.movie-uv-dot.is-active {
  opacity: 1;
  transform: scale(1.5);
  box-shadow: 0 0 0 2px var(--color-bg),
              0 0 0 3px var(--color-text-primary);
}

.movie-temp-strip {
  height: 6px;
  border-radius: 999px;
  /* background-image is set from the forecast in app.js. */
  background-color: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--color-border);
  background-clip: padding-box;
}

/* --- Legends ---
   Static reference keys. Deliberately not a chart: no axis, no time
   scale and no interaction, so they cannot be read as a second
   timeline. */
.movie-legends {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
}

.movie-legend {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.movie-legend-title {
  padding: 0;
  font-size: 0.55rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 3px;
}

.movie-legend-items {
  display: flex;
  flex-wrap: wrap;
  gap: 2px var(--space-sm);
}

.movie-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 0.58rem;
  color: var(--color-text-secondary);
  white-space: nowrap;
}

.movie-legend-chip {
  width: 7px;
  height: 7px;
  border-radius: 2px;
  flex: 0 0 auto;
}

.movie-legend-value {
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

/* UV swatch inside the detail card, matching the timeline and legend. */
.detail-uv-swatch {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-left: var(--space-xs);
  vertical-align: middle;
}

/* --- Precipitation rows ---
   Same axis and dot geometry as the UV row, so the timeline reads as
   one set of layers rather than several competing charts. */
.movie-precip-wrap {
  margin-top: var(--space-sm);
}

.movie-precip-row {
  position: relative;
  width: 100%;
  min-width: 0;
  height: 8px;
  pointer-events: none;
}

/* The amount row draws height as well as position, so a downpour
   reads as visibly bigger than a drizzle. */
.movie-precip-bar {
  position: absolute;
  bottom: 0;
  width: 4px;
  margin-left: -2px;
  border-radius: 1px;
  opacity: 0.9;
}

.movie-precip-bar.is-unknown {
  background: repeating-linear-gradient(
    135deg,
    rgba(148, 163, 184, 0.48) 0,
    rgba(148, 163, 184, 0.48) 1px,
    transparent 1px,
    transparent 2px
  );
  opacity: 0.45;
}

.movie-precip-dot {
  position: absolute;
  top: 50%;
  width: 5px;
  height: 5px;
  margin-left: -2.5px;
  margin-top: -2.5px;
  border-radius: 50%;
  background: var(--color-text-muted);
  opacity: 0.85;
}

.movie-precip-dot.is-dry {
  opacity: 0.2;
}

.movie-precip-dot.is-active {
  opacity: 1;
  transform: scale(1.9);
  box-shadow: 0 0 0 2px var(--color-bg), 0 0 6px rgba(255, 255, 255, 0.6);
}

.movie-precip-readout {
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

/* --- Severe weather alerts ---
   Restrained styling, but severity never relies on colour alone:
   every alert also carries its severity as a text badge. */
.alert-section {
  margin-bottom: var(--space-lg);
}

.alert-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.alert {
  border: 1px solid var(--color-border);
  border-left-width: 4px;
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface);
}

.alert-head {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

.alert-title {
  font-size: 0.9rem;
  font-weight: 500;
}

.alert-badge {
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 1px var(--space-sm);
  border-radius: 999px;
  border: 1px solid currentColor;
  white-space: nowrap;
}

.alert-source-tag {
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.alert[data-severity="warning"],
.alert[data-severity="severe"] {
  background: rgba(239, 68, 68, 0.06);
  border-color: rgba(239, 68, 68, 0.3);
  border-left-color: #ef4444;
}

.alert[data-severity="watch"] {
  background: rgba(251, 191, 36, 0.06);
  border-color: rgba(251, 191, 36, 0.28);
  border-left-color: #fbbf24;
}

.alert[data-severity="advisory"] {
  background: rgba(96, 165, 250, 0.05);
  border-color: rgba(96, 165, 250, 0.24);
  border-left-color: #60a5fa;
}

.alert-badge[data-severity="severe"],
.alert-badge[data-severity="warning"] {
  color: #fca5a5;
}

.alert-badge[data-severity="watch"] {
  color: #fcd34d;
}

.alert-badge[data-severity="advisory"] {
  color: #93c5fd;
}

.alert-detail {
  font-size: 0.78rem;
  color: var(--color-text-secondary);
  margin-top: 2px;
}

.alert-meta {
  font-size: 0.65rem;
  color: var(--color-text-muted);
  margin-top: 2px;
}

.alert-source-note {
  font-size: 0.68rem;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
}

/* Routine states are a single quiet line, not a panel. A real alert
   still uses the cards above, so the two are visually distinct. */
.alert-source-note.is-compact {
  margin-top: 0;
  font-size: 0.75rem;
}

.alert-source-note.is-unavailable {
  color: #fbbf24;
}

/* --- Install advice (phone browsers) ---
   A soft note strip above the location bar. It only ever appears on
   phone-sized or touch browsing contexts that are not the installed
   app, which app.js decides. */
.install-advice {
  margin-bottom: var(--space-lg);
  border: 1px solid rgba(251, 191, 36, 0.35);
  border-left: 3px solid #fbbf24;
  border-radius: var(--radius-md);
  background: rgba(251, 191, 36, 0.07);
  padding: var(--space-sm) var(--space-md);
}

.install-advice-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.install-advice-copy {
  margin: 0;
  font-size: 0.82rem;
  color: var(--color-text-secondary);
}

.install-advice-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: 0 0 auto;
}

.install-dismiss {
  background: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text-muted);
  padding: var(--space-xs) var(--space-sm);
  font-size: 0.75rem;
  font-family: var(--font-sans);
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.install-dismiss:hover {
  color: var(--color-text-primary);
  border-color: rgba(255, 255, 255, 0.2);
}

/* --- Air quality --- */
.air-section,
.prefs-section {
  margin-bottom: var(--space-lg);
}

.air-empty {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin: 0;
}

.air-quality {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-lg);
  align-items: flex-start;
}

.air-block {
  min-width: 0;
}

.air-label {
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.air-value {
  font-size: 1rem;
  font-variant-numeric: tabular-nums;
}

.air-category {
  font-size: 0.72rem;
  color: var(--color-text-secondary);
}

.air-meta,
.prefs-status {
  font-size: 0.68rem;
  color: var(--color-text-muted);
}

.prefs-subtitle {
  font-size: 0.68rem;
  color: var(--color-text-muted);
  margin-top: var(--space-md);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.prefs-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  align-items: center;
  margin-top: var(--space-xs);
}

.check {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.72rem;
  color: var(--color-text-secondary);
  cursor: pointer;
}

.movie-status {
  margin-top: var(--space-xs);
  font-size: 0.72rem;
  color: var(--color-text-muted);
  min-height: 1em;
}

/* --- Weather story ---
   One or two plain-language lines under the UV chip in the hero. */
.weather-story {
  margin-top: var(--space-sm);
  font-size: 0.82rem;
  color: var(--color-text-secondary);
  max-width: 34rem;
}

.weather-story-line {
  display: block;
}

/* --- Saved location chips ---
   Quick one-tap shortcuts to saved places, under the location bar. */
.saved-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}

.saved-chips[hidden] {
  display: none;
}

.saved-chip {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  border-radius: 999px;
  padding: 3px var(--space-md);
  font-size: 0.78rem;
  font-family: var(--font-sans);
  cursor: pointer;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: background var(--transition-fast), color var(--transition-fast),
              border-color var(--transition-fast);
}

.saved-chip:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
  border-color: rgba(255, 255, 255, 0.15);
}

.saved-chip.is-active {
  background: rgba(96, 165, 250, 0.16);
  border-color: rgba(96, 165, 250, 0.5);
  color: var(--color-text-primary);
}

/* --- Insights: tab set and panels --- */
.insight-section {
  margin-bottom: var(--space-xl);
  min-width: 0;
}

.insight-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-md);
}

.insight-tab {
  background: none;
  border: 1px solid transparent;
  border-bottom: 2px solid transparent;
  color: var(--color-text-muted);
  padding: var(--space-sm) var(--space-md);
  font-size: 0.82rem;
  font-family: var(--font-sans);
  cursor: pointer;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  transition: background var(--transition-fast), color var(--transition-fast),
              border-color var(--transition-fast);
}

.insight-tab:hover {
  color: var(--color-text-primary);
  background: var(--color-surface);
}

.insight-tab[aria-selected="true"] {
  color: var(--color-text-primary);
  border-bottom-color: var(--accent-rain);
  background: var(--color-surface);
}

.insight-tab:focus-visible {
  outline: 2px solid var(--accent-rain);
  outline-offset: -2px;
}

.insight-panel {
  min-width: 0;
  font-size: 0.82rem;
  color: var(--color-text-secondary);
}

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

.insight-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-sm) var(--space-lg);
}

.insight-fact {
  min-width: 0;
}

.insight-label {
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.insight-value {
  font-size: 0.98rem;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

.insight-note {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
}

/* Moon: a phase glyph beside its name and illumination. */
.insight-moon {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: var(--space-md) 0 var(--space-sm);
}

.moon-glyph {
  font-size: 1.6rem;
  line-height: 1;
}

/* --- Wind compass --- */
.wind-compass {
  position: relative;
  width: 88px;
  height: 88px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: var(--space-xs) 0 var(--space-sm);
}

.wind-arrow {
  font-size: 1.7rem;
  line-height: 1;
  color: var(--accent-rain);
  transition: transform var(--transition-base);
}

.wind-cardinal {
  position: absolute;
  font-size: 0.5rem;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
}

.wind-cardinal[data-dir="N"] { top: 3px; left: 50%; transform: translateX(-50%); }
.wind-cardinal[data-dir="S"] { bottom: 3px; left: 50%; transform: translateX(-50%); }
.wind-cardinal[data-dir="E"] { right: 4px; top: 50%; transform: translateY(-50%); }
.wind-cardinal[data-dir="W"] { left: 4px; top: 50%; transform: translateY(-50%); }

/* --- Comfort bar ---
   One ~hourly segment across the next 24 hours, coloured by level.
   Non-interactive: it has no axis, no pointer target and no role
   beyond an image with a text alternative. */
.comfort-bar {
  display: flex;
  gap: 1px;
  height: 26px;
  margin-top: var(--space-xs);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--color-surface);
}

.comfort-segment {
  flex: 1 1 0;
  min-width: 0;
  background: var(--color-text-muted);
}

.comfort-segment[data-level="good"] { background: #4ade80; }
.comfort-segment[data-level="fair"] { background: #fbbf24; }
.comfort-segment[data-level="poor"] { background: #64748b; }

.comfort-legend {
  font-size: 0.65rem;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
}

.outlook-air {
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
}

/* --- Footer --- */
.site-footer {
  margin-top: var(--space-xl);
  padding: var(--space-lg) 0;
  text-align: center;
}

.footer-note {
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

/* ============================================================
   Live ASCII Grid Renderer
   ------------------------------------------------------------
   Every cell is a <span> holding one glyph. Colour comes from the
   per-cell tone class; alpha is written inline each frame so
   particles can fade in and out without touching layout.
   ============================================================ */

.ascii-art {
  position: relative;
  z-index: 1;
  display: block;
  /* Explicit width makes clientWidth match the container's inner width,
     which the engine relies on to size its grid exactly. */
  width: 100%;
  font-family: var(--font-mono);
  font-variant-ligatures: none;
  line-height: 1.15;
  color: var(--color-text-primary);
  white-space: pre;
  margin: 0;
  user-select: text;
}

/* Base cell: must stay exactly one glyph wide so the grid aligns. */
.ac {
  display: inline-block;
  color: var(--cell-color, var(--color-text-secondary));
  /* Cells never animate transform, which would break grid alignment. */
  transition: color 420ms ease;
}

/* --- Sky & stars --- */
.ac.g-sky { --cell-color: #1e2a44; }
.ac.g-sky-alt { --cell-color: #2c3a5c; }
.ac.g-star { --cell-color: #cfe4ff; text-shadow: 0 0 5px rgba(160, 200, 255, 0.85); }

/* --- Sun --- */
.ac.g-sun {
  --cell-color: #ffd75e;
  text-shadow: 0 0 8px rgba(251, 191, 36, 0.9), 0 0 18px rgba(251, 146, 60, 0.45);
}
.ac.g-sun-soft {
  --cell-color: #f59e0b;
  text-shadow: 0 0 6px rgba(245, 158, 11, 0.55);
}

/* --- Moon --- */
.ac.g-moon {
  --cell-color: #e8eefc;
  text-shadow: 0 0 7px rgba(200, 214, 255, 0.6);
}
.ac.g-moon-lit { --cell-color: #a8b6d8; }

/* --- Clouds --- */
.ac.g-cloud { --cell-color: #8fa3c4; }
.ac.g-cloud-lit {
  --cell-color: #ffe9b8;
  text-shadow: 0 0 7px rgba(255, 220, 150, 0.5);
}
.ac.g-cloud-dark {
  --cell-color: #4a5570;
  text-shadow: 0 0 6px rgba(30, 40, 70, 0.8);
}

/* --- Precipitation --- */
.ac.g-rain {
  --cell-color: #7cc4ff;
  text-shadow: 0 0 5px rgba(96, 165, 250, 0.6);
}
.ac.g-snow {
  --cell-color: #eaf6ff;
  text-shadow: 0 0 6px rgba(226, 240, 255, 0.7);
}
.ac.g-hail { --cell-color: #d6ecff; text-shadow: 0 0 5px rgba(214, 236, 255, 0.6); }
.ac.g-splash { --cell-color: #9fd4ff; }

/* --- Atmosphere --- */
.ac.g-fog { --cell-color: #b8c4d8; }
.ac.g-wind { --cell-color: #7f93b3; font-style: italic; }

/* --- Lightning --- */
.ac.g-bolt {
  --cell-color: #f8f4ff;
  text-shadow: 0 0 10px rgba(167, 139, 250, 1), 0 0 22px rgba(196, 181, 253, 0.7);
}

/* --- Terrain --- */
.ac.g-land { --cell-color: #3f4a63; }
.ac.g-land-lit { --cell-color: #6b7a99; }

/* Full-frame flash veil for lightning strikes. */
.ascii-veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  z-index: 2;
  background: radial-gradient(
    ellipse at center,
    var(--scene-accent, rgba(255, 255, 255, 1)) 0%,
    transparent 75%
  );
  mix-blend-mode: screen;
  transition: opacity 90ms linear;
}

/* Subtle CRT scanlines over the whole scene. */
.ascii-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  opacity: 0.5;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.028) 0px,
    rgba(255, 255, 255, 0.028) 1px,
    transparent 1px,
    transparent 3px
  );
}

/* Loading shimmer while fetching live conditions. */
.ascii-art-area.is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
  z-index: 4;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 20%,
    rgba(255, 255, 255, 0.06) 50%,
    transparent 80%
  );
  background-size: 220% 100%;
  animation: sweep 1.1s linear infinite;
}

@keyframes sweep {
  from { background-position: 140% 0; }
  to { background-position: -40% 0; }
}

/* ============================================================
   Scene accent glows
   ============================================================ */

.ascii-art-area[data-weather="clear"]::before {
  background: radial-gradient(ellipse at 70% 30%, var(--accent-sunny), transparent 68%);
}
.ascii-art-area[data-weather="partly"]::before {
  background: radial-gradient(ellipse at 62% 28%, var(--accent-partly-cloudy), transparent 70%);
}
.ascii-art-area[data-weather="cloudy"]::before {
  background: radial-gradient(ellipse at center, var(--accent-cloudy), transparent 70%);
}
.ascii-art-area[data-weather="drizzle"]::before,
.ascii-art-area[data-weather="showers"]::before {
  background: radial-gradient(ellipse at center, var(--accent-rain), transparent 70%);
}
.ascii-art-area[data-weather="rain"]::before,
.ascii-art-area[data-weather="downpour"]::before {
  background: radial-gradient(ellipse at center, var(--accent-rain), transparent 72%);
  opacity: 0.22;
}
.ascii-art-area[data-weather="storm"]::before {
  background: radial-gradient(ellipse at center, var(--accent-storm), transparent 70%);
}
.ascii-art-area[data-weather="fog"]::before {
  background: radial-gradient(ellipse at center, #9fb0c7, transparent 72%);
}
.ascii-art-area[data-weather="snow"]::before,
.ascii-art-area[data-weather="sleet"]::before {
  background: radial-gradient(ellipse at center, #bfe3ff, transparent 72%);
}
.ascii-art-area[data-weather="night"]::before {
  background: radial-gradient(ellipse at center, var(--accent-night), transparent 70%);
}
.ascii-art-area[data-weather="clear-night"]::before {
  background: radial-gradient(ellipse at 72% 26%, var(--accent-night), transparent 68%);
}

/* Tropical cyclone / hurricane: red-hot warning wash. */
.ascii-art-area[data-cyclone="tropical"]::before {
  background: radial-gradient(ellipse at center, rgba(239, 68, 68, 0.85), transparent 72%);
  opacity: 0.26;
}

.ascii-art-area[data-cyclone="tropical"] {
  border-color: rgba(239, 68, 68, 0.4);
  box-shadow: 0 0 40px rgba(239, 68, 68, 0.16) inset;
}

/* Hurricanes pulse the frame border. */
@keyframes cyclone-pulse {
  0%, 100% { border-color: rgba(239, 68, 68, 0.35); }
  50% { border-color: rgba(248, 113, 113, 0.75); }
}

.ascii-art-area[data-cyclone="tropical"] {
  animation: cyclone-pulse 2.4s ease-in-out infinite;
}

/* ============================================================
   Search & Location UI
   ============================================================ */

.location-controls {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

.search-wrap {
  position: relative;
  flex: 1 1 220px;
  min-width: 200px;
}

.location-search {
  width: 100%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-primary);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  font-size: 0.95rem;
  font-family: var(--font-sans);
  transition: background var(--transition-fast), border-color var(--transition-fast);
}

.location-search::placeholder {
  color: var(--color-text-muted);
}

.location-search:hover {
  background: var(--color-surface-hover);
}

.location-search::-webkit-search-cancel-button {
  filter: invert(0.7);
  cursor: pointer;
}

.search-results {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  max-height: 260px;
  overflow-y: auto;
  background: var(--color-bg-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
}

.search-results:empty {
  display: none;
}

.search-result {
  background: none;
  border: 0;
  border-bottom: 1px solid var(--color-border);
  color: var(--color-text-primary);
  text-align: left;
  padding: var(--space-sm) var(--space-md);
  font-size: 0.88rem;
  font-family: var(--font-sans);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.search-result:last-child {
  border-bottom: 0;
}

.search-result:hover,
.search-result:focus-visible {
  background: var(--color-surface-hover);
}

.search-message {
  padding: var(--space-sm) var(--space-md);
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--color-text-muted);
}

.search-status {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.data-source {
  margin-top: var(--space-sm);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* --- Use current location --- */

/* Deliberately built from the same surface, border and radius as the
   location selects beside it, so it reads as part of the same control
   group rather than as a separate call to action. */
.location-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  font-size: 0.85rem;
  font-family: var(--font-sans);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--transition-fast), color var(--transition-fast),
              border-color var(--transition-fast), opacity var(--transition-fast);
}

/* The label wraps inside the button at narrow widths, so it is given a
   flex line of its own to shrink on rather than overflowing. */
.location-btn-label {
  min-width: 0;
}

.location-btn:hover:not(:disabled) {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
  border-color: rgba(255, 255, 255, 0.15);
}

.location-btn:disabled {
  opacity: 0.55;
  cursor: progress;
}

.location-btn-icon {
  font-size: 0.9rem;
  line-height: 1;
}

/* While a fix is being taken the icon pulses, which is the only movement
   added. The status line beside it carries the same information in
   words, so nothing depends on seeing it. */
.location-btn[aria-busy="true"] .location-btn-icon {
  animation: location-pulse 1.1s ease-in-out infinite;
}

@keyframes location-pulse {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 1; }
}

/* Reserved space so the first message does not shift the layout. */
.location-status {
  margin-top: var(--space-xs);
  min-height: 1em;
  font-size: 0.72rem;
  color: var(--color-text-muted);
}

.location-status.is-error {
  color: #fbbf24;
}

.location-status.is-ok {
  color: var(--color-text-secondary);
}

.data-source.is-live {
  color: #4ade80;
}

.data-source.is-simulated {
  color: #fbbf24;
}

.data-source.is-stale {
  color: #fbbf24;
}

.data-note {
  margin-top: var(--space-xs);
  text-align: center;
  font-size: 0.72rem;
  color: var(--color-text-muted);
}

.forecast-empty {
  grid-column: 1 / -1;
  font-size: 0.8rem;
  color: var(--color-text-muted);
  text-align: center;
  padding: var(--space-md);
}

.site-footer a {
  color: var(--accent-rain);
}

/* ============================================================
   Responsive Layout
   ============================================================ */

/* Tablet */
@media (min-width: 640px) {
  #app {
    padding: var(--space-xl) var(--space-lg);
  }

  .hero-inner {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2xl);
    text-align: left;
  }

  .ascii-art-area {
    min-height: 280px;
  }

  .hero-info {
    text-align: left;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .details-grid {
    grid-template-columns: repeat(3, 1fr);
  }

  .forecast-cards {
    grid-template-columns: repeat(5, 1fr);
  }
}

/* Desktop */
@media (min-width: 960px) {
  #app {
    padding: var(--space-2xl) var(--space-xl);
  }

  .ascii-art-area {
    min-height: 320px;
    padding: var(--space-2xl);
  }

  .details-grid {
    grid-template-columns: repeat(6, 1fr);
  }
}

/* The header stacks before the three compact controls get too tight
   for the right-hand grid column, so they never wrap into a vertical
   list just above the phone layout. */
@media (max-width: 800px) {
  .header-inner {
    grid-template-columns: 1fr;
    justify-items: center;
  }

  .header-brand,
  .header-controls {
    grid-column: 1;
  }

  .header-controls {
    justify-self: center;
    justify-content: center;
  }
}

/* Mobile adjustments */
@media (max-width: 480px) {
  .location-bar-inner {
    flex-direction: column;
    align-items: stretch;
  }

  .location-select {
    width: 100%;
  }

  /* The button matches the full-width selects above it. */
  .location-btn {
    width: 100%;
    justify-content: center;
  }

  .local-time {
    text-align: center;
  }

  .ascii-art-area {
    padding: var(--space-md);
    min-height: 160px;
  }

  .ascii-art {
    font-size: clamp(0.5rem, 4vw, 0.85rem);
  }

  .details-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .forecast-cards {
    grid-template-columns: repeat(3, 1fr);
  }

  .controls-grid {
    flex-direction: column;
    align-items: stretch;
  }

  .control-btn {
    justify-content: center;
  }

  /* On phones the transport stacks, but the buttons stay side by side so
     the timeline below keeps as much width as possible. */
  .movie-transport {
    justify-content: flex-start;
  }

  .movie-btn {
    justify-content: center;
    flex: 1 1 auto;
    padding: var(--space-sm) var(--space-md);
  }

  .movie-readout {
    margin-left: 0;
    width: 100%;
    justify-content: space-between;
  }

  .movie-section {
    padding: var(--space-md);
  }

  /* 25 hourly markers will not fit legibly at phone widths, so the
     row scrolls horizontally with the tracker pinned to the same
     scroll position rather than being allowed to overflow the panel. */
  .movie-timeline {
    margin-top: var(--space-sm);
  }

  .movie-markers {
    font-size: 0.65rem;
  }

  .movie-marker {
    font-size: 0.62rem;
  }

  .movie-temp-strip {
    height: 5px;
  }

  /* The row captions stay visible: "Temperature trend" is the label that
     stops the bar being read as a cold-to-hot scale, so hiding it at
     small sizes would reintroduce the exact confusion it fixes. The UV
     readout stays too, since it is the only place the number appears on
     the timeline itself. */
  .movie-strip-label {
    font-size: 0.55rem;
  }

  .movie-uv-readout {
    font-size: 0.55rem;
  }

  .movie-uv-dot {
    width: 4px;
    height: 4px;
    margin-left: -2px;
    margin-top: -2px;
  }

  .hero-uv {
    font-size: 0.75rem;
  }

  /* Legends stack so they cannot widen the panel, and the numeric
     detail is dropped first since the colour chip and name carry the
     meaning. */
  .movie-legends {
    flex-direction: column;
    gap: var(--space-sm);
  }

  .movie-legend-value {
    display: none;
  }

  /* The switch keeps a comfortable tap target on phones. */
  .seasonal-switch {
    font-size: 0.75rem;
  }

  /* Insights: the tabs share the row, the fact grid pairs up, and the
     compass and comfort bar shrink so neither can widen the panel. */
  .insight-tabs {
    gap: 0;
  }

  .insight-tab {
    flex: 1 1 auto;
    padding: var(--space-sm);
    font-size: 0.72rem;
    text-align: center;
  }

  .insight-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .wind-compass {
    width: 72px;
    height: 72px;
  }

  .comfort-bar {
    height: 22px;
  }

  .weather-story {
    font-size: 0.78rem;
  }

  .saved-chip {
    font-size: 0.72rem;
    padding: 2px var(--space-sm);
  }
}

/* ============================================================
   Seasonal Backdrop
   ------------------------------------------------------------
   A fixed, decorative layer that sits behind the app. Each season
   carries a low contrast wash plus a few slow particles. Nothing
   here is interactive: the layer ignores pointer events and is
   hidden from assistive technology in the markup.
   ============================================================ */

/* Destination scenery canvas: the geographic landscape for the
   selected place. It sits one layer behind the seasonal backdrop so
   rain, snow and clouds fall in front of the skyline. */
.scenery-canvas {
  position: fixed;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.scenery-canvas[hidden] {
  display: none;
}

.seasonal-backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  background-color: transparent;
  transition: background 1.2s ease;
}

/* The off switch removes the layer entirely, so no particle keeps
   animating behind a page that asked for none. */
.seasonal-backdrop[hidden] {
  display: none;
}

/* Low contrast seasonal washes. Each stays close to the app
   background so foreground text keeps its contrast. */
.seasonal-backdrop[data-season="spring"] {
  background-image:
    radial-gradient(ellipse 70% 50% at 15% 10%, rgba(134, 239, 172, 0.10) 0%, transparent 60%),
    radial-gradient(ellipse 60% 45% at 85% 85%, rgba(249, 168, 212, 0.10) 0%, transparent 60%);
}

.seasonal-backdrop[data-season="summer"] {
  background-image:
    radial-gradient(ellipse 75% 55% at 50% 0%, rgba(251, 191, 36, 0.12) 0%, transparent 62%),
    radial-gradient(ellipse 60% 50% at 90% 90%, rgba(45, 212, 191, 0.08) 0%, transparent 60%);
}

.seasonal-backdrop[data-season="autumn"] {
  background-image:
    radial-gradient(ellipse 70% 50% at 20% 0%, rgba(251, 146, 60, 0.12) 0%, transparent 60%),
    radial-gradient(ellipse 60% 50% at 90% 85%, rgba(180, 83, 9, 0.12) 0%, transparent 60%);
}

.seasonal-backdrop[data-season="winter"] {
  background-image:
    radial-gradient(ellipse 75% 55% at 50% 0%, rgba(147, 197, 253, 0.12) 0%, transparent 62%),
    radial-gradient(ellipse 60% 50% at 15% 90%, rgba(226, 232, 240, 0.08) 0%, transparent 60%);
}

/* --- particles ---
   Motion is transform and opacity only, which keeps the layer off
   the layout and paint critical path. */

.seasonal-particle {
  position: absolute;
  top: -14vh;
  left: var(--sb-x, 50%);
  width: var(--sb-size, 4px);
  height: var(--sb-size, 4px);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  animation: seasonal-fall var(--sb-dur, 16s) linear infinite;
  animation-delay: var(--sb-delay, 0s);
}

.seasonal-backdrop[data-season="spring"] .seasonal-particle {
  background-color: #f9a8d4;
  border-radius: 60% 0 60% 0;
}

.seasonal-backdrop[data-season="autumn"] .seasonal-particle {
  background-color: #fb923c;
  border-radius: 0 60% 0 60%;
}

.seasonal-backdrop[data-season="winter"] .seasonal-particle {
  background-color: #e2e8f0;
}

.seasonal-backdrop[data-season="summer"] .seasonal-particle {
  background-color: #fde68a;
  box-shadow: 0 0 6px rgba(253, 230, 138, 0.55);
  top: auto;
  bottom: -14vh;
  animation-name: seasonal-rise;
}

@keyframes seasonal-fall {
  0% {
    transform: translate3d(0, 0, 0) rotate(0deg);
    opacity: 0;
  }
  10% { opacity: var(--sb-opacity, 0.4); }
  90% { opacity: var(--sb-opacity, 0.4); }
  100% {
    transform: translate3d(var(--sb-drift, 0px), 128vh, 0) rotate(var(--sb-spin, 360deg));
    opacity: 0;
  }
}

@keyframes seasonal-rise {
  0% {
    transform: translate3d(0, 0, 0) scale(0.7);
    opacity: 0;
  }
  15% { opacity: var(--sb-opacity, 0.4); }
  85% { opacity: var(--sb-opacity, 0.4); }
  100% {
    transform: translate3d(var(--sb-drift, 0px), -128vh, 0) scale(1);
    opacity: 0;
  }
}

/* --- weather layers ---
   The scene attribute names the interpreted WMO code and night
   names the day/night flag, so every weather layer below is pure
   presentation: the JS builds the elements this CSS decides how to
   show. Except for the flash, which only a storm gets, the layers
   fade in so a scene change melts instead of popping. */

/* Weather washes float over the seasonal wash and under the
   particles. Each stays low contrast so foreground text keeps its
   contrast; the dark ones stay under the page's own surfaces. */
.seasonal-backdrop::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--weather-wash, none);
  transition: background-image 1.2s ease, opacity 1.2s ease;
}

.seasonal-backdrop[data-weather="clear"]::before {
  --weather-wash:
    radial-gradient(ellipse 60% 40% at 78% 8%, rgba(254, 240, 138, 0.12) 0%, transparent 62%);
}

.seasonal-backdrop[data-weather="clear-night"]::before {
  --weather-wash:
    radial-gradient(ellipse 70% 55% at 50% 0%, rgba(59, 73, 142, 0.16) 0%, transparent 70%);
}

.seasonal-backdrop[data-weather="partly"]::before {
  --weather-wash:
    radial-gradient(ellipse 70% 40% at 70% 12%, rgba(148, 163, 184, 0.10) 0%, transparent 60%),
    radial-gradient(ellipse 55% 35% at 18% 30%, rgba(203, 213, 225, 0.08) 0%, transparent 60%);
}

.seasonal-backdrop[data-weather="cloudy"]::before {
  --weather-wash:
    radial-gradient(ellipse 80% 45% at 40% 6%, rgba(100, 116, 139, 0.13) 0%, transparent 65%),
    radial-gradient(ellipse 60% 40% at 85% 40%, rgba(148, 163, 184, 0.09) 0%, transparent 62%);
}

.seasonal-backdrop[data-weather="drizzle"]::before,
.seasonal-backdrop[data-weather="rain"]::before {
  --weather-wash:
    linear-gradient(rgba(70, 96, 130, 0.11) 0%, rgba(80, 110, 145, 0.05) 45%, transparent 72%);
}

.seasonal-backdrop[data-weather="showers"]::before {
  --weather-wash:
    linear-gradient(rgba(70, 110, 150, 0.11) 0%, transparent 60%);
}

.seasonal-backdrop[data-weather="downpour"]::before {
  --weather-wash:
    linear-gradient(rgba(52, 70, 105, 0.15) 0%, rgba(60, 82, 118, 0.07) 50%, transparent 78%);
}

.seasonal-backdrop[data-weather="storm"]::before {
  --weather-wash:
    linear-gradient(rgba(38, 45, 72, 0.18) 0%, rgba(48, 52, 90, 0.10) 50%, transparent 82%);
}

.seasonal-backdrop[data-weather="sleet"]::before {
  --weather-wash:
    linear-gradient(rgba(94, 120, 152, 0.12) 0%, transparent 66%);
}

.seasonal-backdrop[data-weather="snow"]::before {
  --weather-wash:
    linear-gradient(rgba(203, 222, 244, 0.12) 0%, transparent 64%);
}

/* Fog reads as milk over the whole layer, which is what sells the
   reduced visibility. */
.seasonal-backdrop[data-weather="fog"]::before {
  --weather-wash:
    linear-gradient(rgba(203, 213, 225, 0.14) 0%, rgba(203, 213, 225, 0.10) 55%, rgba(203, 213, 225, 0.07) 100%);
}

/* Night pulls everything down a step, so a sunny backdrop never
   reads as bright at three in the morning. The pseudo-element is
   always present with a transparent wash by default, so crossing
   the day/night boundary transitions the wash rather than popping
   a new element in and out. */
.seasonal-backdrop::after {
  content: "";
  position: absolute;
  inset: 0;
  background: transparent;
  transition: background 1.6s ease;
}

.seasonal-backdrop[data-night="night"]::after {
  background: rgba(2, 6, 23, 0.20);
}

/* Sun and moon glow. Only the day and night builders ever insert
   them, so the selectors below need no show/hide logic. */
.sb-sun {
  --end-opacity: 1;
  position: absolute;
  top: 4%;
  right: 6%;
  width: 36vmin;
  height: 36vmin;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(253, 230, 138, 0.42) 0%,
    rgba(251, 191, 36, 0.18) 40%,
    transparent 68%);
  filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  animation: sb-sun-pulse var(--sb-dur, 11s) ease-in-out infinite,
    sb-fade-in 1.6s ease-out forwards;
}

@keyframes sb-sun-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.06); }
}

.sb-moon {
  --end-opacity: 1;
  position: absolute;
  top: 7%;
  left: 10%;
  width: 20vmin;
  height: 20vmin;
  border-radius: 50%;
  background: radial-gradient(circle at 42% 38%,
    rgba(241, 245, 249, 0.55) 0%,
    rgba(241, 245, 249, 0.38) 14%,
    rgba(203, 213, 225, 0.20) 30%,
    transparent 60%);
  filter: blur(1px);
  pointer-events: none;
  animation: sb-fade-in 1.6s ease-out forwards;
}

.sb-star {
  position: absolute;
  left: var(--sb-x, 50%);
  top: var(--sb-y, 20%);
  width: var(--sb-size, 2px);
  height: var(--sb-size, 2px);
  border-radius: 50%;
  background: #e2e8f0;
  box-shadow: 0 0 4px rgba(226, 232, 240, 0.8);
  opacity: var(--sb-opacity, 0.4);
  pointer-events: none;
  animation: sb-twinkle var(--tw-dur, 4s) ease-in-out infinite alternate;
  animation-delay: var(--sb-delay, 0s);
}

@keyframes sb-twinkle {
  from { opacity: calc(var(--sb-opacity, 0.4) * 0.35); }
  to { opacity: var(--sb-opacity, 0.4); }
}

/* Cloud blobs drift on a slow alternate walk. Storm clouds are
   darker by the scene attribute of their backdrop. */
.sb-cloud {
  --end-opacity: var(--cl-op, 0.2);
  position: absolute;
  top: var(--cl-y, 10%);
  left: var(--sb-x, 30%);
  width: var(--cl-w, 30vmin);
  height: calc(var(--cl-w, 30vmin) * 0.42);
  opacity: var(--cl-op, 0.2);
  pointer-events: none;
  background: radial-gradient(ellipse closest-side,
    rgba(203, 213, 225, 0.55) 0%,
    rgba(203, 213, 225, 0.30) 55%,
    transparent 100%);
  filter: blur(7px);
  border-radius: 999px;
  animation: sb-cloud-drift var(--cl-dur, 22s) ease-in-out infinite alternate,
    sb-fade-in 1.6s ease-out forwards;
  animation-delay: var(--cl-delay, 0s), 0s;
}

.seasonal-backdrop[data-weather="storm"] .sb-cloud,
.seasonal-backdrop[data-weather="downpour"] .sb-cloud {
  background: radial-gradient(ellipse closest-side,
    rgba(51, 60, 92, 0.65) 0%,
    rgba(51, 60, 92, 0.35) 55%,
    transparent 100%);
}

@keyframes sb-cloud-drift {
  from { transform: translate3d(calc(var(--sb-drift, 0%) * 1), 0, 0); }
  to { transform: translate3d(calc(var(--sb-drift, 0%) * -1), 0, 0); }
}

/* Fog bands: wide soft horizontal slabs sliding slowly back and
   forth, staggered via --fb-y so the layer reads as depth. */
.sb-fogband {
  --end-opacity: var(--fb-op, 0.6);
  position: absolute;
  top: var(--fb-y, 30%);
  left: -30%;
  width: var(--fb-w, 150%);
  height: 22vh;
  opacity: var(--fb-op, 0.6);
  pointer-events: none;
  border-radius: 50%;
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(226, 232, 240, 0.16) 30%,
    rgba(241, 245, 249, 0.20) 50%,
    rgba(226, 232, 240, 0.14) 70%,
    transparent 100%);
  filter: blur(9px);
  animation: sb-fog-drift var(--fb-dur, 32s) ease-in-out infinite alternate,
    sb-fade-in 1.6s ease-out forwards;
  animation-delay: var(--fb-delay, 0s), 0s;
}

@keyframes sb-fog-drift {
  from { transform: translate3d(var(--fb-from, -7%), 0, 0); }
  to { transform: translate3d(var(--fb-to, 7%), 0, 0); }
}

@keyframes sb-fade-in {
  from { opacity: 0; }
  to { opacity: var(--end-opacity, 1); }
}

/* Rain streaks. --sb-slant is set once on the backdrop from the
   observed wind speed, so every streak in a scene leans the same
   way. Wet scenes fall inside ~2.5s, which reads as weather rather
   than as confetti. */
.sb-drop {
  position: absolute;
  top: -14vh;
  left: var(--sb-x, 50%);
  width: 1px;
  height: var(--sb-drop-h, 18px);
  border-radius: 999px;
  opacity: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(191, 219, 254, 0) 0%,
    rgba(191, 219, 254, 0.55) 60%,
    rgba(147, 197, 253, 0.7) 100%);
  animation: sb-rain-fall var(--sb-dur, 2s) linear infinite;
  animation-delay: var(--sb-delay, 0s);
}

@keyframes sb-rain-fall {
  0% {
    transform: translate3d(0, 0, 0) rotate(var(--sb-slant, 8deg));
    opacity: 0;
  }
  8% { opacity: var(--sb-opacity, 0.35); }
  90% { opacity: var(--sb-opacity, 0.35); }
  100% {
    transform: translate3d(var(--sb-drift, 10vw), 120vh, 0) rotate(var(--sb-slant, 8deg));
    opacity: 0;
  }
}

/* Weather snowflakes and sleet share the season particle's slow
   fall, but keep their own class so a spring snowfall still reads
   as snow rather than as whatever colour the season implies. */
.sb-flake {
  position: absolute;
  top: -10vh;
  left: var(--sb-x, 50%);
  width: var(--sb-size, 3px);
  height: var(--sb-size, 3px);
  border-radius: 50%;
  background: #e2e8f0;
  box-shadow: 0 0 5px rgba(226, 232, 240, 0.5);
  opacity: 0;
  pointer-events: none;
  animation: sb-flake-fall var(--sb-dur, 12s) linear infinite;
  animation-delay: var(--sb-delay, 0s);
}

@keyframes sb-flake-fall {
  0% {
    transform: translate3d(0, 0, 0) rotate(0deg);
    opacity: 0;
  }
  10% { opacity: var(--sb-opacity, 0.4); }
  90% { opacity: var(--sb-opacity, 0.4); }
  100% {
    transform: translate3d(var(--sb-drift, 0px), 118vh, 0) rotate(var(--sb-spin, 180deg));
    opacity: 0;
  }
}

/* Lightning. The flash overlay paints a soft double pulse from a
   sky position the script randomises per strike; the animation
   itself is a fixed, quick shape. */
.sb-flash {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 55% 40% at var(--fl-x, 65%) var(--fl-y, 15%),
    rgba(255, 255, 255, 0.85) 0%,
    rgba(196, 181, 253, 0.30) 38%,
    transparent 68%);
}

.seasonal-backdrop.is-lightning .sb-flash {
  animation: sb-lightning 1.1s ease-out;
}

@keyframes sb-lightning {
  0% { opacity: 0; }
  5% { opacity: 0.85; }
  12% { opacity: 0.12; }
  20% { opacity: 0.6; }
  38% { opacity: 0.22; }
  100% { opacity: 0; }
}

/* --- on/off slider --- */

.seasonal-toggle {
  display: flex;
  justify-content: center;
  margin-top: var(--space-sm);
}

.seasonal-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
  font-size: 0.8rem;
  color: var(--color-text-secondary);
}

.seasonal-switch-label {
  user-select: none;
}

/* The native checkbox keeps the semantics and the keyboard
   behaviour; the track below is the visible slider. */
.seasonal-switch-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.seasonal-switch-track {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 22px;
  border-radius: 999px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  transition: background var(--transition-fast), border-color var(--transition-fast);
}

.seasonal-switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-text-muted);
  transition: transform var(--transition-fast), background var(--transition-fast);
}

.seasonal-switch-input:checked ~ .seasonal-switch-track {
  background: rgba(96, 165, 250, 0.28);
  border-color: rgba(96, 165, 250, 0.5);
}

.seasonal-switch-input:checked ~ .seasonal-switch-track .seasonal-switch-thumb {
  transform: translateX(18px);
  background: var(--accent-rain);
}

.seasonal-switch-input:focus-visible ~ .seasonal-switch-track {
  outline: 2px solid var(--accent-rain);
  outline-offset: 2px;
}

/* ============================================================
   Playback Stage
   ============================================================
   The installed app on a phone-sized screen (body gets the
   stage-on class from app.js while the forecast plays) shows the
   scene, the hour, the temperature and the UV as one screen. The
   page below the stage keeps its exact normal layout, so the
   website view is never altered by these rules. */

/* The stage reads as its own screen: solid backdrop, small radius. */
body.stage-on {
  overflow: hidden;
}

body.stage-on #app {
  padding: 0;
}

body.stage-on .main-content {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  gap: var(--space-xs);
  padding: var(--space-sm);
  background: rgba(11, 15, 26, 0.98);
  border-radius: 0;
}

/* Everything that is not part of the playing hour steps aside. */
body.stage-on .site-header,
body.stage-on .location-bar,
body.stage-on .install-advice,
body.stage-on .alert-section,
body.stage-on .details-section,
body.stage-on .insight-section,
body.stage-on .air-section,
body.stage-on .forecast-section,
body.stage-on .prefs-section,
body.stage-on .controls-section,
body.stage-on .site-footer,
body.stage-on .movie-header,
body.stage-on .movie-summary,
body.stage-on .movie-legends,
body.stage-on .weather-story {
  display: none;
}

body.stage-on .hero-section {
  margin-bottom: 0;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

body.stage-on .hero-inner {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  gap: var(--space-sm);
  align-items: stretch;
}

/* The scene, not the prose, owns the free vertical space. */
body.stage-on .ascii-art-area {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--space-md);
}

body.stage-on .ascii-art {
  font-size: clamp(0.5rem, min(3vw, 2.6vh), 0.9rem);
  line-height: 1.2;
}

body.stage-on .hero-info {
  flex: 0 0 auto;
  margin-top: 0;
}

body.stage-on .temperature-value {
  font-size: 2.4rem;
}

body.stage-on .movie-section {
  margin-top: 0;
  flex: 0 0 auto;
  padding: var(--space-sm) var(--space-md);
}

body.stage-on .movie-status {
  margin-top: var(--space-xs);
}

/* Short landscape phone: side by side, so the hour still owns the
   middle of the screen and nothing scrolls. */
@media (orientation: landscape) and (max-height: 500px) {
  body.stage-on .hero-inner {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    align-items: center;
    text-align: left;
    gap: var(--space-md);
  }

  body.stage-on .ascii-art-area {
    min-height: 70px;
    padding: var(--space-sm);
  }

  body.stage-on .ascii-art {
    font-size: clamp(0.4rem, min(2vw, 3.4vh), 0.8rem);
  }

  body.stage-on .temperature-value {
    font-size: 1.6rem;
  }

  body.stage-on .condition-display {
    font-size: 0.78rem;
  }

  /* The per hour UV readout also sits on the timeline below, so the
     chip stays compact here rather than being dropped. */
  body.stage-on .hero-uv {
    margin-top: 2px;
    font-size: 0.7rem;
  }

  /* The decorative condition glyphs are the only timeline row that
     carries no reading; dropping them keeps every measurement on
     screen without scrolling. */
  body.stage-on .movie-markers {
    display: none;
  }

  body.stage-on .movie-section {
    padding: var(--space-xs) var(--space-sm) var(--space-sm);
  }

  body.stage-on .movie-btn {
    padding: var(--space-xs) var(--space-sm);
  }

  body.stage-on .movie-clock {
    font-size: 1rem;
  }

  body.stage-on .movie-strip-label {
    font-size: 0.6rem;
  }

  body.stage-on .movie-status {
    font-size: 0.62rem;
    margin-top: var(--space-2xs, 2px);
  }
}

/* ============================================================
   Reduced Motion
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  body::before {
    transition: none;
  }

  /* The pulse is decorative; the status line says the same thing in
     words. */
  .location-btn[aria-busy="true"] .location-btn-icon {
    animation: none;
    opacity: 1;
  }

  /* The badge pulse is decorative; the word beside it already reports
     the playback state. */
  .movie-section.is-playing .movie-badge-icon {
    animation: none;
    opacity: 1;
  }

  /* The scene itself is drawn by JS, which stops the rAF loop when
     reduced motion is requested; this just halts the CSS flourishes. */
  .ascii-overlay,
  .ascii-veil {
    display: none;
  }

  .ascii-art-area[data-cyclone="tropical"] {
    animation: none;
  }

  /* The season atmosphere stays, but the particles hold still and
     stay scattered instead of drifting, so the backdrop is calm
     without losing its character. */
  .seasonal-backdrop[data-season] .seasonal-particle {
    animation: none;
    top: var(--sb-y, 40%);
    bottom: auto;
    opacity: var(--sb-opacity, 0.35);
    transform: rotate(var(--sb-spin, 0deg));
  }

  /* Weather layers calm down the same way. Rain becomes a static
     scatter of hanging streaks, flakes rest where they land, stars
     stop twinkling, clouds and fog sit still, and the tree line
     sun and moon stops breathing. The lightning flash is never
     scheduled at all under reduced motion; this rule additionally
     guarantees it can never paint, even if a timer slipped. */
  .seasonal-backdrop[data-motion="reduced"] .sb-drop,
  .seasonal-backdrop[data-motion="reduced"] .sb-flake {
    animation: none;
  }

  .seasonal-backdrop[data-motion="reduced"] .sb-drop {
    top: var(--sb-y, 40%);
    bottom: auto;
    opacity: calc(var(--sb-opacity, 0.35) * 0.6);
    transform: rotate(var(--sb-slant, 8deg));
  }

  .seasonal-backdrop[data-motion="reduced"] .sb-flake {
    top: var(--sb-y, 40%);
    bottom: auto;
    opacity: var(--sb-opacity, 0.4);
    transform: rotate(var(--sb-spin, 0deg));
  }

  /* A static hanging drop needs a top coordinate of its own: rain
     drops are placed by --sb-x only, so scatter them down the
     layer manually. */
  .seasonal-backdrop[data-motion="reduced"].is-lightning .sb-flash,
  .seasonal-backdrop[data-motion="reduced"] .sb-flash {
    animation: none;
    opacity: 0;
  }
}

/* ============================================================
   High Contrast Mode Support
   ============================================================ */
@media (forced-colors: active) {
  .detail-card,
  .forecast-card,
  .control-btn,
  .location-select {
    border: 1px solid ButtonText;
  }

  .ascii-art-area {
    border: 2px solid ButtonText;
  }
}
