/* =====================================================================
   Live Weather Theme — Monir Hasan
   A fixed "sky" layer behind the whole site that reflects the visitor's
   real local time of day and the current weather conditions.

   Readability first, but not at the sky's expense. The page text is
   #eef2fb, which needs a background luminance under about 0.16 to clear
   WCAG AA. The old scrim pushed it down to roughly 0.02 — five times darker
   than it had to be — which is why every theme looked like the same black
   page. The scrim now aims just inside the limit instead of far past it, so
   the sky is genuinely visible and the contrast still passes.

   What makes a painted sky read as a real one, in order of how much it
   matters: a horizon, light coming from where the sun actually is, and a
   vertical colour structure that goes zenith -> mid -> warm haze rather
   than a flat two-tone fade. All three are built below.

   State is driven by two attributes set on <html> by weather.js:
     data-sky     = dawn | morning | day | afternoon | sunset | dusk | night
     data-weather = clear | cloudy | overcast | fog | drizzle | rain | snow | thunder
   ===================================================================== */

.sky {
    position: fixed;
    inset: 0;
    z-index: -3;
    overflow: hidden;
    pointer-events: none;
    contain: strict;
}

/* ---------- Stacking order inside the sky ----------
   The scrim is a dark sheet that guarantees text contrast, so it has to sit
   above the weather. Lightning is the one exception: it must
   flash ABOVE the scrim, otherwise the scrim swallows it and a thunderstorm
   looks like an ordinary grey sky. That was the bug. The flash peaks briefly
   and blends rather than covers, so text stays readable through a strike. */
.sky-gradient { z-index: 0; }
.sky-stars    { z-index: 1; }
.sky-sun,
.sky-moon     { z-index: 2; }
.sky-clouds   { z-index: 3; }
/* Birds fly in front of the clouds, not behind them. Behind, a bird
   disappears every time it crosses one, which is the opposite of what you see
   looking up. */
.sky-birds    { z-index: 4; }
.sky-rain,
.sky-snow,
.sky-splash,
.sky-fog      { z-index: 4; }
.sky-scrim    { z-index: 5; }
.sky-flash    { z-index: 6; }
.sky-bolt     { z-index: 7; }

/* ---------- Base gradient: time of day ----------
   Each phase is three stacked background layers on ONE element, not three
   elements. Background layers cost the compositor almost nothing; extra
   animated divs cost style and layout on every frame, and this page already
   carries ~170 particles.

     layer 1 (top)  sun glow      light spilling from where the sun actually is
     layer 2        horizon haze  the bright band that makes a sky feel deep
     layer 3        base ramp     zenith -> mid -> horizon -> below-horizon

   The five-stop base ramp is the part that reads as real. A sky is never a
   two-tone fade: it is darkest and most saturated straight up, and turns
   pale and warm as it approaches the horizon, because you are looking
   through more air. */
.sky-gradient {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, #070a14 0%, #0b1020 60%, #070a14 100%);
    transition: background 2.5s ease;
}

[data-sky="dawn"] .sky-gradient {
    background:
        radial-gradient(ellipse 70% 45% at 84% 58%, rgba(255, 150, 120, 0.30), transparent 68%),
        linear-gradient(180deg, transparent 50%, rgba(255, 170, 140, 0.16) 76%, rgba(255, 190, 160, 0.10) 88%, transparent 100%),
        linear-gradient(180deg, #0d1730 0%, #1e2145 28%, #3a2b4e 54%, #6b4050 78%, #241a2a 100%);
}
[data-sky="morning"] .sky-gradient {
    background:
        radial-gradient(ellipse 60% 40% at 84% 34%, rgba(255, 225, 180, 0.24), transparent 66%),
        linear-gradient(180deg, transparent 52%, rgba(190, 215, 235, 0.16) 80%, transparent 100%),
        linear-gradient(180deg, #0d2444 0%, #17406a 30%, #2a628c 58%, #5c8caa 82%, #16202f 100%);
}
[data-sky="day"] .sky-gradient {
    background:
        radial-gradient(ellipse 55% 38% at 84% 13%, rgba(255, 244, 214, 0.28), transparent 64%),
        linear-gradient(180deg, transparent 50%, rgba(196, 224, 240, 0.18) 80%, transparent 100%),
        linear-gradient(180deg, #0a2b52 0%, #12457a 30%, #1e6499 58%, #5793b6 82%, #16283c 100%);
}
[data-sky="afternoon"] .sky-gradient {
    background:
        radial-gradient(ellipse 58% 40% at 84% 26%, rgba(255, 232, 190, 0.24), transparent 66%),
        linear-gradient(180deg, transparent 50%, rgba(224, 216, 210, 0.16) 80%, transparent 100%),
        linear-gradient(180deg, #0e2740 0%, #1a3f5e 30%, #2d5c7c 58%, #6a8ea3 82%, #151d2b 100%);
}
[data-sky="sunset"] .sky-gradient {
    background:
        radial-gradient(ellipse 75% 45% at 84% 62%, rgba(255, 150, 90, 0.38), transparent 66%),
        linear-gradient(180deg, transparent 44%, rgba(255, 120, 80, 0.22) 72%, rgba(255, 170, 110, 0.16) 86%, transparent 100%),
        linear-gradient(180deg, #101a38 0%, #2e2350 26%, #6a3556 52%, #a85440 78%, #2a1820 100%);
}
[data-sky="dusk"] .sky-gradient {
    background:
        radial-gradient(ellipse 80% 35% at 70% 88%, rgba(190, 110, 120, 0.20), transparent 70%),
        linear-gradient(180deg, transparent 58%, rgba(140, 140, 200, 0.12) 84%, transparent 100%),
        linear-gradient(180deg, #060a1c 0%, #101433 30%, #1e1b45 58%, #3a2a4e 84%, #0d0b1a 100%);
}
[data-sky="night"] .sky-gradient {
    background:
        radial-gradient(ellipse 90% 30% at 50% 96%, rgba(60, 90, 140, 0.22), transparent 72%),
        linear-gradient(180deg, transparent 62%, rgba(70, 100, 150, 0.10) 88%, transparent 100%),
        linear-gradient(180deg, #03050d 0%, #060a1a 32%, #0a1026 62%, #101a33 88%, #060a14 100%);
}

/* Heavier cloud cover mutes and greys the whole sky. */
[data-weather="overcast"] .sky-gradient,
[data-weather="rain"] .sky-gradient,
[data-weather="thunder"] .sky-gradient { filter: saturate(0.55) brightness(0.72); }
[data-weather="fog"] .sky-gradient { filter: saturate(0.35) brightness(0.85) blur(2px); }

/* ---------- Scrim: guarantees text contrast over any sky ----------
   Measured, not guessed. The page text is #eef2fb, which needs the surface
   behind it under about 0.16 relative luminance for WCAG AA. The previous
   scrim reached 0.53 alpha at the very top of the screen and 0.96 at the
   bottom, which put the composite near 0.02 — five times darker than
   required, and the reason every theme looked like the same black page.

   These values land the brightest phase (Bright Day) at roughly 5:1 behind
   the hero text while letting the actual sky through at the top. */
.sky-scrim {
    position: absolute;
    inset: 0;
    background:
        linear-gradient(180deg,
            rgba(7, 10, 20, 0.30) 0%,
            rgba(7, 10, 20, 0.46) 34%,
            rgba(7, 10, 20, 0.68) 68%,
            rgba(7, 10, 20, 0.82) 100%),
        radial-gradient(ellipse 120% 70% at 50% 0%, rgba(7, 10, 20, 0) 0%, rgba(7, 10, 20, 0.25) 70%, rgba(7, 10, 20, 0.45) 100%);
}

/* ---------- Sun ----------
   The sun moves with transform, not `top`. Animating `top` forces the browser
   to re-lay-out and re-paint the sun on every frame of the 2.5s transition,
   which Lighthouse reports as a non-composited animation and which also
   registered as a small layout shift. transform runs on the compositor: no
   layout, no paint, no shift.

   --sun-y is a plain number, multiplied by --sky-h to get a length. --sky-h is
   1vh on the real page; the admin preview tiles are 116px tall, so they set
   --sky-h: 1.16px and the same numbers land in the same relative spots. */
.sky-sun {
    position: absolute;
    top: 0;
    right: 16%;
    width: 120px;
    height: 120px;
    border-radius: 50%;
    opacity: 0;
    background: radial-gradient(circle at 50% 50%, rgba(255, 236, 190, 0.95), rgba(255, 190, 110, 0.5) 45%, transparent 70%);
    filter: blur(2px);
    transform: translate3d(0, calc(var(--sun-y, 16) * var(--sky-h, 1vh)), 0);
    transition: opacity 2.5s ease, transform 2.5s ease, background 2.5s ease;
}
[data-sky="dawn"]      .sky-sun { opacity: 0.5; --sun-y: 58; background: radial-gradient(circle, rgba(255, 190, 150, 0.85), rgba(255, 130, 110, 0.4) 45%, transparent 70%); }
[data-sky="morning"]   .sky-sun { opacity: 0.65; --sun-y: 34; }
[data-sky="day"]       .sky-sun { opacity: 0.75; --sun-y: 13; }
[data-sky="afternoon"] .sky-sun { opacity: 0.6; --sun-y: 26; }
[data-sky="sunset"]    .sky-sun { opacity: 0.55; --sun-y: 62; background: radial-gradient(circle, rgba(255, 170, 120, 0.9), rgba(230, 90, 80, 0.45) 45%, transparent 70%); }

/* The sun is hidden when the sky is covered. */
[data-weather="overcast"] .sky-sun,
[data-weather="rain"] .sky-sun,
[data-weather="snow"] .sky-sun,
[data-weather="thunder"] .sky-sun,
[data-weather="fog"] .sky-sun { opacity: 0.12; }

/* ---------- Moon (night, only when the sky is reasonably clear) ---------- */
.sky-moon {
    position: absolute;
    top: 14%;
    right: 18%;
    width: 84px;
    height: 84px;
    border-radius: 50%;
    opacity: 0;
    background: radial-gradient(circle at 36% 34%, #fdfbf2 0%, #e8ecf7 55%, #c3cbdd 100%);
    box-shadow:
        0 0 32px rgba(210, 226, 255, 0.5),
        0 0 90px rgba(150, 190, 255, 0.28);
    /* The crescent shade below is a disc offset UP and RIGHT past the moon's
       own circle. Without this it hung outside the moon and drew a near-black
       blob on the open sky — very obvious at dusk and dawn, when the sky behind
       it is orange rather than dark. overflow clips it back inside the circle.
       The glow above is a box-shadow, and overflow never clips box-shadow, so
       the halo is untouched. */
    overflow: hidden;
    transition: opacity 2.5s ease;
}
/* Surface shading. This used to be a solid #060a16 disc — a hard black bite out
   of the moon. Now it is a soft grey terminator plus two faint craters, so the
   moon reads as a moon at every sky phase and there is no black anywhere. */
.sky-moon::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background:
        radial-gradient(circle at 68% 30%, rgba(140, 152, 178, 0.28) 0%, rgba(140, 152, 178, 0) 22%),
        radial-gradient(circle at 40% 66%, rgba(140, 152, 178, 0.22) 0%, rgba(140, 152, 178, 0) 18%),
        radial-gradient(circle at 22% 26%, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 45%),
        radial-gradient(circle at 88% 80%, rgba(96, 110, 140, 0.4) 0%, rgba(96, 110, 140, 0) 60%);
}
[data-sky="dusk"]  .sky-moon,
[data-sky="night"] .sky-moon { opacity: 0.9; }
[data-sky="dawn"]  .sky-moon { opacity: 0.35; }

[data-weather="overcast"] .sky-moon,
[data-weather="rain"] .sky-moon,
[data-weather="thunder"] .sky-moon,
[data-weather="fog"] .sky-moon { opacity: 0.1; }

/* ---------- Stars ---------- */
.sky-stars {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 2.5s ease;
}

/* The Milky Way. A real dark sky is not evenly speckled — it has a bright,
   dusty band running across it, and that band is most of what makes a night
   sky read as photographic rather than as dots on black.

   It is a pseudo-element, not a div: no extra node, and because nothing
   animates it, the browser paints it once and never touches it again. It
   inherits the parent's opacity, so it appears and hides with the stars. */
.sky-stars::before {
    content: '';
    position: absolute;
    top: -18%;
    left: -30%;
    width: 160%;
    height: 90%;
    transform: rotate(-24deg);
    background:
        radial-gradient(ellipse 42% 46% at 34% 52%, rgba(198, 214, 255, 0.16), transparent 70%),
        radial-gradient(ellipse 34% 40% at 68% 46%, rgba(226, 210, 255, 0.13), transparent 72%),
        linear-gradient(180deg,
            transparent 34%,
            rgba(186, 202, 246, 0.05) 44%,
            rgba(214, 224, 255, 0.11) 50%,
            rgba(186, 202, 246, 0.05) 57%,
            transparent 68%);
    /* One blur on one element that never moves is cheap. One blur per particle
       is not — which is why the drops and flakes no longer have one. */
    filter: blur(14px);
}
[data-sky="dusk"]  .sky-stars { opacity: 0.5; }
[data-sky="night"] .sky-stars { opacity: 1; }
[data-sky="dawn"]  .sky-stars { opacity: 0.3; }

/* Clouds hide the stars — "stars only when the sky is clear". */
[data-weather="overcast"] .sky-stars,
[data-weather="rain"] .sky-stars,
[data-weather="thunder"] .sky-stars,
[data-weather="snow"] .sky-stars,
[data-weather="fog"] .sky-stars { opacity: 0.06 !important; }
[data-weather="cloudy"] .sky-stars { opacity: 0.45; }

/* Real stars are not white. They run from cool blue-white through yellow to
   faint orange, and that spread of colour is the second thing (after the
   Milky Way) that separates a night sky from a field of identical dots.
   --tint is set per star in weather.js. */
.star {
    position: absolute;
    border-radius: 50%;
    background: var(--tint, #fff);
    animation: twinkle var(--dur, 4s) ease-in-out var(--delay, 0s) infinite;
}

@keyframes twinkle {
    0%, 100% { opacity: 0.15; transform: scale(0.8); }
    50%      { opacity: 1;    transform: scale(1.15); }
}

/* A slow shooting star on clear nights. */
.shooting-star {
    position: absolute;
    width: 130px;
    height: 1.5px;
    background: linear-gradient(90deg, rgba(255, 255, 255, 0), #fff, rgba(255, 255, 255, 0));
    opacity: 0;
    animation: shoot 9s ease-in var(--delay, 4s) infinite;
}

@keyframes shoot {
    0%       { opacity: 0; transform: translate(0, 0) rotate(18deg) scaleX(0.2); }
    3%       { opacity: 1; }
    12%      { opacity: 0; transform: translate(340px, 150px) rotate(18deg) scaleX(1); }
    100%     { opacity: 0; transform: translate(340px, 150px) rotate(18deg) scaleX(1); }
}

/* ---------- Clouds ---------- */
.sky-clouds {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 2.5s ease;
}
[data-weather="cloudy"]   .sky-clouds { opacity: 0.5; }
[data-weather="overcast"] .sky-clouds { opacity: 0.85; }
[data-weather="rain"]     .sky-clouds { opacity: 0.9; }
[data-weather="drizzle"]  .sky-clouds { opacity: 0.7; }
[data-weather="snow"]     .sky-clouds { opacity: 0.8; }
[data-weather="thunder"]  .sky-clouds { opacity: 0.95; }
[data-weather="fog"]      .sky-clouds { opacity: 0.6; }

/* ---------- Birds (fair weather, daylight only) ----------
   Two or three distant birds crossing the sky. They are what tells you the
   weather is GOOD — an empty blue sky is pleasant but static, and nothing on
   the page moves on a clear afternoon. Birds are also the cheapest possible
   way to add that: three elements for the whole site, each drawn entirely
   with borders, each animated only with transform and opacity. No images, no
   extra request, nothing for the main thread to do after the first frame.

   They appear ONLY when it is genuinely fair — clear or partly cloudy — and
   only in daylight. Real birds are not out in a downpour, and a bird you
   cannot see against a night sky is just wasted work. */
.sky-birds {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 2.5s ease;
}
[data-weather="clear"]  .sky-birds { opacity: 1; }
[data-weather="cloudy"] .sky-birds { opacity: 0.85; }
/* These come AFTER the two rules above on purpose. All four have identical
   specificity, so source order is what decides — and the night/dawn/dusk
   rules have to be able to overrule "the weather is clear". */
[data-sky="dawn"]  .sky-birds,
[data-sky="dusk"]  .sky-birds,
[data-sky="night"] .sky-birds { opacity: 0; }

.bird {
    position: absolute;
    top: var(--y, 24%);
    left: 0;
    width: var(--size, 22px);
    height: calc(var(--size, 22px) * 0.4);
    /* Starts off the left edge and is carried across by the animation, so the
       first thing a visitor sees is a bird arriving, not one already parked. */
    animation: bird-cross var(--dur, 44s) linear var(--delay, 0s) infinite;
}

/* Each wing is one curved line: a box with only its top border drawn, bent
   into an arc by the border-radius. Two of them, tilted apart, make the
   shallow "m" that a bird at distance actually reads as. Pseudo-elements
   rather than child <span>s so a bird stays a single DOM node. */
.bird::before,
.bird::after {
    content: '';
    position: absolute;
    top: 0;
    width: 52%;
    height: 100%;
    border-top: 2px solid rgba(28, 38, 58, 0.62);
    border-radius: 60% 60% 0 0;
}
.bird::before {
    left: 0;
    /* The pivot is the bird's body — the shoulder — so the wing hinges the way
       a real one does instead of sliding sideways. */
    transform-origin: 100% 0;
    animation: flap-left var(--flap, 0.42s) ease-in-out infinite;
}
.bird::after {
    right: 0;
    transform-origin: 0 0;
    animation: flap-right var(--flap, 0.42s) ease-in-out infinite;
}

/* Silhouettes darken as the light goes: at midday a bird is a grey speck, at
   sunset it is a black cut-out against an orange sky. */
[data-sky="sunset"]    .bird::before,
[data-sky="sunset"]    .bird::after,
[data-sky="afternoon"] .bird::before,
[data-sky="afternoon"] .bird::after { border-top-color: rgba(18, 22, 36, 0.78); }

/* Across the screen and gently up, fading in and out at the two edges so no
   bird ever pops into existence in the middle of the sky. The long gap at the
   end is deliberate: for most of the cycle the sky is empty, and a bird
   appearing every now and then is what makes it feel unplanned. */
@keyframes bird-cross {
    0%    { opacity: 0; transform: translate3d(-12vw, 0, 0); }
    4%    { opacity: 1; }
    30%   { transform: translate3d(30vw, -4vh, 0); }
    56%   { opacity: 1; }
    62%   { opacity: 0; transform: translate3d(112vw, -9vh, 0); }
    100%  { opacity: 0; transform: translate3d(112vw, -9vh, 0); }
}

/* Wings beat out of phase with each other by a hair — perfectly mirrored
   wings look mechanical. */
@keyframes flap-left {
    0%, 100% { transform: rotate(-16deg); }
    50%      { transform: rotate(26deg); }
}
@keyframes flap-right {
    0%, 100% { transform: rotate(16deg); }
    46%      { transform: rotate(-26deg); }
}

/* A single blurred ellipse reads as a smudge. Four overlapping lobes of
   different sizes, with a flatter and darker base, reads as a cloud: puffy
   on top, heavy and flat underneath, the way real cumulus sits. */
.cloud {
    position: absolute;
    border-radius: 999px;
    background:
        radial-gradient(ellipse 40% 76% at 26% 52%, rgba(198, 212, 236, 0.46), transparent 72%),
        radial-gradient(ellipse 34% 96% at 50% 38%, rgba(208, 221, 243, 0.50), transparent 74%),
        radial-gradient(ellipse 42% 68% at 74% 54%, rgba(190, 205, 230, 0.42), transparent 72%),
        radial-gradient(ellipse 78% 42% at 50% 74%, rgba(150, 168, 200, 0.30), transparent 78%);
    filter: blur(13px);
    animation: drift var(--dur, 90s) linear var(--delay, 0s) infinite;
}
/* Night clouds are darker so they read as silhouettes, not glowing blobs. */
[data-sky="night"] .cloud,
[data-sky="dusk"] .cloud {
    background:
        radial-gradient(ellipse 40% 76% at 26% 52%, rgba(96, 112, 145, 0.42), transparent 72%),
        radial-gradient(ellipse 34% 96% at 50% 38%, rgba(104, 120, 154, 0.46), transparent 74%),
        radial-gradient(ellipse 42% 68% at 74% 54%, rgba(92, 106, 138, 0.40), transparent 72%),
        radial-gradient(ellipse 78% 42% at 50% 74%, rgba(50, 62, 88, 0.30), transparent 78%);
}
/* Rain-bearing cloud: darker, denser, less blurred at the base. */
[data-weather="rain"] .cloud,
[data-weather="overcast"] .cloud { filter: blur(12px) brightness(0.66); }
[data-weather="thunder"] .cloud   { filter: blur(11px) brightness(0.5) contrast(1.15); }

@keyframes drift {
    from { transform: translateX(-30vw); }
    to   { transform: translateX(130vw); }
}

/* ---------- Rain ----------
   Rain never falls straight down and it is never all the same distance away.
   Three things make it read as real:
     1. One shared wind angle. Every drop leans and travels the same way, so
        the eye reads "wind", not "random streaks".
     2. Depth. Far drops are thin, short, pale and slow; near drops are thick,
        long, bright and fast. JS assigns each drop a layer.
     3. It lands. Splash rings at the bottom finish the illusion.

   The container is wider than the screen because slanted drops have to enter
   from off the left edge, or the top-left corner stays permanently dry. */
.sky-rain {
    position: absolute;
    inset: -12% -22% 0 -22%;
    opacity: 0;
    transition: opacity 1.2s ease;
}
[data-weather="rain"]    .sky-rain { opacity: 1; }
[data-weather="drizzle"] .sky-rain { opacity: 0.55; }
[data-weather="thunder"] .sky-rain { opacity: 1; }

/* --gust is a per-condition multiplier on top of --wind (the live wind speed,
   set on <html> by weather.js). Two separate variables on purpose: if the
   condition rule also wrote --wind it would sit closer to the raindrops in the
   cascade and quietly override the real measurement. A storm gusts harder than
   steady rain, and drizzle barely moves. */
[data-weather="drizzle"] .sky-rain { --gust: 0.72; }
[data-weather="thunder"] .sky-rain { --gust: 1.32; }

/* No filter: blur() here, deliberately. A blur forces the browser to give that
   one element its own render surface, and there are up to 130 drops — that
   alone was a measurable slice of the main-thread style-and-layout time. The
   softness of a distant drop is carried by the gradient instead: far drops are
   sub-pixel thin and low-opacity, which the antialiaser already renders soft.
   Same look, no extra surfaces. */
.raindrop {
    position: absolute;
    top: 0;
    width: var(--w, 1.5px);
    height: var(--len, 78px);
    border-radius: 999px;
    /* Faint tail, bright head — a drop smeared by its own speed. */
    background: linear-gradient(180deg,
        rgba(174, 208, 255, 0) 0%,
        rgba(190, 220, 255, 0.32) 45%,
        rgba(228, 241, 255, 0.92) 100%);
    animation: fall var(--dur, 0.8s) linear var(--delay, 0s) infinite;
}
[data-weather="drizzle"] .raindrop { height: calc(var(--len, 78px) * 0.45); opacity: 0.7; }
/* Drizzle was the same 130 drops as a downpour, only shorter and fainter — so
   it still read as heavy rain seen through a dirty window. Hiding every third
   drop actually thins it out. display:none also stops those drops animating,
   so drizzle is now cheaper to render than rain, not merely dimmer. */
[data-weather="drizzle"] .raindrop:nth-child(3n+1) { display: none; }
/* A downpour: heavier and brighter. Done with a brighter gradient rather than
   filter: brightness(), for the same render-surface reason. */
[data-weather="thunder"] .raindrop {
    background: linear-gradient(180deg,
        rgba(198, 226, 255, 0) 0%,
        rgba(214, 235, 255, 0.4) 45%,
        rgba(255, 255, 255, 1) 100%);
}

/* --tilt is negative because a positive (clockwise) rotation would lean the
   streak against its own direction of travel. --drift is in vh, not vw, so
   the slant angle stays the same on a phone and on a widescreen monitor.

   Both are now multiplied by --wind (the real measured wind speed, set on
   <html> once per weather refresh) and --gust (a per-condition nudge). The
   lean and the sideways travel are multiplied by the SAME pair, which is the
   whole point: if a drop leaned one way and travelled another it would look
   like it was sliding sideways rather than being blown.

   Both fall back to 1, so the rain still works exactly as before if the
   weather API is unreachable. Changing a variable re-evaluates these two
   calc()s — it does not restart the animation or touch any element. */
@keyframes fall {
    from {
        transform:
            translate3d(0, -18vh, 0)
            rotate(calc(var(--tilt, -8deg) * var(--wind, 1) * var(--gust, 1)));
    }
    to {
        transform:
            translate3d(calc(var(--drift, 20vh) * var(--wind, 1) * var(--gust, 1)), 118vh, 0)
            rotate(calc(var(--tilt, -8deg) * var(--wind, 1) * var(--gust, 1)));
    }
}

/* Splash rings where the rain meets the bottom of the screen. */
.sky-splash {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 14vh;
    opacity: 0;
    transition: opacity 1.2s ease;
}
[data-weather="rain"]    .sky-splash { opacity: 0.85; }
[data-weather="thunder"] .sky-splash { opacity: 1; }
[data-weather="drizzle"] .sky-splash { opacity: 0.4; }

.splash {
    position: absolute;
    bottom: var(--y, 3vh);
    width: var(--size, 26px);
    height: calc(var(--size, 26px) * 0.32);
    border-radius: 50%;
    border: 1px solid rgba(196, 224, 255, 0.55);
    border-top-color: transparent;
    opacity: 0;
    animation: splash-ring var(--dur, 1.4s) ease-out var(--delay, 0s) infinite;
}

@keyframes splash-ring {
    0%   { opacity: 0;    transform: scale(0.15); }
    10%  { opacity: 0.7; }
    60%  { opacity: 0.18; }
    100% { opacity: 0;    transform: scale(1.3); }
}

/* ---------- Snow ---------- */
.sky-snow {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 1.2s ease;
}
[data-weather="snow"] .sky-snow { opacity: 1; }

/* The soft halo used to be a box-shadow plus a blur filter — two things that
   each cost the compositor real work, on every flake. A radial gradient that
   fades to transparent gives the same soft-edged flake as part of the
   element's own paint, for free. */
.snowflake {
    position: absolute;
    top: -6%;
    background: radial-gradient(circle at 50% 50%,
        rgba(255, 255, 255, 0.95) 0%,
        rgba(255, 255, 255, 0.72) 38%,
        rgba(220, 236, 255, 0.22) 68%,
        rgba(220, 236, 255, 0) 100%);
    animation: snowfall var(--dur, 12s) linear var(--delay, 0s) infinite;
}

/* Snow does not fall, it wanders down. Four sway points instead of one give
   each flake a drifting S-path; --sway is set per flake so near flakes swing
   wide and distant ones barely move.

   There used to be a rotate() on each step. A round flake looks identical at
   every angle, so it was interpolating a rotation nobody could see. */
@keyframes snowfall {
    0%   { transform: translate3d(0, -8vh, 0); }
    25%  { transform: translate3d(var(--sway, 24px), 22vh, 0); }
    50%  { transform: translate3d(calc(var(--sway, 24px) * -0.8), 52vh, 0); }
    75%  { transform: translate3d(calc(var(--sway, 24px) * 0.6), 82vh, 0); }
    100% { transform: translate3d(0, 112vh, 0); }
}

/* ---------- Fog ---------- */
.sky-fog {
    position: absolute;
    inset: 0;
    opacity: 0;
    background:
        linear-gradient(180deg, transparent 0%, rgba(190, 202, 222, 0.12) 45%, rgba(190, 202, 222, 0.18) 72%, transparent 100%);
    transition: opacity 2s ease;
}
/* Two banks of mist sliding past each other at different speeds. A single
   layer moving as one block looks like a sheet of grey; two layers at
   different rates read as depth, which is what real fog does. */
.sky-fog::before,
.sky-fog::after {
    content: '';
    position: absolute;
    inset: -20% -40%;
    background:
        radial-gradient(ellipse 55% 30% at 22% 62%, rgba(206, 216, 234, 0.22), transparent 70%),
        radial-gradient(ellipse 48% 24% at 68% 48%, rgba(196, 208, 228, 0.18), transparent 72%),
        radial-gradient(ellipse 62% 22% at 46% 80%, rgba(186, 198, 220, 0.20), transparent 74%);
    filter: blur(16px);
}
.sky-fog::after {
    inset: -10% -40% 10%;
    opacity: 0.7;
    filter: blur(26px);
}
[data-weather="fog"] .sky-fog { opacity: 1; }
[data-weather="fog"] .sky-fog::before { animation: fog-drift 44s ease-in-out infinite; }
[data-weather="fog"] .sky-fog::after  { animation: fog-drift 68s ease-in-out -22s infinite reverse; }
/* A thinner haze also hangs around during rain and snow. */
[data-weather="rain"] .sky-fog,
[data-weather="snow"] .sky-fog,
[data-weather="thunder"] .sky-fog { opacity: 0.4; }

@keyframes fog-drift {
    0%, 100% { transform: translate3d(-6%, 0, 0); }
    50%      { transform: translate3d(6%, 1.5%, 0); }
}

/* ---------- Lightning ----------
   Two parts, because real lightning has two: the sheet flash that lights the
   whole cloud base, and the bolt itself. The flash uses screen blending so it
   BRIGHTENS the scrim above it instead of painting a pale film over it — that
   is what makes a strike feel like light rather than a flat white rectangle.
   Peaks last 40-90ms, so text never becomes unreadable. */
.sky-flash {
    position: absolute;
    inset: 0;
    background:
        radial-gradient(ellipse 70% 42% at var(--x, 62%) 6%, rgba(232, 243, 255, 0.95) 0%, rgba(190, 214, 255, 0.42) 34%, transparent 70%),
        linear-gradient(180deg, rgba(206, 228, 255, 0.3) 0%, transparent 55%);
    mix-blend-mode: screen;
    opacity: 0;
}
/* Fires once, when weather.js adds .strike — it is NOT a loop any more.
   It used to be `animation: lightning 9s infinite`, which meant a strike at
   exactly the same two moments of every nine seconds, for ever. A storm on a
   metronome. Now JS decides when, and each strike gets its own duration. */
.sky-flash.strike { animation: lightning var(--flash-dur, 900ms) linear 1; }

/* Uneven on purpose. An evenly spaced flicker reads as a blinking light;
   lightning stutters — bright, nearly out, brighter, then a slow decay.
   These are the same stutter points as before, rescaled: they used to be
   squeezed into the first 6% of a 9-second loop, and now they fill the whole
   of a short one-shot animation. */
@keyframes lightning {
    0%    { opacity: 0; }
    14%   { opacity: 0.8; }
    24%   { opacity: 0.1; }
    33%   { opacity: 1; }
    45%   { opacity: 0.16; }
    55%   { opacity: 0.55; }
    72%   { opacity: 0.06; }
    100%  { opacity: 0; }
}

/* The bolt. A forked zig-zag cut out of a bright vertical gradient with a
   glow bled around it, so it looks lit rather than drawn.

   There is deliberately NO clip-path here any more. It used to carry one
   hard-coded polygon, so every strike on every visit was the same drawing.
   weather.js now sets a freshly generated polygon on the element immediately
   before each strike, the way the old canvas version drew a new zig-zag with
   lineTo() — but without a 60fps loop to pay for it. Until the first strike
   there is no clip-path at all, which is invisible because opacity is 0. */
.sky-bolt {
    position: absolute;
    top: -3%;
    left: var(--x, 58%);
    /* Half the width back, so --x is the middle of the bolt rather than its
       left edge. JS picks --x between 14% and 86%; without this the bolt would
       always sit a little right of where the flash brightens from. */
    margin-left: -60px;
    width: 120px;
    height: 66vh;
    opacity: 0;
    background: linear-gradient(180deg, #ffffff 0%, #e6f1ff 42%, #b9d4ff 78%, rgba(170, 205, 255, 0) 100%);
    filter:
        drop-shadow(0 0 12px rgba(200, 226, 255, 0.95))
        drop-shadow(0 0 40px rgba(120, 170, 255, 0.6));
    transform: scale(var(--scale, 1));
    transform-origin: 50% 0;
}
.sky-bolt.strike { animation: bolt-strike var(--dur, 620ms) linear 1; }

@keyframes bolt-strike {
    0%    { opacity: 0; }
    6%    { opacity: 1; }
    22%   { opacity: 0.08; }
    38%   { opacity: 0.95; }
    56%   { opacity: 0.12; }
    70%   { opacity: 0.5; }
    100%  { opacity: 0; }
}

/* ---------- Weather badge (optional, bottom-left) ---------- */
.weather-badge {
    position: fixed;
    left: 16px;
    bottom: 16px;
    z-index: 120;
    display: none;
    align-items: center;
    gap: 9px;
    padding: 8px 15px;
    border-radius: 999px;
    background: rgba(10, 14, 24, 0.66);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid var(--border, rgba(255, 255, 255, 0.09));
    color: var(--text, #eef2fb);
    font-size: 0.82rem;
    line-height: 1;
    box-shadow: 0 10px 30px -14px rgba(0, 0, 0, 0.8);
}
.weather-badge.visible { display: inline-flex; }
.weather-badge .wb-icon { font-size: 1.05rem; }
.weather-badge .wb-sep { opacity: 0.35; }
.weather-badge .wb-place { color: var(--muted, #96a2bd); }

@media (max-width: 900px) {
    /* Keep clear of the floating chat / back-to-top controls on phones. */
    .weather-badge { left: 12px; bottom: 12px; font-size: 0.76rem; padding: 7px 12px; }
    .weather-badge .wb-place { display: none; }
}

/* Dim the site's ambient brand orbs so they don't fight the sky. */
body.weather-active::before { opacity: 0.22; }
body.weather-active::after  { opacity: 0.18; }

/* ---------- Motion & data preferences ---------- */
@media (prefers-reduced-motion: reduce) {
    .star, .shooting-star, .cloud, .raindrop, .snowflake, .splash,
    .sky-fog, .sky-fog::before, .sky-fog::after,
    .sky-flash, .sky-bolt { animation: none !important; }
    .sky-gradient, .sky-sun, .sky-moon, .sky-stars, .sky-clouds { transition: none !important; }
    /* With the animation off a raindrop would sit frozen at the top of the
       screen as a bare line, so hide the falling particles outright. */
    .raindrop, .snowflake, .splash, .sky-bolt { display: none !important; }
    /* A bird frozen mid-flap is a pair of crooked lines stuck in the sky. */
    .sky-birds { display: none !important; }
    /* The gradient, sun, moon and stars still change with the time of day —
       only the motion is removed. */
}

/* Admin pages keep their light UI: never paint the sky there. */
.admin-body .sky,
.admin-body .weather-badge { display: none !important; }
