/* ═══════════════════════════════════════════════════════════════════
   YeetCast — NIGHT MODE (vaporwave broadcast)                     v1

   One file, loaded into BOTH documents:
     · the parent TV shell page   (rules under body.yc-night #yc-wrapper / .tv-shell)
     · the ws4kp iframe           (rules under body.yc-night #container / .weather-display)
   The selectors are scoped to elements that only exist in one of the
   two, so a single stylesheet is unambiguous.

   Cascade order, deliberately:  tv-shell.css → yc-portrait.css → yc-night.css
   Night is always LAST, so its !important beats portrait's at equal
   specificity, and where portrait already uses !important the night
   rule adds .yc-portrait to outrank it.

   Nothing here paints unless body.yc-night is set. Removing the class
   restores the day site exactly — there are no shared edits. (The one
   deliberate exception is the TV's bias light and the room glow, which
   exist at opacity 0 in day so they can fade in with the transition.)

   The animated switch between the two looks lives at the bottom of this
   file, under THE TRANSITION.

   The reskin replaces the baked-in 640x480 background art with a CSS
   composition (sunset sky + retro sun + perspective grid), because the
   art is a fixed raster and this has to survive the portrait engine
   stretching the canvas to any height.

   GEOMETRY IS LOCKED TO DAY. Toggling night must not move a single
   pixel of layout, so this file changes paint only — never border
   width, padding, margin, size or position of a day element. Frames
   night draws (header band, inset panel, forecast cards, ticker edge)
   sit exactly where the day art draws them; the coordinates below were
   measured from images/backgrounds/*.png. Any new rule here must pass
   the flip check in scripts/verify-night.mjs (0 moved).
   ═══════════════════════════════════════════════════════════════════ */

body.yc-night {
  /* The day text shadow (3px drop + 1.5px outline) reused verbatim, so
     night text keeps the exact day ink and only ADDS its glow. */
  --n-ink: 3px 3px 0 #000, -1.5px -1.5px 0 #000, 0 -1.5px 0 #000,
           1.5px -1.5px 0 #000, 1.5px 0 0 #000, 1.5px 1.5px 0 #000,
           0 1.5px 0 #000, -1.5px 1.5px 0 #000, -1.5px 0 0 #000;
  --n-violet:  #7b2fff;
  --n-magenta: #ff2f9e;
  --n-pink:    #ff6ec7;
  --n-cyan:    #21f3ff;
  --n-amber:   #ffd166;
  --n-horizon: 71%;

  /* The light. yc-vapor-scene.js rewrites all of these on <body> once a
     minute from its vapor day (the clock, and the real sunrise); these
     defaults are the classic sunset, which is what shows if that script
     never runs. */
  --n-sky-0: #06000f;
  --n-sky-1: #14012e;
  --n-sky-2: #35056b;
  --n-sky-3: #7a0e8f;
  --n-sky-4: #d81e77;
  --n-sky-5: #ff5a3c;
  --n-sky-6: #ffd166;
  --n-ground-0: #1a0333;
  --n-ground-1: #0a0018;
  --n-sun-0: #ffe887;
  --n-sun-1: #ffd166;
  --n-sun-2: #ff8a3d;
  --n-sun-3: #ff2f9e;
  --n-sun-glow: rgba(255,90,60,.35);
  --n-sun-alt: 110px;
  --n-slats: 1;
  --n-stars: .22;
  --n-moon: .15;
  --n-cloud: rgb(255,120,160);
  --n-clouds: .75;
  --n-grid-v: rgba(33,243,255,.85);
  --n-grid-h: rgba(255,47,158,.75);
}

/* ═══════════════════ IFRAME: the broadcast picture ═══════════════════ */

/* ── The sky. Lives on #container so it sits behind every screen and
      never restarts when the rotation advances. Seven stops of the live
      palette down to the horizon, then the ground under the grid. ── */
body.yc-night #container,
body.yc-night.wide #container {
  background: linear-gradient(180deg,
      var(--n-sky-0)  0%,
      var(--n-sky-1) 22%,
      var(--n-sky-2) 42%,
      var(--n-sky-3) 55%,
      var(--n-sky-4) 63%,
      var(--n-sky-5) 68%,
      var(--n-sky-6) 70.5%,
      var(--n-ground-0) var(--n-horizon),
      var(--n-ground-1) 100%) var(--n-sky-0) !important;
  overflow: hidden !important;
}

/* ── The scene (injected by the template as the first child of
      #container): sky layer clipped at the horizon (stars, moon, sun),
      the pixel city standing on the horizon, and the floor. One stacking
      context at z 0, under the screens at z 1. Off in day. ── */
.yc-vapor-scene {
  display: none;
}
body.yc-night .yc-vapor-scene {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
body.yc-night .yc-vapor-sky {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: var(--n-horizon);
  overflow: hidden;
}

/* Stars: three cheap repeating layers; the palette fades them by day. */
body.yc-night .yc-vapor-stars {
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(1.5px 1.5px at 34px 22px,  rgba(255,255,255,.9), transparent),
    radial-gradient(1.5px 1.5px at 158px 71px, rgba(120,240,255,.75), transparent),
    radial-gradient(1.5px 1.5px at 96px 130px, rgba(255,160,230,.65), transparent),
    radial-gradient(2px 2px at 61px 188px,     rgba(255,255,255,.8), transparent);
  background-size: 210px 190px, 190px 230px, 250px 260px, 330px 290px;
  -webkit-mask-image: linear-gradient(180deg, #000 45%, transparent 92%);
  mask-image: linear-gradient(180deg, #000 45%, transparent 92%);
  opacity: var(--n-stars);
}

/* The moon: the Yeet Tonight mascot (images/yc-yeet-tonight.webp) with
   his slogan, in chunky pixels (the art is 123px wide) so he sits in the
   pixel-art sky with the city instead of floating over it. yc-vapor-scene.js fades him in with the stars (--n-moon);
   the real moon he replaced is in git, commit 0ec1556b. Both layouts
   place him with the same three properties (left, bottom from the
   horizon, width) so a switch between them glides instead of jumping. */
body.yc-night .yc-vapor-moon {
  position: absolute;
  /* landscape: tucked in the left margin, clear of the big temperature */
  left: 1.5%;
  bottom: 136px;
  width: 120px;
  height: auto;
  filter: saturate(.88) drop-shadow(0 0 8px rgba(160,190,255,.32));
  opacity: calc(var(--n-moon) * .92);
  transition: left .45s ease, bottom .45s ease, width .45s ease;
}
/* portrait: in the open band between the wind line and the city's name,
   the slogan dead centre (it sits at 71.5% of the art's width), so the
   crescent hangs a little left of centre */
body.yc-night.yc-portrait .yc-vapor-moon {
  left: calc(50% - 184px * .715 + 38px);
  bottom: 214px;
  width: 184px;
  image-rendering: pixelated;
}

/* The sun: a banded retro disc whose height follows the real sun. Its
   centre sits --n-sun-alt above the horizon; the slats (a second layer
   confined to the lower half by per-layer background-size) fade out as
   the day gets bright. Clipped by the sky layer, so it sets behind the
   city and never shows under the floor. */
body.yc-night .yc-vapor-sun {
  position: absolute;
  left: 50%;
  bottom: calc(var(--n-sun-alt) - 150px);
  width: 300px;
  height: 300px;
  margin-left: -150px;
  border-radius: 50%;
  background-image:
    repeating-linear-gradient(180deg, transparent 0 11px, rgba(11,0,24,.9) 11px 19px),
    linear-gradient(180deg, var(--n-sun-0) 0%, var(--n-sun-1) 32%, var(--n-sun-2) 58%, var(--n-sun-3) 100%);
  background-size: 100% 52%, 100% 100%;
  background-position: 0 100%, 0 0;
  background-repeat: no-repeat, no-repeat;
  box-shadow: 0 0 90px 20px var(--n-sun-glow);
}
body.yc-night .yc-vapor-sun::after {
  /* bright day washes the slats out; one layer, one opacity */
  content: '';
  position: absolute;
  inset: 48% 0 0 0;
  border-radius: 0 0 150px 150px;
  background: linear-gradient(180deg, var(--n-sun-2), var(--n-sun-3));
  opacity: calc(1 - var(--n-slats));
}

/* Clouds: pixel clouds drifting across the sky, in front of the sun,
   tinted by the hour (white at noon, pink at the sunset, gone at night).
   Each is a mask over a flat colour so the palette can recolour it, and
   the drift is a transform, so it costs the compositor, not a repaint. */
body.yc-night .yc-vapor-clouds {
  position: absolute;
  inset: 0;
  opacity: var(--n-clouds);
}
body.yc-night .yc-vapor-clouds i {
  position: absolute;
  left: -170px;
  width: 104px;
  height: 32px;
  background: var(--n-cloud);
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2026%208'%20shape-rendering='crispEdges'%3E%3Cpath%20d='M11%200h5v1h-5zM7%201h13v1h-13zM5%202h17v1h-17zM3%203h21v1h-21zM1%204h24v1h-24zM0%205h26v1h-26zM0%206h26v1h-26zM2%207h22v1h-22z'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
  mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2026%208'%20shape-rendering='crispEdges'%3E%3Cpath%20d='M11%200h5v1h-5zM7%201h13v1h-13zM5%202h17v1h-17zM3%203h21v1h-21zM1%204h24v1h-24zM0%205h26v1h-26zM0%206h26v1h-26zM2%207h22v1h-22z'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
  animation: yc-cloud-drift 170s linear infinite;
}
body.yc-night .yc-vapor-clouds i:nth-child(2),
body.yc-night .yc-vapor-clouds i:nth-child(4) {
  width: 72px;
  height: 24px;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2018%206'%20shape-rendering='crispEdges'%3E%3Cpath%20d='M7%200h5v1h-5zM4%201h11v1h-11zM2%202h14v1h-14zM1%203h16v1h-16zM0%204h18v1h-18zM2%205h14v1h-14z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2018%206'%20shape-rendering='crispEdges'%3E%3Cpath%20d='M7%200h5v1h-5zM4%201h11v1h-11zM2%202h14v1h-14zM1%203h16v1h-16zM0%204h18v1h-18zM2%205h14v1h-14z'/%3E%3C/svg%3E");
}
body.yc-night .yc-vapor-clouds i:nth-child(1) { top: 17%; animation-delay: -40s; }
body.yc-night .yc-vapor-clouds i:nth-child(2) { top: 31%; animation-duration: 230s; animation-delay: -150s; opacity: .8; }
body.yc-night .yc-vapor-clouds i:nth-child(3) { top: 45%; width: 156px; height: 48px; animation-duration: 130s; animation-delay: -95s; }
body.yc-night .yc-vapor-clouds i:nth-child(4) { top: 59%; animation-duration: 260s; animation-delay: -30s; opacity: .65; }
@keyframes yc-cloud-drift {
  to { transform: translateX(1200px); }
}

/* A shooting star, flown now and then at night by the scene script. */
body.yc-night .yc-vapor-meteor {
  position: absolute;
  left: var(--m-x, 60%);
  top: var(--m-y, 12%);
  width: 90px;
  height: 2px;
  background: linear-gradient(90deg, rgba(190,250,255,0), rgba(220,252,255,.95));
  opacity: 0;
}
body.yc-night .yc-vapor-meteor.is-falling {
  animation: yc-meteor .95s ease-out;
}
@keyframes yc-meteor {
  0%   { opacity: 0; transform: rotate(152deg) translateX(0) scaleX(.3); }
  18%  { opacity: var(--n-stars); }
  100% { opacity: 0; transform: rotate(152deg) translateX(170px) scaleX(1); }
}

/* The city: a 2x pixel-art canvas standing on the horizon line. */
body.yc-night .yc-vapor-city {
  position: absolute;
  left: 0;
  width: 100%;
  height: 200px;
  top: calc(var(--n-horizon) - 200px);
  image-rendering: pixelated;
}
/* Phones: 3 CSS px per city pixel (yc-vapor-scene.js, PX_PORTRAIT), so
   the same skyline, YEET sign and all, stands half again as tall. */
body.yc-night.yc-portrait .yc-vapor-city {
  height: 300px;
  top: calc(var(--n-horizon) - 300px);
}

/* ── The floor: a real perspective grid, not a fake one. rotateX on a
      flat grid is what makes the lines converge at the horizon. It is
      over-wide on purpose so the outer lines do not end mid-screen.
      It is a real element rather than a pseudo, so the endless scroll is
      a compositor transform instead of a background repaint every frame:
      the sheet inside is one cell taller than the floor and slides down
      exactly one cell, then loops. ── */
body.yc-night .yc-vapor-floor {
  position: absolute;
  left: -60%;
  right: -60%;
  top: var(--n-horizon);
  bottom: -30%;
  transform: perspective(190px) rotateX(58deg);
  transform-origin: 50% 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 16%, #000 100%);
  mask-image: linear-gradient(180deg, transparent 0, #000 16%, #000 100%);
}
/* Phones show far more floor than the 4:3 screen, which made the fan of
   lines read steep. A lower eye (shorter perspective, more tilt) lays
   the grid flatter, the way the 4:3 screen looks. */
body.yc-night.yc-portrait .yc-vapor-floor {
  transform: perspective(150px) rotateX(66deg);
}
body.yc-night .yc-vapor-floor__grid {
  position: absolute;
  top: -46px;
  left: 0;
  right: 0;
  bottom: 0;
  background-image:
    repeating-linear-gradient(90deg, var(--n-grid-v) 0 2px, transparent 2px 46px),
    repeating-linear-gradient(0deg,  var(--n-grid-h) 0 2px, transparent 2px 46px);
  animation: yc-night-grid 2.6s linear infinite;
}
@keyframes yc-night-grid {
  to { transform: translateY(46px); }
}

/* Screens ride above the backdrop instead of hiding it. The ID selectors
   in ws4kp (#radar-html.weather-display etc.) each set their own art, so
   these need !important to clear all seven at once. */
body.yc-night .weather-display,
body.yc-night #loading {
  background-image: none !important;
  background-color: transparent !important;
  position: relative;
  z-index: 1;
}

/* Portrait does not use the 640x480 PNGs at all — it redraws that frame as
   CSS gradients on .weather-display.show, because a raster cannot stretch
   to a phone-height canvas. So night has to clear those too, at a higher
   specificity than the portrait engine's own !important rules. The almanac
   has its own portrait art (an orange band + gray moon panel), hence the
   second selector.
   The one piece kept is the bottom band behind the parent's controls
   dock: day runs its navy band from the ticker to the bottom edge, so
   night carries the ticker's dark band down the same distance. */
body.yc-night.yc-portrait .weather-display.show,
body.yc-night.yc-portrait #almanac-html.weather-display.show {
  background:
    linear-gradient(180deg, rgba(45,10,82,.92) 0%, rgba(11,0,24,.94) 100%)
    left bottom / 640px var(--yc-dock-height, 0px) no-repeat !important;
}

/* ── Header band: replaces the orange diagonal baked into the art, over
      the same rows the art uses. The strip above it stays sky, as day
      keeps it dark: the band starts at y=30 in landscape (y=23 on the
      radar, whose header is taller), and at y=28 in portrait, where
      yc-portrait.css redraws it. Direct children only — the almanac has
      its own .header cells inside the panel. ── */
body.yc-night .weather-display > .header {
  --n-band-top: 30px;
  background:
    linear-gradient(103deg,
      rgba(255,47,158,.92)  0%,
      rgba(123,47,255,.88) 44%,
      rgba(33,243,255,.62) 100%)
    0 var(--n-band-top) / 100% calc(100% - var(--n-band-top)) no-repeat;
  box-shadow:
    0 2px 0 rgba(33,243,255,.55),
    0 12px 26px rgba(0,0,0,.5);
}
body.yc-night:not(.yc-portrait) #radar-html > .header { --n-band-top: 23px; }
body.yc-night.yc-portrait .weather-display > .header { --n-band-top: 28px; }

/* Chromatic split on the big titles — the one place the effect reads as
   broadcast rather than as a mistake. */
body.yc-night .weather-display > .header .title {
  color: #fff8b0;
  text-shadow:
    -1.5px 0 rgba(255,47,158,.9),
     1.5px 0 rgba(33,243,255,.85),
     var(--n-ink),
     0 0 16px rgba(255,110,199,.6);
}
body.yc-night .weather-display > .header .date-time {
  color: #d6f7ff;
  text-shadow: var(--n-ink), 0 0 12px rgba(33,243,255,.75);
}
/* ── The vapor logo: a neon sign ──
   Day keeps the YeetCast box; vapor swaps it for "YEET Weathercast" in neon
   script, straight on the header band, no plate or frame. The markup
   (.yc-neon, beside every corner logo) comes from page-yeetcast.php. The
   box stays where it is, invisible, so nothing moves; the sign sits out of
   flow on top of it. The two words are artwork, not text:
   images/logos/yc-neon-*.svg hold the letter shapes and their glow, drawn
   to overlay exactly on a 160x100 frame with 24px of room for the glow on
   every side; .yc-neon is that frame, and its scale() is the logo's size.
   The ?v= on the two files must change whenever they are rebuilt, here and
   in page-yeetcast.php (the preload and the warm-up).
   Two files, so each word can flicker on its own. They are backgrounds of
   elements that are display:none in day, so they only ever download in
   vapor mode.
   Lettering: Tritopani (Riyadh Rahman, Ergibi Studio); how the artwork is
   made is in docs/YEETCAST-NIGHT-MODE-TODO.md. */
.yc-neon { display: none; }
body.yc-night .weather-display > .header .logo img { opacity: 0; }
/* At 88% the lettering is about 119px wide (the day box is 104), centred on
   the day box (x 97) and on the band (y 60), which its 14deg tilt
   overhangs by about 5px at the top right and the bottom left. */
body.yc-night .yc-neon {
  display: block;
  position: absolute;
  left: -33px;
  top: -20px;
  width: 160px;
  height: 100px;
  z-index: 11;
  pointer-events: none;
  transform: scale(.88);
}
.yc-neon__wc,
.yc-neon__yeet {
  position: absolute;
  left: -24px;
  top: -24px;
  width: 208px;
  height: 148px;
  background: 0 0 / 100% 100% no-repeat;
}
/* "Weathercast": white-hot core, hot-pink tube and bloom, a cyan fringe on
   the left edge (the titles' chromatic split). "Yeet": the cyan tube in the
   pocket right of the W. */
.yc-neon__wc { background-image: url(images/logos/yc-neon-weathercast.svg?v=20260924-yeet2); }
.yc-neon__yeet { background-image: url(images/logos/yc-neon-yeet.svg?v=20260924-yeet2); }
/* Now and then the small tube buzzes, like a real sign
   (yc-vapor-scene.js sets the class for a second). */
body.yc-night.yc-neon-buzz .yc-neon__yeet {
  animation: yc-neon-buzz .9s both;
}
@keyframes yc-neon-buzz {
  0%   { opacity: 1;   animation-timing-function: steps(1, end); }
  12%  { opacity: .3;  animation-timing-function: steps(1, end); }
  20%  { opacity: 1;   animation-timing-function: steps(1, end); }
  34%  { opacity: .45; animation-timing-function: steps(1, end); }
  42%, 100% { opacity: 1; }
}

/* ── The inset panel glass, one look for both orientations.
      Translucent on purpose: the sun and the grid are the whole point,
      and a solid panel would hide both. The type stays readable on its
      own black shadow. ── */
body.yc-night {
  --n-glass-edge: rgba(33,243,255,.55);
  --n-glass-fill: linear-gradient(180deg, rgba(28,4,58,.5) 0%, rgba(58,8,96,.3) 100%);
  --n-glass-glow:
    0 0 0 1px rgba(255,47,158,.35),
    0 0 26px rgba(33,243,255,.25),
    inset 0 0 30px rgba(123,47,255,.4);
}

/* Landscape: the panel lives in the background art (backgrounds/1.png,
   52,90 → 584,399), not on .main.has-box, which is narrower (64 → 576)
   and has no border. So the glass is drawn on the screen itself at the
   art's coordinates, and .main.has-box is left exactly as day has it. */
body.yc-night:not(.yc-portrait) .weather-display:has(> .main.has-box)::before {
  content: '';
  position: absolute;
  left: 52px;
  top: 90px;
  width: 532px;
  height: 309px;
  box-sizing: border-box;
  border: 2px solid var(--n-glass-edge);
  background: var(--n-glass-fill);
  box-shadow: var(--n-glass-glow);
  pointer-events: none;
}

/* Portrait: the panel IS .main.has-box's 16px border-image frame. Keep
   that 16px border (the content sits inside it) and only swap the art
   for glass; the fill runs under the border and the outline draws the
   edge line on the frame's outer 2px. */
body.yc-night.yc-portrait .weather-display.show > .main.has-box {
  border-image: none !important;
  background: var(--n-glass-fill) border-box !important;
  box-shadow: var(--n-glass-glow) !important;
  outline: 2px solid var(--n-glass-edge);
  outline-offset: -2px;
}

/* ── Extended-forecast day cards, same glass. Drawn on a pseudo so the
      .day box itself is never touched. isolation keeps the card under
      its own text without positioning anything. ── */
body.yc-night .main.extended-forecast .day {
  isolation: isolate;
}
body.yc-night .main.extended-forecast .day::before {
  content: '';
  position: absolute;
  z-index: -1;
  box-sizing: border-box;
  border: 2px solid rgba(255,47,158,.5);
  background:
    linear-gradient(180deg, rgba(37,6,74,.62) 0%, rgba(80,12,120,.48) 100%);
  box-shadow:
    0 0 18px rgba(255,47,158,.25),
    inset 0 0 20px rgba(33,243,255,.18);
  pointer-events: none;
}
/* Landscape: the three cards are baked into backgrounds/2.png at x 38,
   232 and 426, y 100, 174x297. .day is not positioned there, so these
   coordinates are against .main, which starts at y=90. */
body.yc-night:not(.yc-portrait) .main.extended-forecast .day::before {
  top: 10px;
  width: 174px;
  height: 297px;
}
body.yc-night:not(.yc-portrait) .main.extended-forecast .day:nth-child(1)::before { left: 38px; }
body.yc-night:not(.yc-portrait) .main.extended-forecast .day:nth-child(2)::before { left: 232px; }
body.yc-night:not(.yc-portrait) .main.extended-forecast .day:nth-child(3)::before { left: 426px; }
/* Portrait: the card IS the .day's 10px border-image frame. Keep the
   10px border, drop the art, and lay the glass over the border box. */
body.yc-night.yc-portrait .main.extended-forecast .day {
  position: relative;
  border-image: none !important;
  background: none !important;
}
body.yc-night.yc-portrait .main.extended-forecast .day::before {
  inset: -10px;
}

/* ── The banded row backgrounds on hourly and travel. ── */
body.yc-night .main.hourly .hourly-lines,
body.yc-night .main.travel .travel-lines {
  background: repeating-linear-gradient(0deg,
    rgba(11,0,32,.55)   0px,
    rgba(64,10,110,.5)  136px,
    rgba(64,10,110,.5)  202px,
    rgba(11,0,32,.55)   338px) !important;
}

/* ── Ticker. The divider above it is shadows, not a border: day draws a
      2px dark + 2px light rule in the 4px just above the ticker box, and
      a border would grow the box instead. Same 4px, same order. ── */
body.yc-night #container > .scroll {
  background:
    linear-gradient(180deg, rgba(11,0,24,.9) 0%, rgba(45,10,82,.92) 100%) !important;
  box-shadow:
    0 -2px 0 rgba(33,243,255,.5),
    0 -4px 0 rgba(11,0,24,.9),
    0 -8px 20px rgba(0,0,0,.45);
}
body.yc-night #container > .scroll .fixed,
body.yc-night #container > .scroll .scroll-header {
  color: #e8fbff;
  text-shadow: var(--n-ink), 0 0 10px rgba(33,243,255,.5);
}

/* ── Maps. Duotone, not hue-rotate: the maps are blue ocean over gray
      land, and rotating those hues lands on olive green. Flattening to
      grayscale first and re-tinting from there gives one violet map where
      land and water separate by lightness alone, and the baked-in airport
      labels stay readable because contrast is pushed back up.
      The doppler frames are deliberately NOT filtered, so precipitation
      still matches the PRECIP scale printed in the header. ── */
body.yc-night #radar-html .map-tiles img,
body.yc-night #radar-html .overlay-tiles img,
body.yc-night .main.regional-forecast .map {
  filter: grayscale(1) sepia(1) hue-rotate(228deg) saturate(2.3) brightness(.55) contrast(1.3);
}

/* Weather icons keep their shape but pick up the room's glow. */
body.yc-night .main img.icon,
body.yc-night .icon img,
body.yc-night .main.regional-forecast .location .icon img {
  filter: saturate(1.25) drop-shadow(0 0 6px rgba(255,110,199,.55));
}

/* ── Body copy across every screen. ── */
body.yc-night .main,
body.yc-night .main .temp,
body.yc-night .main .value,
body.yc-night .main .label {
  text-shadow: var(--n-ink), 0 0 10px rgba(123,47,255,.45);
}
body.yc-night .messages .message {
  color: #b8fcff;
  text-shadow:
    -1.2px 0 rgba(255,47,158,.8),
     1.2px 0 rgba(33,243,255,.75),
     var(--n-ink);
}

/* ── Scanlines pick up a magenta cast instead of flat black. ── */
body.yc-night .scanlines:after {
  background: repeating-linear-gradient(to bottom,
    transparent 0,
    transparent var(--scanline-thickness, 1px),
    rgba(60,0,70,.42) var(--scanline-thickness, 1px),
    rgba(60,0,70,.42) calc(var(--scanline-thickness, 1px) * 2)) !important;
}
body.yc-night .scanlines:before {
  background: rgba(120,255,255,.35) !important;
  box-shadow: 0 0 12px rgba(33,243,255,.7);
}

/* ── Loading / progress screens. ── */
body.yc-night #loading .title {
  color: var(--n-cyan);
  /* 4px 4px is the shadow day's #loading hands down; only the glow is new. */
  text-shadow: 4px 4px #000, 0 0 20px rgba(33,243,255,.8);
}
body.yc-night .weather-display .progress .container .item .name {
  color: #e8fbff;
}

/* ═══════════════════ PARENT: the room ═══════════════════ */

/* The TV itself is never repainted: the splash TV and the commercial-break
   TV look exactly the same in both modes. What vapor changes is the room
   around it — a faint neon bias light on the wall behind the cabinet.
   z-index -2 puts it under the cabinet and its feet (-1) but above the
   .lab background, since .lab is the fixed-position stacking context. */
.tv-shell::after {
  content: '';
  position: absolute;
  inset: -22% -14%;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(closest-side at 20% 62%, rgba(255,47,158,.34), transparent),
    radial-gradient(closest-side at 80% 38%, rgba(33,243,255,.22), transparent),
    radial-gradient(closest-side at 50% 54%, rgba(123,47,255,.3), transparent);
  opacity: 0;
  transition: opacity 1.6s ease;
}
body.yc-night .tv-shell::after {
  opacity: 1;
}

/* ...and that neon catches the tube: a faint magenta/cyan reflection
   across the glass, on the one bezel layer tv-shell.css leaves unused.
   Screen-blended at low strength so it tints, never covers, the picture. */
body.yc-night .tv-screen-bezel::after {
  display: block;
  z-index: 5;
  background:
    linear-gradient(118deg,
      transparent 12%, rgba(255,47,158,.085) 26%, transparent 40%,
      transparent 62%, rgba(33,243,255,.07) 78%, transparent 92%);
  mix-blend-mode: screen;
}

/* The letterbox around the broadcast picks up the same violet, fading in
   with the transition rather than snapping. */
body.yc-state-app::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(120% 90% at 50% 105%, rgba(123,47,255,.35), transparent 60%);
  opacity: 0;
  transition: opacity 2.4s ease;
}
body.yc-night.yc-state-app::before {
  opacity: 1;
}

/* Parent overlay controls read cyan at night. (The vapor sun's own
   sunset colours are in the template: they follow aria-pressed.) */
body.yc-night .yc-audio-btn,
body.yc-night .yc-fs-btn { color: #d6f7ff; }
@media (hover: hover) {
  body.yc-night .yc-audio-btn:hover,
  body.yc-night .yc-fs-btn:hover { color: var(--n-cyan); }
}
body.yc-night .yc-audio-track-line { color: var(--n-pink); }

/* Motion is decoration here; honour the OS setting. */
@media (prefers-reduced-motion: reduce) {
  body.yc-night .yc-vapor-floor__grid,
  body.yc-night .yc-vapor-clouds i { animation: none; }
  body.yc-night .yc-vapor-meteor { display: none; }
  body.yc-night.yc-neon-buzz .yc-neon__yeet { animation: none; }
}

/* ═══════════════════ THE TRANSITION ═══════════════════
   The template plays day <-> vapor as a View Transition inside the
   iframe, and tags both documents while it runs:
     iframe  html.yc-fx.yc-fx--dusk.yc-fx--on|off  (+ .yc-fx--pre)
     parent  #yc-wrapper, same classes, plus .yc-fx--run from the
             transition's first frame, so the light on the glass starts
             in step with the picture.
   ::view-transition-old(root) is a still photograph of the look being
   left; ::view-transition-new(root) is the live page in the look being
   entered, still animating underneath. The photograph is lifted ABOVE
   the live page, so the transition works by taking the old look away.
   The live page's own entrance moves (sun, floor, band) are ordinary
   animations on the real elements, keyed off html.yc-fx--*.
   Durations here must match YC_DUSK_MS / YC_SUNRISE_PRE_MS /
   YC_SUNRISE_MS in page-yeetcast.php.
   ═══════════════════════════════════════════════════════════════════ */

html.yc-fx::view-transition-old(root),
html.yc-fx::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
html.yc-fx::view-transition-old(root) {
  z-index: 1;
}

/* How high the sun starts before it sets (and where it climbs back to).
   Scales with the canvas: portrait pushes --yc-total-height in. */
html.yc-fx body.yc-night {
  --n-sun-lift: calc(var(--yc-total-height, 480px) * -0.42);
}

/* The parent's light layer sits over the picture, under the scanlines. */
#yc-fx-layer {
  z-index: 10014;
  opacity: 0;
  visibility: hidden;
}
#yc-wrapper.yc-fx #yc-fx-layer {
  visibility: visible;
}

/* The toggle's sun dips (sets) and comes back up as it works, on the
   transition's first second and a half. Motion and fade only: a clip
   here cut the sun's glow into a visible square. */
#yc-night-btn.yc-fx-spin .yc-i {
  animation: yc-icon-sun-set 1.5s cubic-bezier(.5,0,.15,1) both;
}
@keyframes yc-icon-sun-set {
  from { transform: translateY(0) scaleY(1); opacity: 1; }
  45%  { transform: translateY(34%) scaleY(.8); opacity: .3; }
  to   { transform: translateY(0) scaleY(1); opacity: 1; }
}

/* Controls drift to their new tint with the picture instead of snapping. */
#yc-wrapper.yc-fx .yc-audio-btn,
#yc-wrapper.yc-fx .yc-fs-btn {
  transition: color 1.8s ease .8s;
}

/* ── DUSK (and Sunrise back) ─────────────────────────────────────────
   Into vapor (4.2s): the day picture warms to golden hour, then night
   falls from the top of the sky down, a soft warm edge riding the line
   between them. Behind it the sun sinks into its seat on the horizon,
   its slats cutting in as it lands; the header band sweeps across; the
   grid unrolls from the horizon toward you; the titles settle their
   chromatic split. Back to day (0.9s + 1.7s): the grid rolls back into
   the horizon and the sun climbs out, then morning light rises from the
   bottom and washes the day look back in. */

/* The mask is 240% tall: transparent top 41.7%, a soft eased edge, solid
   below 58.3%. Sliding it from 0 100% to 0 0% walks the edge down the
   whole picture; the numbers make both ends exactly solid / exactly
   clear. */
html.yc-fx--dusk::view-transition-old(root) {
  -webkit-mask-size: 100% 240%;
  mask-size: 100% 240%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
html.yc-fx--dusk.yc-fx--on::view-transition-old(root) {
  -webkit-mask-image: linear-gradient(180deg,
    transparent 41.7%, rgba(0,0,0,.05) 44%, rgba(0,0,0,.22) 47%, rgba(0,0,0,.5) 50%,
    rgba(0,0,0,.78) 53%, rgba(0,0,0,.95) 56%, #000 58.3%);
  mask-image: linear-gradient(180deg,
    transparent 41.7%, rgba(0,0,0,.05) 44%, rgba(0,0,0,.22) 47%, rgba(0,0,0,.5) 50%,
    rgba(0,0,0,.78) 53%, rgba(0,0,0,.95) 56%, #000 58.3%);
  animation:
    yc-dusk-grade 4.2s linear both,
    yc-dusk-fall 4.2s linear both;
}
@keyframes yc-dusk-fall {
  0%, 12% {
    -webkit-mask-position: 0 100%;
    mask-position: 0 100%;
    animation-timing-function: cubic-bezier(.55,.06,.35,.98);
  }
  80%, 100% {
    -webkit-mask-position: 0 0%;
    mask-position: 0 0%;
  }
}
@keyframes yc-dusk-grade {
  0%   { filter: none; }
  22%  { filter: sepia(.42) saturate(1.6) hue-rotate(-12deg) brightness(1.1); }
  62%  { filter: sepia(.28) saturate(1.3) hue-rotate(-32deg) brightness(.86); }
  100% { filter: sepia(.2) saturate(1.2) hue-rotate(-45deg) brightness(.7); }
}
/* The night picture starts with the lights down and brings them up. */
html.yc-fx--dusk.yc-fx--on::view-transition-new(root) {
  animation: yc-dusk-lights 4.2s ease-out both;
}
@keyframes yc-dusk-lights {
  0%, 15%   { filter: brightness(.6) saturate(.75); }
  90%, 100% { filter: none; }
}

/* The sun sets. */
html.yc-fx--dusk.yc-fx--on body.yc-night .yc-vapor-sun {
  animation: yc-dusk-sun 4.2s linear both;
}
@keyframes yc-dusk-sun {
  0%, 10% {
    transform: translateY(var(--n-sun-lift));
    background-size: 100% 0%, 100% 100%;
    filter: brightness(1.3) saturate(.9);
    box-shadow: 0 0 150px 50px rgba(255,140,60,.5);
    animation-timing-function: cubic-bezier(.3,.1,.25,1);
  }
  70% {
    transform: translateY(0);
    background-size: 100% 0%, 100% 100%;
    filter: none;
    box-shadow: 0 0 110px 28px rgba(255,100,60,.42);
    animation-timing-function: cubic-bezier(.4,0,.2,1);
  }
  88%, 100% {
    transform: translateY(0);
    background-size: 100% 52%, 100% 100%;
    box-shadow: 0 0 90px 20px var(--n-sun-glow);
  }
}

/* The grid unrolls from the horizon toward the viewer, lines hot at first. */
html.yc-fx--dusk.yc-fx--on body.yc-night .yc-vapor-floor {
  animation: yc-dusk-floor 4.2s linear both;
}
@keyframes yc-dusk-floor {
  0%, 50% {
    transform: perspective(190px) rotateX(58deg) scaleY(0);
    filter: brightness(2.2);
    animation-timing-function: cubic-bezier(.2,.7,.2,1);
  }
  84%  { transform: perspective(190px) rotateX(58deg) scaleY(1); filter: brightness(1.3); }
  100% { transform: perspective(190px) rotateX(58deg) scaleY(1); filter: none; }
}

/* The header band sweeps in once dusk has reached it. */
html.yc-fx--dusk.yc-fx--on body.yc-night .weather-display > .header {
  animation: yc-dusk-band 4.2s linear both;
}
@keyframes yc-dusk-band {
  0%, 20% {
    background-size: 0% calc(100% - var(--n-band-top));
    box-shadow: 0 2px 0 rgba(33,243,255,0), 0 12px 26px rgba(0,0,0,0);
    animation-timing-function: cubic-bezier(.6,0,.25,1);
  }
  44%, 100% {
    background-size: 100% calc(100% - var(--n-band-top));
    box-shadow: 0 2px 0 rgba(33,243,255,.55), 0 12px 26px rgba(0,0,0,.5);
  }
}

/* Titles come in with a wide chromatic split and pull it tight. */
html.yc-fx--dusk.yc-fx--on body.yc-night .weather-display > .header .title {
  animation: yc-fx-chroma-in 4.2s linear both;
}
@keyframes yc-fx-chroma-in {
  0%, 26% {
    text-shadow: -7px 0 rgba(255,47,158,.9), 7px 0 rgba(33,243,255,.85), var(--n-ink), 0 0 26px rgba(255,110,199,.9);
    animation-timing-function: cubic-bezier(.2,.8,.2,1);
  }
  56%, 100% {
    text-shadow: -1.5px 0 rgba(255,47,158,.9), 1.5px 0 rgba(33,243,255,.85), var(--n-ink), 0 0 16px rgba(255,110,199,.6);
  }
}

/* The logo: the day box goes out with the daylight (it is in the
   photograph dusk takes away). Once night has reached the header, the
   unlit tubes appear, then they strike, flickering the way neon does,
   "Yeet" a beat after "Weathercast". */
html.yc-fx--dusk.yc-fx--on body.yc-night .yc-neon {
  animation: yc-neon-in 4.2s linear both;
}
@keyframes yc-neon-in {
  0%, 30%   { opacity: 0; }
  48%, 100% { opacity: 1; }
}
html.yc-fx--dusk.yc-fx--on body.yc-night .yc-neon__wc {
  animation: yc-neon-strike 4.2s both;
}
html.yc-fx--dusk.yc-fx--on body.yc-night .yc-neon__yeet {
  animation: yc-neon-strike 4.2s .45s both;
}
@keyframes yc-neon-strike {
  0%, 50% { opacity: .1;  animation-timing-function: steps(1, end); }
  52%     { opacity: 1;   animation-timing-function: steps(1, end); }
  54%     { opacity: .2;  animation-timing-function: steps(1, end); }
  57%     { opacity: .85; animation-timing-function: steps(1, end); }
  59%     { opacity: .3;  animation-timing-function: steps(1, end); }
  62%, 100% { opacity: 1; }
}

/* The glass panels and cards materialise just after dusk passes them. */
html.yc-fx--dusk.yc-fx--on body.yc-night:not(.yc-portrait) .weather-display:has(> .main.has-box)::before,
html.yc-fx--dusk.yc-fx--on body.yc-night .main.extended-forecast .day::before {
  animation: yc-dusk-glass 4.2s linear both;
}
@keyframes yc-dusk-glass {
  0%, 36%   { opacity: 0; }
  62%, 100% { opacity: 1; }
}

/* Parent: the warm terminator glow that rides the dusk edge. The band is
   40% of the picture tall and travels -100% -> 250% of its own height on
   the same curve and clock as the mask, which keeps its middle on the
   edge the whole way down. */
#yc-wrapper.yc-fx--dusk #yc-fx-layer {
  top: 0;
  bottom: auto;
  height: 40%;
  background: linear-gradient(180deg,
    transparent 0%, rgba(255,150,70,.10) 30%, rgba(255,120,80,.30) 48%,
    rgba(255,80,160,.22) 58%, transparent 100%);
  mix-blend-mode: screen;
  transform: translateY(-100%);
}
#yc-wrapper.yc-fx--dusk.yc-fx--on.yc-fx--run #yc-fx-layer {
  animation:
    yc-dusk-glow-move 4.2s linear both,
    yc-dusk-glow-fade 4.2s linear both;
}
@keyframes yc-dusk-glow-move {
  0%, 12% { transform: translateY(-100%); animation-timing-function: cubic-bezier(.55,.06,.35,.98); }
  80%, 100% { transform: translateY(250%); }
}
@keyframes yc-dusk-glow-fade {
  0%, 12% { opacity: 0; }
  24%, 68% { opacity: 1; }
  82%, 100% { opacity: 0; }
}

/* Back to day, part one (pre-roll on the live vapor picture): the grid
   rolls back into the horizon and the sun climbs out, bleaching as it
   goes. Held at the end so the snapshot is taken of an empty sky. */
html.yc-fx--dusk.yc-fx--off.yc-fx--pre body.yc-night .yc-vapor-floor {
  animation: yc-rise-floor .8s cubic-bezier(.55,0,.8,.5) forwards;
}
@keyframes yc-rise-floor {
  to { transform: perspective(190px) rotateX(58deg) scaleY(0); filter: brightness(2); }
}
html.yc-fx--dusk.yc-fx--off.yc-fx--pre body.yc-night .yc-vapor-sun {
  animation: yc-rise-sun .9s cubic-bezier(.5,0,.75,.4) forwards;
}
@keyframes yc-rise-sun {
  to {
    transform: translateY(var(--n-sun-lift));
    background-size: 100% 0%, 100% 100%;
    filter: brightness(1.6) saturate(.7);
    box-shadow: 0 0 170px 70px rgba(255,200,130,.55);
    opacity: .75;
  }
}
html.yc-fx--dusk.yc-fx--off.yc-fx--pre body.yc-night .weather-display > .header .title {
  animation: yc-fx-chroma-out .9s cubic-bezier(.6,0,.9,.5) forwards;
}
/* The sign cuts out (a stutter, then dark), so the photograph the
   sunrise wipes away already has an empty corner. */
html.yc-fx--dusk.yc-fx--off.yc-fx--pre body.yc-night .yc-neon__wc,
html.yc-fx--dusk.yc-fx--off.yc-fx--pre body.yc-night .yc-neon__yeet {
  animation: yc-neon-cut .6s both;
}
@keyframes yc-neon-cut {
  0%   { opacity: 1;   animation-timing-function: steps(1, end); }
  22%  { opacity: .25; animation-timing-function: steps(1, end); }
  34%  { opacity: 1;   animation-timing-function: steps(1, end); }
  52%  { opacity: .15; animation-timing-function: steps(1, end); }
  62%  { opacity: .7; }
  100% { opacity: 0; }
}
html.yc-fx--dusk.yc-fx--off.yc-fx--pre body.yc-night .yc-neon {
  animation: yc-neon-out .9s ease-in both;
}
@keyframes yc-neon-out {
  0%, 55% { opacity: 1; }
  100%    { opacity: 0; }
}
@keyframes yc-fx-chroma-out {
  to { text-shadow: -6px 0 rgba(255,47,158,.9), 6px 0 rgba(33,243,255,.85), var(--n-ink), 0 0 24px rgba(255,110,199,.9); }
}

/* Part two: morning light rises from the bottom (the mask runs the other
   way) and the day look comes in bright and warm, then settles. */
html.yc-fx--dusk.yc-fx--off::view-transition-old(root) {
  -webkit-mask-image: linear-gradient(180deg,
    #000 41.7%, rgba(0,0,0,.95) 44%, rgba(0,0,0,.78) 47%, rgba(0,0,0,.5) 50%,
    rgba(0,0,0,.22) 53%, rgba(0,0,0,.05) 56%, transparent 58.3%);
  mask-image: linear-gradient(180deg,
    #000 41.7%, rgba(0,0,0,.95) 44%, rgba(0,0,0,.78) 47%, rgba(0,0,0,.5) 50%,
    rgba(0,0,0,.22) 53%, rgba(0,0,0,.05) 56%, transparent 58.3%);
  animation: yc-sunrise-lift 1.7s linear both;
}
@keyframes yc-sunrise-lift {
  0% {
    -webkit-mask-position: 0 0%;
    mask-position: 0 0%;
    filter: brightness(1.05);
    animation-timing-function: cubic-bezier(.5,.05,.3,1);
  }
  100% {
    -webkit-mask-position: 0 100%;
    mask-position: 0 100%;
    filter: brightness(1.35) saturate(.8);
  }
}
html.yc-fx--dusk.yc-fx--off::view-transition-new(root) {
  animation: yc-sunrise-light 1.7s ease-out both;
}
/* The header is the last thing the morning light reaches; the day box
   fades back in as it gets there. */
html.yc-fx--dusk.yc-fx--off body:not(.yc-night) .weather-display > .header .logo img {
  animation: yc-day-logo-in 1.7s ease-out both;
}
@keyframes yc-day-logo-in {
  0%, 50% { opacity: 0; }
  100%    { opacity: 1; }
}
@keyframes yc-sunrise-light {
  0%, 30%   { filter: brightness(1.3) sepia(.25) saturate(1.2); }
  100%      { filter: none; }
}
#yc-wrapper.yc-fx--dusk.yc-fx--off #yc-fx-layer {
  background: linear-gradient(180deg,
    transparent 0%, rgba(255,235,190,.16) 38%, rgba(255,210,150,.34) 52%,
    rgba(255,170,120,.12) 66%, transparent 100%);
}
#yc-wrapper.yc-fx--dusk.yc-fx--off.yc-fx--run #yc-fx-layer {
  animation:
    yc-sunrise-glow-move 1.7s linear both,
    yc-sunrise-glow-fade 1.7s linear both;
}
@keyframes yc-sunrise-glow-move {
  0% { transform: translateY(250%); animation-timing-function: cubic-bezier(.5,.05,.3,1); }
  100% { transform: translateY(-100%); }
}
@keyframes yc-sunrise-glow-fade {
  0% { opacity: 0; }
  12%, 75% { opacity: 1; }
  100% { opacity: 0; }
}
