Every page on this site has a claymorphic hero image — a little clay version of me at a desk, Valencia sunshine streaming through the window, a cat doing cat things nearby. They’re handmade (well, AI-generated with careful prompting) and they set the tone for the whole site.
So naturally, I started wondering: what if those scenes reacted to real weather? Rain drumming on the window. Fog rolling across the desk. Night falling and the whole mood shifting. The plan sounded brilliant.
It was not brilliant.
The Grand Plan
The idea was a CSS weather overlay system. Pseudo-elements layered on top of the hero images, driven by live weather data from the Open-Meteo API. Rain would be diagonal particle animations. Fog would be a blurred gradient sweep. Night would dim everything with a brightness() filter and add a blue-purple tint.
About 200 lines of CSS. Keyframe animations for rain and snow particles. A JavaScript fetch to get current conditions in Valencia and apply the right class. Here’s the kind of thing I was writing for rain:
.weather-rain::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
105deg,
transparent,
transparent 10px,
rgba(174, 194, 224, 0.15) 10px,
rgba(174, 194, 224, 0.15) 11px
);
animation: rainFall 0.8s linear infinite;
}
Diagonal stripes falling on loop. Layer a few of these with varying speeds and opacities and you get something that looks vaguely like rain. I was pleased with myself.
The First Crack
The rain effect worked — technically. But it was heavy. Diagonal stripes across the entire image, even at low opacity, obscured the carefully crafted clay scene underneath. I dialled it back, made the stripes thinner, reduced the opacity. Better, but still fighting the image.
Then I remembered prefers-reduced-motion. As it should, this media query disabled all the animations. Correct behaviour — you don’t force motion on people who’ve asked for less of it. But what does frozen rain look like? Static diagonal stripes across an image. It looked like a rendering glitch, not weather.
That should have been the warning sign. When your fallback state looks broken, your approach might be the problem.
The Fundamental Problem
Here’s what killed the whole idea, and it took me embarrassingly long to see it.
The hero images show a character at a desk with clear blue skies through a window. That’s what they depict. CSS can change how they’re displayed — darker, lighter, tinted, blurred — but it cannot change what the image shows.
filter: brightness(0.3) darkens everything uniformly. It makes the sky through the window dark. Great. But it also makes the desk dark, the character dark, the cat dark. It can’t keep a desk lamp bright while darkening the sky. It can’t add stars outside the window. It can’t make the cat curl up and fall asleep.
CSS rain overlaid on an image that shows clear blue skies through a window is a visual contradiction. It’s like putting a “CLOSED” sign on a shop with all the lights on and people visibly shopping inside. The overlay says one thing; the image says another.
(Narrator voice: This is the kind of problem you can’t fix with more code.)
I tried. Multiple overlay layers with masks to darken only the sky region. Gradient maps to shift colour temperature. Each “fix” added complexity while making the result look worse. The core problem wasn’t execution — it was the approach. You cannot CSS your way into a different scene.
The Revert
I deleted weather-effects.css. Removed the API integration scripts. All of it. Back to zero.
200 lines of working CSS, gone. But working code that solves the wrong problem is worse than no code at all. The sunk cost fallacy is strong at 11 PM, but I managed to resist it. git checkout -- . and breathe.
The Better Idea
If CSS can’t change what an image depicts, the answer is obvious: change the image.
I generated actual nighttime variants of each hero image. Same scene, same character, same desk — but at night. Dark sky through the window with moon and stars. Desk lamp casting warm light. Monitors glowing blue. Cat curled up asleep instead of alert. The colour temperature shifts from warm daylight to cool moonlight with a warm lamp pool.
Things CSS filters could never produce. Because they’re not filters — they’re intentionally designed scenes.
The swap logic is almost comically simple:
fetch('https://api.open-meteo.com/v1/forecast?latitude=39.47&longitude=-0.376¤t=is_day&timezone=Europe/Madrid')
.then(r => r.json())
.then(data => {
if (data.current && data.current.is_day === 0) {
img.src = '/images/home/hero-night.png';
}
})
.catch(() => {});
No CSS. No animations. No particle systems. No filter stacking. Just: is it dark in Valencia? Swap the image. The catch silently falls back to daytime if the API is unreachable. The whole thing is non-blocking — daytime is the default, nighttime is the enhancement.
15 lines of JavaScript replacing 200 lines of CSS. Not because JavaScript is better than CSS, but because I was solving the problem at the wrong layer.
Scaling It
Once the pattern existed, extending it was straightforward. I generated nighttime variants for all the main pages — Home, About, Blog, Projects, Cerebro, Contact. Each one matches its page context: the About page shows a relaxed evening scene, the Blog page shows late-night writing, the Contact page has a lamp-lit desk.
This sits on top of a seasonal calendar system that already handles 20+ holiday and season variants (Valencia’s Fallas, Easter, Christmas, summer, and a dozen more). The nighttime check runs after the seasonal selection, so you get the right seasonal image during the day and a nighttime swap after dark. Three layers: seasonal selection (blocking, prevents flash), image swap (synchronous), nighttime detection (non-blocking API call). Each layer is independent and fails gracefully.
What This Actually Taught Me
CSS is powerful for styling what exists. It can make things bigger, smaller, darker, brighter, blurred, tinted, animated. What it cannot do is change what things depict. A photo of a sunny day with brightness(0.3) applied is a dark photo of a sunny day. It’s not a photo of night.
The best solution here wasn’t more code — it was better assets. The generated nighttime scenes look right because they are right. The desk lamp glows warm because it was rendered that way, not because a CSS gradient is trying to fake localised lighting on a flat image.
Sometimes the answer to “how do I make this work with code?” is “you don’t — you make a better thing and swap it in.” The site now quietly reflects whether it’s day or night in Valencia. Most visitors will never notice. That’s the point.