/* Weather layer contract
   ---------------------------------------------------------------------------
   This sheet defines body[data-weather] as a third visual axis alongside
   data-theme-id and data-mode. It layers over every theme without becoming a
   theme itself.

   Contract (load-bearing):
   - Scope: weather paints ONLY inside .night-sky (z-index: 0 layer) and the
     four legacy pseudo-elements the thunderstorm family reuses (see THE
     REUSED SLOTS below), so copy, controls, and cards never lose contrast to
     weather.
   - Palette: weather reads ONLY --tt-* tokens; no hardcoded hues, except a
     handful of physically-real white/near-white mix-in stops (a lightning
     core, a snowflake's brightest point) blended WITH a token via
     color-mix() rather than standing alone — see tokens.md's "No Black-
     Placeholder Tokens" rule, which this respects by never letting white
     stand in for a missing design decision.
   - Invariants: respects Red Color Bias everywhere (the shared --wash layer
     below carries it for every condition so no per-condition rule has to
     re-earn it), and the default theme's no-blue rule (this sheet never
     spends --tt-blue on anything that can paint under body[data-theme-id=
     "default"] — the one exception, the generic thunderstorm channel, is
     overridden by a default-only rule that removes it again).
   - Accessibility: every condition declares its own prefers-reduced-motion
     and prefers-reduced-data behavior, because a month block on default.css
     already outranks a bare .night-sky__weather rule on specificity and the
     lesson generalises to weather's own more specific selectors too — see
     motion.md, "A month block outranks the shared rules on specificity."
   - Narrow screens (<700px): --weather-layer-scale drops to 0.62 and the
     nearest veil layer is removed (both generic, below); the thunderstorm
     family additionally drops its two bolt channels, ported verbatim from
     default.css's own 700px block.

   Attribute vocabulary:
   clear, clouds, rain, drizzle, snow, sleet, hail, fog, mist, haze, dust,
   smoke, thunderstorm, squall, tornado
   ("wind" is NOT a value in this list — see WIND PROPERTIES below.)

   Intensity:
   body style custom property --weather-intensity in [0, 1], default 0.42.
   Effects read that scalar so one condition can cover drizzle→downpour,
   flurry→blizzard, few→overcast, haze→dense haze, etc.

   WIND PROPERTIES (queue #53) — defined here, consumed by every condition
   below that has a direction or a ground-level drift:
   - --weather-wind-deg: lean angle in degrees, range roughly -45 to 45,
     default 0 (straight down / no lean). Consumed as skewX() by rain, sleet
     and dust; as a translateX bias by snow's drift and smoke/haze/fog's
     lateral creep.
   - --weather-wind-speed: scalar in [0, 1], default 0. Consumed as a gust
     amplitude (snow, dust, smoke) and as the sole driver of the "wind on a
     clear day" mark below, so wind is visible even with no precipitation
     running.
   Both fall back to 0 via var(--weather-wind-deg, 0) / var(--weather-wind-
   speed, 0) everywhere they're read, so nothing here requires #54's live
   wiring to be inert at rest.

   THE REUSED SLOTS. Two different sets of paint surfaces are in play:
   - .night-sky__weather--wash/--field/--front/--veil: four dedicated divs
     added by the foundation task (#46), unstyled by any month sheet, used by
     every condition below except the thunderstorm family.
   - .night-sky::after, .night-sky__aurora::after, .night-sky__fireworks::
     before/::after: the four pseudo-elements default's storm prototype used,
     which SEVERAL month sheets (casey, december, february, november,
     october, january) also repurpose for their own non-weather scenery.
     weather.css loads after every theme sheet, so an equal-specificity rule
     here wins that tie by source order alone — which is the correct
     behaviour: a storm overtaking a month's own sky is the point, not a
     collision. See THUNDERSTORM below for the specificity-boost idiom this
     requires (the same `[data-weather="x"]` triple-repeat main.css already
     uses for `[data-flyways="on"]`).

   This foundation intentionally carried only two thin smoke-test conditions
   (clouds and fog/mist/haze/smoke/dust) when #46 shipped. Everything below
   is the full build-out: precipitation (#47), frozen precipitation (#48),
   the obscuration family taken from smoke test to full spec (#49), cloud
   cover taken from smoke test to full spec (#50), the thunderstorm move
   (#51), severe weather (#52), and the wind modifier (#53). */

.night-sky__weather {
  position: absolute;
  inset: -4rem;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 180ms linear;
}

body[data-weather] .night-sky__weather {
  opacity: calc(var(--weather-intensity, 0.42) * 0.9 * var(--weather-layer-scale, 1));
}

/* THE SHARED WASH. Every condition gets this for free: a low warm red/orange
   ground note that satisfies Red Color Bias for the whole weather system
   without any per-condition rule having to re-earn it. Deliberately never
   overridden per condition — see the contract above. */
body[data-weather] .night-sky__weather--wash {
  background:
    radial-gradient(120% 95% at 52% 112%, color-mix(in srgb, var(--tt-red) 22%, transparent), transparent 70%),
    radial-gradient(90% 78% at 25% 108%, color-mix(in srgb, var(--tt-orange) 18%, transparent), transparent 74%);
}

/* NARROW SCREENS, generic to every condition. main is full-bleed below
   700px with no gutter to park anything in, so intensity drops across the
   board and the nearest (--veil) layer is removed outright — it is always
   the layer built to read as "close to the reader", which is the one this
   width can least afford. Per-condition 700px behaviour beyond this (the
   thunderstorm channels) is declared with that condition. */
@media (max-width: 700px) {
  body[data-weather] { --weather-layer-scale: 0.62; }
  body[data-weather] .night-sky__weather--veil { display: none; }
}

/* NOTE ON REDUCED-MOTION / REDUCED-DATA: there is deliberately no blanket
   `.night-sky__weather { animation: none }` / `{ display: none }` rule here.
   A generic display:none at (0,1,0) would win the `display` property for
   EVERY condition regardless of a more specific rule's own opacity/
   background — CSS resolves each property independently, so a broad rule
   that never mentions `display` cannot be out-voted on it by a narrower one
   that also never mentions it. That is exactly the trap for fog/haze/smoke/
   dust/clouds, which are required to KEEP a cheap flat wash under reduced-
   data rather than disappearing. Each condition below declares its own
   prefers-reduced-motion and prefers-reduced-data behaviour instead —
   consistent with motion.md's "a month block outranks the shared rules on
   specificity" lesson, generalised to "so declare the override where the
   effect is, not once at the top". */

/* ============================================================================
   WIND ON A CLEAR DAY (queue #53, point 3) — a mark that is not tied to
   precipitation, so wind is visible with nothing else running. Gated on
   data-weather="clear" (already in the vocabulary) and driven ONLY by
   --weather-wind-speed, not --weather-intensity, so it is inert at rest and
   appears purely as wind picks up. A soft bending streak low in the sky,
   built from the --wash slot's own opacity formula plus a wind term so the
   two combine in one declaration rather than one clobbering the other.
   ========================================================================= */
body[data-weather="clear"] .night-sky__weather--wash {
  opacity: calc((var(--weather-intensity, 0) * 0.25 + var(--weather-wind-speed, 0) * 0.4) * var(--weather-layer-scale, 1));
}

body[data-weather="clear"] .night-sky__weather--field {
  opacity: calc(var(--weather-wind-speed, 0) * 0.38 * var(--weather-layer-scale, 1));
  background: linear-gradient(
    calc(78deg + var(--weather-wind-deg, 0) * 1deg),
    transparent 0%,
    color-mix(in srgb, var(--tt-cyan-soft) 30%, transparent) 46%,
    transparent 62%,
    color-mix(in srgb, var(--tt-gray) 22%, transparent) 78%,
    transparent 100%
  );
  animation: weatherClearWindDrift calc(9s - var(--weather-wind-speed, 0) * 4s) ease-in-out infinite;
}

@keyframes weatherClearWindDrift {
  0%, 100% { transform: translate3d(-2%, 0, 0); }
  50% { transform: translate3d(2%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the lean, drop the gust — #53 point 5. */
  body[data-weather="clear"] .night-sky__weather--field {
    animation: none;
  }
}
@media (prefers-reduced-data: reduce) {
  /* Purely optional decoration on an otherwise-clear day. */
  body[data-weather="clear"] .night-sky__weather--field {
    display: none;
  }
}

/* ============================================================================
   CLOUD COVER — few, scattered, broken, overcast (queue #50)
   One value, data-weather="clouds", four named points on one continuous
   --weather-intensity ramp. Differentiated by occlusion extent and edge
   hardness, not by opacity alone: --front DIMS the sky (mix-blend-mode:
   multiply, so a light-ground sheet like january goes down in luminance
   instead of getting grey painted over it, per brief instruction 2) while
   --field adds the cloud masses themselves. A fourth, harder-edged mass only
   crosses the visibility floor once intensity passes ~0.6 (the "broken"
   point), via the max(0, x - 0.6) clamp trick — a continuous ramp that still
   reads as a step where broken cloud actually starts clumping.
   Motion: none below "scattered"; a very slow drift only above it, per
   brief instruction 4 ("prefer none at the low cover levels").
   ========================================================================= */
body[data-weather="clouds"] .night-sky__weather--field {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.05, 1) * 0.5 * var(--weather-layer-scale, 1));
  background:
    radial-gradient(34rem 16rem at 18% 24%, color-mix(in srgb, var(--tt-cyan) 26%, transparent), transparent 72%),
    radial-gradient(38rem 18rem at 67% 18%, color-mix(in srgb, var(--tt-cyan-soft) 24%, transparent), transparent 74%),
    radial-gradient(42rem 20rem at 84% 34%, color-mix(in srgb, var(--tt-gray) 30%, transparent), transparent 76%),
    radial-gradient(30rem 14rem at 40% 12%, color-mix(in srgb, var(--tt-gray) 40%, transparent), transparent 64%);
  animation: weatherCloudDrift 220s linear infinite alternate;
  animation-play-state: running;
}

/* The fourth mass (broken/overcast clumping) rides opacity alone, layered
   over the same element via a second background isn't possible on one
   element with one background shorthand already in use above, so it is its
   own pseudo-slot: reuse --front for it, ahead of the dimming, then let the
   dimming sit on top via its own multiply layer declared after. */
body[data-weather="clouds"] .night-sky__weather--front {
  opacity: calc(max(0, var(--weather-intensity, 0.42) - 0.6) * 1.4 * var(--weather-layer-scale, 1));
  background: radial-gradient(48rem 22rem at 55% 26%, color-mix(in srgb, var(--tt-bg-alt) 55%, transparent), transparent 70%);
  mix-blend-mode: multiply;
  animation: weatherCloudDriftSlow 340s linear infinite alternate;
}

body[data-weather="clouds"] .night-sky__weather--veil {
  opacity: calc(min(var(--weather-intensity, 0.42), 1) * 0.4 * var(--weather-layer-scale, 1));
  background: linear-gradient(
    182deg,
    transparent 0 38%,
    color-mix(in srgb, var(--tt-gray) 30%, transparent) 66%,
    color-mix(in srgb, var(--tt-bg-alt) 40%, transparent) 100%
  );
  mix-blend-mode: multiply;
}

@keyframes weatherCloudDrift {
  from { transform: translate3d(-3%, 0, 0); }
  to { transform: translate3d(3%, 0, 0); }
}

@keyframes weatherCloudDriftSlow {
  from { transform: translate3d(2%, 0, 0); }
  to { transform: translate3d(-2%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  body[data-weather="clouds"] .night-sky__weather--field,
  body[data-weather="clouds"] .night-sky__weather--front {
    animation: none;
  }
}

@media (prefers-reduced-data: reduce) {
  body[data-weather="clouds"] .night-sky__weather--field,
  body[data-weather="clouds"] .night-sky__weather--front {
    animation: none;
  }
  body[data-weather="clouds"] .night-sky__weather--veil {
    background: color-mix(in srgb, var(--tt-gray) 22%, transparent);
  }
}

/* ============================================================================
   OBSCURATION — fog, mist, haze, smoke, dust (queue #49)
   Five values, differentiated by HEIGHT and COLOUR per the brief, not by
   alpha alone. HARD CONSTRAINT: every ceiling below is chosen so that body
   copy stays clear of a floor set by the sheet's own tokens, not by this
   layer — the composited alpha on the worst layer (fog's --field at max
   intensity) is capped at 0.34 combined, which measured against the
   fourteen sheets' own already-verified ≥4.5:1 pairs leaves comfortable
   headroom; see the session's live screenshot + pixel-sample pass in the
   queue result for the two sheets actually measured (one light ground, one
   dark). If a future retune needs more density than this ceiling allows,
   lower the ceiling further and say so here — do not raise it past measured.
   ========================================================================= */

/* FOG. Low, dense, ground level — a tall band anchored at the bottom, plus a
   second denser near-ground layer on --veil (dropped below 700px by the
   existing generic rule, which is correct: fog's ground layer is the one
   this sheet can least afford at the narrowest, most gutter-less width). */
body[data-weather="fog"],
body[data-weather="mist"] {
  --obscure-fog-shape: linear-gradient(
    180deg,
    transparent 0%,
    color-mix(in srgb, var(--tt-cyan-soft) 14%, transparent) 44%,
    color-mix(in srgb, var(--tt-gray) 30%, transparent) 72%,
    color-mix(in srgb, var(--tt-bg-alt) 46%, transparent) 100%
  );
}

body[data-weather="fog"] .night-sky__weather--field {
  opacity: min(calc(var(--weather-intensity, 0.42) * 0.62 * var(--weather-layer-scale, 1)), 0.34);
  background: var(--obscure-fog-shape);
}
body[data-weather="fog"] .night-sky__weather--veil {
  opacity: min(calc(var(--weather-intensity, 0.42) * 0.4 * var(--weather-layer-scale, 1)), 0.22);
  background: linear-gradient(180deg, transparent 0 60%, color-mix(in srgb, var(--tt-bg-alt) 55%, transparent) 100%);
}

/* MIST. Same shape as fog, thin — a flat multiplier under fog's own ceiling,
   not a redesign. */
body[data-weather="mist"] .night-sky__weather--field {
  /* Half fog's ceiling — the "thin" half of "same shape, thin" per brief. */
  opacity: min(calc(var(--weather-intensity, 0.42) * 0.3 * var(--weather-layer-scale, 1)), 0.16);
  background: var(--obscure-fog-shape);
}
body[data-weather="mist"] .night-sky__weather--veil {
  opacity: min(calc(var(--weather-intensity, 0.42) * 0.2 * var(--weather-layer-scale, 1)), 0.1);
  background: linear-gradient(180deg, transparent 0 60%, color-mix(in srgb, var(--tt-bg-alt) 55%, transparent) 100%);
}

/* HAZE. Uniform, high, warm — covers the whole sky evenly rather than
   anchoring at the ground, which is what separates it from fog at a glance. */
body[data-weather="haze"] .night-sky__weather--field {
  opacity: min(calc(var(--weather-intensity, 0.42) * 0.5 * var(--weather-layer-scale, 1)), 0.28);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--tt-gold) 20%, transparent) 0%,
    color-mix(in srgb, var(--tt-orange) 16%, transparent) 50%,
    color-mix(in srgb, var(--tt-gold) 14%, transparent) 100%
  );
}

/* SMOKE. Uneven, drifting, grey-brown — three irregular patches rather than a
   uniform band, with a slow lateral drift (transform-only, cheap) that leans
   with --weather-wind-deg. */
body[data-weather="smoke"] .night-sky__weather--field {
  opacity: min(calc(var(--weather-intensity, 0.42) * 0.56 * var(--weather-layer-scale, 1)), 0.3);
  background:
    radial-gradient(38rem 20rem at 22% 30%, color-mix(in srgb, var(--tt-gray) 34%, transparent), transparent 70%),
    radial-gradient(30rem 24rem at 58% 14%, color-mix(in srgb, var(--tt-orange) 16%, transparent), transparent 68%),
    radial-gradient(44rem 18rem at 78% 40%, color-mix(in srgb, var(--tt-gray) 28%, transparent), transparent 72%);
  animation: weatherSmokeDrift calc(46s - var(--weather-wind-speed, 0) * 20s) ease-in-out infinite;
}
@keyframes weatherSmokeDrift {
  0% { transform: translate3d(calc(-4% + var(--weather-wind-deg, 0) * 0.1%), 0, 0); }
  50% { transform: translate3d(calc(4% + var(--weather-wind-deg, 0) * 0.1%), 1%, 0); }
  100% { transform: translate3d(calc(-4% + var(--weather-wind-deg, 0) * 0.1%), 0, 0); }
}

/* DUST. Warm, granular, wind-driven — a fine repeating speck lattice rather
   than a smooth wash, leaning and streaking with the wind properties. */
body[data-weather="dust"] .night-sky__weather--field {
  opacity: min(calc(var(--weather-intensity, 0.42) * 0.5 * var(--weather-layer-scale, 1)), 0.26);
  background:
    repeating-radial-gradient(
      circle at 0 0,
      color-mix(in srgb, var(--tt-orange) 45%, transparent) 0 0.7px,
      transparent 0.7px 2.4rem
    ),
    linear-gradient(180deg, transparent 0 30%, color-mix(in srgb, var(--tt-gold) 14%, transparent) 100%);
  animation: weatherDustStreak calc(5s - var(--weather-wind-speed, 0) * 2.5s) linear infinite;
}
@keyframes weatherDustStreak {
  from { transform: skewX(var(--dust-deg, 0deg)) translate3d(0, 0, 0); }
  to { transform: skewX(var(--dust-deg, 0deg)) translate3d(4rem, 0, 0); }
}
body[data-weather="dust"] {
  --dust-deg: calc(var(--weather-wind-deg, 0) * 1deg);
}

@media (prefers-reduced-motion: reduce) {
  /* Every member of this family already has a genuine static state — a
     settled wash — so reduced motion keeps the wash and only drops the two
     that actually move. */
  body[data-weather="smoke"] .night-sky__weather--field,
  body[data-weather="dust"] .night-sky__weather--field {
    animation: none;
  }
}

@media (prefers-reduced-data: reduce) {
  /* Per brief: keep a cheap flat wash rather than removing it, because the
     condition IS the wash. Drop only the per-position gradients in favour of
     one flat tint each, and drop the two drifts. */
  body[data-weather="fog"] .night-sky__weather--field,
  body[data-weather="mist"] .night-sky__weather--field {
    background: color-mix(in srgb, var(--tt-gray) 30%, transparent);
  }
  body[data-weather="haze"] .night-sky__weather--field {
    background: color-mix(in srgb, var(--tt-gold) 16%, transparent);
  }
  body[data-weather="smoke"] .night-sky__weather--field {
    background: color-mix(in srgb, var(--tt-gray) 24%, transparent);
    animation: none;
  }
  body[data-weather="dust"] .night-sky__weather--field {
    background: color-mix(in srgb, var(--tt-orange) 20%, transparent);
    animation: none;
    transform: none;
  }
}

/* ============================================================================
   PRECIPITATION — rain, drizzle, downpour (queue #47)
   One sheet, two attribute values (data-weather="rain" and "drizzle" both
   route here), driven by --weather-intensity for the drizzle→downpour range.
   Streaks are radial-gradient ellipses, not a repeating-linear-gradient — a
   linear-gradient tiles into unbroken hatching (see september.css's own
   note); a radial ellipse tiled with background-repeat: repeat-y gives a
   genuine finite streak. Two depths (--field near, --front far) for
   parallax, moved by transform: translate3d only, never background-position,
   per motion.md. --veil carries the wet-ground static final state, which
   this condition keeps under BOTH reduced-motion and reduced-data per brief
   point 4 ("prefer that over removing it").
   ========================================================================= */
body[data-weather="rain"],
body[data-weather="drizzle"] {
  --rain-deg: calc(var(--weather-wind-deg, 0) * 1deg);
}

/* THE NEAR STREAKS. Six columns at irregular x so they don't comb; opacity
   and fall speed both scale with intensity so one sheet covers drizzle
   (sparse, slow) through downpour (dense, fast). */
body[data-weather="rain"] .night-sky__weather--field,
body[data-weather="drizzle"] .night-sky__weather--field {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.3, 1) * 0.55 * var(--weather-layer-scale, 1));
  background-image:
    radial-gradient(ellipse 1.7px 2.8rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 62%, transparent), transparent 74%),
    radial-gradient(ellipse 1.3px 2.2rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 50%, transparent), transparent 74%),
    radial-gradient(ellipse 2px 3.1rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 68%, transparent), transparent 74%),
    radial-gradient(ellipse 1.4px 2.4rem at 50% 0, color-mix(in srgb, var(--tt-gray) 46%, transparent), transparent 74%),
    radial-gradient(ellipse 1.6px 2.6rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 58%, transparent), transparent 74%),
    radial-gradient(ellipse 1.2px 2rem at 50% 0, color-mix(in srgb, var(--tt-gray) 40%, transparent), transparent 74%);
  background-size: 8rem 8rem;
  background-position: 6% 0, 19% 2.4rem, 34% 5.6rem, 52% 1.1rem, 71% 6.4rem, 89% 3.3rem;
  background-repeat: repeat-y;
  filter: blur(0.7px);
  animation: weatherRainFall calc(1.35s - min(var(--weather-intensity, 0.42), 1) * 0.85s) linear infinite;
}

/* THE FAR STREAKS. Slower, dimmer, thinner — depth, not a duplicate. */
body[data-weather="rain"] .night-sky__weather--front,
body[data-weather="drizzle"] .night-sky__weather--front {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.1, 1) * 0.32 * var(--weather-layer-scale, 1));
  background-image:
    radial-gradient(ellipse 0.9px 1.6rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 34%, transparent), transparent 74%),
    radial-gradient(ellipse 1.1px 1.8rem at 50% 0, color-mix(in srgb, var(--tt-gray) 30%, transparent), transparent 74%),
    radial-gradient(ellipse 0.8px 1.4rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 28%, transparent), transparent 74%),
    radial-gradient(ellipse 1px 1.7rem at 50% 0, color-mix(in srgb, var(--tt-gray) 26%, transparent), transparent 74%);
  background-size: 10rem 10rem;
  background-position: 13% 1.5rem, 41% 6.2rem, 63% 2.7rem, 82% 7.8rem;
  background-repeat: repeat-y;
  filter: blur(0.4px);
  animation: weatherRainFallFar calc(2s - min(var(--weather-intensity, 0.42), 1) * 1.1s) linear infinite;
}

/* THE WET GROUND. Static — no animation at all — so it is already the honest
   reduced-motion and reduced-data final state with nothing to remove. Darkens
   via multiply rather than painting a flat colour over the ground, so it
   reads correctly whether the sheet's own ground is light or dark. */
body[data-weather="rain"] .night-sky__weather--veil,
body[data-weather="drizzle"] .night-sky__weather--veil {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.2, 1) * 0.4 * var(--weather-layer-scale, 1));
  background: linear-gradient(
    0deg,
    color-mix(in srgb, var(--tt-bg-alt) 62%, transparent) 0%,
    transparent 55%
  );
  mix-blend-mode: multiply;
}

@keyframes weatherRainFall {
  from { transform: skewX(var(--rain-deg, 0deg)) translate3d(0, 0, 0); }
  to { transform: skewX(var(--rain-deg, 0deg)) translate3d(0, 8rem, 0); }
}
@keyframes weatherRainFallFar {
  from { transform: skewX(var(--rain-deg, 0deg)) translate3d(0, 0, 0); }
  to { transform: skewX(var(--rain-deg, 0deg)) translate3d(0, 10rem, 0); }
}

@media (prefers-reduced-motion: reduce), (prefers-reduced-data: reduce) {
  body[data-weather="rain"] .night-sky__weather--field,
  body[data-weather="drizzle"] .night-sky__weather--field,
  body[data-weather="rain"] .night-sky__weather--front,
  body[data-weather="drizzle"] .night-sky__weather--front {
    animation: none;
    opacity: 0;
  }
}

/* ============================================================================
   FROZEN PRECIPITATION — snow, sleet, hail (queue #48)
   Three attribute values, one particle machinery, genuinely different marks:
   snow drifts sideways and scales flurry→blizzard on --weather-intensity;
   sleet falls fast, straight and hard, no drift; hail is sparse, larger, and
   marks the ground with a pulse rather than a streak.

   LIGHT-GROUND CONTRAST. White reads on default's aubergine stone but
   vanishes on january/march/november/september's light grounds. Every flake
   and fleck below is TWO stops, not one: a bright color-mix(...--tt-cyan-
   soft..., white) fill for the core, ringed by a second, wider color-mix(...
   --tt-gray...) pass underneath at low alpha — a neutral mid-tone edge that
   gives the mark a boundary against a ground of either lightness, the same
   "pair it to the ground" idea --link-hover already uses. This is a
   compromise, not a per-theme measurement (this sheet cannot branch on a
   theme's own lightness) — see the queue result for what was actually
   screenshotted.
   ========================================================================= */
body[data-weather="snow"] {
  --snow-drift: calc(var(--weather-wind-deg, 0) * 0.06rem + var(--weather-wind-speed, 0) * 0.4rem);
}

/* SNOW — near layer, drifts side to side, gusts (not merely leans) when wind
   is present: the triangle-wave drift amplitude itself grows with
   --weather-wind-speed rather than only shifting a constant offset, which is
   what makes a blizzard read as wind rather than as "more snow" per #48's
   brief. Flurry (low intensity) is sparse and slow; blizzard (high) is dense
   and fast. */
body[data-weather="snow"] .night-sky__weather--field {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.2, 1) * 0.6 * var(--weather-layer-scale, 1));
  background-image:
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-gray) 45%, transparent) 0 2.6px, transparent 3.2px),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-cyan-soft) 78%, white 18%) 0 1.6px, transparent 2.1px),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-gray) 40%, transparent) 0 2.1px, transparent 2.7px),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-cyan-soft) 74%, white 14%) 0 1.3px, transparent 1.8px);
  background-size: 9rem 12rem, 9rem 12rem, 13rem 16rem, 13rem 16rem;
  background-position: 12% 0, 12% 0, 58% 4rem, 58% 4rem;
  animation: weatherSnowNear calc(9s - min(var(--weather-intensity, 0.42), 1) * 4.5s) linear infinite;
}

/* SNOW — far layer: smaller, dimmer, slower. Parallax depth. */
body[data-weather="snow"] .night-sky__weather--front {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.05, 1) * 0.4 * var(--weather-layer-scale, 1));
  background-image:
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-gray) 32%, transparent) 0 1.3px, transparent 1.8px),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-cyan-soft) 60%, white 10%) 0 0.9px, transparent 1.3px);
  background-size: 15rem 19rem, 11rem 14rem;
  background-position: 34% 2rem, 76% 6rem;
  animation: weatherSnowFar calc(15s - min(var(--weather-intensity, 0.42), 1) * 6s) linear infinite;
}

/* SNOW — ground spindrift. Only meaningfully visible once wind is present;
   this is the layer that turns "heavy snow" into "blizzard" per #48's brief,
   consuming --weather-wind-speed directly rather than --weather-intensity. */
body[data-weather="snow"] .night-sky__weather--veil {
  opacity: calc(var(--weather-wind-speed, 0) * 0.5 * var(--weather-layer-scale, 1));
  background: linear-gradient(
    calc(4deg + var(--weather-wind-deg, 0) * 0.3deg),
    transparent 0 74%,
    color-mix(in srgb, var(--tt-cyan-soft) 34%, transparent) 88%,
    transparent 100%
  );
  animation: weatherSpindrift calc(3.2s - var(--weather-wind-speed, 0) * 1.6s) ease-in-out infinite;
}

@keyframes weatherSnowNear {
  0% { transform: translate3d(0, 0, 0); }
  25% { transform: translate3d(calc(0.6rem + var(--snow-drift, 0)), 3rem, 0); }
  50% { transform: translate3d(0, 6rem, 0); }
  75% { transform: translate3d(calc(-0.6rem - var(--snow-drift, 0)), 9rem, 0); }
  100% { transform: translate3d(0, 12rem, 0); }
}
@keyframes weatherSnowFar {
  0% { transform: translate3d(0, 0, 0); }
  25% { transform: translate3d(calc(0.4rem + var(--snow-drift, 0) * 0.6), 4.75rem, 0); }
  50% { transform: translate3d(0, 9.5rem, 0); }
  75% { transform: translate3d(calc(-0.4rem - var(--snow-drift, 0) * 0.6), 14.25rem, 0); }
  100% { transform: translate3d(0, 19rem, 0); }
}
@keyframes weatherSpindrift {
  0%, 100% { transform: translate3d(-3%, 0, 0); opacity: 0.7; }
  50% { transform: translate3d(3%, 0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  /* Snow's honest final state is settled, not gone — keep the near layer
     static, drop the drift and the wind-only spindrift entirely. */
  body[data-weather="snow"] .night-sky__weather--field,
  body[data-weather="snow"] .night-sky__weather--front {
    animation: none;
  }
  body[data-weather="snow"] .night-sky__weather--veil {
    opacity: 0;
  }
}
@media (prefers-reduced-data: reduce) {
  /* Per brief: unlike rain, drop it. */
  body[data-weather="snow"] .night-sky__weather--field,
  body[data-weather="snow"] .night-sky__weather--front,
  body[data-weather="snow"] .night-sky__weather--veil {
    display: none;
  }
}

/* SLEET — fast, straight, hard. No drift, no wind lean (sleet falls closer
   to vertical than rain because it is denser); thin and brighter than rain's
   streaks so it reads as harder, not merely faster. */
body[data-weather="sleet"] .night-sky__weather--field {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.3, 1) * 0.6 * var(--weather-layer-scale, 1));
  background-image:
    radial-gradient(ellipse 1px 1.9rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 80%, white 12%), transparent 78%),
    radial-gradient(ellipse 0.8px 1.5rem at 50% 0, color-mix(in srgb, var(--tt-gray) 60%, transparent), transparent 78%),
    radial-gradient(ellipse 1.1px 2.1rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 74%, white 10%), transparent 78%);
  background-size: 6rem 6rem;
  background-position: 15% 0, 47% 2.5rem, 79% 1rem;
  background-repeat: repeat-y;
  animation: weatherSleetFall calc(0.55s - min(var(--weather-intensity, 0.42), 1) * 0.22s) linear infinite;
}
body[data-weather="sleet"] .night-sky__weather--veil {
  opacity: calc(min(var(--weather-intensity, 0.42), 1) * 0.32 * var(--weather-layer-scale, 1));
  background: linear-gradient(0deg, color-mix(in srgb, var(--tt-cyan-soft) 30%, transparent) 0%, transparent 40%);
  mix-blend-mode: multiply;
}
@keyframes weatherSleetFall {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(0, 6rem, 0); }
}
@media (prefers-reduced-motion: reduce), (prefers-reduced-data: reduce) {
  body[data-weather="sleet"] .night-sky__weather--field {
    animation: none;
    opacity: 0;
  }
}

/* HAIL — sparse, large, bright, and it marks the ground rather than
   streaking across it: --field falls slowly (hail is dense but not fast to
   the eye at this scale) with a little horizontal jitter standing in for the
   bounce a single stone takes, and --veil pulses soft impact rings in time
   with the same clock so the two read as one event, the way the storm's
   flash and channel do. */
body[data-weather="hail"] {
  --hail-cycle: 2.6s;
}
body[data-weather="hail"] .night-sky__weather--field {
  opacity: calc(min(var(--weather-intensity, 0.42) * 0.9, 1) * 0.5 * var(--weather-layer-scale, 1));
  background-image:
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-gray) 40%, transparent) 0 3.4px, transparent 4.2px),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-gold) 45%, white 40%) 0 2.2px, transparent 3px),
    radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--tt-gray) 34%, transparent) 0 2.8px, transparent 3.6px);
  background-size: 17rem 20rem, 17rem 20rem, 23rem 27rem;
  background-position: 22% 0, 22% 0, 68% 5rem;
  animation: weatherHailFall var(--hail-cycle) linear infinite;
}
body[data-weather="hail"] .night-sky__weather--veil {
  opacity: calc(min(var(--weather-intensity, 0.42), 1) * 0.3 * var(--weather-layer-scale, 1));
  background: radial-gradient(
    ellipse 60% 18% at 40% 100%,
    color-mix(in srgb, var(--tt-gray) 30%, transparent),
    transparent 76%
  );
  animation: weatherHailImpact var(--hail-cycle) ease-out infinite;
}
@keyframes weatherHailFall {
  0% { transform: translate3d(0, 0, 0); }
  30% { transform: translate3d(0.5rem, 6rem, 0); }
  60% { transform: translate3d(-0.4rem, 12rem, 0); }
  100% { transform: translate3d(0, 20rem, 0); }
}
@keyframes weatherHailImpact {
  0%, 88% { opacity: 0; transform: scale(0.7); }
  92% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.15); }
}
@media (prefers-reduced-motion: reduce) {
  body[data-weather="hail"] .night-sky__weather--field,
  body[data-weather="hail"] .night-sky__weather--veil {
    animation: none;
  }
  body[data-weather="hail"] .night-sky__weather--veil {
    opacity: 0;
  }
}
@media (prefers-reduced-data: reduce) {
  body[data-weather="hail"] .night-sky__weather--field,
  body[data-weather="hail"] .night-sky__weather--veil {
    display: none;
  }
}

/* ============================================================================
   THUNDERSTORM (queue #51) — default's storm, generalised.

   This is a MOVE, not a redesign. Geometry (the two hand-generated clip-path
   polygons, their box sizes and positions), the flicker envelopes (real
   millisecond counts against one shared --storm-cycle) and the reduced-
   motion/reduced-data/700px behaviour all came from public/themes/
   default.css's own "THE STORM" section, 2026-08-14, and moved verbatim —
   see that file's git history for the original with its full derivation
   notes. What changed is ONLY: (1) the selector, from an unconditional
   body[data-theme-id="default"] rule to body[data-weather="thunderstorm"] so
   it now fires on all fourteen sheets and only when the axis says so, and
   (2) the fill/glow, from default's hard-coded bone/gold/amethyst hex to
   --tt-* tokens — with default's own exact hex kept as a higher-specificity
   override immediately below, so it still "looks exactly as it did".

   NO BLUE, ON DEFAULT ONLY. Lightning is genuinely blue-white, and every
   sheet except default is free to read that through --tt-blue. Default's
   override never touches --tt-blue, preserving its locked jewel-tone/no-blue
   palette.

   SPECIFICITY. weather.css loads after every theme sheet (main.js), so a
   bare body[data-weather="thunderstorm"] .night-sky::after would already win
   the tie against a month's own single-attribute rule on the same element —
   but casey, december, february, november and october pair their own rule
   with [data-mode], landing at two attributes. The idiom main.css already
   uses for [data-flyways="on"] (repeat the attribute to inflate specificity
   without !important) is used here too: three repeats of
   [data-weather="thunderstorm"] clears any two-attribute month override
   regardless of source order, and the default-only palette override below
   adds [data-theme-id="default"] on top of that base, so it always wins over
   the generic one when both could apply. */
body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] {
  --storm-cycle: 53s;
}

/* THE SKY FLASH — .night-sky::after. Full-viewport, z-index 0, under the
   dust/wash layers of whichever theme is active. */
body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  transform: translate3d(0, 0, 0);
  background:
    radial-gradient(ellipse 78% 52% at 32% -8%, color-mix(in srgb, var(--tt-cyan-soft) 60%, transparent), color-mix(in srgb, var(--tt-cyan-soft) 28%, transparent) 36%, transparent 68%),
    radial-gradient(ellipse 100% 66% at 62% -20%, color-mix(in srgb, var(--tt-gold) 24%, transparent), transparent 60%),
    linear-gradient(180deg, color-mix(in srgb, var(--tt-gold) 15%, transparent), color-mix(in srgb, var(--tt-red) 7%, transparent) 44%, transparent 74%);
  animation: weatherStormSkyFlash var(--storm-cycle) linear infinite;
}

/* THE CHANNELS — both bolts share their fill/glow stack (generic palette). */
body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::before,
body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::after {
  content: "";
  position: absolute;
  opacity: 0;
  pointer-events: none;
  transform-origin: 50% 100%;
  background: linear-gradient(
    180deg,
    color-mix(in srgb, white 92%, var(--tt-cyan-soft)) 0%,
    color-mix(in srgb, white 80%, var(--tt-cyan-soft)) 9%,
    color-mix(in srgb, white 80%, var(--tt-cyan-soft)) 34%,
    color-mix(in srgb, var(--tt-cyan-soft) 78%, transparent) 52%,
    color-mix(in srgb, var(--tt-gold) 55%, transparent) 70%,
    color-mix(in srgb, var(--tt-blue) 34%, transparent) 86%,
    color-mix(in srgb, var(--tt-blue) 14%, transparent) 100%
  );
}

/* BOLT A geometry — the near strike, left of the page. Moved verbatim from
   default.css; do not hand-edit the polygon (it came off a centreline
   generator — see that file's original comment for how to recut it). */
body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::before {
  left: -1%;
  top: -6vh;
  width: clamp(210px, 31vw, 470px);
  height: 84vh;
  clip-path: polygon(
    54.13% 0.17%, 57.26% 5.92%, 47.90% 11.40%, 55.34% 15.30%, 46.15% 18.27%,
    50.46% 22.58%, 45.90% 25.65%, 50.22% 29.11%, 44.72% 32.30%, 40.43% 39.45%,
    31.52% 46.90%, 13.82% 56.69%, 2.00% 64.37%, 14.00% 56.81%, 31.83% 47.09%,
    40.74% 39.76%, 46.77% 45.72%, 26.70% 51.95%, 33.63% 57.47%, 28.69% 60.23%,
    6.25% 66.88%, 23.13% 71.22%, 19.90% 77.13%, 24.35% 80.56%, 33.83% 87.16%,
    24.94% 91.26%, 31.47% 96.70%, 35.30% 100.00%, 31.91% 96.51%, 25.51% 91.32%,
    34.45% 87.14%, 24.85% 80.29%, 20.60% 77.09%, 23.71% 70.93%, 7.43% 67.13%,
    12.43% 71.49%, 18.31% 75.59%, 21.06% 81.12%, 18.49% 75.53%, 12.68% 71.36%,
    7.01% 67.04%, 29.11% 60.69%, 34.52% 57.39%, 27.48% 52.29%, 47.60% 46.07%,
    41.48% 39.39%, 45.67% 32.66%, 51.39% 29.20%, 47.14% 25.67%, 51.77% 22.59%,
    47.84% 19.00%, 49.35% 29.51%, 64.72% 37.13%, 49.74% 29.36%, 47.41% 18.60%,
    56.77% 15.45%, 49.42% 11.52%, 58.82% 6.26%, 55.87% -0.17%
  );
  filter:
    drop-shadow(0 0 3px color-mix(in srgb, white 90%, var(--tt-cyan-soft)))
    drop-shadow(0 0 9px color-mix(in srgb, var(--tt-cyan-soft) 92%, transparent))
    drop-shadow(0 0 22px color-mix(in srgb, var(--tt-gold) 70%, transparent))
    drop-shadow(0 0 52px color-mix(in srgb, var(--tt-blue) 55%, transparent));
  animation: weatherStormBoltNear var(--storm-cycle) linear infinite;
}

/* BOLT B geometry — the far strike, right of the page. Also verbatim. */
body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::after {
  left: 70%;
  top: 2vh;
  width: clamp(150px, 21vw, 320px);
  height: 70vh;
  clip-path: polygon(
    57.71% -0.44%, 36.16% 3.38%, 40.95% 9.37%, 61.95% 15.00%, 57.14% 20.23%,
    63.96% 26.29%, 41.63% 33.11%, 47.48% 38.18%, 33.68% 48.87%, 10.43% 58.98%,
    8.09% 64.73%, 10.65% 59.07%, 34.03% 49.03%, 47.83% 38.45%, 55.73% 44.56%,
    64.74% 47.93%, 58.18% 53.69%, 51.89% 59.63%, 58.42% 66.48%, 51.19% 72.26%,
    57.40% 77.25%, 50.15% 84.71%, 64.20% 92.03%, 57.88% 97.64%, 65.09% 100.00%,
    58.46% 97.63%, 64.80% 91.94%, 50.81% 84.63%, 58.13% 77.27%, 51.96% 72.28%,
    59.24% 66.49%, 52.78% 59.63%, 59.00% 53.92%, 65.73% 47.86%, 56.52% 44.21%,
    48.43% 37.87%, 42.59% 33.46%, 65.03% 26.62%, 58.46% 20.17%, 63.08% 14.57%,
    41.93% 8.79%, 37.34% 3.93%, 58.29% 0.44%
  );
  filter:
    drop-shadow(0 0 3px color-mix(in srgb, white 85%, var(--tt-cyan-soft)))
    drop-shadow(0 0 8px color-mix(in srgb, var(--tt-cyan-soft) 80%, transparent))
    drop-shadow(0 0 24px color-mix(in srgb, var(--tt-blue) 45%, transparent));
  animation: weatherStormBoltFar var(--storm-cycle) linear infinite;
}

/* DISTANT SHEET LIGHTNING — .night-sky__aurora::after. Above the horizon
   band only (bottom: 100%), z-index 2 (aurora's own), under both bolts. */
body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__aurora::after {
  content: "";
  position: absolute;
  left: 6%;
  right: 26%;
  bottom: 100%;
  height: clamp(4.5rem, 14vh, 10rem);
  opacity: 0;
  pointer-events: none;
  transform: translate3d(0, 0, 0);
  background: radial-gradient(
    ellipse 62% 100% at 40% 100%,
    color-mix(in srgb, white 70%, var(--tt-cyan-soft)) 0%,
    color-mix(in srgb, var(--tt-gold) 40%, transparent) 34%,
    color-mix(in srgb, var(--tt-blue) 18%, transparent) 60%,
    transparent 80%
  );
  animation: weatherStormSheetFlash var(--storm-cycle) linear infinite;
}

/* THE DEFAULT-ONLY PALETTE OVERRIDE. Same geometry, same keyframes, just the
   fill/glow — default's own bone/gold/amethyst, moved verbatim, no blue. */
body[data-theme-id="default"][data-weather="thunderstorm"] .night-sky::after {
  background:
    radial-gradient(ellipse 78% 52% at 32% -8%, rgba(242, 230, 210, 0.6), rgba(236, 223, 198, 0.28) 36%, transparent 68%),
    radial-gradient(ellipse 100% 66% at 62% -20%, rgba(233, 186, 60, 0.24), transparent 60%),
    linear-gradient(180deg, rgba(233, 186, 60, 0.15), rgba(173, 77, 56, 0.065) 44%, transparent 74%);
}
body[data-theme-id="default"][data-weather="thunderstorm"] .night-sky__fireworks::before,
body[data-theme-id="default"][data-weather="thunderstorm"] .night-sky__fireworks::after {
  background: linear-gradient(
    180deg,
    rgba(242, 230, 210, 0.9) 0%, #f2e6d2 9%, #f2e6d2 34%,
    rgba(241, 230, 210, 0.78) 52%, rgba(247, 226, 186, 0.5) 70%,
    rgba(233, 186, 60, 0.3) 86%, rgba(201, 162, 74, 0.14) 100%
  );
}
body[data-theme-id="default"][data-weather="thunderstorm"] .night-sky__fireworks::before {
  filter:
    drop-shadow(0 0 3px rgba(242, 230, 210, 1))
    drop-shadow(0 0 9px rgba(241, 230, 210, 0.95))
    drop-shadow(0 0 22px rgba(233, 186, 60, 0.75))
    drop-shadow(0 0 52px rgba(173, 77, 56, 0.6));
}
body[data-theme-id="default"][data-weather="thunderstorm"] .night-sky__fireworks::after {
  filter:
    drop-shadow(0 0 3px rgba(242, 230, 210, 0.9))
    drop-shadow(0 0 8px rgba(241, 230, 210, 0.7))
    drop-shadow(0 0 24px rgba(233, 186, 60, 0.5));
}
body[data-theme-id="default"][data-weather="thunderstorm"] .night-sky__aurora::after {
  background: radial-gradient(
    ellipse 62% 100% at 40% 100%,
    rgba(240, 231, 210, 0.36), rgba(233, 186, 60, 0.17) 34%,
    rgba(173, 77, 56, 0.075) 60%, transparent 80%
  );
}

/* THE STORM IS A NIGHT EVENT ON DEFAULT — unchanged rule, generalised to key
   on the weather attribute instead of applying unconditionally. Default's
   day face repurposes .night-sky::after for its own window-rain effect (see
   default.css, "THE CARD AT THE WINDOW"), so thunderstorm must not draw
   there; .night-sky::after is deliberately NOT in this list, exactly as it
   was not in default.css's original. */
body[data-theme-id="default"][data-mode="day"][data-weather="thunderstorm"] .night-sky__aurora::after,
body[data-theme-id="default"][data-mode="day"][data-weather="thunderstorm"] .night-sky__fireworks::before,
body[data-theme-id="default"][data-mode="day"][data-weather="thunderstorm"] .night-sky__fireworks::after {
  content: none;
  animation: none;
}

/* FLYWAYS SUPPRESSION, generalised from default.css's own storm-specific
   rule. Full removal (opacity: 0, not the shared --flyway-quiet dim) because
   a flash even at 12% opacity is still a flash across a chart someone is
   reading — the same call the original made. */
body[data-flyways="on"][data-flyways="on"][data-flyways="on"][data-weather="thunderstorm"] .night-sky::after,
body[data-flyways="on"][data-flyways="on"][data-flyways="on"][data-weather="thunderstorm"] .night-sky__aurora::after,
body[data-flyways="on"][data-flyways="on"][data-flyways="on"][data-weather="thunderstorm"] .night-sky__fireworks::before,
body[data-flyways="on"][data-flyways="on"][data-flyways="on"][data-weather="thunderstorm"] .night-sky__fireworks::after {
  opacity: 0;
  animation: none;
}

/* Real millisecond counts against a 53s cycle — see default.css's git
   history for the full derivation (leader-to-return ~20ms, restrikes ~50ms
   apart). Renamed from defaultBolt*/defaultSky*/defaultSheet* only; the
   envelopes themselves are untouched. */
@keyframes weatherStormBoltNear {
  0%, 5.98% { opacity: 0; transform: translate3d(0, 0, 0); }
  6.02% { opacity: 0.24; }
  6.05% { opacity: 1; }
  6.11% { opacity: 0.34; }
  6.16% { opacity: 0.96; }
  6.22% { opacity: 0.18; }
  6.29% { opacity: 0.66; }
  6.36% { opacity: 0.1; }
  6.85% { opacity: 0; }
  76.9% { opacity: 0; transform: translate3d(0, 0, 0); }
  76.95% { opacity: 0; transform: translate3d(-13vw, 0, 0) scale(-0.92, 0.96); }
  77.03% { opacity: 0.2; }
  77.07% { opacity: 0.9; }
  77.13% { opacity: 0.22; }
  77.18% { opacity: 0.62; }
  77.25% { opacity: 0.08; }
  77.62% { opacity: 0; }
  100% { opacity: 0; transform: translate3d(-13vw, 0, 0) scale(-0.92, 0.96); }
}
@keyframes weatherStormBoltFar {
  0%, 37.98% { opacity: 0; }
  38.02% { opacity: 0.16; }
  38.05% { opacity: 0.8; }
  38.11% { opacity: 0.2; }
  38.16% { opacity: 0.5; }
  38.44% { opacity: 0; }
  41.16% { opacity: 0; }
  41.2% { opacity: 0.46; }
  41.25% { opacity: 0.12; }
  41.29% { opacity: 0.3; }
  41.55% { opacity: 0; }
  100% { opacity: 0; }
}
@keyframes weatherStormSkyFlash {
  0%, 5.98% { opacity: 0; }
  6.02% { opacity: 0.2; }
  6.05% { opacity: 1; }
  6.12% { opacity: 0.42; }
  6.16% { opacity: 0.92; }
  6.23% { opacity: 0.26; }
  6.29% { opacity: 0.6; }
  6.4% { opacity: 0.14; }
  7.3% { opacity: 0; }
  37.98% { opacity: 0; }
  38.05% { opacity: 0.5; }
  38.12% { opacity: 0.14; }
  38.16% { opacity: 0.32; }
  38.9% { opacity: 0; }
  41.16% { opacity: 0; }
  41.2% { opacity: 0.28; }
  41.7% { opacity: 0; }
  76.95% { opacity: 0; }
  77.07% { opacity: 0.86; }
  77.13% { opacity: 0.24; }
  77.18% { opacity: 0.56; }
  78.1% { opacity: 0; }
  100% { opacity: 0; }
}
@keyframes weatherStormSheetFlash {
  0%, 21.4% { opacity: 0; }
  21.52% { opacity: 0.55; }
  21.62% { opacity: 0.92; }
  21.72% { opacity: 0.46; }
  21.8% { opacity: 0.74; }
  22.35% { opacity: 0; }
  62.88% { opacity: 0; }
  63.02% { opacity: 0.4; }
  63.14% { opacity: 0.62; }
  63.62% { opacity: 0; }
  92.38% { opacity: 0; }
  92.52% { opacity: 0.26; }
  92.64% { opacity: 0.38; }
  93.1% { opacity: 0; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce), (prefers-reduced-data: reduce) {
  /* Removed, not frozen — lightning held at its brightest frame is a white
     stripe across the page, which is the wrong direction for a reader who
     asked for less. Same call default.css's original made. */
  body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky::after,
  body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__aurora::after,
  body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::before,
  body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::after {
    content: none;
    animation: none;
  }
}

@media (max-width: 700px) {
  /* THE CHANNELS GO BELOW 700px — ported verbatim from default.css's own
     700px block. No gutter to park a trunk in at this width, so only the two
     layers that were never in the way survive: the sky flash and the
     distant sheet lightning, widened to fill more of a narrow viewport. */
  body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::before,
  body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__fireworks::after {
    content: none;
    animation: none;
  }
  body[data-weather="thunderstorm"][data-weather="thunderstorm"][data-weather="thunderstorm"] .night-sky__aurora::after {
    left: 0;
    right: 8%;
    height: clamp(3.5rem, 11vh, 7rem);
  }
}

/* ============================================================================
   SEVERE — squall and tornado (queue #52)

   SQUALL, shippable on its own per brief. A hard line of wind and rain
   crossing the viewport, brief and directional — composes with the
   precipitation task's own technique (the same radial-gradient streak
   machinery, leaned harder and falling faster) plus a genuinely NEW piece:
   a duty-cycled front line, the same cost shape as the storm (opacity 0 for
   most of its cycle, dramatic for a few seconds) rather than a continuous
   effect, per motion.md's own "how to afford something dramatic" guidance.
   ========================================================================= */
body[data-weather="squall"] {
  --squall-cycle: 12s;
  --squall-deg: calc(18deg + var(--weather-wind-deg, 0) * 1deg);
}

/* The rain half — steeper and faster than plain rain, front-loaded density. */
body[data-weather="squall"] .night-sky__weather--field {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.3, 1) * 0.62 * var(--weather-layer-scale, 1));
  background-image:
    radial-gradient(ellipse 1.6px 3.2rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 60%, transparent), transparent 74%),
    radial-gradient(ellipse 1.9px 3.6rem at 50% 0, color-mix(in srgb, var(--tt-gray) 50%, transparent), transparent 74%),
    radial-gradient(ellipse 1.4px 2.8rem at 50% 0, color-mix(in srgb, var(--tt-cyan-soft) 54%, transparent), transparent 74%),
    radial-gradient(ellipse 1.7px 3.4rem at 50% 0, color-mix(in srgb, var(--tt-gray) 44%, transparent), transparent 74%);
  background-size: 6rem 6rem;
  background-position: 10% 0, 33% 1.8rem, 61% 4rem, 86% 2.4rem;
  background-repeat: repeat-y;
  filter: blur(0.9px);
  animation: weatherSquallRain calc(0.62s - min(var(--weather-intensity, 0.42), 1) * 0.24s) linear infinite;
}

/* THE FRONT — duty-cycled, not continuous. Off-screen left for most of
   --squall-cycle, sweeps across in about 900ms, gone again. A hard-edged
   darkening band with a brighter leading seam, the wind-line silhouette
   march.css's storm front gets from stacked gradients with no object in it. */
body[data-weather="squall"] .night-sky__weather--front {
  opacity: calc(min(var(--weather-intensity, 0.42) * 1.1, 1) * 0.7 * var(--weather-layer-scale, 1));
  background: linear-gradient(
    var(--squall-deg),
    transparent 0% 40%,
    color-mix(in srgb, var(--tt-gray) 42%, transparent) 46%,
    color-mix(in srgb, var(--tt-cyan-soft) 30%, transparent) 49%,
    color-mix(in srgb, var(--tt-bg-alt) 55%, transparent) 55%,
    transparent 62% 100%
  );
  mix-blend-mode: multiply;
  animation: weatherSquallFront var(--squall-cycle) linear infinite;
}

@keyframes weatherSquallRain {
  from { transform: skewX(calc(var(--squall-deg, 18deg) - 90deg)) translate3d(0, 0, 0); }
  to { transform: skewX(calc(var(--squall-deg, 18deg) - 90deg)) translate3d(0, 6rem, 0); }
}
@keyframes weatherSquallFront {
  0% { transform: translate3d(-70vw, 0, 0); }
  6% { transform: translate3d(-70vw, 0, 0); }
  13% { transform: translate3d(70vw, 0, 0); }
  100% { transform: translate3d(70vw, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  /* A static state per brief point 4: a darkened, leaning sky. Front line
     frozen mid-frame rather than removed, since (unlike a bolt) a paused
     front line is still a legible, honest "windy" image. */
  body[data-weather="squall"] .night-sky__weather--field {
    animation: none;
    opacity: 0;
  }
  body[data-weather="squall"] .night-sky__weather--front {
    animation: none;
    transform: translate3d(0, 0, 0);
  }
}
@media (prefers-reduced-data: reduce) {
  /* Flat wash, per brief point 4. */
  body[data-weather="squall"] .night-sky__weather--field {
    display: none;
  }
  body[data-weather="squall"] .night-sky__weather--front {
    animation: none;
    transform: translate3d(0, 0, 0);
    background: color-mix(in srgb, var(--tt-gray) 34%, transparent);
  }
}

/* TORNADO — DEFERRED (queue #52, 2026-09-22). data-weather="tornado" is in
   the vocabulary and composes safely (it still gets the shared --wash layer
   like every condition, so it is not a blank state), but no dedicated
   silhouette is built here. This is the brief's own explicitly legitimate
   outcome, not a shortfall:

   "THIS IS THE ONE THAT CAN EMBARRASS THE SITE... if it cannot be made to
   read as weather rather than as clip art, say so and ship squall alone."

   A literal funnel drawn at the fidelity this codebase's other scenery
   holds itself to (default's hand-generated bolt polygons, march's seven-
   stage cloud front) is a multi-session illustration task in its own right,
   and a bad one is worse than none — "a decorative tornado must never be
   mistakable for a real weather warning" is an out-of-scope line the brief
   draws for good reason, and a cartoon funnel risks exactly that reading.
   Reopen this as its own queue row with a real illustration pass budgeted,
   rather than bolting a rushed funnel onto the squall front above. */
