/*
 * Falling rain and snow.
 *
 * Driven entirely by `data-precip` on <html> ("rain" | "snow" | ""), which is
 * set by all three theming layers (edge function, inline head script,
 * theme.js) exactly like data-theme — so precipitation is painted on first
 * byte and crossfades with the theme during a View Transition.
 *
 * Structure: a fixed, clipped, non-interactive container holds two depth
 * layers (near and far). Each layer is a span that animates the *fall*, and a
 * ::before inside it that carries the drops and animates the *sway*. Keeping
 * the two transforms on separate elements lets the fall stay perfectly linear
 * while the sway eases back and forth on its own period and phase — snow that
 * weaves, rather than a rigid sheet sliding past.
 *
 * COST — read before adding anything here. Every layer is a full-viewport
 * composited texture, and a moving one: the compositor rasters it in tiles as
 * the transform exposes them, so it is redrawing continuously, at device pixel
 * ratio. The price of a layer is therefore (its area) x (its number of
 * background-images), and SVG images cost far more than gradients because each
 * redraw re-runs the SVG renderer.
 *
 * That product is easy to blow. An earlier revision ran three layers of up to
 * sixteen images, twelve of them SVG, on insets sized for the worst case:
 * snow measured 11.6fps against 120 idle, with 115ms frames. The same scene
 * now runs at 120fps. What fixed it: two layers instead of three, four SVG
 * images instead of twelve (several flakes packed into each tile rather than
 * one flake per tile), shorter loops so the layers need less headroom, and
 * per-type insets so snow is not padded for rain's slant. Keep the totals low.
 *
 * Beating the repeat: a tiled background normally gives itself away, because
 * every tile repeats on the same grid. Here each background-image has its own
 * background-size, at widths that are prime and mutually coprime (149, 173,
 * 199, 227...), so the composite only repeats at their least common multiple —
 * tens of thousands of pixels at least, far past any screen. The one hard
 * constraint is vertical: every tile height in a layer must divide that
 * layer's --precip-loop, because the fall translates by exactly one loop and
 * each tile has to land back on a boundary for the cycle to be seamless.
 *
 * Slant: the fall is `rotate(...) translate3d(...)` in that order, so the
 * translation happens inside the rotated space and both the streak angle and
 * the seamless tiling stay correct. Rain leans hard (9deg); each snow layer
 * leans a couple of degrees its own way, which is how the layers separate
 * laterally now that a horizontal drift is off the table (see precip-fall).
 */

.precip {
  display: none;
  position: fixed;
  inset: 0;
  /* Behind every bit of content. <body>'s background-color propagates to the
   * canvas (<html> sets none), so body does not paint it itself and a negative
   * z-index here still sits above it. The one background that would otherwise
   * cover this layer is the homepage hero gradient, which is pushed to -2 in
   * components.css for exactly that reason. */
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

[data-precip="rain"] .precip,
[data-precip="snow"] .precip {
  display: block;
}

/* Falls. Oversized so the motion never drags an empty edge into view: above
 * the viewport by that type's largest loop plus what the rotation lifts a far
 * corner by (half the layer's width x sin(slant)), and sideways for the same
 * rotation plus the widest sway. The headroom is per-type rather than one
 * worst case, because every extra pixel is composited texture that gets
 * re-rastered as the layer moves: rain's 9deg slant needs far more margin than
 * snow's 3-4deg, and padding snow to match cost real frames. */
.precip__layer {
  position: absolute;
  top: calc(-1 * var(--precip-head));
  right: calc(-1 * var(--precip-side));
  bottom: calc(-1 * var(--precip-side));
  left: calc(-1 * var(--precip-side));
  animation: precip-fall var(--precip-duration) linear infinite;
}

/* Carries the drops, and weaves. */
.precip__layer::before {
  content: "";
  position: absolute;
  inset: 0;
  animation: precip-sway var(--precip-sway-duration) ease-in-out infinite
    alternate;
  animation-delay: var(--precip-sway-delay, 0s);
}

/* Strictly vertical, inside the layer's rotated space. It must NOT carry a
 * horizontal component: the vertical distance is one --precip-loop and every
 * tile height divides it, so the fall wraps invisibly — but since each drop
 * now has its own tile *width*, no single horizontal offset could ever be a
 * multiple of all of them, and any would snap back visibly once per cycle.
 * Sideways motion belongs to --precip-slant (which rotates the whole space,
 * so it stays seamless) and to the sway below. */
@keyframes precip-fall {
  from {
    transform: rotate(var(--precip-slant, 0deg)) translate3d(0, 0, 0);
  }
  to {
    transform: rotate(var(--precip-slant, 0deg))
      translate3d(0, var(--precip-loop), 0);
  }
}

@keyframes precip-sway {
  from {
    transform: translate3d(calc(var(--precip-sway) * -1), 0, 0);
  }
  to {
    transform: translate3d(var(--precip-sway), 0, 0);
  }
}

/* ---------- Rain ----------
 * Hairline vertical ellipses, slanted as a body by the layer rotation. The
 * sub-pixel widths antialias down to fine threads rather than hard lines,
 * which is what keeps the effect from reading as scratches. The colour holds
 * solid to 72% of the radius before falling away, so each thread stays crisp
 * instead of blurring out at these widths. Sway is barely there — just enough
 * that the sheet does not look rigid. */

[data-precip="rain"] .precip__layer {
  --precip-slant: 9deg;
  --precip-head: 400px;
  --precip-side: 130px;
}

[data-precip="rain"] .precip__layer:nth-child(1) {
  --precip-loop: 240px;
  --precip-duration: 0.6s;
  --precip-sway: 6px;
  --precip-sway-duration: 6.1s;
  opacity: 0.55;
}
[data-precip="rain"] .precip__layer:nth-child(1)::before {
  background-size:
    149px 240px,
    199px 120px,
    251px 240px,
    167px 240px;
  background-image:
    radial-gradient(0.75px 12px at 18% 21%, var(--precip-rain) 0 72%, transparent),
    radial-gradient(0.65px 10px at 57% 63%, var(--precip-rain) 0 72%, transparent),
    radial-gradient(0.75px 13px at 84% 38%, var(--precip-rain) 0 72%, transparent),
    radial-gradient(0.65px 11px at 39% 82%, var(--precip-rain) 0 72%, transparent);
}

[data-precip="rain"] .precip__layer:nth-child(2) {
  --precip-loop: 180px;
  --precip-duration: 0.95s;
  --precip-sway: 5px;
  --precip-sway-duration: 7.7s;
  --precip-sway-delay: -3s;
  opacity: 0.35;
}
[data-precip="rain"] .precip__layer:nth-child(2)::before {
  background-size:
    173px 180px,
    227px 90px,
    181px 180px;
  background-image:
    radial-gradient(0.55px 9px at 12% 44%, var(--precip-rain) 0 72%, transparent),
    radial-gradient(0.55px 7px at 48% 17%, var(--precip-rain) 0 72%, transparent),
    radial-gradient(0.5px 8px at 76% 69%, var(--precip-rain) 0 72%, transparent);
}

/* ---------- Snow ----------
 * Flakes fall far slower than rain and weave widely, each layer on its own
 * period and phase.
 *
 * Shape: six-arm snowflakes — three strokes crossing at 60 degrees — in two
 * kinds only, plain and branched, at radii of 1.7 to 3.4px. They are inline
 * SVG rather than gradients because a gradient cannot be rotated on its own,
 * and six arms need 60-degree geometry with a different angle per flake.
 *
 * The branched ones are stroked much finer than the plain (0.55 against 1.0,
 * with a tighter outline). At this size their branches otherwise close up
 * against the arms and the flake reads as a solid wheel; fine lines are what
 * keep the lattice open. Each tile therefore draws two path pairs rather than
 * one. That is free — the cost is background-images, not paths within an SVG.
 *
 * Each SVG tile holds SEVERAL flakes rather than one, which is what keeps the
 * image count (and so the raster cost) down — see COST at the top. The flakes
 * within a tile repeat together, but with the tiles at coprime widths the
 * composite still never repeats on screen.
 *
 * Colour is baked in, unlike rain's: each tile draws its flakes twice from one
 * path, a translucent dark outline first and white over it. The outline is
 * what makes a white flake visible against the near-white daytime themes, and
 * it is self-adapting — dark on light backgrounds, invisible on the night
 * ones. Snow therefore has no colour token, unlike rain: an SVG cannot read a
 * custom property, and the obvious workaround — a themed gradient halo behind
 * each flake — costs a second background-image per flake, which is precisely
 * the expense this revision set out to remove.
 *
 * Every SVG carries an explicit viewBox. Rendering is 1:1 so it makes no
 * difference in place, but without it the flakes cannot be scaled at all,
 * which makes them impossible to inspect. */

[data-precip="snow"] .precip__layer {
  --precip-head: 320px;
  --precip-side: 100px;
}

[data-precip="snow"] .precip__layer:nth-child(1) {
  --precip-loop: 240px;
  --precip-duration: 5.04s;
  --precip-slant: -4deg;
  --precip-sway: 24px;
  --precip-sway-duration: 5.5s;
  opacity: 1;
}
[data-precip="snow"] .precip__layer:nth-child(1)::before {
  background-size:
    311px 240px,
    229px 240px;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='311' height='240' viewBox='0 0 311 240'%3E%3Cg fill='none' stroke-linecap='round'%3E%3Cpath d='M52.6 28.3L59.3 29.3M54.7 25.6L57.3 32.0M58.1 26.1L53.9 31.5M57.8 29.1L58.3 30.1M57.8 29.1L58.6 28.2M56.7 30.5L56.0 31.5M56.7 30.5L57.8 30.7M54.8 30.3L53.7 30.2M54.8 30.3L55.2 31.4M54.1 28.5L53.7 27.5M54.1 28.5L53.4 29.4M55.3 27.1L56.0 26.1M55.3 27.1L54.1 26.9M57.1 27.3L58.3 27.4M57.1 27.3L56.7 26.2M258.2 210.2L264.3 212.2M260.6 208.1L261.9 214.3M263.7 209.1L258.8 213.3M262.9 211.8L263.1 212.8M262.9 211.8L263.7 211.1M261.6 212.9L260.8 213.7M261.6 212.9L262.6 213.3M259.9 212.4L258.9 212.0M259.9 212.4L260.1 213.4M259.6 210.6L259.3 209.6M259.6 210.6L258.7 211.3M260.9 209.5L261.7 208.7M260.9 209.5L259.9 209.1M262.6 210.0L263.6 210.4M262.6 210.0L262.4 209.0' stroke='%2326393f' stroke-opacity='.16' stroke-width='1.00'/%3E%3Cpath d='M190.3 96.7L195.3 100.1M193.0 95.4L192.6 101.4M195.5 97.1L190.1 99.7M113.4 173.2L116.7 177.2M116.0 172.8L114.1 177.6M117.6 174.8L112.5 175.6' stroke='%2326393f' stroke-opacity='.16' stroke-width='1.80'/%3E%3Cpath d='M52.6 28.3L59.3 29.3M54.7 25.6L57.3 32.0M58.1 26.1L53.9 31.5M57.8 29.1L58.3 30.1M57.8 29.1L58.6 28.2M56.7 30.5L56.0 31.5M56.7 30.5L57.8 30.7M54.8 30.3L53.7 30.2M54.8 30.3L55.2 31.4M54.1 28.5L53.7 27.5M54.1 28.5L53.4 29.4M55.3 27.1L56.0 26.1M55.3 27.1L54.1 26.9M57.1 27.3L58.3 27.4M57.1 27.3L56.7 26.2M258.2 210.2L264.3 212.2M260.6 208.1L261.9 214.3M263.7 209.1L258.8 213.3M262.9 211.8L263.1 212.8M262.9 211.8L263.7 211.1M261.6 212.9L260.8 213.7M261.6 212.9L262.6 213.3M259.9 212.4L258.9 212.0M259.9 212.4L260.1 213.4M259.6 210.6L259.3 209.6M259.6 210.6L258.7 211.3M260.9 209.5L261.7 208.7M260.9 209.5L259.9 209.1M262.6 210.0L263.6 210.4M262.6 210.0L262.4 209.0' stroke='%23fff' stroke-opacity='1' stroke-width='0.55'/%3E%3Cpath d='M190.3 96.7L195.3 100.1M193.0 95.4L192.6 101.4M195.5 97.1L190.1 99.7M113.4 173.2L116.7 177.2M116.0 172.8L114.1 177.6M117.6 174.8L112.5 175.6' stroke='%23fff' stroke-opacity='1' stroke-width='1.00'/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='229' height='240' viewBox='0 0 229 240'%3E%3Cg fill='none' stroke-linecap='round'%3E%3Cpath d='M61.3 72.9L67.0 75.9M64.0 71.2L64.3 77.6M66.8 72.7L61.4 76.1M65.7 75.2L65.8 76.3M65.7 75.2L66.6 74.6M64.2 76.2L63.3 76.8M64.2 76.2L65.2 76.7M62.6 75.4L61.7 74.9M62.6 75.4L62.7 76.4M62.6 73.6L62.5 72.5M62.6 73.6L61.6 74.2M64.0 72.6L64.9 72.0M64.0 72.6L63.1 72.1M65.6 73.4L66.6 73.9M65.6 73.4L65.6 72.4' stroke='%2326393f' stroke-opacity='.16' stroke-width='1.00'/%3E%3Cpath d='M160.6 146.9L164.6 150.7M163.3 146.1L161.9 151.5M165.3 148.0L159.9 149.6M107.6 217.9L112.3 218.9M109.2 216.1L110.7 220.7M111.5 216.6L108.3 220.2' stroke='%2326393f' stroke-opacity='.16' stroke-width='1.80'/%3E%3Cpath d='M61.3 72.9L67.0 75.9M64.0 71.2L64.3 77.6M66.8 72.7L61.4 76.1M65.7 75.2L65.8 76.3M65.7 75.2L66.6 74.6M64.2 76.2L63.3 76.8M64.2 76.2L65.2 76.7M62.6 75.4L61.7 74.9M62.6 75.4L62.7 76.4M62.6 73.6L62.5 72.5M62.6 73.6L61.6 74.2M64.0 72.6L64.9 72.0M64.0 72.6L63.1 72.1M65.6 73.4L66.6 73.9M65.6 73.4L65.6 72.4' stroke='%23fff' stroke-opacity='1' stroke-width='0.55'/%3E%3Cpath d='M160.6 146.9L164.6 150.7M163.3 146.1L161.9 151.5M165.3 148.0L159.9 149.6M107.6 217.9L112.3 218.9M109.2 216.1L110.7 220.7M111.5 216.6L108.3 220.2' stroke='%23fff' stroke-opacity='1' stroke-width='1.00'/%3E%3C/g%3E%3C/svg%3E");
}

[data-precip="snow"] .precip__layer:nth-child(2) {
  --precip-loop: 180px;
  --precip-duration: 10.4s;
  --precip-slant: 3deg;
  --precip-sway: 14px;
  --precip-sway-duration: 8.3s;
  --precip-sway-delay: -3s;
  opacity: 1;
}
[data-precip="snow"] .precip__layer:nth-child(2)::before {
  background-size:
    271px 180px,
    193px 180px;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='271' height='180' viewBox='0 0 271 180'%3E%3Cg fill='none' stroke-linecap='round' d=''%3E%3Cpath d='M57.5 33.4L61.8 35.0M59.3 31.9L60.0 36.5M61.4 32.8L57.8 35.6M177.3 95.9L180.4 98.5M179.2 95.2L178.5 99.2M180.7 96.5L177.0 97.9M239.0 147.4L243.4 147.8M240.3 145.6L242.1 149.6M242.5 145.8L239.9 149.4' stroke='%2326393f' stroke-opacity='.16' stroke-width='1.6'/%3E%3Cpath d='M57.5 33.4L61.8 35.0M59.3 31.9L60.0 36.5M61.4 32.8L57.8 35.6M177.3 95.9L180.4 98.5M179.2 95.2L178.5 99.2M180.7 96.5L177.0 97.9M239.0 147.4L243.4 147.8M240.3 145.6L242.1 149.6M242.5 145.8L239.9 149.4' stroke='%23fff' stroke-opacity='1' stroke-width='0.85'/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='193' height='180' viewBox='0 0 193 180'%3E%3Cg fill='none' stroke-linecap='round' d=''%3E%3Cpath d='M77.5 60.2L80.8 62.2M79.1 59.3L79.1 63.1M80.8 60.2L77.5 62.2M148.5 127.3L152.6 128.3M150.0 125.8L151.1 129.8M152.0 126.3L149.1 129.3M25.9 157.1L28.2 159.7M27.5 156.8L26.5 160.0M28.7 158.0L25.4 158.8' stroke='%2326393f' stroke-opacity='.16' stroke-width='1.6'/%3E%3Cpath d='M77.5 60.2L80.8 62.2M79.1 59.3L79.1 63.1M80.8 60.2L77.5 62.2M148.5 127.3L152.6 128.3M150.0 125.8L151.1 129.8M152.0 126.3L149.1 129.3M25.9 157.1L28.2 159.7M27.5 156.8L26.5 160.0M28.7 158.0L25.4 158.8' stroke='%23fff' stroke-opacity='1' stroke-width='0.85'/%3E%3C/g%3E%3C/svg%3E");
}

/* White flakes carry far more contrast against the night backgrounds than
 * against the pale daytime ones, so the whole field drops to a quarter
 * strength there. This is the only place snow is not fully opaque: the ink
 * and both layers are, so that flakes read as white rather than milky on the
 * near-white daytime backgrounds. */
[data-precip="snow"][data-theme$="-night"] .precip {
  opacity: 0.25;
}

/* A static field of specks would just read as dirt on the screen, so drop the
 * whole effect rather than freezing it. */
@media (prefers-reduced-motion: reduce) {
  [data-precip] .precip {
    display: none;
  }
}
