GLM 5.3
by Z.aiReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 13 min 15 s
- Cost
- $0.236
- Output tokens
- 65,468
- Spent thinking
- 58,144 tokens
- Speed
- 82.4 tok/s
- Console errors
- 0
- First token after
- 344 ms
- First visible text after
- 12 min 32 s
- Prompt tokens
- 499
- Code size
- 20.3 KB
- Finish reason
- stop
- Served by
- InferenceNet
- Model id
z-ai/glm-5.3- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Create a set of four animated weather cards (sunny, rainy, snowy, stormy), made with HTML and CSS only. Requirements: - Absolutely no JavaScript: the file must not contain any <script> tag or inline event handler. - No images, no SVG, no icon fonts: every weather icon is built from HTML elements and pseudo-elements styled with CSS. - Each card shows an animated icon (rotating sun rays, falling rain drops, drifting snowflakes, flashing lightning), a city name, a temperature and a short description. - Each card has its own background gradient matching the weather. - The cards are displayed in a responsive grid (4 per row on desktop, stacked on mobile) and lift up on hover. - Smooth infinite animations using @keyframes, and respect prefers-reduced-motion.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 586 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Four Skies · Pure-CSS Weather Cards</title>
<style>
/* ================= reset & tokens ================= */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
color-scheme: dark;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}
html {
background:
radial-gradient(640px 420px at 10% -10%, rgba(90, 130, 255, .16), transparent 62%),
radial-gradient(760px 500px at 96% 112%, rgba(255, 140, 60, .13), transparent 62%),
#070b16;
}
body {
font-family: var(--font);
color: #f4f7ff;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
code {
font-family: var(--mono);
font-size: .85em;
background: rgba(126, 152, 208, .13);
border: 1px solid rgba(126, 152, 208, .22);
border-radius: 6px;
padding: .08em .4em;
}
/* ================= page shell ================= */
.wrap { max-width: 1180px; margin: 0 auto; padding: clamp(30px, 5vh, 52px) clamp(16px, 4vw, 40px) 48px; }
.head { text-align: center; margin-bottom: clamp(26px, 4.5vh, 44px); }
.eyebrow { font-family: var(--mono); font-size: 11px; font-weight: 600; letter-spacing: .24em; text-transform: uppercase; color: #92a6cf; margin-bottom: 14px; }
h1 { font-size: clamp(1.75rem, 3.4vw, 2.65rem); font-weight: 800; letter-spacing: -.025em; line-height: 1.08; color: #fbfcff; margin-bottom: 13px; text-wrap: balance; }
.lede { max-width: 660px; margin: 0 auto; font-size: clamp(.92rem, 1.3vw, 1rem); line-height: 1.68; color: rgba(190, 203, 230, .85); }
.foot { margin-top: 28px; text-align: center; font-size: .8rem; line-height: 1.7; color: rgba(163, 178, 207, .62); }
/* ================= grid & card ================= */
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; }
.card {
--ink: #ffffff;
--ink-2: rgba(255, 255, 255, .88);
--ink-3: rgba(255, 255, 255, .64);
--line: rgba(255, 255, 255, .22);
--chip-bg: rgba(255, 255, 255, .16);
--chip-line: rgba(255, 255, 255, .32);
--tshadow: 0 1px 2px rgba(9, 14, 30, .28);
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 24px;
box-shadow:
0 16px 34px rgba(2, 6, 18, .5),
inset 0 1px 0 rgba(255, 255, 255, .25),
inset 0 0 0 1px rgba(255, 255, 255, .06);
transition: transform .45s cubic-bezier(.22, .68, .34, 1.08), box-shadow .45s ease;
}
.card:hover {
transform: translateY(-10px);
box-shadow:
0 32px 60px rgba(2, 6, 18, .62),
inset 0 1px 0 rgba(255, 255, 255, .3),
inset 0 0 0 1px rgba(255, 255, 255, .1);
}
/* per-weather gradients */
.card--sun { background: linear-gradient(168deg, #ffb43e 0%, #ff8a00 48%, #ef5e0c 100%); }
.card--rain { background: linear-gradient(168deg, #4f9ede 0%, #33689c 56%, #26507e 100%); }
.card--snow { background: linear-gradient(168deg, #8cc2ea 0%, #6194c4 55%, #42739e 100%); }
.card--storm { background: linear-gradient(168deg, #47516b 0%, #2b3247 55%, #1a1f30 100%); }
/* the bright card uses dark "ink" for contrast */
.card--sun {
--ink: #3b1d02;
--ink-2: rgba(59, 29, 2, .85);
--ink-3: rgba(59, 29, 2, .66);
--line: rgba(59, 29, 2, .26);
--chip-bg: rgba(59, 29, 2, .15);
--chip-line: rgba(59, 29, 2, .32);
--tshadow: none;
}
/* readability veil (sits between the gradient and the text) */
.card::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
background: linear-gradient(to top, rgba(8, 12, 26, .34), rgba(8, 12, 26, .08) 46%, rgba(8, 12, 26, 0) 66%);
pointer-events: none;
}
.card--sun::after { background: linear-gradient(to top, rgba(8, 12, 26, .15), rgba(8, 12, 26, 0) 55%); }
/* shine sweep on hover */
.card::before {
content: "";
position: absolute;
top: -12%;
bottom: -12%;
left: -80%;
width: 55%;
z-index: 3;
background: linear-gradient(105deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, .28) 50%, rgba(255, 255, 255, 0) 100%);
transform: skewX(-16deg);
transition: transform 1s ease;
pointer-events: none;
}
.card:hover::before { transform: skewX(-16deg) translateX(420%); }
/* ================= scene (icon stage) ================= */
.scene {
position: relative;
z-index: 2;
flex: none;
height: 178px;
overflow: hidden;
transition: transform .45s cubic-bezier(.22, .68, .34, 1.08);
}
.card:hover .scene { transform: scale(1.05); }
/* ---- sunny: soft halo behind the sun ---- */
.card--sun .scene::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 330px;
height: 330px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 240, 180, .62) 0%, rgba(255, 215, 120, .22) 45%, rgba(255, 200, 90, 0) 68%);
animation: halo-breathe 6s ease-in-out infinite;
}
/* the sun: rotating conic-gradient rays masked into a ring + pulsing core */
.sun {
position: absolute;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
transform: translate(-50%, -50%);
}
.sun-rays {
position: absolute;
inset: 0;
background: repeating-conic-gradient(from -8deg at 50% 50%,
#ffefb8 0deg 16deg, rgba(255, 239, 184, 0) 16deg 45deg);
-webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 57%, #000 88%, transparent 94%);
mask: radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 57%, #000 88%, transparent 94%);
animation: spin 26s linear infinite;
}
.sun-core {
position: absolute;
inset: 34px;
border-radius: 50%;
background: radial-gradient(circle at 36% 32%, #fff7d2 0%, #ffdd55 48%, #ffb52e 100%);
box-shadow: 0 0 30px 8px rgba(255, 199, 72, .6), 0 0 90px 26px rgba(255, 170, 60, .35);
animation: sun-pulse 4.6s ease-in-out infinite;
}
/* ---- clouds: one pill + two pseudo-element puffs ---- */
.cloud { position: absolute; }
.cloud::before, .cloud::after { content: ""; position: absolute; border-radius: 50%; }
.cloud--rain {
top: 30px;
left: calc(50% - 52px);
width: 104px;
height: 34px;
border-radius: 40px;
background: linear-gradient(to bottom, #e3edfa 0%, #b9cde8 100%);
filter: drop-shadow(0 14px 16px rgba(18, 46, 92, .35));
animation: cloud-bob 5.6s ease-in-out infinite;
}
.cloud--rain::before, .cloud--rain::after { background: #e3edfa; }
.cloud--rain::before { width: 44px; height: 44px; top: -21px; left: 15px; }
.cloud--rain::after { width: 31px; height: 31px; top: -14px; right: 15px; }
.cloud--snow {
top: 26px;
left: calc(50% - 50px);
width: 100px;
height: 33px;
border-radius: 40px;
background: linear-gradient(to bottom, #eef5ff 0%, #ccd9ee 100%);
filter: drop-shadow(0 14px 16px rgba(60, 95, 148, .35));
animation: cloud-bob 6.6s ease-in-out infinite;
}
.cloud--snow::before, .cloud--snow::after { background: #eef5ff; }
.cloud--snow::before { width: 43px; height: 43px; top: -20px; left: 13px; }
.cloud--snow::after { width: 30px; height: 30px; top: -13px; right: 14px; }
.cloud--storm {
top: 26px;
left: calc(50% - 58px);
width: 116px;
height: 40px;
border-radius: 50px;
background: linear-gradient(to bottom, #6d7896 0%, #3f4864 100%);
filter: drop-shadow(0 16px 18px rgba(4, 9, 22, .55));
animation: storm-bob 6s ease-in-out infinite;
}
.cloud--storm::before, .cloud--storm::after { background: #6d7896; }
.cloud--storm::before { width: 52px; height: 52px; top: -25px; left: 15px; }
.cloud--storm::after { width: 37px; height: 37px; top: -17px; right: 16px; }
.cloud--mini {
top: 22px;
left: 9%;
width: 56px;
height: 19px;
border-radius: 20px;
background: #ffffff;
opacity: .92;
filter: drop-shadow(0 6px 8px rgba(200, 110, 20, .25));
animation: cloud-drift 9s ease-in-out infinite;
}
.cloud--mini::before, .cloud--mini::after { background: #ffffff; }
.cloud--mini::before { width: 25px; height: 25px; top: -12px; left: 7px; }
.cloud--mini::after { width: 18px; height: 18px; top: -8px; right: 8px; }
/* ---- rain: haze, falling drops, puddle ripples ---- */
.card--rain .scene::before {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 56px;
background: linear-gradient(to top, rgba(22, 50, 88, .35), rgba(22, 50, 88, 0));
}
.drops { position: absolute; inset: 0; }
.drops span {
position: absolute;
width: 3px;
height: 15px;
border-radius: 2px;
background: linear-gradient(to bottom, rgba(210, 236, 255, 0), rgba(210, 236, 255, .95));
opacity: .9;
transform: rotate(9deg);
animation: rainfall 1.55s linear infinite;
}
.card--rain .drops span:nth-child(1) { left: 26%; top: 84px; animation-delay: -0.20s; }
.card--rain .drops span:nth-child(2) { left: 37%; top: 96px; animation-delay: -0.75s; }
.card--rain .drops span:nth-child(3) { left: 48%; top: 86px; animation-delay: -1.10s; animation-duration: 1.45s; }
.card--rain .drops span:nth-child(4) { left: 60%; top: 99px; animation-delay: -0.40s; animation-duration: 1.7s; }
.card--rain .drops span:nth-child(5) { left: 71%; top: 90px; animation-delay: -0.95s; }
.card--rain .drops span:nth-child(6) { left: 81%; top: 97px; animation-delay: -1.35s; animation-duration: 1.5s; }
.card--rain .drops span:nth-child(7) { left: 32%; top: 103px; animation-delay: -0.60s; }
.ripple {
position: absolute;
bottom: 12px;
width: 28px;
height: 10px;
border: 2px solid rgba(214, 236, 255, .65);
border-radius: 50%;
opacity: .55;
animation: ripple 2.8s ease-out infinite;
}
.ripple.r1 { left: 22%; }
.ripple.r2 { left: 60%; animation-delay: -1.4s; }
/* ---- snow: frosty ground haze + drifting six-armed flakes ---- */
.card--snow .scene::before {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 52px;
background: linear-gradient(to top, rgba(235, 246, 255, .4), rgba(235, 246, 255, 0));
}
.flakes { position: absolute; inset: 0; }
.flake {
position: absolute;
width: 12px;
height: 2.5px;
border-radius: 2px;
background: #ffffff;
filter: drop-shadow(0 0 3px rgba(255, 255, 255, .85));
opacity: .95;
animation: flake-drift 6.4s linear infinite;
}
.flake::before, .flake::after {
content: "";
position: absolute;
left: 0; top: 0;
width: 100%;
height: 100%;
border-radius: 2px;
background: #ffffff;
}
.flake::before { transform: rotate(60deg); }
.flake::after { transform: rotate(-60deg); }
.card--snow .flake:nth-child(1) { left: 23%; top: 74px; animation-duration: 6.8s; animation-delay: -1.2s; }
.card--snow .flake:nth-child(2) { left: 35%; top: 92px; width: 9px; animation-duration: 5.4s; animation-delay: -3.1s; }
.card--snow .flake:nth-child(3) { left: 49%; top: 78px; width: 14px; animation-duration: 7.6s; animation-delay: -4.6s; }
.card--snow .flake:nth-child(4) { left: 62%; top: 96px; width: 10px; animation-duration: 6.1s; animation-delay: -2.2s; }
.card--snow .flake:nth-child(5) { left: 74%; top: 82px; animation-duration: 7.0s; animation-delay: -5.4s; }
.card--snow .flake:nth-child(6) { left: 85%; top: 70px; width: 9px; animation-duration: 5.7s; animation-delay: -0.6s; }
/* ---- storm: heavy slanted rain + flashing bolt + sky flash ---- */
.card--storm .scene::before {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 60px;
background: linear-gradient(to top, rgba(10, 14, 28, .45), rgba(10, 14, 28, 0));
}
.card--storm .scene::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(65% 55% at 52% 18%, rgba(255, 255, 255, .5), rgba(255, 255, 255, 0) 70%);
opacity: 0;
animation: sky-flash 5.2s linear infinite;
}
.card--storm .drops span {
background: linear-gradient(to bottom, rgba(198, 222, 255, 0), rgba(198, 222, 255, .92));
transform: rotate(14deg);
animation: storm-fall 1.15s linear infinite;
}
.card--storm .drops span:nth-child(1) { left: 14%; top: 92px; animation-delay: -0.10s; }
.card--storm .drops span:nth-child(2) { left: 25%; top: 104px; animation-delay: -0.55s; }
.card--storm .drops span:nth-child(3) { left: 59%; top: 95px; animation-delay: -0.90s; }
.card--storm .drops span:nth-child(4) { left: 71%; top: 105px; animation-delay: -0.35s; animation-duration: 1.05s; }
.card--storm .drops span:nth-child(5) { left: 83%; top: 90px; animation-delay: -0.70s; }
.bolt-wrap {
position: absolute;
top: 62px;
left: calc(50% - 17px);
width: 54px;
height: 88px;
filter: drop-shadow(0 0 10px rgba(255, 216, 92, .95)) drop-shadow(0 0 30px rgba(255, 176, 32, .5));
animation: bolt-flash 5.2s linear infinite;
}
.bolt {
width: 100%;
height: 100%;
background: linear-gradient(to bottom, #fff6c8 0%, #ffd84e 45%, #ffb226 100%);
clip-path: polygon(35% 0%, 95% 0%, 66% 45%, 90% 45%, 20% 100%, 28% 55%, 0% 55%);
}
/* ================= card body ================= */
.card-body {
position: relative;
z-index: 4;
flex: 1;
display: flex;
flex-direction: column;
padding: 18px 20px 20px;
text-shadow: var(--tshadow);
}
.body-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
}
.city { font-size: 1.1rem; font-weight: 700; letter-spacing: .01em; color: var(--ink); }
.chip {
flex: none;
font-size: 10px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--ink);
padding: 5px 10px;
border-radius: 999px;
background: var(--chip-bg);
border: 1px solid var(--chip-line);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
white-space: nowrap;
}
.temp {
font-size: 3.35rem;
font-weight: 200;
line-height: .95;
letter-spacing: -.03em;
color: var(--ink);
margin-bottom: 10px;
}
.desc { font-size: .9rem; line-height: 1.55; color: var(--ink-2); margin-bottom: 16px; }
.meta {
margin-top: auto;
display: flex;
flex-wrap: wrap;
padding-top: 12px;
border-top: 1px solid var(--line);
font-size: .72rem;
font-weight: 600;
letter-spacing: .04em;
color: var(--ink-3);
}
.meta span + span { border-left: 1px solid var(--line); margin-left: 10px; padding-left: 10px; }
/* ================= keyframes ================= */
@keyframes spin { to { transform: rotate(1turn); } }
@keyframes sun-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.07); } }
@keyframes halo-breathe { 0%, 100% { opacity: .75; } 50% { opacity: 1; } }
@keyframes cloud-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-7px); } }
@keyframes storm-bob { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(8px, -5px); } }
@keyframes cloud-drift { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(36px); } }
@keyframes rainfall {
0% { transform: translate3d(0, -8px, 0) rotate(9deg); opacity: 0; }
12% { opacity: .95; }
82% { opacity: .8; }
100% { transform: translate3d(-10px, 60px, 0) rotate(9deg); opacity: 0; }
}
@keyframes storm-fall {
0% { transform: translate3d(0, -8px, 0) rotate(14deg); opacity: 0; }
14% { opacity: .9; }
100% { transform: translate3d(-16px, 64px, 0) rotate(14deg); opacity: 0; }
}
@keyframes ripple {
0% { transform: scale(.35); opacity: .75; }
70% { opacity: .3; }
100% { transform: scale(1.6); opacity: 0; }
}
@keyframes flake-drift {
0% { transform: translate3d(0, -10px, 0) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
32% { transform: translate3d(10px, 18px, 0) rotate(130deg); }
60% { transform: translate3d(-9px, 40px, 0) rotate(255deg); }
85% { opacity: 1; }
100% { transform: translate3d(7px, 64px, 0) rotate(360deg); opacity: 0; }
}
@keyframes bolt-flash {
0%, 40% { opacity: 0; }
43% { opacity: 1; } 45% { opacity: .12; } 48% { opacity: .95; }
51% { opacity: .2; } 54% { opacity: 0; }
57% { opacity: .85; } 60% { opacity: .1; } 63% { opacity: .7; }
66% { opacity: 0; } 74% { opacity: 0; }
76% { opacity: 1; } 78% { opacity: .18; } 81% { opacity: .9; }
84% { opacity: .3; } 87%, 100% { opacity: 0; }
}
@keyframes sky-flash {
0%, 40% { opacity: 0; }
43% { opacity: .8; } 45% { opacity: .06; } 48% { opacity: .65; }
51% { opacity: .12; } 54% { opacity: 0; }
57% { opacity: .6; } 60% { opacity: .05; } 63% { opacity: .5; }
66% { opacity: 0; } 74% { opacity: 0; }
76% { opacity: .85; } 78% { opacity: .1; } 81% { opacity: .6; }
84% { opacity: .2; } 87%, 100% { opacity: 0; }
}
/* ================= responsive ================= */
@media (max-width: 1020px) {
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
.grid { grid-template-columns: 1fr; gap: 18px; max-width: 460px; margin-inline: auto; }
}
/* ================= reduced motion ================= */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation: none !important;
transition: none !important;
}
.card:hover, .card:hover .scene { transform: none; }
}
</style>
</head>
<body>
<main class="wrap">
<header class="head">
<p class="eyebrow">No JavaScript · No images · No SVG — just HTML & CSS</p>
<h1>Four skies, drawn in pure CSS</h1>
<p class="lede">Every sun ray, raindrop, snowflake and bolt of lightning below is built from plain elements and pseudo-elements, animated with nothing but <code>@keyframes</code>.</p>
</header>
<section class="grid">
<!-- ================= SUNNY ================= -->
<article class="card card--sun">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--mini"></div>
<div class="sun">
<div class="sun-rays"></div>
<div class="sun-core"></div>
</div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">Lisbon</h2>
<span class="chip">Sunny</span>
</div>
<p class="temp">24°</p>
<p class="desc">Clear skies and golden light, with a soft breeze rolling in off the Atlantic.</p>
<p class="meta"><span>H 27°</span><span>L 18°</span><span>Wind 12 km/h</span></p>
</div>
</article>
<!-- ================= RAINY ================= -->
<article class="card card--rain">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--rain"></div>
<div class="drops">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="ripple r1"></div>
<div class="ripple r2"></div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">London</h2>
<span class="chip">Rain</span>
</div>
<p class="temp">13°</p>
<p class="desc">Steady drizzle settling in for the afternoon — keep the umbrella close.</p>
<p class="meta"><span>H 15°</span><span>L 9°</span><span>Wind 20 km/h</span></p>
</div>
</article>
<!-- ================= SNOWY ================= -->
<article class="card card--snow">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--snow"></div>
<div class="flakes">
<span class="flake"></span><span class="flake"></span><span class="flake"></span>
<span class="flake"></span><span class="flake"></span><span class="flake"></span>
</div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">Reykjavík</h2>
<span class="chip">Snow</span>
</div>
<p class="temp">−2°</p>
<p class="desc">Light snow drifting down on a cold northerly wind.</p>
<p class="meta"><span>H 1°</span><span>L −8°</span><span>Wind 15 km/h</span></p>
</div>
</article>
<!-- ================= STORMY ================= -->
<article class="card card--storm">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--storm"></div>
<div class="drops">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="bolt-wrap"><div class="bolt"></div></div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">Singapore</h2>
<span class="chip">Thunder</span>
</div>
<p class="temp">28°</p>
<p class="desc">Thunderstorms rumbling through, clearing by early evening.</p>
<p class="meta"><span>H 30°</span><span>L 25°</span><span>Wind 38 km/h</span></p>
</div>
</article>
</section>
<p class="foot">Hover a card to lift it. Animations pause automatically when your system prefers reduced motion (<code>prefers-reduced-motion</code>).</p>
</main>
</body>
</html>Reasoning trace (58,144 tokens)
The user wants a set of four animated weather cards, pure HTML + CSS, no JavaScript, no SVG, no images. Icons built from HTML elements and pseudo-elements only. Each card: animated icon (rotating sun rays, falling rain drops, drifting snowflakes, flashing lightning), city name, temperature, short description. Each card has its own background gradient. Responsive grid (4 per row desktop, stacked mobile), hover lift. Smooth infinite animations with @keyframes, respect prefers-reduced-motion.
Constraints from the system prompt: single fenced code block with complete HTML file, no external files, no script. Must look right at 1280×800 and adapt down to 375px. Something meaningful visible on first paint.
Let me plan the design carefully.
Overall page: dark-ish neutral background (or a subtle gradient), a heading, and a grid of 4 cards. On desktop 1280px wide, 4 cards per row. On mobile (< ~900px), stack — the requirement says "4 per row on desktop, stacked on mobile". I could do 2 per row on a middle breakpoint then 1, but requirement says stacked on mobile. Let me do: grid-template-columns: repeat(4, 1fr) by default; at max-width 900px, repeat(2, 1fr); at max-width 560px, 1fr. That satisfies "stacked on mobile" (1 column on small phones) while gracefully adapting. Actually "stacked on mobile" might strictly mean 1 column below some breakpoint. 2-column at tablet is fine and then 1 column at phone width. Hmm, but at 375px, two cards per row would be cramped; 1 column at ≤560px is good.
Card design: rounded corners, gradient background per weather, big icon area (a "scene" with a stage), then city name, temperature, description. Hover: translateY(-8px) + bigger shadow.
Icons built from HTML elements and pseudo-elements only. Since each icon needs many parts (sun disc + 8 rays + rain drops + clouds), a single element with two pseudo-elements won't suffice for the sun with rays... Actually, we can use nested divs. Each icon is a small tree of divs, and pseudo-elements add extra layers. That's allowed — "built from HTML elements and pseudo-elements".
Let me design each icon:
**Sunny (Lisbon?)**:
- A `.sun` element: a circle (the disc) with radial gradient, positioned in center of icon stage.
- Rays: I need rotating rays. Approach: a `.sun-rays` container div rotating around center via `transform: rotate()` animation; inside it, 8 ray elements... but that requires 8 HTML children. Alternative: use pseudo-elements on the rays container — each pseudo can only be one element but with `box-shadow` we can create multiple copies! Classic trick: use box-shadow with multiple offsets to create multiple rays. Hmm, box-shadow creates rectangles/blurs, not rotated bars.
Better approach: rays via `repeating-conic-gradient` on a pseudo-element? E.g., a square div behind the sun with a conic gradient alternating transparent and orange, masked to a ring via mask radial-gradient. Conic gradients with `repeating-conic-gradient` create a starburst; mask with radial-gradient makes a ring so it looks like separate rays. Then rotate that element infinitely. That's pure CSS, no images. Nice and smooth.
Actually simpler: `.sun::before` = the disc? No wait. Let me structure:
```html
<div class="icon icon-sun">
<div class="sun">
<div class="rays"></div>
<div class="core"></div>
</div>
</div>
```
Hmm, but requirement says "every weather icon is built from HTML elements and pseudo-elements styled with CSS" — nested divs are HTML elements, fine.
Rays: `.rays` is a square centered, `background: repeating-conic-gradient(from 0deg, rgba(255,200,60,.95) 0deg 12deg, transparent 12deg 45deg)`; `mask: radial-gradient(circle, transparent 30%, black 32%, black 55%, transparent 58%)` to make ray segments between radii 32%-55% of the element. Then animate rotate 360deg over ~24s linear infinite. The core is the sun disc on top with radial-gradient and a soft glow (box-shadow). Also animate a subtle pulse (scale) on the core.
Actually with `repeating-conic-gradient(from 0deg, color 0 12deg, transparent 12deg 45deg)` you get 8 rays (360/45 = 8). Good.
Mask: use `-webkit-mask` and `mask` for Chrome compatibility. Latest Chrome supports unprefixed `mask`, but I'll include both to be safe.
Alternatively simpler rays: 4 line divs (each spanning full diameter) rotated at 0/45/90/135 degrees... but that requires 4 children. The conic gradient approach is elegant and one element. Let's do conic.
Wait — but `repeating-conic-gradient` with hard stops: `repeating-conic-gradient(#ffd54a 0deg 10deg, transparent 10deg 45deg)`. Hmm, hard transitions between rays. The edges will be hard, which is what we want for distinct rays. With the radial mask creating the ring shape, we get 8 ray bars. The rays will be pie-slice shaped (wedge), which actually looks nice — sun rays as wedges. Or I can make them look more like rounded rays... wedges are fine and classic.
Also add a glow behind the sun: `.core` box-shadow: 0 0 40px rgba(255,190,60,.8), plus `filter: drop-shadow` maybe.
Pulse animation on core: scale 1 → 1.06 → 1, 4s ease-in-out infinite.
**Rainy (London)**:
- A cloud: built from a base div + pseudo-elements (two circles via ::before and ::after) — the classic CSS cloud: main body is a rounded rectangle, ::before is a big circle on left-ish top, ::after circle on right. Or use a single element with border-radius and box-shadows to build circles. Classic cloud: element with `background`, `border-radius: 50px` (pill), `::before` circle at top-left, `::after` circle at top-right. Cloud floats up/down (translateY bob).
- Rain drops: several small elements falling. Options: a container with N drop divs each with animation-delay staggered. Or one element with multiple box-shadows animated... box-shadows can't be animated to fall independently. Best: a `.rain` container with e.g. 6 `.drop` children, each with `animation: fall ...; animation-delay` via nth-child. Each drop styled as a small elongated rounded line (width 3px, height 12px, border-radius, semi-transparent light blue, slight tilt? Vertical is fine, maybe rotate 15deg for slanted rain). Falling animation: translateY from below cloud to bottom with opacity fading.
Wait — drops as divs are HTML elements, allowed. Alternatively use pseudo-elements of the drop for a splash. Keep it simple: 6-7 drops with staggered delays.
Drop shape: a teardrop could be done with border-radius: 50% 50% 50% 50% / 60% 60% 40% 40% or the classic `border-radius: 0 50% 50% 50%; transform: rotate(45deg)` teardrop. Simpler: elongated rounded bar (like a rain streak). Looks good. Maybe make them slightly slanted: parent `.rain` with `transform: rotate(12deg)`? Or each drop rotated. Let me have the drops fall straight with slight `rotate(10deg)` and translate along Y. Actually a slanted rain container: rotate the container and let drops translate within it — the translation would then be along the rotated axis if we rotate the container. Simplest: each drop `transform: translate(...) rotate(10deg)` in keyframes; keep rotation constant, vary translate. Or just vertical drops — clean.
I'll do slightly slanted: keyframes: from { transform: translate3d(0,-10px,0) rotate(12deg); opacity: 0 } 15% opacity .9 ... to { transform: translate3d(-14px, 70px, 0) rotate(12deg); opacity: 0 }. Hmm, simpler to keep vertical to avoid fuss. I'll add slight x drift for realism. Fine.
**Snowy (Reykjavík?)**:
- Cloud (same construction, different colors, maybe light gray/white).
- Snowflakes: drifting — swaying side to side while falling. Snowflakes as small white circles (dots) with fall + sway. To combine two animations on one element (fall linear + sway), I can put sway on the flake and fall on a wrapper... or use one keyframes that includes both translateX and translateY: keyframes fall-sway: 0% {translate(0,0)} 25% {translate(6px, 18px)} 50% {translate(-2px,36px)} 75%{translate(5px,54px)} 100%{translate(0,72px)} with different delays. Each flake is a small white dot with box-shadow? Actually could make snowflakes look like actual 6-armed flakes using pseudo-elements: a `.flake` with `::before` and `::after` as crossed bars, plus the main element as a bar → an asterisk-like 6-point star (3 bars at 0°, 60°, 120°... main horizontal, before rotated 60°, after rotated 120°). That looks like a snowflake ❄ shape!
Flake construction:
```css
.flake { width: 10px; height: 2px; background: white; border-radius: 2px; position: absolute; }
.flake::before, .flake::after { content:""; position:absolute; left:0; top:0; width:10px; height:2px; background: white (inherit? no—set); border-radius:2px; }
.flake::before { transform: rotate(60deg); }
.flake::after { transform: rotate(-60deg); }
```
Since ::before/::after positioned absolute with left:0 top:0 relative to the flake (which has height 2px), rotation around its own center: transform-origin center — the pseudo is 10px wide 2px tall at top:0 left:0, its center is at (5px,1px) relative to itself which coincides with the parent's center horizontally but vertically parent center is 1px — yes since parent is 2px tall, top:0 puts pseudo's top at parent's top, pseudo center y = 1px = parent center.
So we get a 6-spoke asterisk. Add small dots at ends? Could use box-shadow on pseudo to add tip dots: too fancy. The asterisk reads as a snowflake at small size, especially with a subtle glow. Also add rotation animation to the flake while it falls — spinning snowflake is very charming. So flake has `animation: flake-fall Xs linear infinite, flake-spin Ys linear infinite;` two animations on same element: both animate transform → they'd conflict (last one wins per property). So combine in one keyframes or nest: put spin on flake, fall on wrapper... but wrappers = more HTML. Alternative: keyframes that do translateY + rotate + translateX sway all in one:
```css
@keyframes flake-drift {
0% { transform: translate3d(0, -12px, 0) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
30% { transform: translate3d(8px, 18px, 0) rotate(120deg); }
60% { transform: translate3d(-6px, 42px, 0) rotate(240deg); }
90% { opacity: 1; }
100% { transform: translate3d(4px, 68px, 0) rotate(360deg); opacity: 0; }
}
```
That gives fall, sway and spin. Each flake different delays/durations via nth-child.
Actually simpler alternative: since flakes are pure CSS asterisks with pseudo-elements, having 5-7 flakes each with nth-child delays is fine.
**Stormy (Singapore?)**:
- Dark cloud (darker gray).
- Lightning bolt: classic CSS bolt — a div with a clip-path? clip-path polygon is CSS, allowed. Or the classic two-pseudo-element zigzag. A nice bolt: element with `background: gold; clip-path: polygon(...)`. Or build with borders: The classic lightning bolt with CSS uses a rectangle with clip-path: polygon(40% 0, 100% 0, 60% 45%, 85% 45%, 20% 100%, 40% 55%, 10% 55%) or similar. Let me write a good bolt polygon:
Standard bolt polygon: `clip-path: polygon(45% 0%, 100% 0%, 55% 45%, 90% 45%, 15% 100%, 40% 55%, 5% 55%)` — hmm, let me think about the shape. A lightning bolt zigzag: top-right wide, goes down-left, notch, down to point.
Common bolt:
```
clip-path: polygon(62% 0, 100% 0, 45% 55%, 75% 55%, 20% 100%, 38% 62%, 0 62%);
```
Hmm I need to be careful to produce a visually correct bolt. Let me construct: The bolt occupies a 100×160 area conceptually (taller than wide). Points (x%, y%):
- Start at top: (60,0) → (95,0): top edge segment.
- Down-left diagonal to (45,50) — wait let me use a known-good one.
Known good CSS lightning bolt polygon (from various demos):
```
polygon(40% 0%, 70% 0%, 100% 60%, 65% 60%, 85% 100%, 10% 55%, 45% 55%, 15% 10%)
```
Hmm risky. Let me just carefully design my own.
Think of the bolt as a "Z" going down. Vertices (x from 0-100 left→right, y 0-100 top→bottom):
1. (55, 0) — top-left of the wide top segment... Actually classic bolt:
- Top band goes from upper-left to the right: from (30,0)? Let me draw:
A typical bolt: It starts at top with a horizontal-ish wide part, slants down-left to a middle, juts right (the middle step), then slants down-left to a sharp point at bottom. Like ⚡.
Vertices for polygon (going clockwise):
- P1 = (50, 0) top-left corner of the top segment
- P2 = (100, 0)?? That makes it full width at top. Let me use a canvas of width ~60px, height ~100px.
Let me define with percentage points:
- (45, 0) — top-left
- (100, 0) — top-right
- (55, 48) — where the right edge slants down to (the notch) — this is the inner right point (where middle step starts)
- (85, 48) — the step out to the right? Hmm.
Standard "thunderbolt" polygon that I recall being good:
```
clip-path: polygon(60% 0, 100% 0, 60% 40%, 90% 40%, 0 100%, 30% 50%, 0 50%);
```
Hmm wait that one: Let me trace: Start (60,0) → (100,0): top edge. → (60,40): slant down-left (right edge of upper diagonal band). → (90,40): step right (creates the horizontal middle step). → (0,100): long slant down-left to the bottom point. → (30,50): back up-right to inner corner. → (0,50): step left. → close back to (60,0): slant up-right (left edge of upper band).
Check the shape: Upper band: from (0,50)...(0,50)→(60,0)→(100,0)→(60,40)→(30,50). Hmm the upper portion: left edge from (0,50) to (60,0) — that's a diagonal going up-right; top edge (60,0)-(100,0); right edge (100,0)→(60,40) diagonal down-left; bottom edge of upper band from (60,40)→(30,50)→(0,50). So upper band is a parallelogram-ish quad slanting down-left. Then lower part: (90,40)→(0,100)→(30,50): triangle-ish going down-left to point at (0,100). The step between (60,40) and (90,40) creates the zigzag notch.
Hmm, but wait: point (0,100) — the very bottom tip at left edge. And (0,50) is at left edge middle — the upper band's bottom-left corner is at x=0. So the bolt touches the left edge twice. The overall bolt: wide at top, slanting down to the left, tip at bottom-left. That's a legit bolt shape (like ⚡ slanted). But typically bolts look better centered with tip near bottom-center.
Let me shift: use polygon: `clip-path: polygon(60% 0, 100% 0, 55% 45%, 90% 45%, 15% 100%, 40% 55%, 5% 55%)`. Trace: (60,0)→(100,0) top edge. →(55,45) down-left (right edge upper). →(90,45) step right. →(15,100) long diagonal down-left to bottom tip. →(40,55) up-right to inner corner. →(5,55) step left. → close to (60,0): up-right diagonal (left edge upper band).
Left edge upper: from (5,55) to (60,0). Bottom tip at (15,100). Upper band bottom edge: (55,45)→(40,55) hmm wait order: after (90,45) we go to (15,100), then (40,55), then (5,55). So inner region: from bottom tip (15,100) up to (40,55), then left to (5,55), then diagonal up to (60,0). The upper band spans between diagonals (5,55)→(60,0) and (100,0)→(55,45)...
I think this works but honestly, hard to verify mentally precisely. Alternative: build the bolt from two pseudo-elements (the classic double-triangle bolt):
- Element: main triangle pointing down.
- ::before: top parallelogram.
Or the truly classic approach with skewed rectangles...
Alternative simpler & reliable: build the bolt with two overlapping elements using clip-path triangles:
- Part A (upper): polygon(0 0, 100% 0, 30% 100%) — a triangle whose apex points down at 30% from left.
- Part B (lower): polygon(70% 0, 100% 0, 0 100%)??
Classic bolt as two triangles: Upper part: right triangle with vertices (0,0),(100,0),(20,100) — apex bottom-left-ish. Lower part: triangle with vertices (60,0),(100,0),(0,100)... hmm when overlapped they form a zigzag bolt if positioned right. Actually the classic: ⚡ can be decomposed as: top quadrilateral + bottom triangle.
You know what — clip-path with a hand-verified polygon is fine; even if slightly off from a perfect bolt, at small size with glow it will read as lightning. But let me instead use a construction I'm confident about: the bolt via `clip-path` using a well-known snippet. A very commonly used one:
```css
clip-path: polygon(50% 0%, 100% 25%, 80% 75%, 100% 100%, 0% 25%, ...)
```
no, that's noise.
Let me construct carefully on graph paper. Bolt in a 100×100 box, drawn as ⚡ (top-left wide part, bottom-right... no—standard ⚡: the top is a parallelogram slanting from upper-right down to lower-left, and the bottom is a triangle continuing to a point at bottom, with a notch step in the middle on the right side).
Take these vertices in order (clockwise starting top-left):
A = (35, 0)
B = (95, 0)
C = (95, 15)?? no.
OK here's a clean way to think. The bolt consists of 7 vertices:
1. Top-left of upper band: T1 = (45, 0)
2. Top-right: T2 = (100, 0)
3. Right edge goes down-left to the step's outer-top corner: M1 = (65, 45) — wait the step: after the upper band's right edge descends to y=45 at x=65, we step RIGHT to x=95 (M2 = (95,45)), then descend along the lower band's right edge to the bottom tip: Tip = (25, 100). Then from the tip go back up along lower band's left edge to the step's inner-bottom corner: M3 = (55, 55). Then step LEFT to M4 = (25, 55). Then up along upper band's left edge back to T1 = (45, 0).
Polygon: (45,0) (100,0) (65,45) (95,45) (25,100) (55,55) (25,55). Hmm wait, check consistency: from (95,45) to (25,100): the lower band's right edge — passes through (55,55)? The line from (95,45) to (25,100): slope = (100-45)/(25-95) = 55/(-70) ≈ -0.786 per unit x. At x=55: y = 45 + (95-55)*0.786 = 45 + 31.4 = 76.4. But M3=(55,55) is above that line, i.e., outside the lower band's right edge... For the polygon to be simple (non-self-intersecting), going (25,100)→(55,55)→(25,55)→(45,0): the segment (25,100)→(55,55) is the lower band's LEFT edge. And (25,55)→(45,0) is the upper band's left edge. The step between (55,55) and (25,55) is horizontal at y=55.
So the polygon: (45,0) → (100,0) → (65,45) → (95,45) → (25,100) → (55,55) → (25,55) → close to (45,0).
Is it simple? Edges:
E1 (45,0)-(100,0): top.
E2 (100,0)-(65,45): upper right diagonal.
E3 (65,45)-(95,45): horizontal step right at y=45.
E4 (95,45)-(25,100): lower right diagonal (the long one).
E5 (25,100)-(55,55): lower left diagonal.
E6 (55,55)-(25,55): horizontal step left at y=55.
E7 (25,55)-(45,0): upper left diagonal.
Does E4 cross E6 or E7? E4 from (95,45) to (25,100): parametrize x from 95→25, y from 45→100. At y=55: x = 95 - (55-45)*(70/55) = 95 - 12.7 = 82.3. E6 spans x∈[25,55] at y=55 — no overlap (82.3 > 55). At y=55 E7 hasn't started (E7 goes from y=55 at x=25 up to y=0 at x=45 — E7's point at y=55 is exactly (25,55)). E4 at y=55 is x=82.3, E7 only exists for y∈[0,55]. E4 and E7 could intersect where E7's y range overlaps: E7 y∈[0,55]; E4 at y≤55 has x≥82.3; E7 has x∈[25,45]. No intersection. E5 (25,100)-(55,55): x∈[25,55], y∈[55,100]. E4 at y∈[55,100]: x from 82.3 down to 25. They meet only at the shared tip (25,100)? E4 endpoint is (25,100) and E5 endpoint is (25,100) — shared vertex, fine. Does E5 cross E4 elsewhere? E5 at y=76: x = 25 + (100-76)*(30/45)= 25+16 = 41. E4 at y=76: x = 95 - (76-45)*(70/55) = 95 - 39.45 = 55.5. Not equal; E4 is to the right of E5 throughout except at tip. Good — so between E4 and E5 there's a wedge forming the lower band with tip at bottom-left (25,100). Wait, tip at (25,100): the lower band is bounded by E4 on right and E5 on left, converging at (25,100). The band width at top (y=55): right edge x=82.3, left edge x=55. OK.
And the step: at y=45, from x=65 (E2 ends) to x=95 (E4 starts) — the horizontal segment E3. Below that on the left side, at y=55, from x=55 to x=25 — E6.
Hmm, so the shape: upper band from top edge (45..100 at y=0) narrowing down: left edge E7 from (45,0) to (25,55); right edge E2 from (100,0) to (65,45). Then at y=45 the right side jumps out to x=95 (step), and at y=55 the left side jumps in to x=25... wait E6 goes from (55,55) to (25,55), and then E7 goes from (25,55) up to (45,0). So the upper band's left edge lower endpoint is (25,55), and there's a horizontal segment at y=55 from x=55 to x=25 which is the TOP edge of the lower band's left part. Hmm wait, I need to re-examine: the polygon boundary in order: ... E4 goes down-right-edge to tip (25,100), then E5 goes UP-LEFT?? E5 from (25,100) to (55,55): going up and to the right. Then E6 from (55,55) to (25,55): going left. Then E7 from (25,55) to (45,0): going up-right.
So interior: The lower band: bounded above partially by E6 (y=55, x from 25 to 55), on the right by E4, on the left by E5. So the lower band is a triangle-ish quad: (25,55), (55,55), (82.3-ish at y=55? no...). Hold on: E4 starts at (95,45). At y=55, E4 is at x=82.3. The lower band region: bounded left by E5 (from (55,55) down to tip (25,100)), right by E4 (from (95,45) down to tip), top by E6 (from (25,55) to (55,55)) and partially by E3? E3 is at y=45 from x=65 to 95. Hmm, the region between E3 (y=45) and E6 (y=55) on the left side x∈[25,55]...
Let me just fill the polygon mentally with even-odd rule tracing: vertices in order: V1(45,0), V2(100,0), V3(65,45), V4(95,45), V5(25,100), V6(55,55), V7(25,55).
The polygon: top band V1→V2→V3 (descends), then step out V3→V4, then long diagonal V4→V5 (down-left, this is the right/outer edge of the lower part), then V5→V6 (up-right, this is left/inner edge of lower part... wait no). Hmm, V5(25,100)→V6(55,55): if this is the lower part's LEFT edge, then the lower part is to the RIGHT of it, i.e., between line V5-V6 and line V4-V5. Then V6→V7 (25,55): horizontal leftward — this is the TOP edge of... the lower part's top-left region. Then V7→V1: up-right diagonal, the upper band's left edge.
So the union: Upper band = quad V1,V2,V3,?? — bounded by V1→V2 (top), V2→V3 (right), then from V3 where does the upper band's bottom edge go? After V3 we step right to V4 then go down to V5... So actually the upper band's "bottom" isn't a single edge; the shape is non-convex with the notch being the region between the step E3 (y=45, x∈[65,95]) and E6 (y=55, x∈[25,55]).
Let me compute the actual filled region via even-odd/scanline at y=50 (between 45 and 55): Boundary crossings at y=50: E2? E2 spans y∈[0,45] — no. E4: at y=50, x = 95 - (50-45)*(70/55) = 95-6.36 = 88.6. E5: y∈[55,100] — no. E7: spans y∈[0,55]: E7 from (45,0) to (25,55): at y=50, x = 45 - (50/55)*20 = 45-18.2 = 26.8. So at y=50, interior spans x∈[26.8, 88.6]. That's a wide band — at y=50 the bolt is wide from 26.8 to 88.6. Hmm, that means the "notch" isn't a notch at all at y=50 — the shape is solid there. That's wrong for a lightning bolt; there should be a zigzag notch where the shape indents.
The issue: my step arrangement creates a shape that's locally convex-ish. For a bolt, the middle should have the characteristic step: the boundary goes ... let me reconsider. In ⚡: the upper parallelogram's bottom-right region connects to the lower triangle's top-right region via a step. Actually in the real ⚡ glyph:
Looking at ⚡: It's like: top-left has a wide horizontal top edge. Right side slants slightly down-left. Then at the middle, there's a horizontal jog to the LEFT (inward), then continues down-left more steeply to the bottom point? No wait.
⚡ shape description: Start at top-left corner, go right along top edge to top-right corner. Then diagonal down-left to the middle-right point. Then horizontal LEFT (inward jog) to middle-left point. Then diagonal down-RIGHT?? no...
Hmm, let me think about the actual glyph: The bolt's outline (clockwise from top-left):
1. Top edge: from (30,0) to (85,0).
2. Down-left diagonal: from (85,0) to (40,50) — reaching the middle "waist" on the... hmm.
Ugh. Let me look at it differently. The bolt = big arrow-like: It's essentially two chevrons stacked. The upper part is a quadrilateral: (30,0),(85,0),(50,55),(10,55)? No...
OK here's the thing — I recall the standard CSS lightning bolt polygon used everywhere:
```css
clip-path: polygon(40% 0, 100% 0, 60% 60%, 100% 60%, 0 100%, 30% 60%, 0 60%);
```
Hmm, no. Let me instead recall from a different angle: Font Awesome's bolt is basically ⚡. The Unicode glyph ⚡ "HIGH VOLTAGE SIGN" outline:
Actually, the classic CSS-tricks style bolt:
```css
clip-path: polygon(50% 0%, 89% 24%, 100% 100%, 11% 76%, 0% 0%);
```
no that's random.
Let me just design it definitively on graph paper, being very careful. I'll design a bolt that looks like: a slanted "Z" thick band. Coordinates, y down, box 100 wide × 100 tall:
Upper band (parallelogram slanting down-left):
- A(70,0) top-left...
You know, the simplest guaranteed-correct bolt: **two triangles + clip** or even simpler: use a skewed rectangle approach:
Bolt = two overlapping skewed rectangles:
- Rect 1 (upper): a parallelogram: points (20,0),(90,0),(50,50),(? ) hmm.
**Much simpler reliable decomposition**: The bolt is the union of:
- Triangle U: (25,0), (95,0), (45,55) — pointing down.
- Triangle L: (95,45), (25,100)...
Standard bolt actually = upper parallelogram + lower triangle where they overlap in the middle creating the step. Let me define:
Upper parallelogram P: vertices (30,0), (90,0), (55,55), (-5,55)?? negative... Let me use: P = (35,0), (95,0), (60,55), (0,55). This is a parallelogram: top edge (35,0)-(95,0), right edge (95,0)-(60,55), bottom edge (60,55)-(0,55), left edge (0,55)-(35,0). It slants down-left. Check parallel: top edge vector (60,0); bottom edge from (0,55) to (60,55): vector (60,0) ✓. Left edge (0,55)→(35,0): vector (35,-55); right edge (95,0)→(60,55): vector (-35,55) ✓ anti-parallel. Good parallelogram.
Lower triangle T: vertices (95,35), (95,70)?? Hmm. For the bolt, the lower triangle: (20,100) tip at bottom-left; it should connect to the upper parallelogram's right portion with a step. T = (30,45), (90,45), (20,100): triangle with top edge from (30,45) to (90,45), tip at (20,100).
Union of P and T: P spans y∈[0,55]; T spans y∈[45,100]. They overlap for y∈[45,55]. Union outline (clockwise): Start (35,0) → (95,0) [top edge] → down right edge of P to where T's top edge intersects P's right edge: P right edge: from (95,0) to (60,55): x = 95 - (35/55)y = 95 - 0.636y. At y=45: x = 95-28.6 = 66.4. So point (66.4, 45) is inside T? T's top edge is y=45, x∈[30,90] — yes (66.4,45) is on T's top edge. So union outline: (35,0) → (95,0) → (66.4,45) → (90,45) [step right along T's top edge] → (20,100) [down T's right edge to tip] → then back up T's LEFT edge: from (20,100) to (30,45): but part of that is inside P? P's left edge: x = 0 + 0.636y... P left edge from (0,55) to (35,0): x = 35 - (35/55)y = 35-0.636y. At y=45: x = 35-28.6 = 6.4; at y=55: x=0. P's bottom edge is y=55 from x=0 to x=60. T's left edge from (20,100) up to (30,45): x = 30 - (10/55)(100-y)... at y=55: x = 30 - (10/55)(45) = 30-8.2 = 21.8. Is (21.8,55) inside P? P at y=55 spans x∈[0,60] — yes. So T's left edge enters P at y where T-left meets P... T's left edge crosses P's bottom edge (y=55) at x=21.8. So union outline continues: from tip (20,100) up T's left edge to (21.8,55) → then LEFT along P's bottom edge to (0,55) → then up P's left edge to (35,0).
Union polygon (clockwise): (35,0), (95,0), (66.4,45), (90,45), (20,100), (21.8,55), (0,55).
Hmm, the segment from (20,100) to (21.8,55) then to (0,55): there's a very shallow indentation — the step. Wait, is this the classic bolt? The outline: top edge wide; right edge slants down-left to (66.4,45); then juts RIGHT to (90,45) — an outward step (like ⚡'s middle jog); then long diagonal down-left to the tip (20,100); then up slightly-right to (21.8,55); then left to (0,55); then diagonal up-right to (35,0).
Hmm, this gives: on the left side at the middle: the outline goes from tip up to (21.8,55), then LEFT to (0,55). So the left side has a jog too — at y=55 the left boundary jumps from x=21.8 to x=0. But wait — that means the upper part extends further LEFT (to x=0 at y=55) than the lower part. And the lower part's left edge is nearly vertical (from (20,100) to (30,45))... In the real ⚡, the lower tail is a thinner continuation slanting down-LEFT, and the step on the RIGHT side at the middle. And the upper part's bottom-left corner — the upper parallelogram's leftmost point — creates the left side of the middle step. Yes! In ⚡, looking carefully: the glyph has the middle jog on the right side where the upper band's right edge steps out to become the lower tail's right edge...
Honestly, this union shape is exactly the classic CSS bolt built from a parallelogram + triangle, and it's used in many tutorials (e.g., the famous "CSS lightning bolt" is `polygon(57% 0, 100% 0, 55% 50%, ...)` hmm). My derived polygon: (35,0),(95,0),(66,45),(90,45),(20,100),(22,55),(0,55) — wait, one issue: the segment (21.8,55)→(0,55) then (0,55)→(35,0): the left middle step. In the real bolt, I believe the middle-left goes: from the lower tail's left edge UP, then there's a jog LEFT, then up-right along upper band's left edge. Yes — that matches ⚡: the middle has two horizontal jogs, one on the right (outward) and one on the left (outward-left)... Hmm, actually in ⚡, at the middle: the right side steps RIGHT and the left side steps LEFT? Let me visualize ⚡:
```
████████
███ ███
███ ███
██ ███
███ ███ ← no...
```
Real ⚡:
```
███████
██ ██
██ ██
██ ██
██
██
██
██
```
Hmm no. The actual glyph: top is a parallelogram slanting down-left; at the bottom-right of the parallelogram, the tail continues down-left but THINNER, with a step. The step appears as: on the right side, below the parallelogram's bottom-right corner, the tail's right edge is further RIGHT (creating an overhang notch) — yes that's the (66,45)→(90,45) jog. On the left side, the tail's left edge... the parallelogram's bottom-left corner at (0,55) sticks out LEFT compared to the tail — creating the other part of the zigzag. Yes! That's exactly the double-jog that makes it look like lightning.
So polygon: `polygon(35% 0%, 95% 0%, 66% 45%, 90% 45%, 20% 100%, 22% 55%, 0% 55%)`. Hmm wait, one more check on ordering/orientation — vertices must be in order around the shape: (35,0) → (95,0): along top. (95,0) → (66,45): down the right edge of upper band. (66,45) → (90,45): horizontal RIGHT (the jog). (90,45) → (20,100): down-left to tip. (20,100) → (22,55): up the tail's left edge. (22,55) → (0,55): horizontal LEFT (the left jog). (0,55) → (35,0): up-right closing.
This traces a simple polygon (non-self-intersecting) — verified by construction from union.
But wait: is (66,45) correct? P's right edge at y=45: x = 95 - (35/55)*45 = 95 - 28.64 = 66.36 ≈ 66. And T's top edge spans x∈[30,90] at y=45, so the intersection (66.36,45) lies on T's top edge ✓. And is (22,55) right? T's left edge from (30,45) to (20,100): x = 30 - 10*(y-45)/55; at y=55: x = 30 - 10*10/55 = 30-1.82 = 28.2. Hmm wait, I made an error before! Let me recompute: T's left edge goes from (30,45) to (20,100). Parametrize by y: at y=55, fraction t = (55-45)/(100-45) = 10/55 = 0.1818. x = 30 + t*(20-30) = 30 - 1.82 = 28.2. So the intersection with P's bottom edge (y=55, x∈[0,60]) is at (28.2, 55), not 21.8. I miscalculated earlier. Let me redo: earlier I wrote "x = 30 - (10/55)(100-y)": at y=55: 30 - (10/55)(45) = 30 - 8.18 = 21.8 — that's wrong: (100-y) at y=55 is 45, and the parametrization from (20,100): x = 20 + (100-y)/55 * (30-20) = 20 + (45/55)*10 = 20+8.18 = 28.2. Yes, 28.2 is correct. So the left jog: (28.2,55) → (0,55).
So polygon: `polygon(35% 0%, 95% 0%, 66% 45%, 90% 45%, 20% 100%, 28% 55%, 0% 55%)`.
Hmm, but let me double check the tail: tail = triangle T minus P-overlap: T = (30,45),(90,45),(20,100). The visible tail below y=55: bounded left by T's left edge, right by T's right edge from (90,45) to (20,100). T's right edge: from (90,45) to (20,100): at y=55: x = 90 - 70*(10/55) = 90-12.7 = 77.3. So at y=55 the tail spans x∈[28.2,77.3], width ~49; at y=100 it converges to tip (20,100). Meanwhile the upper band at y=55 spans [0,60]... wait P at y=55: bottom edge x∈[0,60]. So at y=55, union spans [0,77.3] minus... the union at y=55: P gives [0,60], T gives [28.2,77.3] → union [0,77.3]. OK continuous. Fine, the shape is a valid bolt.
Actually, hold on. Is the tail direction right? In ⚡ the tail goes down-LEFT from the middle. Tail right edge from (90,45) to (20,100) — yes goes down-left. Left edge from (30,45) to (20,100) — also down-left, steeper (more vertical). So the tail slants down-left ✓. Tip at bottom-left-ish (20,100) ✓. Upper band top edge (35,0)-(95,0) with left edge slanting down-left ✓.
Great: `clip-path: polygon(35% 0, 95% 0, 66% 45%, 90% 45%, 20% 100%, 28% 55%, 0% 55%)`. I'll use this for the bolt. Width ~55-60px, height ~85px, golden gradient background, drop-shadow glow. Actually clip-path clips box-shadow... I'll wrap: put glow via `filter: drop-shadow(...)` on the bolt element — filter applies to the clipped result ✓ (drop-shadow follows the clip-path shape since clip happens before filter? Hmm — order: clip-path is applied... Actually filter creates a containing block and is applied BEFORE clip-path in painting? Let me think: The filter is applied to the element, then clip-path clips the filtered result? Per spec, clip-path clips the element including its filter output? The order of operations: filters are applied, then clipping? Actually: clip-path applies to the element after filter? Hmm. CSS: "The clip-path property applies to the element after filters"? Let me recall: In practice, `clip-path` + `drop-shadow` filter: the drop-shadow of the clipped shape — I believe filter is applied first, then clip-path clips everything including the shadow → the shadow would be clipped away. Common workaround: wrap in a parent with `filter: drop-shadow()` and the child has the clip-path. Yes — apply drop-shadow on the PARENT (or use a wrapper). So structure:
```html
<div class="bolt-wrap"> <!-- filter: drop-shadow(gold) -->
<div class="bolt"></div> <!-- clip-path -->
</div>
```
Lightning animation: flash — opacity flicker: keyframes bolt-flash: 0%, 88%: opacity 0 or low; then quick on/off flashes around 90-100%... But infinite animations should be smooth; a flash pattern: mostly dim/invisible then bright flickers. E.g.:
```css
@keyframes bolt-flash {
0%, 100% { opacity: 0; }
72% { opacity: 0; }
75% { opacity: 1; }
77% { opacity: .2; }
80% { opacity: 1; }
82% { opacity: .1; }
85% { opacity: .9; }
92% { opacity: .25; }
}
```
Duration ~4s infinite. Also, add a background flash on the whole stormy card? Could add a `.flash` overlay div on the stormy card that brightens the sky briefly, synced-ish. Maybe simpler: also animate the card's subtle overlay. I could add a ::after on card-s storm that flashes with similar keyframes (slightly offset). That'd be a nice touch: the whole sky flashes. Let me add an absolutely-positioned overlay `.flash` (or use `.card--storm .scene::after`). Using the scene's ::after with a radial white glow at top, animated opacity in the same rhythm.
Also rain on the stormy card? Storm could have heavy rain + lightning. Requirement says "flashing lightning" for stormy. I can add a couple of slanted rain streaks too — storm usually has rain. Let me add rain drops to the storm card as well (fewer, faster, more slanted). That enriches it. But keep DOM manageable. I'll add 4-5 streaks.
**Cloud construction** (for rain and snow; storm gets a darker cloud; sun card could have the sun alone or sun + small cloud? Keep sun pure sun for clarity):
```html
<div class="cloud">
<span class="puff"></span>?
```
Classic single-element cloud with pseudo-elements:
```css
.cloud {
position: relative;
width: 90px; height: 34px;
background: #fff;
border-radius: 40px;
}
.cloud::before, .cloud::after {
content: "";
position: absolute;
background: inherit; /* or same color */
border-radius: 50%;
}
.cloud::before { width: 40px; height: 40px; top: -20px; left: 12px; }
.cloud::after { width: 28px; height: 28px; top: -14px; right: 14px; }
```
But `background: inherit` on a gradient background would restart the gradient over the pseudo's own box — inconsistent coloring. Better: solid colors for cloud (with maybe subtle shading). For gradient clouds, use `background: inherit` won't match. Alternative: give the cloud container `border-radius` and use `overflow: visible`... The classic trick for gradient clouds: put the puffs as pseudos with the SAME gradient is hard. Simplest: use solid colors — rain cloud: soft slate blue-gray (#8fa3c7-ish? on a blue gradient bg, cloud should be lighter or darker). Or make cloud from a parent that has the gradient and children clipped... overkill.
Alternative cloud approach that supports gradients: one element with `border-radius: 50px` and box-shadow? No.
I'll use solid-ish cloud colors with a subtle inset shadow for depth:
- Rain cloud (on blue-gray gradient): light gray-blue #dfe8f5 or white-ish? On a rainy sky (blue-gray gradient, medium tone), cloud light: #e8eef7 with slight inner shadow.
- Snow cloud: similar but maybe white #f4f8ff.
- Storm cloud: dark slate #3a4256 / #2c3447.
Actually, gradients on clouds: I could make the base `.cloud` a gradient (linear-gradient top light → bottom slightly darker) and give pseudos `background: inherit`... The pseudo's background: inherit takes the computed value which is the gradient — but the gradient is painted relative to the pseudo's own box, so the big puff circle would show the whole gradient range across its 40px — could look slightly different in tone than the base at that position. It might actually look fine-ish, but risky. Solid colors with a subtle `box-shadow: inset` are safe. Or: give each pseudo its own explicit color slightly lighter than base to simulate lighting: e.g., base #cfd9ea, before (big puff) #dbe4f2, after #c4cfe3... The mismatched tones might look off. Better: all same solid color, plus a large soft `box-shadow` underneath for shading (`inset 0 -6px 10px rgba(0,0,0,.08)` won't show on pseudos though).
Cleaner: make the entire cloud ONE element using a trick: the main pill + ::before big circle + ::after small circle, all solid same color — then apply `filter: drop-shadow(0 10px 18px rgba(...))` on the cloud container for an ambient shadow, and optionally a subtle gradient overlay via... eh. Solid color clouds look fine, honestly, especially with drop-shadow filter for depth. Also add slight gradient: `background: linear-gradient(#eef3fb, #c9d6e8)` on the base pill and solid #dfe8f5 on puffs — the tone difference might read as the puffs being lit. Let me try: puffs slightly lighter (they're on top, catching light) — actually that's realistic! Puff circles = tops of cumulus, lighter; base pill = bottom, slightly darker. So: puffs #e9f0fa, base pill gradient #e9f0fa → #c7d3e6. That reads well.
For the storm cloud: dark: puffs #4a5570, base #39415a→#2b3247 gradient-ish. With drop-shadow.
Cloud bob animation: translateY(0)→translateY(-6px)→0, 5-6s ease-in-out infinite. For storm, maybe a subtle horizontal drift ±4px.
Now, DOM structure per card:
```html
<article class="card card--sun">
<div class="scene">
<div class="sun">
<div class="sun-rays"></div> <!-- rotating -->
<div class="sun-core"></div> <!-- pulsing -->
</div>
</div>
<div class="card-body">
<div class="meta">
<h2 class="city">Lisbon</h2>
<span class="tag">Sunny</span>?
</div>
<div class="temp">24°</div>
<p class="desc">Clear skies with a gentle Atlantic breeze.</p>
</div>
</article>
```
For rain:
```html
<div class="scene">
<div class="cloud cloud--rain"></div>
<div class="drops">
<span></span> × 6 or 7
</div>
</div>
```
Drops: `.drops span` styled; nth-child delays & left positions. Each span is an HTML element ✓.
Snow:
```html
<div class="scene">
<div class="cloud cloud--snow"></div>
<div class="flakes">
<span class="flake"></span> × 6
</div>
</div>
```
Each flake: asterisk via pseudo-elements; fall+sway+spin keyframes; nth-child delays/positions/durations.
Storm:
```html
<div class="scene">
<div class="cloud cloud--storm"></div>
<div class="drops drops--storm"><span></span>×5</div>
<div class="bolt-wrap"><div class="bolt"></div></div>
(flash overlay via .scene::after or a .flash div)
</div>
```
Card layout:
- Scene: fixed height ~150-170px, position relative, overflow hidden (so drops fall "out of view" at bottom? Drops should fall within the scene and fade; overflow hidden clips them nicely). But hover lift + overflow hidden fine.
- Body: padding, city, temp, desc.
Card backgrounds:
- Sun card: warm — `linear-gradient(160deg, #FFB627 ... )`? Something like `linear-gradient(165deg, #ffd45c 0%, #ff9d3c ...)`? A sunny sky: warm amber to orange. Maybe `linear-gradient(180deg, #4fc3f7 ... )`? Sunny = blue sky with sun; but each card should be distinct: sun card could be warm golden/amber. Let me pick: sun: `linear-gradient(165deg, #ffcf5c, #ff9a3d)` hmm that's very orange. Alternatively `#FFD194 → #FF9A8B`? Let me choose distinct, tasteful palettes:
- Sunny: sky blue to warm: `linear-gradient(160deg, #56ccf2 0%, #f2b53a 100%)`? Hmm. I want the icons to pop. Let me design:
- Sunny: `linear-gradient(160deg, #ffb347, #ff8c42)` too monotone-orange... Actually a golden gradient with a bright yellow sun works: bg `#f7b733 → #fc4a1a`? too red.
Let me think like a designer: four cards in a row, need variety but harmony. Dark-ish page background (deep navy #0d1220) makes colorful cards pop with white text? But text on cards: city/temp in white/dark depending on card brightness.
Palette plan:
- Sunny: warm amber gradient `linear-gradient(165deg, #fbb03b, #f76b1c)` — dark text? White text on amber is low contrast; use deep brown/near-black text `#3a2200`? Or keep card text white with a slightly deeper gradient `#f7971e → #ff5858`? Hmm red. Let me use: `linear-gradient(165deg, #ffb75e 0%, #ed8f03 100%)` with dark text `rgba(60,30,0,.9)`? Mixed text colors across cards could look inconsistent.
Alternative approach: all four cards use fairly saturated mid-to-dark gradients so white text works everywhere:
- Sunny: `linear-gradient(160deg, #f7b733, #d95f18)`? White on #d95f18: contrast ~3:1... borderline. Use darker: `#e1801a → #b64a08`? Might look muddy.
Or embrace light cards with dark text on some. Many weather UI designs use light gradient cards with dark navy text. Let me do: page bg deep navy; cards:
- Sun: bright warm `linear-gradient(165deg, #ffd452 0%, #ff9231 100%)`, text: `#4a2500`-ish dark brown — actually deep warm brown text `rgba(73, 34, 0, 0.85)`... hmm, or `#5d3a00`.
- Rain: cool blue `linear-gradient(165deg, #6ea8d8 ... )`? Rain: `#5b9bd5 → #3a6ea5`? White text works on the darker parts... inconsistent.
Simpler & consistent: white text on all cards, with each gradient dark enough at bottom and use text-shadows for legibility:
- Sunny: `linear-gradient(160deg, #ff9d2e, #e2551c)`? Hmm.
Let me look at it differently — dark-mode style cards with vivid gradient "sky" but the lower info section slightly darkened via an overlay so white text always reads. I can add a subtle darkening at the bottom of each card via an overlay gradient (transparent → rgba(0,0,0,.25)) using the card's ::after. Then white text with slight shadow is readable on all. That's a robust trick.
Gradients (vivid):
- Sun: `linear-gradient(165deg, #ffbe45 0%, #ff7b00 55%, #e85d04 100%)` — vivid amber-orange.
- Rain: `linear-gradient(165deg, #7cc0ee ... )`? Rain: steel blue: `#6a9fd8 → #40699e`? More vivid: `#54a0d8 → #2d5f8f`. Or a moodier `#8fb8d8 → #4a6f92`. I'll go `linear-gradient(165deg, #6db3e8 0%, #3f6fa8 100%)`? Hmm, maybe with a touch of purple: `#5d9cec → #35577d`.
- Snow: frosty pale — white text won't read on pale. Snow card: `linear-gradient(165deg, #a8c8e8, #e6f0fa)` pale → dark text needed. Conflict. Make snow a deeper ice blue instead: `#8fb7de → #5b7fa8`? Or frosty teal `#7fc9e8 → #4a90b8`. Hmm. With the darkening overlay, pale cards still problematic for white text. Decide: snow card uses icy blue deep enough: `linear-gradient(165deg, #9fc5e8 0%, #6a8cae 100%)`? white text on #6a8cae ~2.8:1 — meh but it's a demo; with overlay + text-shadow it's okay. Alternatively give snow card dark navy text? Let me instead not use pure white text everywhere — use CSS variables per card: `--ink` set per card. Sun & snow: dark ink (`#43290a`, `#173a5e`); rain & storm: white ink. Hmm, mixed.
Actually — hmm, let me reconsider: a cleaner professional look: each card has a "sky" gradient in the scene area at top, and the body/lower area is a deeper, shared dark surface... but requirement says "each card has its own background gradient matching the weather" — the whole card should be the gradient. Fine.
Let me just pick gradients that all support white text reasonably (medium-dark), with a bottom overlay for extra safety:
- Sunny: `linear-gradient(170deg, #f9a825 0%, #ef6c00 100%)` (amber → deep orange). White on #ef6c00: ~3.2:1, on #f9a825 ~2.2:1 — weak. With dark overlay bottom only, top text? Text is in the lower half, so the overlay helps where text sits. Scene is top. City name sits mid-card where gradient is ~#ea7a15 + overlay → OK with text-shadow.
Hmm, I keep going back and forth. Let me be decisive with a solid system:
**Design system**: Page bg: deep navy `#0b1020` with subtle radial glows. Cards: rounded 24px, each with its own vivid gradient, white text everywhere, each card gets an inner overlay `linear-gradient(to top, rgba(9,12,28,.38), rgba(9,12,28,0) 55%)` — wait that would gray-out the vivid gradient bottom. It's fine — it grounds the text. Plus `text-shadow: 0 1px 2px rgba(0,0,0,.25)`.
Gradients (chosen for both beauty and contrast, medium-dark):
- Sun: `linear-gradient(168deg, #ffb340 0%, #ff8a00 42%, #f4630e 100%)`. Vivid.
- Rain: `linear-gradient(168deg, #5ea9e0 0%, #3c6ea5 55%, #2c5586 100%)`? Slightly dull. More atmospheric: `#4b9ce0 → #245a94`. OK.
- Snow: `linear-gradient(168deg, #b8d8f0... ` pale. Instead: `#8fc1e9 → #5e88b8`? Let me do frosty: `linear-gradient(168deg, #9ed2f0 0%, #6ea3cf 60%, #4d7cae 100%)` — icy, medium. White text on #6ea3cf: ~2.6:1 + overlay ok.
- Storm: `linear-gradient(168deg, #4b5568 0%, #2e3547 60%, #1d2233 100%)` — moody slate. White text great.
These four look distinct and cohesive. The sun card is the brightest — could use dark ink for the sun card only... you know what, many real weather dashboards (e.g., Apple Weather) use white text on warm gradients with subtle shadows. I'll go white everywhere + overlay + shadow. For extra legibility, city name font-weight 800.
Actually, let me reconsider once more: I could make the bottom overlay stronger and also add a slight `backdrop` behind text via the body section: `.card-body` could have `background: linear-gradient(...)`. Nah — overlay approach is fine.
**Page layout**:
```html
<main>
<header class="page-head">
<p class="eyebrow">Live forecast · CSS only</p>
<h1>Four skies, one stylesheet</h1>
<p class="lede">Hand-built weather icons — no images, no SVG, no JavaScript. Every drop, flake and ray is pure HTML + CSS.</p>
</header>
<section class="grid">
4 × article.card
</section>
<footer note about hover / reduced motion? maybe a small hint line.
</main>
```
Wait — "no JavaScript" is required; a small footer note mentioning pure CSS is a nice touch. Also a hint "hover a card" maybe. Keep header compact so cards fit in 1280×800 viewport: header ~120px + cards ~420px → fits.
Typography: no external fonts allowed? "No external files, images, fonts or network requests, except CDN URLs explicitly allowed by the task." The task didn't allow any CDN. So system font stack only: `-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`. That's fine; I'll craft nice sizes/weights/letter-spacing so it still looks designed. Maybe use a serif display for the h1 via Georgia/"Iowan Old Style"? A mixed serif/sans could add character. Let me use `Georgia, 'Times New Roman', serif` italic for the lede? Hmm — I'll do: h1 in a strong sans with tight letter-spacing, eyebrow in uppercase letterspaced mono-ish (`ui-monospace, "SF Mono", Menlo, Consolas, monospace`). Temperatures huge, thin-ish weight with `font-feature-settings`? Just large bold-light. Temperature could be `font-weight: 200` large — with system fonts, thin weights render on macOS; on Windows, Segoe UI Light exists (weight 200 supported). Fine.
Grid:
```css
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
@media (max-width: 1000px) { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 560px) { grid-template-columns: 1fr; }
```
Max content width ~1200px centered. At 1280 viewport: 4 cards ~285px each — good.
Card sizes: min-height ~ 400-430px? Scene ~ 170px, body ~ 200px. Let me let content define height; set `.scene { height: 168px }` and body padding. Cards in a row stretch to equal height automatically (grid).
Hover lift: `.card { transition: transform .35s cubic-bezier(...), box-shadow .35s; } .card:hover { transform: translateY(-10px); box-shadow: bigger; }` Also maybe `scale(1.02)`. Add `will-change`? Not necessary. Since no JS, hover only — fine. Also `:focus-within`? Cards aren't focusable (no links). Could add `tabindex`? Without JS, focus styles via `:hover` and maybe `.card:hover .scene` slight zoom of icon. Add small interactive flourish: on hover, the icon scales up slightly and animations speed up? Speeding up CSS animations on hover requires re-declaring animation-duration — changing duration mid-animation restarts the animation (in Chrome, changing animation-duration... actually changing duration causes a jump/restart? Changing animation-duration recalculates current progress: it can jump). Avoid changing durations. Just scale scene contents on hover: `.card:hover .sun, .card:hover .cloud { transform: scale(1.06) }` — but these elements already have animations on transform (bob) — the hover transform would be overridden by animation (animations override the transform property... CSS animations override inline/declarative values during the animation since they're at the "animation origin" which beats normal author styles). So hover scale on an animated element won't apply unless I scale a wrapper. Scale the `.scene` itself (scene has no transform animation): `.card:hover .scene { }` hmm scaling the scene scales drops too — actually that's fine and looks nice. Or wrap icon groups in `.icon` divs and scale those. Let me scale `.scene` by 1.05 with transform-origin center, transition. But the sun-ray rotation is inside, unaffected. Nice.
Wait, careful: `.scene` has `overflow: hidden` — scaling scene scales the clip region too (whole rendered box scales), so drops still clipped consistently. Fine.
Also add a subtle "shine" sweep on hover? Could do with `.card::before` gradient sweep animated on hover via transition — a diagonal white translucent band that moves across on hover: `.card::before { content:""; position:absolute; inset:0; background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.18) 45%, rgba(255,255,255,.05) 55%, transparent 70%); transform: translateX(-120%); transition: transform .8s; } .card:hover::before { transform: translateX(120%); }`. Nice premium touch. Need `.card { overflow: hidden }` for the sweep — but overflow hidden on card + absolutely positioned scene is fine. Hmm, overflow hidden on the card is fine since everything's inside.
But wait: overflow:hidden on `.card` + hover translateY — fine.
**Reduced motion**:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-play-state: paused; }
...
}
```
Better: disable animations entirely: `animation: none !important; transition-duration: 0.01ms? ` But then rain drops would be frozen mid-air at their natural position (their static CSS position). For static positions: drops' base position — if animation none, each span sits at its CSS-positioned location with default opacity. Let me set drops' default opacity to something visible (e.g., .9) so with animations off they still show as a static icon — a still weather icon. That's actually ideal: reduced motion → static but complete icon. So design static states to look right: each drop positioned at staggered heights with opacity .9; flakes positioned mid-fall, static; sun rays static (rotation paused at 0); bolt visible (opacity 1? its keyframes end at 0... with animation:none it takes the declared opacity value — set `.bolt { opacity: 0 }` declared? Then with reduced motion it's invisible. Better: declare `.bolt { opacity: 1 }`? Then during animation the keyframes control opacity anyway (they fully override between 0-100%). Yes: keyframes with 0% and 100% specified override the base value throughout. So base `opacity: 1` → reduced motion shows a static bolt ✓. Similarly the flash overlay base opacity 0 or low.
Also transitions: for reduced motion, `transition: none` to make hover instant (still functional).
And maybe respect that completely:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;?
```
Hmm, using `animation: none !important` is cleanest but `!important` — allowed in CSS obviously. But careful: `*::before` etc. Let me write:
```css
@media (prefers-reduced-motion: reduce) {
.sun-rays, .sun-core, .cloud, .drops span, .flakes .flake, .bolt-wrap .bolt, .scene-flash, ... { animation: none !important; }
.card, .scene { transition: none !important; }
}
```
Enumerating is verbose but explicit; or use the universal selector. I'll use universal:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation: none !important;
transition: none !important;
}
}
```
This kills all motion. Then ensure static fallbacks look right (positions/opacity defaults). Let me make sure:
- `.sun-rays` static: full starburst behind sun ✓ (rotation 0 base).
- `.sun-core` static: scale 1 ✓.
- `.cloud` static: at natural position ✓ (bob keyframes start at translateY(0), base transform none ✓).
- drops: base CSS sets each drop's `left` and `top` start; with animation none, drop stays at its start position — that's right under the cloud, which looks like the moment before falling — a bit odd but fine; better to statically position them at varied heights: I'll set each drop's base `top` to a staggered value (e.g., 0%, 20%, 10%, 35%... within the drop field) so a static rendering shows scattered drops ✓. But during animation the keyframes override transform — keyframes translate from 0 to 70px; base top staggers ✓.
- flakes: same — base top staggered, plus static rotation is 0 (asterisk upright ✓).
- bolt: base opacity — during animation keyframes fully define opacity (0% at opacity 0 ... 100% opacity 0) so base 1 only matters when animation is off ✓. But hold on: with animation running, is the bolt invisible most of the time? The flash keyframes: I want it visible briefly each cycle, dim rest. Also the storm card with reduced motion shows a static bolt ✓.
- storm flash overlay: base opacity 0 ✓ (static: no flash, fine).
Also for reduced motion, the hover lift transition none = instant jump — acceptable (or keep a gentle transition? No—reduce means minimize motion; instant is fine/standard).
**Now detailed icon specs.**
Scene: height ~170px, relative, overflow hidden. Contents absolutely positioned.
*Sun card*:
```css
.card--sun .scene { display:grid; place-items:center; } /* or position relative + absolute icon */
.sun { position: relative; width: 130px; height: 130px; }
.sun-rays { position:absolute; inset: 0; border-radius: 50%?;
background: repeating-conic-gradient(from -6.5deg, rgba(255,214,102,.95) 0deg 13deg, rgba(255,214,102,0) 13deg 45deg);
-webkit-mask: radial-gradient(circle at 50% 50%, transparent 34%, #000 37% 58%, transparent 62%);
mask: same;
animation: spin 28s linear infinite;
}
.sun-core { position:absolute; inset: 26px; border-radius: 50%;
background: radial-gradient(circle at 35% 32%, #fff6c8, #ffd23e 55%, #ffb300);
box-shadow: 0 0 34px 8px rgba(255, 200, 60, .55);
animation: pulse 4.5s ease-in-out infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }
@keyframes pulse { 0%,100% { transform: scale(1);} 50% { transform: scale(1.07);} }
```
Rays count: 45° period → 8 rays, each 13° wide with 32° gaps. from -6.5deg centers the first ray at 0deg (top). Rays are pie slices between radius 34% and 58% of 130px → 44 to 75px radius; core is inset 26 → radius 39px, slightly overlapping ray start (rays start at 44px radius, core 39 + glow) — good, small gap then rays. Actually mask percentages relative to the element box: `radial-gradient(circle, transparent 34%, black 37%)` — percentages of the gradient ray... for `circle` default size is farthest-corner, which for a 130×130 square is corner distance ≈ 91.9px... hmm! radial-gradient default `farthest-corner`: percentages resolve against that (radius = distance to farthest corner = 130*√2/2 ≈ 91.9). So 34% of 91.9 ≈ 31px, 37% ≈ 34px, 58% ≈ 53px, 62% ≈ 57px. So rays from ~34px to ~53px radius; core radius 39px — core would cover inner part of rays; rays visible 39→53px. Hmm, rays would be short (14px). Let me instead specify explicit sizes: `mask: radial-gradient(circle closest-side, transparent 55%, #000 60%, #000 88%, transparent 92%)` — `closest-side` radius = 65px. 55%→35.75px, 88%→57px. Rays between ~39px and ~57px → 18px long. With 130px element and core inset 26 (r=39), rays from 39 to 57: length 18px, decent. Make element a bit bigger: 150px, core inset 30 (r=45), mask closest-side (r=75): rays 45%*75=... let me define mask stops in % of 75: inner transparent to 52% (39px), black 56%-85% (42-63.75px), transparent 90%. Core r=45 → rays visible 45→63.75 ≈ 19px. OK similar.
Actually simpler: make the rays element larger than the core with the mask producing ring, and tune numbers visually. Let me set `.sun { width: 150px; height: 150px }`, `.sun-rays { inset: 0 }` with `mask: radial-gradient(circle closest-side, transparent 52%, #000 57%, #000 88%, transparent 94%)`. closest-side = 75px → inner black starts 42.75px, ends 66px. `.sun-core { inset: 34px }` → r = 41px. Core (41) covers to 41px; rays 42.75→66px. Ray length ~23px ✓. Glow box-shadow extends beyond.
Rays with `repeating-conic-gradient(from -8deg, rgba(...) 0 16deg, transparent 16deg 45deg)` → 8 rays, 16° wide, centered? from -8 means first ray spans -8..8 → centered at top ✓.
Ray color: on the amber card bg (#ffb340→#f4630e), rays should be a bright yellow-white: `rgba(255,241,190,.95)`? Core bright yellow. Rays slightly softer: `rgba(255, 224, 130, .9)`. Hmm rays maybe should be a lighter tone of the sun: #ffe08a. And maybe give rays rounded tips? Can't easily. Wedge rays fine.
Also, could add a soft halo behind everything: `.sun::before` big radial glow? The core's box-shadow already glows. Add scene-level glow: `.card--sun .scene::after { content:""; position:absolute; inset:auto; width: 240px; height: 240px; border-radius: 50%; background: radial-gradient(circle, rgba(255,235,160,.5), transparent 70%); ... }` A big soft halo centered — makes it dreamy. Nice. Place behind icon (z-index). Actually the scene could have a `::before` halo. But scene::after is used for storm flash; different cards can define differently.
*Rain card*:
Scene relative. Cloud top-center: `.cloud--rain { position: absolute; top: 26px; left: 50%; transform: translateX(-50%); }` — but bob animation uses translateY... combining translateX(-50%) base with animation transform translateY: the animation overrides transform entirely, losing the centering! Careful. Options: position cloud with left: calc(50% - width/2) using fixed width (width known: 96px → left: calc(50% - 48px)). Then animation transform translateY only ✓. Or animate a wrapper. I'll use explicit widths & `left: calc(50% - Wpx)`. But `.cloud` width differs per type; set per type.
Alternatively make bob keyframes include translateX(-50%): `@keyframes bob { 0%,100% { transform: translate(-50%, 0);} 50% { transform: translate(-50%, -7px);} }` — that works with left:50%. Different cloud sizes share it ✓. But storm cloud drift adds translateX variation: separate keyframes with -50% base ✓.
Cloud sizes: rain cloud: width 104px, height 36px; ::before circle 44px at top:-22px left:16px; ::after 34px top:-16px right:18px. Colors: puffs #e6eef9; base gradient `linear-gradient(#e6eef9, #bccfe6)`. Hmm ::before/::after solid #e6eef9 (matches top of gradient) so seams invisible where they meet the pill top ✓ (pill top color equals puff color → seamless ✓). The pill then shades darker toward bottom = nice volume.
`filter: drop-shadow(0 12px 18px rgba(30,60,110,.35))` on cloud — drop-shadow on element with pseudos applies to union shape ✓ (filter applies to the element including pseudo rendering ✓).
Rain drops: `.drops { position: absolute; inset: 0; }` with spans:
```css
.drops span {
position: absolute;
width: 3px; height: 14px;
border-radius: 2px; /* or 50% 50% 60% 60% ... make it a streak: border-radius: 40% */
background: linear-gradient(rgba(190,225,255,0), rgba(190,225,255,.95))? or solid rgba(210,235,255,.85);
top: 84px; /* below cloud */
animation: rainfall 1.6s linear infinite;?
}
```
Wait — with staggered static tops for reduced-motion, plus animation translate 0→~60px. Keyframes:
```css
@keyframes rainfall {
0% { transform: translate3d(0, -6px, 0) rotate(9deg); opacity: 0; }
12% { opacity: .95; }
80% { opacity: .85; }?
100% { transform: translate3d(-8px, 62px, 0) rotate(9deg); opacity: 0; }
}
```
Hmm rotate with translate x drift -8px: slanted rain ✓. Actually rotate(9deg) on the streak tilts the streak itself; combined with slight x drift — nice.
Drop positions: 7 spans with nth-of-type:
1: left 30%, top 86px, delay 0s, dur 1.5s
2: left 42%, top 96px, delay .45s
3: left 54%, top 88px, delay .9s
4: left 66%, top 98px, delay .2s
5: left 78%, top 90px, delay .7s
6: left 36%, top 102px, delay 1.1s
7: left 60%, top 100px, delay .55s
Also vary duration slightly (1.4-1.7s) so pattern doesn't look mechanical. Scene height 170; cloud bottom ~ 26+36 = 62px + drop start ~86 → falls to ~148 ✓ within scene.
Also add a subtle "puddle ripple" at scene bottom? Could add `.drops span::after` — no, ::after on each span = splash ellipse at bottom of fall? The span moves; a splash should appear where the drop lands, not move with it. Could do separate ripple elements at scene bottom with expanding ring animation — cute but adds DOM. Skip; or one ripple via `.scene::before`? Hmm. Keep scope controlled — I'll skip puddles. Maybe add them... Let me consider adding 2 puddle ripples on the rain card: `.ripple` divs at bottom: border ellipse, animation scale+fade infinite. It's a lovely detail. `border: 2px solid rgba(220,240,255,.5); border-radius: 50%; width: 24px; height: 8px;` animate scale 0.3→1.6 opacity .8→0, staggered. With reduced motion: static visible ellipse — fine (looks like a puddle ✓). I'll add 2 ripples to the rain scene. OK.
*Snow card*:
Cloud similar, slightly whiter. Flakes: 6 spans, each `.flake`:
```css
.flake { position: absolute; width: 12px; height: 2.5px; background: #fff; border-radius: 2px; opacity: .95;
filter: drop-shadow(0 0 3px rgba(255,255,255,.8)); }
.flake::before, .flake::after { content:""; position:absolute; left:0; top:0; width:12px; height:2.5px; border-radius:2px; background:#fff; }
.flake::before { transform: rotate(60deg); }
.flake::after { transform: rotate(-60deg); }
```
Then animation `flake-drift` as designed. Center the asterisk: the parent is 12×2.5; pseudos at left:0 top:0 rotate about their own centers — pseudo center = (6, 1.25) = parent center ✓ since parent is 2.5 tall, its center is (6,1.25) ✓.
But wait — `transform` on `.flake` from the animation (translate+rotate) — and pseudos have their own rotate ✓ independent.
Positions: 6 flakes across, staggered tops (static good), durations 4.5-7s, delays spread. Keyframes:
```css
@keyframes flake-drift {
0% { transform: translate3d(0,-8px,0) rotate(0deg); opacity: 0; }
8% { opacity: 1; }
30% { transform: translate3d(9px, 20px, 0) rotate(140deg); }
55% { transform: translate3d(-7px, 42px, 0) rotate(260deg); }
85% { opacity: 1; }
100% { transform: translate3d(6px, 66px, 0) rotate(360deg); opacity: 0; }
}
```
Also scale variance: some flakes smaller: `.flake.small { transform scale }` — careful: animation overrides transform. Instead vary size via width: a `.flake` with width 9px → pseudo width 9px too (set pseudos to inherit width: `width: 100%`? pseudo `width:100%` of parent ✓, height 100%? height:100% = 2.5px ✓). Use `width: 100%; height: 100%; left: 0; top: 0;` for pseudos → scales with parent width ✓. Then set parent width 12px or 8px per nth-child.
Hmm wait: pseudo `height: 100%` of a 2.5px parent ✓.
Also add a second tiny snowfall? 6 flakes enough. Add scene-level faint dots via box-shadow? Skip.
Also maybe add snow drift near bottom: a soft white haze at scene bottom: `.card--snow .scene::after { content:""; position:absolute; left:0; right:0; bottom:0; height: 40px; background: linear-gradient(to top, rgba(255,255,255,.35), transparent); }` = snowy ground mist ✓. Nice.
Similarly rain card could have a darker haze at bottom. Sun card: warm halo. Storm: flash overlay.
*Storm card*:
Dark cloud (bigger, lower). Colors: puffs #565f7a? Let me do: puffs #4e5878, base gradient `linear-gradient(#4e5878, #2b3350)`. Hmm on a slate card bg (#4b5568→#1d2233) the cloud needs to read — lighter than bg: puffs #6a7594, base → #3c4560. Let me pick bg storm: `linear-gradient(168deg, #454f66 0%, #272e42 55%, #161b2b 100%)`. Cloud puffs #79839f? Slightly bluish-gray, brighter than bg ✓. Base gradient `#79839f → #545d78`. drop-shadow dark.
Heavy rain streaks (angled more, faster, slightly bluish-white rgba(200,220,255,.8)) — 5 spans, slanted ~14deg.
Bolt: below cloud, center-right: `.bolt-wrap { position:absolute; top: 70px; left: 50%; margin-left: 6px;? width: 56px; height: 90px; filter: drop-shadow(0 0 12px rgba(255, 214, 90, .9)); animation: bolt-glow? }` — wait, filter on wrap creates the glow; flicker via opacity on wrap or bolt. Opacity animation on the wrap (filter+all) — fine.
`.bolt { width: 100%; height: 100%; background: linear-gradient(#fff7c4, #ffd54a 45%, #ffb300); clip-path: polygon(35% 0, 95% 0, 66% 45%, 90% 45%, 20% 100%, 28% 55%, 0% 55%); }`
Bolt width 54px, height 88px. Position: top ~64px (just below cloud bottom ~ cloud top 30 + height 40 = 70) — put bolt top at 66px, overlapping cloud slightly. Bolt height 88 → bottom at 154 ✓ within 170 scene.
Flash keyframes:
```css
@keyframes bolt-flash {
0%, 100% { opacity: 0; }
70% { opacity: 0; }
73% { opacity: 1; }
75% { opacity: .15; }
78% { opacity: .95; }
82% { opacity: .3; }
86% { opacity: 0; }
}
```
duration 5s infinite. Base opacity: for reduced motion static → want bolt visible: base `opacity: 1`? With animation running, keyframes specify opacity at 0% (0) and 100% (0) and all ranges covered by keyframes → base never used. But between 86% and 100%... keyframes: 0%(0), 70%(0), 73%(1), 75%(.15), 78%(.95), 82%(.3), 86%(0), 100%(0) — full coverage ✓. So base opacity:1 gives static bolt under reduced motion ✓.
Wait — but hold on: base opacity 1 means during the animation-delay before start? No delay. Also on hover... fine.
Also add scene flash: `.card--storm .scene::after { content:""; position:absolute; inset:0; background: radial-gradient(ellipse at 50% 18%, rgba(255,255,255,.55), rgba(255,255,255,0) 60%); opacity: 0; animation: sky-flash 5s infinite; }` with sky-flash keyframes offset from bolt (e.g., flash slightly after bolt start: peaks at 74%, 80%):
```css
@keyframes sky-flash {
0%, 100% { opacity: 0; }
72% { opacity: 0; }
74% { opacity: 1; }
76% { opacity: .1; }
79% { opacity: .8; }
83% { opacity: 0; }
}
```
Both 5s so they stay synced ✓. Cloud could also get a light edge during flash — the scene::after overlay sits above cloud (z-index order: pseudo of scene paints after children if positioned... `.scene::after` with position absolute — stacking: it's a child of scene painted per tree order — pseudo ::after paints after (above) the scene's children if both non-positioned? Children with position absolute create stacking participation by tree order; ::after comes last in tree order → paints above ✓ if no explicit z-index issues. The bolt wrap also absolute — tree order: cloud, drops, bolt-wrap, then ::after ✓ flash above all ✓ good — flash tints everything slightly ✓ realistic.
Also thunder cloud darkens/pulses? Enough.
Card body content:
- Sunny — **Lisbon**, 24°, "Clear skies with a warm Atlantic breeze."
- Rainy — **London**, 14°, "Steady drizzle all afternoon; grab a brolly."
- Snowy — **Reykjavík**, -3°, "Light snow drifting in from the north."
- Stormy — **Singapore**, 27°, "Thunderstorms rolling through until dusk." (Singapore storms ✓)
Maybe include a small "H:xx° L:xx°" line for realism — adds credibility. Structure body:
```html
<div class="card-body">
<div class="card-top">
<h2 class="city">Lisbon</h2>
<div class="hi-lo"><span>H 26°</span><span>L 17°</span></div>
</div>
<div class="temp">24°</div>
<p class="desc">...</p>
</div>
```
Layout: city + temp maybe on one row? Classic weather card: big temp, city below or beside. Let me do: city row (city left, hi/lo right), then huge temp with a description. Temp with degree. Maybe temp row: `24°` big + right side could show weather label like "SUNNY" chip. Add a condition label chip: small pill with uppercase text ("Sunny", "Rain", "Snow", "Thunder") — put next to city or above temp. Let me structure:
```html
<div class="card-body">
<div class="row1">
<h2>Lisbon</h2>
<span class="cond">Sunny</span>
</div>
<div class="temp">24°</div>
<p class="desc">Clear skies with a gentle breeze off the Atlantic.</p>
<div class="row2">
<span>H 26°</span> <span>L 17°</span> <span wind? "Wind 12 km/h">
</div>
</div>
```
The extra stats row adds realism: `H 26° / L 17° / ♸ wind` — no icons (no icon fonts!) — text only: "H 26° · L 17°". Or three mini stats with labels: "HUMIDITY 40%", "WIND 12KM/H", "UV 5". Keep simple: one line: `H 26° · L 17° · Wind 14 km/h`. Hmm — keep it as small meta row: `H 26° · L 17° · Feels 26°`? I'll do `H:26° L:17°` plus humidity/wind per card with small separators.
Also each card could have a top-right "live" dot? A little "NOW" pill with pulsing dot — cute: `<span class="live"><i class="dot"></i> Live</span>`? Pulsing dot = tiny animation ✓. Maybe skip to avoid clutter. Actually a small touch: put the condition chip ("SUNNY") top-right of body row 1 — good enough.
Fonts/sizes: city 1.15rem 800; temp 3.4rem weight 250? Use weight 200 with fallback; system fonts OK. desc .92rem, line-height 1.5, color rgba(255,255,255,.85). meta row .78rem uppercase letterspaced, rgba(255,255,255,.65).
Header design: eyebrow `PURE CSS · NO IMAGES · NO JS` in mono uppercase, h1 "Four skies, one stylesheet." with maybe gradient text? A subtle white. lede gray. Footer note: "Hover a card — everything you see is HTML elements, pseudo-elements and keyframes."
Page bg: `#070b16` with two radial gradient glows (purple/blue top-left, orange bottom-right, very subtle). Also maybe a faint grid? Keep clean.
Card shadow: `box-shadow: 0 18px 40px rgba(3,7,18,.45)`; hover: translateY(-10px) + `box-shadow: 0 30px 60px rgba(3,7,18,.55)`.
Card border: 1px solid rgba(255,255,255,.14) + inner top highlight `inset 0 1px 0 rgba(255,255,255,.25)`.
Also each card gets the darkening overlay: `.card::after { content:""; position:absolute; inset:0; border-radius: inherit; background: linear-gradient(to top, rgba(10,14,30,.32), rgba(10,14,30,0) 45%); pointer-events: none; }` — wait, I also wanted a shine sweep via `.card::before` ✓ and flash uses `.scene::after` ✓ so `.card::after` is free for the dark overlay ✓. But careful: `.card::after` overlay must sit above scene but text? It's above everything (tree order pseudo last) — text is in card-body, painted before ::after → overlay dims text slightly too — acceptable (uniform darkening at bottom). Actually it would wash the white text slightly — but since it's dark translucent, it INCREASES contrast for white text ✓ good.
z-index: `.card::before` shine above `::after`? Both fine, shine is translucent white band.
Wait, one more consideration: `.card { overflow: hidden }` — with the shine sweep translating beyond bounds, clipped ✓ good.
The shine sweep: on hover translateX from -130% to 130% — with a skewed band. Implementation:
```css
.card::before {
content:""; position: absolute; top: 0; bottom: 0; left: -40%; width: 40%;
background: linear-gradient(100deg, transparent, rgba(255,255,255,.28), transparent);
transform: skewX(-18deg) translateX(-160%);
transition: transform .9s ease;
z-index: 3; pointer-events:none;
}
.card:hover::before { transform: skewX(-18deg) translateX(460%); }
```
Hmm need to make sure it fully crosses: element width 40% of card, starts at left:-40% translated -160% (off left), on hover translateX(460%)?? Let me simplify: position left:-75%, width 50%; base transform translateX(0) → occupies [-75%, -25%] offscreen (card overflow hidden clips). Hover: translateX(400%) → moves 400% of its own width (200% of card) → final occupies [125%, 175%]? -75+200 = 125% ✓ fully off right. OK: `left:-75%; width:50%; transform: skewX(-15deg); transition: transform 1s; .card:hover::before { transform: skewX(-15deg) translateX(380%); }` -75 + 190 = 115% > 100 ✓. Good.
Reduced motion kills transitions → sweep jumps instantly on hover; acceptable. Or hide shine under reduced motion. Fine either way.
Now think about the "stacked on mobile" — at 375px, single column, cards full width; scene still 170px; body text fine. Header text sizes scale down via clamp.
Also page padding: `padding: 48px clamp(16px, 4vw, 40px)`. Max-width 1180px.
At 1280×800: header (~140px) + grid one row (~430px) + footer (~40px) ≈ 660px + paddings ≈ fits in 800 ✓ nice, all four cards visible on first paint ✓.
Let me now also double-check the sun rays mask + conic gradient for cross-browser: `mask` unprefixed supported in Chrome 120+ ✓ (and we include -webkit-mask for safety). `repeating-conic-gradient` Chrome 88+ ✓. `clip-path` polygon ✓. All good, no JS ✓.
One more requirement detail: "rotating sun rays" ✓, "falling rain drops" ✓, "drifting snowflakes" ✓, "flashing lightning" ✓.
Temperature values: sunny 24°, rainy 13°, snowy −2°, storm 28°.
Let me write descriptions:
- Lisbon 24° — "Clear skies and golden light with a gentle Atlantic breeze."
- London 13° — "Persistent drizzle settling in for the afternoon."
- Reykjavík −2° — "Light snow drifting in on a northerly wind."
- Singapore 28° — "Rumbling thunderstorms sweeping across until dusk."
Meta rows:
- Lisbon: H 27° · L 18° · UV 7
- London: H 15° · L 9° · 96% humidity? maybe "Wind 24 km/h"
- Reykjavík: H 1° · L −7° · Wind 18 km/h
- Singapore: H 30° · L 25° · Wind 40 km/h
Make meta uniform: `H 27° / L 18° / Wind 12 km/h` — three segments separated by dots. Use spans with separators via `.meta span + span::before { content:"·" }`? Simpler: put middot in markup: `<span>H 27°</span><span>L 18°</span><span>Wind 12 km/h</span>` and CSS gap + a `::before` dot on subsequent spans. Or just include "·" characters in HTML text. Fine: `<div class="meta"><span>H 27°</span><span>L 18°</span><span>Wind 12 km/h</span></div>` with `display:flex; gap: 14px` and `.meta span:not(:first-child)::before { content: "·"; margin-right: 14px; opacity:.5 }` hmm double spacing; simpler: use gap 8px and ::before content "· " no... Let me do: `.meta span + span::before { content: "·"; padding-right: 8px; color: rgba(255,255,255,.4) }` with no flex gap. Eh, I'll use gap: 0 and margin-right on ::before... Simplest robust: markup with explicit separators: `<p class="meta">H 27° · L 18° ...` hmm. I'll go with spans + CSS ::before dots and flex gap 10px; the ::before adds "·" right before each non-first span with margin-right 10px. Slight asymmetry invisible at small size. Or skip dots: use gap and a midline divider `border-left` on spans? `<span>` with `padding-left: 12px; border-left: 1px solid rgba(255,255,255,.25)` for non-first — clean tick dividers ✓. I'll do border-left dividers.
Now the scene background: should the scene area have any additional styling? Maybe a subtle inner sky detail per card: e.g., sunny scene has a brighter radial at top; keep via the halo.
Also, the sun should maybe not be dead-center but slightly high with rays — center is fine.
Snow scene ground mist ✓, rain scene bottom haze: `linear-gradient(to top, rgba(255,255,255,.08), transparent)`? For rain maybe a darker vignette at bottom: rgba(20,40,70,.25). OK.
Let me also consider adding small clouds on the sunny card? A tiny distant cloud drifting horizontally would be charming: a mini cloud (reuse .cloud with --scale?) drifting across the scene slowly. Sizes differ... `.cloud` has fixed dims; add `.cloud--mini` with smaller sizes and its own drift animation `translateX(-20%)→(20%)`? It'd need left positioning and translateX keyframes that keep it within scene: place at left: 12%, animate translateX(0→60px). With a 300px scene width... At mobile 375 card width ~340 scene width. Drift 0→60px safe. Hmm — but the mini cloud crossing behind the big sun... it'd pass over/under the halo. Could put it near top-left, small, semi-transparent white, drifting gently ±20px. Cute but risk of clutter; I'll add it — it makes the sunny scene feel alive. Mini cloud: width 54px height 18px, puffs scaled: ::before 24px top:-12 left:7; ::after 17px top:-9 right:9. Opacity .9. Animation: drift 9s ease-in-out infinite alternate: translateX(-14px)→translateX(26px)? With `alternate` — infinite alternate ✓ smooth ✓. Position: top: 22px; left: 16%. z-index under sun? Sun is centered; mini cloud at left 16% won't overlap much ✓. Actually alternate direction with transform — fine.
Hmm wait: `.cloud--mini` needs the bob too? Just drift. OK.
Now — cloud base positions: rain/snow clouds centered horizontally: `left: 50%` + keyframes include `translate(-50%, ...)`. For the storm cloud same. Let me define shared keyframes:
```css
@keyframes cloud-bob {
0%, 100% { transform: translate(-50%, 0); }
50% { transform: translate(-50%, -6px); }
}
```
Storm uses:
```css
@keyframes storm-bob {
0%, 100% { transform: translate(-52%, 0); }
50% { transform: translate(-48%, -5px); }
}
```
And `.cloud { left: 50%; position: absolute; }`. Static (reduced motion): transform none → cloud sits with left edge at 50% → half off-card! Problem: with animation:none, `.cloud` has no transform → left:50% means its left edge at center → cloud appears right-shifted and clipped! Fix: don't rely on transform for centering. Use `left: calc(50% - 52px)` with known width, and keyframes translate(0,0)→(0,-6px) ✓ static = perfect center ✓. Since each cloud variant has fixed width, calc offsets fine:
- .cloud--rain: width 104px → left: calc(50% - 52px)
- .cloud--snow: width 100px → calc(50% - 50px)
- .cloud--storm: width 116px → calc(50% - 58px)
- .cloud--mini: left: 10% (no centering needed), static transform none ✓ drift keyframes translateX only ✓.
Cloud heights: rain 34, snow 34, storm 40.
Base pill border-radius: `border-radius: 40px` (pill for h≥? fine).
Puff geometry per variant:
- rain (104×34): ::before w44 h44 top:-20 left:14; ::after w30 h30 top:-14 right:16.
- snow (100×34): ::before w42 h42 top:-19 left:14; ::after w29 h29 top:-13 right:15.
- storm (116×40): ::before w52 h52 top:-24 left:14; ::after w36 h36 top:-17 right:16.
- mini (54×18): ::before w24 h24 top:-11 left:6; ::after w17 h17 top:-8 right:7.
Colors:
- rain cloud: puffs #dfe9f7; base gradient `linear-gradient(#dfe9f7, #b9c9e4)`. drop-shadow(0 14px 18px rgba(25,50,95,.35)).
- snow cloud: puffs #eef5ff; base `linear-gradient(#eef5ff, #cad9ee)`; drop-shadow rgba(70,100,150,.35).
- storm cloud: puffs #6b7695; base `linear-gradient(#6b7695, #454e6b)`; drop-shadow rgba(5,10,25,.5).
- mini cloud: solid #ffffff with slight transparency? puffs/base #fff, opacity .85.
Hmm — one nuance: `.cloud::before/::after` need `background` solid matching base top color ✓. But the base pill is a gradient — its TOP color matches puffs ✓ seamless.
Also add a subtle inner shadow on pill bottom? The gradient handles it.
Now, snowflakes & raindrops positioned relative to scene; scene has overflow hidden — drops that animate beyond bottom get clipped ✓ (fade out anyway).
Detailed check — rain drops' base positions + keyframes travel: keyframes translateY -6 → 62px. Base tops ~86-104px → animated positions 80→~166 max ✓ scene 170 ✓.
Storm rain: base tops ~90; travel to 60px → 150 ✓. Bolt occupies center; storm rain drops placed left side and right side around bolt? Bolt at center-right (left: 52%?). Let me place bolt horizontally: left: calc(50% - 10px)? Bolt is 54 wide; center it slightly right: left: calc(50% - 27px + 10px) = calc(50% - 17px). Drops at left: 18%, 30%, 62%, 74%, 86% — avoiding the center band where the bolt is (44%-64%). Drop slant: rotate(14deg) with x drift -12px.
Hmm — with bolt flashing in the center and rain around, the composition reads well.
Storm cloud positioned slightly higher: top: 24px.
Now grid & responsive details:
```css
.grid { display: grid; gap: 22px; grid-template-columns: repeat(4, minmax(0,1fr)); }
@media (max-width: 1020px) { .grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 600px) { .grid { grid-template-columns: 1fr; } }
```
Card:
```css
.card { position: relative; border-radius: 22px; overflow: hidden; padding: 0 0 22px; ... display flex column? }
```
Body uses padding: `padding: 18px 20px 20px`. Scene: `height: 172px; margin-bottom: 6px?` Not needed.
Hover: `transform: translateY(-10px)` — note: `.card:hover` also used with `.card:focus-within`? no interactive content. Fine.
Also: hovering could reveal an extra detail? Keep simple.
Title/typography:
```css
h1 { font-size: clamp(1.9rem, 3.2vw, 2.6rem); letter-spacing: -0.02em; }
```
Also let me add a header-right element? Like a small legend "Hover to lift · 100% HTML+CSS". Put in footer line.
Also: maybe include `lang="en"` ✓, `<meta viewport>` ✓, title.
Now — the requirement "no images, no SVG, no icon fonts" — ✓ none. No emoji either (safer to avoid since emoji are fonts/icons — the requirement bans icon fonts; emoji as decorative characters might be judged as icons — avoid them entirely). Degree sign ° is fine (typographic).
Check "no inline event handler" ✓ none. No `<script>` ✓.
Let me also double check: something meaningful on first paint ✓ (cards fully visible, animations running immediately — infinite animations start automatically).
Now write all keyframes:
1. `spin`: `to { transform: rotate(1turn) }` — used by .sun-rays, 26s linear infinite.
2. `sun-pulse`: scale 1↔1.06, 4.5s ease-in-out infinite.
3. `cloud-bob`: rain & snow: 0/100 translate(0,0), 50% translate(0,-7px), 5.5s ease-in-out infinite. (No -50% needed since left uses calc ✓.)
4. `storm-bob`: include slight x sway: 0/100 translate(0,0); 50% translate(6px,-4px)? With alternate direction? linear timeline: 0%: (0,0); 50%: (7px,-5px); 100%: (0,0) — that's a back-and-forth sway (goes right then returns) — smooth with ease-in-out ✓.
5. `drift` (mini cloud): 0/100 translateX(0); 50% translateX(34px); 8s ease-in-out infinite.
6. `rainfall`: as above, 1.6s linear infinite (per-drop duration overrides).
7. `ripple`: scale(.4) opacity .0?? For static reduced-motion visible puddle: base opacity .5; keyframes: 0% transform scale(.35) opacity .7; 100% scale(1.5) opacity 0. Full coverage overrides base... base used only when animation off ✓ visible ✓.
8. `flake-drift`: as designed; per-flake durations 4.8–7.5s.
9. `bolt-flash`, `sky-flash`: as designed, 5s linear? Use `steps`? No—use ease-out per segment default `linear` timing between keyframes; flicker wants sharp jumps — with closely spaced keyframes it looks flickery ✓. Set `animation-timing-function: linear`? Actually for flicker, `steps(1)` per segment could give instant toggles: e.g. timing function `step-end`? Simpler: place keyframes 1% apart → transitions over 1% of 5s = 50ms — flickery enough ✓ with linear.
10. `sweep` not needed (transition).
Also the "live" dot? skipped.
Sun halo: `.card--sun .scene::before { content:""; position:absolute; width: 260px; height: 260px; left: calc(50% - 130px); top: calc(50% - 130px)... }` — scene is 172 tall; halo 260 centered on sun (sun centered in scene via place-items center? Let me absolutely position `.sun` too: `.sun { position:absolute; left: calc(50% - 75px); top: calc(50% - 75px); width:150px; height:150px; }` → top = (172-150)/2 = 11px ✓.
Halo as radial-gradient circle, centered same point: width 300 height 300, left calc(50% - 150px), top calc(50% - 150px) → extends beyond scene top (−28) and bottom (+100 → clipped) — overflow hidden clips ✓ fine. background: radial-gradient(circle, rgba(255,236,170,.55), rgba(255,236,170,0) 65%). Also maybe animate halo breathing (scale/opacity slow) — subtle ✓: `animation: halo 6s ease-in-out infinite;` keyframes opacity .7↔1? Use opacity only to avoid transform-origin fuss ✓. But under reduced motion: base opacity .8 static ✓.
Also sun scene bottom warm haze? skip.
Order of elements in sunny scene: halo (scene::before), sun (rays, core), mini cloud. Mini cloud should be behind sun or front? Behind sun visually — but sun is centered; mini at left 8% top 24px — probably no overlap; give mini cloud `z-index: 0` and sun `z-index: 1`? All are positioned; tree order: ::before first (halo), then children in order: put mini cloud first, then sun ✓.
Wait — scene::before painted before children? Pseudo ::before is first child in tree order ✓ paints below later siblings (same stacking context, tree order for positioned elements with auto z-index) ✓.
For rain scene: `.scene::before` = bottom haze. children: cloud, drops, ripples. For snow: haze + cloud + flakes. For storm: `.scene::after` = flash overlay (needs to be above children — ::after is last ✓). And maybe storm also wants bottom haze via ::before ✓ both pseudos usable ✓.
Now, are ripples needed? Nice-to-have; include 2 in rain scene:
```css
.ripple { position:absolute; bottom: 10px; width: 26px; height: 9px; border: 2px solid rgba(215,235,255,.55); border-radius: 50%; animation: ripple 2.6s ease-out infinite; }
.ripple.r2 { left: 66%; animation-delay: 1.3s; }
.ripple.r1 { left: 28%; }
@keyframes ripple { 0% { transform: scale(.3); opacity: .8; } 100% { transform: scale(1.6); opacity: 0; } }
```
Static (reduced motion): base opacity? Keyframes cover full range; set base `opacity: .55` so static shows a puddle ring ✓.
Hmm — but wait, `transform: scale(.3)` on an ellipse (width 26 height 9) — scales around center ✓.
Snow ground mist gives static nice too.
Body text colors: white with opacity variants.
Temperature display: big: `font-size: 3.6rem; font-weight: 200; letter-spacing: -0.03em; line-height: 1;` plus small "feels" note? Add superscript? Keep.
Maybe put temp and condition side by side: temp left big; right column: condition chip + hi/lo stacked? Let me do:
```html
<div class="card-body">
<div class="body-top">
<h2 class="city">Lisbon</h2>
<span class="chip">Sunny</span>
</div>
<div class="temp" aria-hidden? >24°</div>
<p class="desc">…</p>
<div class="meta"><span>H 27°</span><span>L 18°</span><span>Wind 12 km/h</span></div>
</div>
```
Chip: uppercase 10px bold letterspace, bg rgba(255,255,255,.16), border 1px rgba(255,255,255,.25), padding 4px 10px, radius 999px, backdrop-filter blur(4px)? backdrop-filter is fine (no network). Adds glassy feel ✓.
Divider between desc and meta: meta has border-top rgba(255,255,255,.18), padding-top 10px, margin-top auto? To align meta at card bottom across cards (equal heights via grid stretch): make `.card-body { display:flex; flex-direction: column; flex: 1 }` and `.card { display:flex; flex-direction:column }`, `.meta { margin-top: auto; }` — since cards stretch, bodies equal → meta aligned bottom ✓ nice.
Card as flex: `.card { display:flex; flex-direction: column; }`, `.scene { flex: none; }`, `.card-body { flex: 1; display:flex; flex-direction:column; }`.
Temp row could include a mini stat: "Feels 26°" next to temp small. Eh, meta covers it.
Now, footer note: `<p class="foot">Hover any card to lift it · Built with zero JavaScript — just elements, pseudo-elements and @keyframes.</p>` Use `·` middots ✓.
Header eyebrow: `PURE CSS WEATHER · NO IMAGES · NO SVG · NO JS`.
Page background decoration: two radial-gradient blobs via body::before/::after fixed.
```css
body { background: #070b17; }
body::before { content:""; position: fixed; inset: 0; background:
radial-gradient(600px 400px at 12% -5%, rgba(83, 120, 255, .14), transparent 60%),
radial-gradient(700px 460px at 95% 108%, rgba(255, 140, 50, .10), transparent 60%);
pointer-events: none; }
```
Also a subtle dotted texture? skip.
Check horizontal scrollbar: everything within widths; halo inside overflow hidden scene ✓ body::before fixed inset 0 ✓.
Fonts: define `--font-ui` and `--font-mono` variables.
Edge case: `repeating-conic-gradient(from -8deg ...)` — syntax: `repeating-conic-gradient(from -8deg at 50% 50%, rgba(255,225,130,.95) 0deg 16deg, transparent 16deg 45deg)` ✓.
The mask on rays: `radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 57%, #000 88%, transparent 93%)`. closest-side = 75px. Inner edge 52%*75=39px; black 57% = 42.75; outer black 88% = 66; fade to 93% ≈ 70. Core radius: inset 34 → (150-68)/2 = 41. Core 41 vs rays start ~42.75 → tiny 1.75px gap ✓ then rays to 66px → 23px ray length ✓ good.
Ray visual: wedge 16° wide at radius 42-66px: at r=50 the wedge is ~14px wide ✓ proportionate.
Ray color — needs to contrast against the amber bg: brighter than bg: rgba(255,240,180,.95)? bg top #ffb340. Rays in near-white warm: #fff3cd solid-ish. Let me use `#ffedab` with slight transparency? Use solid #ffe9a8 (no alpha) — crisp ✓. Add drop-shadow glow on rays? filter drop-shadow(0 0 6px rgba(255,210,110,.8)) — hmm may blur wedges pleasantly. The core glow suffices.
Sun core gradient: `radial-gradient(circle at 38% 34%, #fff8d6 0%, #ffde5a 45%, #ffb62e 100%)` + box-shadow `0 0 36px 10px rgba(255, 196, 70, .55)` ✓ pulse scale 1↔1.06.
Also maybe slight rotation on core? No.
Now storm bolt glow via wrap filter: `drop-shadow(0 0 14px rgba(255,213,79,.85)) drop-shadow(0 0 34px rgba(255,180,40,.45))` — two drop-shadows for stronger glow ✓.
The bolt-wrap opacity animation flickers the whole thing ✓. During invisible phase (opacity 0), drop-shadow hidden too ✓.
Additionally maybe a faint second bolt? Skip.
Also add subtle always-on ambience to the storm scene: dark vignette at top? The gradient bg handles.
Alright — also **rain drop shape**: make it a proper teardrop? At 3×14px a rounded streak is fine: `border-radius: 2px`. Or teardrop: `border-radius: 50% 50% 50% 50% / 70% 70% 45% 45%`? Meh — streaks read best at small size, slight rotate ✓.
Drop color: `rgba(215, 240, 255, .95)` with slight gradient along length: `linear-gradient(to bottom, rgba(215,240,255,0), rgba(215,240,255,.95))` — gives motion feel ✓. Height 16px, width 3px.
Snow flake glow: `filter: drop-shadow(0 0 4px rgba(255,255,255,.9))` on flake — applies to pseudo-asterisk union ✓. Note filter + animation on same element OK.
Flake color: #fff. On pale-ish snow bg (#9ed2f0→#4d7cae) white reads ✓.
Flake sizes: 12px (2), 9px (2), 14px?? Let me set per nth-child widths: 12, 9, 14→ hmm width also affects perceived spin ✓ fine. Use: 12, 8, 14, 10, 12, 9. Height fixed 2.5px? Pseudo inherits height 100% ✓. Actually height: use 2.5px thick — with width 8 → asterisk arm 8px ✓ fine.
Wait, `.flake` height 2.5px with `height: 100%` pseudos: pseudo height = 100% of 2.5px content height... `.flake` has explicit height: 2.5px, so children `height: 100%` = 2.5px ✓.
Positions of flakes (scene 172 tall, cloud bottom ~ 26+34=60; hmm snow cloud top: 24px, height 34 → bottom 58): flakes start tops ~70-95, travel +66 → up to 161 ✓.
nth-child config for flakes (6):
1: left 26%, top 74px, w 12, dur 6.5s, delay 0s
2: left 38%, top 92px, w 8, dur 5.2s, delay -2s (negative delays spread phase ✓ and with animation none, ignored ✓)
3: left 52%, top 78px, w 14, dur 7.4s, delay -4s
4: left 63%, top 96px, w 10, dur 5.8s, delay -1s
5: left 74%, top 82px, w 12, dur 6.8s, delay -3s
6: left 86%, top 70px, w 9, dur 5.5s, delay -5s
Negative delays are great for phase spread without initial wait ✓ (they make animations appear already mid-cycle ✓, and since infinite, fine).
Rain drops (7) with negative delays similarly:
1: left 28%, top 84, delay 0, dur 1.5s
2: left 38%, top 96, delay -.5, dur 1.6
3: left 50%, top 86, delay -1.0, dur 1.45
4: left 62%, top 98, delay -.3, dur 1.6
5: left 72%, top 88, delay -.9, dur 1.5
6: left 82%, top 94, delay -1.2, dur 1.65
7: left 33%, top 104, delay -.7, dur 1.55
Storm drops (5): rotate 14deg, drift x -14px, dur ~1.1s (heavier):
1: left 16%, top 88, delay 0
2: left 27%, top 100, delay -.4
3: left 63%, top 92, delay -.8
4: left 74%, top 102, delay -.2
5: left 84%, top 90, delay -.6
Wait — storm bolt zone: bolt at left calc(50% - 17px) width 54 → spans ~48%-70% of a ~280px scene... scene width varies with viewport! left % of scene. Bolt uses calc(50% - 17px) — centered-ish ✓. Drops at 63%,74% may cross the bolt — drops render above/below bolt? Tree order: cloud, drops, bolt-wrap → bolt above drops ✓ fine, rain behind bolt is natural.
Hmm, but drops at 63-84% right side with bolt center: composition ok.
Now the CSS mask for rays — one concern: `mask` with radial-gradient + `mask-mode`? default mask-mode is match-source → alpha for gradients ✓ #000 alpha 1 ✓.
Also Safari/Chrome: include `-webkit-mask` shorthand before `mask`? Order: write both; last valid wins — same values anyway.
**Now check equal card heights & scene widths.** Scene width = card width - 0 (body padding doesn't affect scene; scene is direct child full width) ✓. Cloud positions with left calc(50% - 52px) ✓ responsive ✓. Sun centered ✓. Bolt centered-ish ✓.
Mobile 375px: card width = 375 - 2*page padding (16) = 343 minus... grid 1 col, card full width ✓ scene 343 wide, 172 tall — wide scene, icons centered ✓ fine. Maybe cap card max-width on mobile & center: `.grid { max-width: 480px; margin-inline: auto }` at ≤600px? Good touch so cards aren't overly wide. I'll add in the mobile media query: grid max-width 440px centered.
Header: center-aligned or left? Left-aligned feels editorial; center feels landing-page. I'll go center for symmetric card grid. Hmm — center with eyebrow, big title, lede. Then grid. Footer note centered small. Yes.
Let me now also think about the H1 phrasing & typography more:
- Eyebrow: `NO JAVASCRIPT · NO IMAGES · NO SVG — JUST CSS` with letterspacing .2em, size 11px, color #8fa3c8, mono font.
- H1: "Four skies, drawn in pure CSS." — nice.
- Lede: "Every sun ray, raindrop, snowflake and lightning bolt below is built from HTML elements and pseudo-elements, animated entirely with @keyframes."
Footer: "Hover a card to lift it · Animations pause automatically if your system prefers reduced motion."
Now — is there any risk with `repeating-conic-gradient` banding on the ray edges? Fine.
One more check — the requirement "rotating sun rays" ✓ rays rotate. "falling rain drops" ✓. "drifting snowflakes" ✓ (drift = sway+fall ✓). "flashing lightning" ✓.
"Each card has its own background gradient matching the weather" ✓.
"lift up on hover" ✓ translateY(-10px).
"Smooth infinite animations using @keyframes" ✓ all infinite.
"respect prefers-reduced-motion" ✓ media query disabling animations/transitions.
Also should transitions be smooth for hover ✓ cubic-bezier(.22,.61,.36,1) ~ .45s.
Potential issue: applying `animation: none !important` under reduced motion also kills the shimmer etc ✓ intended.
Another subtlety: with reduced motion, `.sun-rays` static shows full starburst — nice ✓. Static bolt visible ✓, drops static scattered ✓, ripples visible ✓, halo static ✓, mini cloud static ✓.
Now — write the HTML skeleton:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Four Skies — Pure CSS Weather Cards</title>
<style> ... </style>
</head>
<body>
<main class="wrap">
<header class="head">
<p class="eyebrow">No JavaScript · No Images · No SVG — just CSS</p>
<h1>Four skies, drawn in pure CSS</h1>
<p class="lede">Every sun ray, raindrop, snowflake and lightning bolt is built from plain HTML elements and pseudo-elements, animated with @keyframes alone.</p>
</header>
<section class="grid">
<article class="card card--sun"> ... </article>
<article class="card card--rain"> ... </article>
<article class="card card--snow"> ... </article>
<article class="card card--storm"> ... </article>
</section>
<p class="foot">Hover a card to lift it <span>·</span> animations switch off automatically when <code>prefers-reduced-motion</code> is set.</p>
</main>
</body>
</html>
```
Using `<code>` for prefers-reduced-motion — styling code inline mono. Nice touch.
Card markup:
Sunny:
```html
<article class="card card--sun">
<div class="scene">
<div class="halo"></div> <!-- or scene::before -->
<div class="cloud cloud--mini" aria-hidden="true"></div>
<div class="sun" aria-hidden="true">
<div class="sun-rays"></div>
<div class="sun-core"></div>
</div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">Lisbon</h2>
<span class="chip">Sunny</span>
</div>
<p class="temp">24°</p>
<p class="desc">Clear skies and golden light, with a gentle breeze off the Atlantic.</p>
<p class="meta"><span>H 27°</span><span>L 18°</span><span>Wind 12 km/h</span></p>
</div>
</article>
```
Use `role="img" aria-label="Animated sun icon"` on scene? aria-hidden on decorative scenes since text describes weather ✓. I'll put `aria-hidden="true"` on `.scene` since it's decorative duplication ✓ accessibility nicety.
Halo via `.card--sun .scene::before` — but `.card--rain .scene::before` is haze... per-card pseudo usage consistent.
Wait — careful: `.scene::before` positioned absolute requires `.scene { position: relative }` ✓.
Rain scene markup:
```html
<div class="scene" aria-hidden="true">
<div class="cloud cloud--rain"></div>
<div class="drops">
<span></span>×7
</div>
<div class="ripple r1"></div>
<div class="ripple r2"></div>
</div>
```
Snow:
```html
<div class="scene" aria-hidden="true">
<div class="cloud cloud--snow"></div>
<div class="flakes">
<span class="flake"></span> ×6
</div>
</div>
```
Storm:
```html
<div class="scene" aria-hidden="true">
<div class="cloud cloud--storm"></div>
<div class="drops drops--storm">
<span></span>×5
</div>
<div class="bolt-wrap"><div class="bolt"></div></div>
</div>
```
Now full CSS. Let me write carefully, watching for typos.
```css
:root {
--ink: #f5f8ff;
--ink-dim: rgba(245,248,255,.72);
--ink-faint: rgba(245,248,255,.55);
--radius: 24px;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%? } not needed.
body {
font-family: var(--font);
background: #070b16;
color: var(--ink);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
body::before { ... fixed glows ... }
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: clamp(28px, 5vw, 56px) clamp(16px, 4vw, 40px) 44px;
}
.head { text-align: center; margin-bottom: clamp(24px, 4vw, 44px); }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: .22em;
text-transform: uppercase;
color: #93a7cf;
margin-bottom: 14px;
}
h1 {
font-size: clamp(1.7rem, 3.6vw, 2.7rem);
font-weight: 800;
letter-spacing: -.02em;
line-height: 1.1;
margin-bottom: 12px;
color: #fdfdff;
}
.lede {
max-width: 62ch;? maybe 640px
margin: 0 auto;
font-size: clamp(.92rem, 1.4vw, 1.02rem);
line-height: 1.65;
color: var(--ink-dim);
max-width: 660px;
}
```
Grid & card:
```css
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 20px;
}
.card {
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--radius);
overflow: hidden;
isolation: isolate;
color: var(--ink);
box-shadow: 0 16px 34px rgba(2, 6, 18, .5), inset 0 1px 0 rgba(255,255,255,.22), inset 0 0 0 1px rgba(255,255,255,.07);
transition: transform .45s cubic-bezier(.22,.68,.32,1.05), box-shadow .45s ease;
will-change: transform;
}
.card:hover, .card:focus-within {
transform: translateY(-10px);
box-shadow: 0 30px 55px rgba(2,6,18,.6), inset 0 1px 0 rgba(255,255,255,.28), inset 0 0 0 1px rgba(255,255,255,.1);
}
```
Hmm `inset 0 0 0 1px` — inset shadows are painted inside above background but below content ✓ acts as border ✓. Alternatively real border. Inset ring is fine.
Card backgrounds:
```css
.card--sun { background: linear-gradient(168deg, #ffb43e 0%, #ff8a00 46%, #f4630e 100%); }
.card--rain { background: linear-gradient(168deg, #4f9ede 0%, #34699d 58%, #27517f 100%); }
.card--snow { background: linear-gradient(168deg, #8ec7ec 0%, #6ba1cf 55%, #4f7fae 100%); }
.card--storm { background: linear-gradient(168deg, #46506a 0%, #2b3247 55%, #1a1f30 100%); }
```
Overlay & shine:
```css
.card::after { /* readability veil */
content: "";
position: absolute;
inset: 0;
z-index: 2;
border-radius: inherit;
background: linear-gradient(to top, rgba(8,12,26,.38), rgba(8,12,26,.10) 42%, rgba(8,12,26,0) 62%);
pointer-events: none;
}
.card::before { /* hover shine */
content: "";
position: absolute;
top: -10%;
bottom: -10%;
left: -80%;
width: 55%;
z-index: 3;
background: linear-gradient(105deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.25) 50%, rgba(255,255,255,0) 100%);
transform: skewX(-16deg) translateX(-40%);
transition: transform 1s ease;
pointer-events: none;
}
.card:hover::before { transform: skewX(-16deg) translateX(420%); }
```
Check travel: element left:-80%, width 55%. Base translateX(-40%) → occupies x from -80-22=-102% to -47% → fully offscreen ✓. Hover translateX(420%): moves 420%×55% = 231% → occupies 151%→206% → fully offscreen right ✓. Sweep crosses entire card ✓. With overflow hidden ✓.
Hmm — z-index 3 above veil z2, above text (auto). The shine passes over text — translucent white band over text briefly = OK stylistically ✓. Text has z-index? Content z auto → under both pseudos. The veil dims text?? The veil darkens the whole card INCLUDING text — white text under a dark veil loses contrast? No wait — the veil is dark translucent; it darkens white text (making it grayish) but darkens the background behind it too. Net effect: text-vs-bg contrast roughly preserved (both darkened similarly, actually text dims more... hmm: white text 100% → overlay dark 38% alpha at bottom → text becomes 62% white-ish over darkened bg... The bg (colored) also darkened by 38% black. Relative contrast: text now 0.62·255 ≈ 158 gray vs bg darkened... contrast actually can INCREASE for mid-tone backgrounds since darkening bg helps. Fine — and I positioned the veil stronger at the bottom (where text is). It'll look good.
Alternative: put the veil BELOW text using z-index: 1 and give card-body z-index: 4? Then veil only darkens the gradient, text stays pure white ✓ better! Let me do: `.card::after { z-index: 1 }` and `.card-body { position: relative; z-index: 4 }` and scene z-index auto... scene should also be above veil? If veil z1 covers scene, icons get slightly darkened — for storm card the flash overlay is scene::after... The veil darkening the scene icons slightly is fine/nice (consistent grading). Actually let me put veil z-index 1, scene z-index 2? Then veil only affects bg gradient below scene... but veil is meant to darken lower card where body text is; body is z4. Scene z2 above veil → scene colors pure ✓. Card-body z4 above veil ✓. So veil sits between bg and content — it darkens the gradient behind text ✓ exactly the intent.
But hold on — the veil gradient covers the full card including scene area, but scene (z2) renders above it → scene area unaffected ✓, only body area darkened ✓
Shine z3: between scene z2 and body z4 → sweep passes over scene but under text — hmm, sweeping under text looks odd? The band passing under text (text stays clean above) — actually looks classy ✓.
Wait — z-index only applies to positioned elements (or flex children... flex items with z-index work only if positioned? In a flex container, z-index on flex items applies even without position? Per spec, flex items are treated as if position relative for z-index? Actually: "flex items paint as inline blocks... z-index applies to flex items even if position is static" ✓ true for flex items). `.scene` and `.card-body` are flex items of `.card` — z-index works ✓. But safer to also add `position: relative` ✓ I'll add.
Scene:
```css
.scene {
position: relative;
z-index: 2;
height: 176px;
flex: none;
overflow: hidden;
transition: transform .45s ease;
}
.card:hover .scene { transform: scale(1.045); }
```
Hmm — scaling the scene on hover: subtle zoom ✓ nice. transform-origin center ✓ default 50% 50% ✓.
Careful: `.scene` transition transform, and reduced-motion kills transitions ✓.
Now per-card scenes:
```css
/* halo */
.card--sun .scene::before {
content: "";
position: absolute;
left: 50%; top: 50%;
width: 320px; height: 320px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: radial-gradient(circle, rgba(255,238,178,.6) 0%, rgba(255,214,120,.22) 42%, rgba(255,200,90,0) 68%);
animation: halo-breathe 6s ease-in-out infinite;
}
@keyframes halo-breathe { 0%,100% { opacity: .75; } 50% { opacity: 1; } }
```
(only opacity animates; transform stays ✓.)
```css
.sun {
position: absolute;
left: 50%; top: 50%;
width: 150px; height: 150px;
transform: translate(-50%, -50%);
}
```
Hmm — but `.sun-rays` inside is animated (rotate) ✓ independent elements so centering transform on `.sun` persists ✓. But reduced-motion: `.sun` static transform translate(-50%,-50%) ✓ centered ✓ (this static transform is not animated so it survives `animation: none` ✓).
```css
.sun-rays {
position: absolute;
inset: 0;
background: repeating-conic-gradient(from -8deg at 50% 50%,
#ffe9a6 0deg 16deg, rgba(255,233,166,0) 16deg 45deg);
-webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 56%, #000 88%, transparent 94%);
mask: radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 56%, #000 88%, transparent 94%);
animation: spin 26s linear infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }
.sun-core {
position: absolute;
inset: 34px;
border-radius: 50%;
background: radial-gradient(circle at 36% 32%, #fff7d2 0%, #ffdd55 48%, #ffb52e 100%);
box-shadow: 0 0 34px 8px rgba(255,199,71,.6);
animation: sun-pulse 4.6s ease-in-out infinite;
}
@keyframes sun-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.07); } }
```
Mini cloud:
```css
.cloud { position: absolute; }
.cloud::before, .cloud::after {
content: "";
position: absolute;
border-radius: 50%;
}
```
Sizes per variant... set common fields:
```css
.cloud--rain, .cloud--snow, .cloud--storm {
height: 34-40; etc
}
```
Let me write each variant fully:
```css
.cloud--rain {
top: 30px;
left: calc(50% - 52px);
width: 104px; height: 34px;
border-radius: 40px;
background: linear-gradient(to bottom, #e2ecf9 0%, #bccfe8 100%);
filter: drop-shadow(0 14px 16px rgba(20,48,92,.35));
animation: cloud-bob 5.5s ease-in-out infinite;
}
.cloud--rain::before, .cloud--rain::after { background: #e2ecf9; }
.cloud--rain::before { width: 44px; height: 44px; top: -21px; left: 15px; }
.cloud--rain::after { width: 31px; height: 31px; top: -14px; right: 15px; }
```
Wait — puffs need to overlap the pill: puff centers... ::before at top:-21, left:15 → circle spans y -21..23, x 15..59 ✓ overlapping pill (0..34) ✓ sticks above by 21 ✓. ::after spans y -14..17, x from right:15 → x = 104-15-31=58..89 ✓. Cloud total visual: y from -21 to 34 (55 tall) — top at 30 → cloud top at 9 ✓ within scene ✓.
Snow cloud:
```css
.cloud--snow {
top: 26px;
left: calc(50% - 50px);
width: 100px; height: 33px;
border-radius: 40px;
background: linear-gradient(to bottom, #eef5ff 0%, #ccd9ee 100%);
filter: drop-shadow(0 14px 16px rgba(60,95,148,.35));
animation: cloud-bob 6.5s ease-in-out infinite;
}
.cloud--snow::before, .cloud--snow::after { background: #eef5ff; }
.cloud--snow::before { width: 43px; height: 43px; top: -20px; left: 13px; }
.cloud--snow::after { width: 30px; height: 30px; top: -13px; right: 14px; }
```
Storm cloud:
```css
.cloud--storm {
top: 26px;
left: calc(50% - 58px);
width: 116px; height: 40px;
border-radius: 50px;
background: linear-gradient(to bottom, #6d7896 0%, #3f4864 100%);
filter: drop-shadow(0 16px 18px rgba(4,9,22,.55));
animation: storm-bob 6s ease-in-out infinite;
}
.cloud--storm::before, .cloud--storm::after { background: #6d7896; }
.cloud--storm::before { width: 52px; height: 52px; top: -25px; left: 15px; }
.cloud--storm::after { width: 37px; height: 37px; top: -17px; right: 16px; }
```
Storm cloud bottom at 66. Bolt top at 60 → overlaps cloud bottom ✓ emerges from cloud ✓.
Mini cloud:
```css
.cloud--mini {
top: 22px;
left: 9%;
width: 56px; height: 19px;
border-radius: 30px;
background: #fff;
opacity: .9;
filter: drop-shadow(0 6px 8px rgba(200,110,20,.25));
animation: cloud-drift 9s ease-in-out infinite;
}
.cloud--mini::before, .cloud--mini::after { background: #fff; }
.cloud--mini::before { width: 25px; height: 25px; top: -12px; left: 7px; }
.cloud--mini::after { width: 18px; height: 18px; top: -8px; right: 8px; }
```
Keyframes:
```css
@keyframes cloud-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-7px); } }
@keyframes storm-bob { 0%,100% { transform: translate(0,0); } 50% { transform: translate(8px,-5px); } }
@keyframes cloud-drift { 0%,100% { transform: translateX(0); } 50% { transform: translateX(36px); } }
```
Hmm cloud-drift moves right then back — "drifting" ✓.
Rain drops:
```css
.drops, .flakes { position: absolute; inset: 0; }
.drops span {
position: absolute;
width: 3px;
height: 15px;
border-radius: 2px;
background: linear-gradient(to bottom, rgba(212,238,255,0), rgba(212,238,255,.95));
opacity: .9;
transform: rotate(9deg);
animation: rainfall 1.55s linear infinite;
}
```
Wait — base `transform: rotate(9deg)` is overridden during animation; when reduced motion, base applies ✓ tilt static ✓ good.
```css
@keyframes rainfall {
0% { transform: translate3d(0,-8px,0) rotate(9deg); opacity: 0; }
12% { opacity: .95; }
82% { opacity: .8; }
100% { transform: translate3d(-10px,60px,0) rotate(9deg); opacity: 0; }
}
```
Hmm — 82% opacity .8 → 100% 0: fades near end ✓.
nth-child positions (`.card--rain .drops span:nth-child(n)`):
```css
.drops span:nth-child(1) { left: 26%; top: 86px; animation-delay: -0.2s; }
.drops span:nth-child(2) { left: 37%; top: 98px; animation-delay: -0.7s; }
.drops span:nth-child(3) { left: 49%; top: 88px; animation-delay: -1.2s; }
.drops span:nth-child(4) { left: 61%; top: 100px; animation-delay: -0.45s; animation-duration: 1.7s; }
.drops span:nth-child(5) { left: 71%; top: 90px; animation-delay: -0.95s; }
.drops span:nth-child(6) { left: 81%; top: 97px; animation-delay: -1.4s; animation-duration: 1.45s; }
.drops span:nth-child(7) { left: 32%; top: 104px; animation-delay: -0.6s; }
```
Careful: these selectors inside `.card--rain` scope only; storm drops use `.drops--storm span:nth-child`. Since storm markup has its own `.drops--storm`, I'll scope: define generic `.drops span` styling but nth positions under `.card--rain .drops span` and `.card--storm .drops span`. Or make positions part of `.drops span:nth` generic and storm overrides left/top... cleaner to scope per card. Let me define shared `.drops span { ... }` in a `.field span`? I'll just write both scopes.
Rain haze bottom:
```css
.card--rain .scene::before {
content:"";
position: absolute; left: 0; right: 0; bottom: 0; height: 56px;
background: linear-gradient(to top, rgba(24,52,90,.35), rgba(24,52,90,0));
}
```
Snow haze:
```css
.card--snow .scene::before {
... height: 52px;
background: linear-gradient(to top, rgba(255,255,255,.38), rgba(255,255,255,0));
}
```
Storm haze:
```css
.card--storm .scene::before {
height: 60px;
background: linear-gradient(to top, rgba(10,14,28,.45), rgba(10,14,28,0));
}
.card--storm .scene::after {
content:"";
position: absolute; inset: 0;
background: radial-gradient(ellipse 70% 55% at 50% 20%, rgba(255,255,255,.5), rgba(255,255,255,0) 70%);
opacity: 0;
animation: sky-flash 5s linear infinite;
mix-blend-mode: screen? not needed.
}
```
Ripples:
```css
.ripple {
position: absolute;
bottom: 12px;
width: 28px; height: 10px;
border: 2px solid rgba(210,236,255,.6);
border-radius: 50%;
opacity: .5;
animation: ripple 2.8s ease-out infinite;
}
.ripple.r1 { left: 24%; }
.ripple.r2 { left: 62%; animation-delay: -1.4s; }
@keyframes ripple {
0% { transform: scale(.35); opacity: .75; }
70% { opacity: .25; }
100% { transform: scale(1.5); opacity: 0; }
}
```
Flakes:
```css
.flake {
position: absolute;
width: 12px;
height: 2px; /* maybe 2.5px */
border-radius: 2px;
background: #fff;
filter: drop-shadow(0 0 3px rgba(255,255,255,.85));
opacity: .95;
animation: flake-drift 6.4s linear infinite;
}
.flake::before, .flake::after {
content: "";
position: absolute;
left: 0; top: 0;
width: 100%; height: 100%;
border-radius: 2px;
background: #fff;
}
.flake::before { transform: rotate(60deg); }
.flake::after { transform: rotate(-60deg); }
@keyframes flake-drift {
0% { transform: translate3d(0,-10px,0) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
32% { transform: translate3d(10px,18px,0) rotate(130deg); }
60% { transform: translate3d(-9px,40px,0) rotate(255deg); }
85% { opacity: 1; }
100% { transform: translate3d(7px,64px,0) rotate(360deg); opacity: 0; }
}
```
Positions:
```css
.card--snow .flake:nth-child(1) { left: 24%; top: 76px; animation-duration: 6.8s; animation-delay: -1s; }
.card--snow .flake:nth-child(2) { left: 36%; top: 94px; width: 9px; animation-duration: 5.4s; animation-delay: -3s; }
.card--snow .flake:nth-child(3) { left: 50%; top: 80px; width: 14px; animation-duration: 7.6s; animation-delay: -5s; }
.card--snow .flake:nth-child(4) { left: 62%; top: 98px; width: 10px; animation-duration: 6s; animation-delay: -2s; }
.card--snow .flake:nth-child(5) { left: 74%; top: 84px; animation-duration: 7s; animation-delay: -4s; }
.card--snow .flake:nth-child(6) { left: 84%; top: 72px; width: 9px; animation-duration: 5.6s; animation-delay: -6s; }
```
Storm drops:
```css
.card--storm .drops span {
background: linear-gradient(to bottom, rgba(196,216,255,0), rgba(196,216,255,.9));
transform: rotate(14deg);
animation: storm-fall 1.15s linear infinite;
}
@keyframes storm-fall {
0% { transform: translate3d(0,-8px,0) rotate(14deg); opacity: 0; }
14% { opacity: .9; }
100% { transform: translate3d(-16px,64px,0) rotate(14deg); opacity: 0; }
}
.card--storm .drops span:nth-child(1) { left: 15%; top: 92px; animation-delay: -0.1s; }
(2) left 26%, top 104, delay -0.5
(3) left 60%, top 96, delay -0.9
(4) left 72%, top 106, delay -0.3, duration 1.05s
(5) left 83%, top 92, delay -0.7
```
Bolt:
```css
.bolt-wrap {
position: absolute;
top: 62px;
left: calc(50% - 17px);
width: 54px; height: 88px;
opacity: 1; /* static fallback when reduced motion */
filter: drop-shadow(0 0 10px rgba(255,216,92,.95)) drop-shadow(0 0 30px rgba(255,176,32,.5));
animation: bolt-flash 5s linear infinite;
}
.bolt {
width: 100%; height: 100%;
background: linear-gradient(to bottom, #fff6c8 0%, #ffd84e 45%, #ffb226 100%);
clip-path: polygon(35% 0%, 95% 0%, 66% 45%, 90% 45%, 20% 100%, 28% 55%, 0% 55%);
}
@keyframes bolt-flash {
0%, 64% { opacity: 0; }
68% { opacity: 1; }
70% { opacity: .1; }
73% { opacity: .95; }
76% { opacity: .25; }
80% { opacity: 0; }
100% { opacity: 0; }
}
@keyframes sky-flash {
0%, 66% { opacity: 0; }
69% { opacity: 1; }
71% { opacity: .08; }
74% { opacity: .75; }
78% { opacity: 0; }
100% { opacity: 0; }
}
```
Timing linear between keyframes: 64%→68% over 200ms ramp ✓ flicker ✓.
Bolt position: top 62, height 88 → bottom at 150 ✓.
Hmm — bolt left: calc(50% - 17px): center + 10px offset (since half width 27, -17 shifts center right by 10px) ✓ slight asymmetry nice.
Card body:
```css
.card-body {
position: relative;
z-index: 4;
flex: 1;
display: flex;
flex-direction: column;
padding: 18px 20px 20px;
text-shadow: 0 1px 2px rgba(10,15,30,.25);
}
.body-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
}
.city {
font-size: 1.12rem;
font-weight: 750;? use 700
letter-spacing: .01em;
}
.chip {
font-size: 9.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .14em;
padding: 5px 9px 4px;
border-radius: 999px;
background: rgba(255,255,255,.16);
border: 1px solid rgba(255,255,255,.28);
backdrop-filter: blur(4px);
white-space: nowrap;
}
.temp {
font-size: 3.4rem;
font-weight: 200;
line-height: .95;
letter-spacing: -.03em;
margin-bottom: 10px;
font-variant-numeric: tabular-nums;? fine
}
.desc {
font-size: .9rem;
line-height: 1.55;
color: var(--ink-dim);
margin-bottom: 16px;
}
.meta {
margin-top: auto;
display: flex;
flex-wrap: wrap;
gap: 8px 0;
padding-top: 12px;
border-top: 1px solid rgba(255,255,255,.2);
font-size: .74rem;
font-weight: 600;
letter-spacing: .05em;
color: var(--ink-faint);
}
.meta span { display: inline-block; }
.meta span + span { border-left: 1px solid rgba(255,255,255,.22); margin-left: 10px; padding-left: 10px; }
```
Hmm `.meta` flex wrap — with border-left dividers ✓.
Snow card white text on #6ba1cf — with veil ✓ ok. Sun card white on #ff8a00 at mid — city/temp near top of body where veil ~.10-.20 alpha... white on #ff8a00 (contrast ~2.3) — marginal. Improve: strengthen veil? If veil is z1 (behind text but over bg), strengthen bottom-up: `linear-gradient(to top, rgba(8,12,26,.42), rgba(8,12,26,.12) 55%, rgba(8,12,26,.04) 100%)`. Even top has .04. City row sits right below scene — bg there ~#ff9a1c + .06 dark ≈ still bright. Hmm. Options: (a) darken sun gradient overall: `#ffae2e → #ef6c0a → #d9580a`? white on #d9580a ≈ 3.5:1 — decent for large text; body text small though. (b) Use dark text on the sun card: `color: #402005` for text + rgba dark variants — the sun card would look distinct (light card, dark ink) — actually stylistically nice: the sun card as the bright one with espresso text could be a feature, not a bug. But chip/meta borders adapt... I can define per-card `--ink` variables! `.card--sun { --ink: #3d2000; --ink-dim: rgba(61,32,0,.78); --ink-faint: rgba(61,32,0,.6); --line: rgba(61,32,0,.2) }` etc. But chip uses white borders/bg — would need var too. Doable: use CSS variables everywhere in card body: colors, borders, chip bg. Let me do it properly:
```css
.card { --ink: #fff; --ink-2: rgba(255,255,255,.8); --ink-3: rgba(255,255,255,.62); --line: rgba(255,255,255,.22); --chip-bg: rgba(255,255,255,.18); --chip-line: rgba(255,255,255,.3); }
.card--sun { --ink: #3c1d00; --ink-2: rgba(60,29,0,.82); --ink-3: rgba(60,29,0,.62); --line: rgba(60,29,0,.22); --chip-bg: rgba(61,29,0,.14); --chip-line: rgba(61,29,0,.25); }
```
And snow? Snow bg #8ec7ec top → #4f7fae bottom; body text near bottom on ~#5f8fbe + veil → white on #5f8fbe ≈ 3.0 — acceptable for demo with shadow. Keep white for snow (icy look ✓ white text on ice blue is classic). Sun card with dark espresso ink on amber — warm and readable ✓. This is the polish that makes it feel designed.
But text-shadow: for sun card, dark text needs light glow? Remove text-shadow for sun (set to none via var: `--tshadow`). Use `text-shadow: var(--tshadow, 0 1px 2px rgba(8,12,26,.3))` and `.card--sun { --tshadow: none; }` ✓.
Also the chip on sun: dark translucent bg ✓.
Temp color = --ink ✓. desc = --ink-2, meta = --ink-3, borders = --line ✓.
Hmm, wait — also check storm: white on #46506a ✓ great.
Now — "scene" bg for the sun card is amber; the halo + rays + core all warm ✓.
Media queries:
```css
@media (max-width: 1020px) {
.grid { grid-template-columns: repeat(2, minmax(0,1fr)); }
}
@media (max-width: 620px) {
.grid { grid-template-columns: 1fr; max-width: 460px; margin-inline: auto; gap: 18px; }
}
```
Also at 2-col (tablet), scenes get wider — fine.
Also maybe reduce scene height slightly on small screens? Keep 176 ✓ fine.
prefers-reduced-motion:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation: none !important;
transition: none !important;
}
.card:hover .scene { transform: none; }?
```
transition none makes hover lift instant — that's "reduced motion" compliant? Instant state change isn't motion ✓ acceptable. But `.card:hover` still translates -10px instantly — fine.
Also under reduced motion, maybe show a tiny note? The footer already mentions it ✓.
Now — the "no horizontal scrollbar" check: `.wrap` max-width + padding ✓. body::before fixed inset 0 ✓. Grid minmax(0,1fr) prevents blowout ✓.
One more thing: at exactly 1280×800, `.wrap` max 1180 → margins 50px each side ✓. Cards: (1180 - 3*20)/4 = 280px each ✓. Scene 176 + body (~230) ≈ 410 card height. Header ~ 170 + grid 430 + foot 60 + padding ~ 100 → ~760 ≤ 800 ✓ tight but fits. Let me trim: lede max-width 640, header margin-bottom ~36, wrap top padding 40. Card body: temp 3.4rem... total card ≈ 176 + 18pad + 22(city) + 14 + 53(temp) + 10 + ~70(desc 3 lines? desc ~2-3 lines at 280-40=240px width... desc text ~60 chars → at .9rem ≈ 48 chars/line → 2 lines ≈ 44px) + 16 + meta 12+30 + bottom pad 20 ≈ 176+ 18+22+14+53+10+44+16+42+20 = 415. Header: eyebrow 14+11 + h1 ~40 + 12 + lede ~54 (2 lines) ≈ 130 + margin 36 = 166. Foot ~ 40. Total ≈ 40(top pad) + 166 + 415 + 40 + 44(bottom) ≈ 705 ✓ fits 800 ✓.
Wait, will desc wrap to 2-3 lines consistently across cards making heights equal? Grid stretches to tallest ✓ equal heights ✓ meta pinned bottom ✓.
Also I want the chip to be `white-space: nowrap` ✓ ("Thunder" chip longest ✓).
Chips: "Sunny", "Rain", "Snow", "Thunder" — or "Stormy". Use "Stormy"? "Thunder" reads better as condition. Card requirement says description short — chip is the weather condition ✓.
City names + temps + hi/lo + wind:
- Lisbon — 24° — H 27° L 18° — Wind 12 km/h — desc: "Clear skies and golden light, with a soft breeze rolling in off the Atlantic."
- London — 13° — H 15° L 9° — Wind 20 km/h — "Steady drizzle settling in for the afternoon — keep the umbrella close."
- Reykjavík — −2° — H 1° L −8° — Wind 15 km/h — "Light snow drifting down on a cold northerly wind."
- Singapore — 28° — H 30° L 25° — Wind 38 km/h — "Thunderstorms rumbling through, clearing by early evening."
Meta format: `<span>H 27°</span><span>L 18°</span><span>Wind 12 km/h</span>` ✓.
Temp display: `24°` big; for negative: `−2°` using proper minus sign − (U+2212) ✓ nicer.
Now, potential pitfalls to verify:
1. `repeating-conic-gradient(from -8deg at 50% 50%, #ffe9a6 0deg 16deg, rgba(255,233,166,0) 16deg 45deg)` — valid syntax ✓. The `from -8deg` centers a ray at 0° (top) ✓. Actually first segment from -8 to 8 = ray centered at top ✓ then repeats every 45° ✓ 8 rays ✓.
2. Mask in both prefixed and standard ✓.
3. `clip-path: polygon(...)` — my bolt polygon: `polygon(35% 0%, 95% 0%, 66% 45%, 90% 45%, 20% 100%, 28% 55%, 0% 55%)`. Let me re-verify the union math once more with T top edge at y=45 x∈[30,90] and P right edge x=95−(35/55)y → at 45: 66.36 ✓ inside [30,90] ✓; T left edge from (30,45) to (20,100) crossing y=55 at x=28.2 ✓ P bottom edge y=55 x∈[0,60] ✓ 28.2 ∈ [0,60] ✓. Polygon vertex order (clockwise? Let me check orientation: (35,0)→(95,0)→(66,45): turning... (35,0)→(95,0) vector (60,0); (95,0)→(66,45) vector (-29,45); cross z = 60*45 - 0*(-29) = 2700 > 0. In screen coords (y down), positive cross = clockwise? For y-down coordinate systems, cross product > 0 means clockwise turn... it doesn't matter for polygon fill (nonzero winding handles either) — clip-path uses nonzero winding? Even-odd vs nonzero — for a simple (non-self-intersecting) polygon both give same fill ✓. My polygon is simple ✓ verified earlier.
Let me sanity check "simple": edges E2 (95,0)-(66,45) and E4 (90,45)-(20,100): E2 spans y∈[0,45], E4 y∈[45,100] — meet only potentially at y=45: E2 endpoint (66,45), E4 startpoint (90,45) — distinct ✓ no crossing. E3 (66,45)-(90,45) horizontal ✓. E7 (0,55)-(35,0): x∈[0,35] y∈[0,55]. E5 (20,100)-(28,55): x∈[20,28]. E7 vs E5: E7 at y=55 → x=0... E7 param: from (0,55) to (35,0): x = 35*(1 - y/55) → at y=55, x=0. E5 at y=55 → x=28. Disjoint ranges over y∈[55..] E7 doesn't exist below 55 ✓. E6 (28,55)-(0,55) horizontal at y=55 x∈[0,28] ✓. Does E6 intersect E7? E7 endpoint (0,55) = E6 endpoint ✓ shared vertex fine. Does E6 intersect E5? E5 endpoint (28,55) = E6 startpoint ✓. E4 vs E6: E4 at y=55 → x = 90 − 70*(10/55) = 77.3 ∉ [0,28] ✓. E4 vs E5: meet at (20,100) endpoint ✓; elsewhere E4 x > E5 x for y<100? E4 at y=80: x = 90−70*(35/55)=45.5; E5 at y=80: x = 28 − 8*(25/45)?? E5 from (28,55) to (20,100): x = 28 − 8*(y−55)/45 → at 80: 28−4.44=23.6 ✓ 45.5 > 23.6 ✓ no crossing.
So bolt polygon is valid, shape: upper parallelogram-ish band slanting down-left with a right-then-left jog into a tapering tail ending in a bottom-left tip. Hmm wait — one thing: the tail's tip at (20,100) and the upper band's bottom-left corner at (0,55): the left side has the tail edge from (28,55) going down-left to (20,100) — nearly vertical, and jog from (28,55) to (0,55). So on the left boundary at y=55: from tail (x=28) we jump left to x=0 — meaning above y=55 the shape extends further left (to x=0..) than the tail. The visual: upper band wide, tail narrower ✓ that's exactly ⚡'s silhouette ✓ (in ⚡ the lower tail is narrower than the upper band, offset right? In the emoji glyph the tail continues down-LEFT from the middle; upper band's leftmost point is at middle-left... yes matches).
Actually hold on, in ⚡ the upper band's left edge slants down-LEFT reaching its leftmost at the BOTTOM of the upper band (at the jog), then the jog goes LEFT?? Hmm in my shape the jog at y=55 goes from x=28 to x=0 — the upper band extends LEFT of the tail at its bottom — creating a little left-pointing step ✓ this is the lightning zigzag ✓. I'm fairly confident this reads as a lightning bolt, especially with glow. Let me double-check by plotting roughly:
y=0: x∈[35,95]
y=25: left E7: x=35*(1-25/55)=19.1; right E2: x=95−(35/55)*25=79.1 → [19,79]
y=45: left: 35*(1-45/55)=6.4; right E2: 66.4 → [6,66]; plus jog E3 to 90 → at y=45 the boundary: the step E3 adds [66,90] at exactly y=45 ✓ (below y=45 the region extends to E4)
y=50: left E7: 3.2; right E4: 95−70*(5/55)=88.6 → [3,89]
y=55: left 0 (E6 from 28 to 0, so shape includes [0, ...]); right E4: 77.3 → [0,77]
y=75: left E5: 28−8*(20/45)=24.4; right E4: 95−70*(30/55)=56.8 → [24,57]
y=95: left E5: 28−8*(40/45)=20.9; right: 95−70*(50/55)=31.4 → [21,31]
y=100: tip at (20,100) ✓
So silhouette: wide top (60 units), widening slightly leftward going down to y=55 (width 77), then tapering to a point at y=100 — the tapering lower half is the tail, slanting left. Plus the notch steps at y≈45-55 on the right side: at y=45-55 the right edge extends to ~89-90 while at y=44 it's only ~66 — creating a step out on the right ✓ and left step at y=55 ✓.
This is a solid bolt: think of 🗲.
4. `filter: drop-shadow` on `.cloud` — with `overflow: hidden` scene, shadows near edges clip fine.
5. The `.card:hover .scene { transform: scale(1.045) }` — scene contains overflow hidden + children; scaling whole scene box ✓.
Hmm — one more consideration: hovering the card lifts +10px; the scene zoom transition .45s while card transition .45s ✓ synchronized nicely.
6. `isolation: isolate` on card — not needed; fine either way. I'll include to contain blend if any (none). Skip it, simpler.
7. backdrop-filter on chip — over gradient ✓ works in Chrome ✓ (no network). Under reduced motion no issue.
8. Note: `.drops span` uses `inset: 0` on container `.drops { position:absolute; inset: 0 }` ✓ children absolute with left%/top px ✓.
9. z-index: `.scene` z2 and `.card-body` z4 — need position relative on both ✓ set.
Wait — `.card-body` has `position: relative; z-index: 4` and `.scene { position: relative; z-index: 2 }` — but scene also has `overflow: hidden` and children absolute ✓.
Hmm, one subtlety: `.card::after` (veil) z-index 1 — but `.card` has `overflow: hidden` + `border-radius`; ::after inset 0 border-radius inherit ✓.
But — `z-index: 1` on ::after vs `.scene` z2: both positioned, z2 > z1 ✓ scene above veil ✓. But the veil was intended to darken behind body text: body z4 above veil ✓ so veil darkens bg only ✓.
The shine ::before z3: above scene (z2), below body (z4) ✓ sweeps across the sky area ✓.
10. Reduced motion static check for `.bolt-wrap`: base opacity — I set `.bolt-wrap { opacity: 1 }`? If I never declare opacity, initial = 1 ✓ static visible ✓. But during animation keyframes override ✓. So don't declare. Same for `.card--storm .scene::after { opacity: 0 }` static — flash invisible static ✓ good (declare opacity: 0 ✓).
Similarly `.drops span` base opacity: declare `.95`? With animation, keyframes cover full 0-100 ✓; static shows .95 ✓. Declare opacity .9 base ✓.
`.ripple` base opacity .5 ✓ static puddle ✓.
`.flake` base opacity .95 ✓ static ✓.
`.sun-rays`, `.sun-core`, halo base opacity default 1 ✓.
11. Font-weight 200 for temp — Windows Segoe UI has Light ✓ macOS ✓ Android Roboto has none (falls to 100/300? Roboto lacks 200 → nearest available; fine).
12. `text-transform` etc fine.
13. `aria-hidden="true"` on scenes ✓ decorative.
14. `h2` inside article ✓ semantics fine (h1 in header).
Let me also add a `<p class="foot">` styling: centered, .8rem, color faint, mono for the code bit:
```css
.foot { margin-top: 26px; text-align: center; font-size: .8rem; color: rgba(160,175,205,.55); }
.foot code { font-family: var(--mono); font-size: .74rem; background: rgba(120,140,190,.12); border: 1px solid rgba(120,140,190,.18); padding: 2px 6px; border-radius: 6px; }
```
Also `.foot span` separators just text middots.
Header eyebrow separator styling: maybe color accents. Fine.
Now, one more design flourish: card top could have a subtle "horizon" line? Skip — enough.
Double-check responsiveness edge: at 375px, `.grid` 1 col max-width 460 → card ~343 wide. Scene 176 tall. Cloud centered ✓. Ripples at 24%/62% ✓. Bolt calc(50% - 17px) ✓. Flakes left up to 84% ✓ (flake width 9 → within) ✓. Sun centered 150px ✓. Halo 320 wide centered — clipped by scene overflow hidden ✓ fine.
At 2-col breakpoint (e.g., 768px): cards ~ (768-32-20)/2 = 358 ✓ fine.
Grid gap 20; wrap padding at 4vw.
Now, let me reconsider the sun card ink color: dark espresso #3a1e04 on amber. The chip on sun: `rgba(58,30,4,.14)` bg + border `rgba(58,30,4,.28)` ✓. Text-shadow none ✓. City #3a1e04 — strong contrast on #ffb43e ✓ (contrast ~8:1 ✓). desc rgba(58,30,4,.8) on ~#ff9a1c ✓ ~5:1 ✓. meta rgba(58,30,4,.62) ✓.
Snow: white on #6ba1cf: 2.9:1 for large temp (fine), desc rgba(255,255,255,.85) on ~#5f90c0 → ~3.2:1 — acceptable for a demo; veil adds darkening where body sits (~+10%) → ok. Could deepen snow gradient: `#85bde4 → #5b8cb8 → #43719e`? Let me deepen slightly: `linear-gradient(168deg, #8cc2ea 0%, #6296c6 55%, #45759f 100%)` — bottom #45759f with veil → white text ~3.6:1 ✓ better. Keep.
Rain: `#4f9ede → #34699d → #27517f` bottom dark ✓ white ✓ good.
Alright — also the mini cloud on the sun card sits at left 9% top 22 — with dark-ink? no, cloud is white ✓ fine on amber.
Also — should the sun card scene have the veil? Scene is z2 above veil(z1) ✓ unaffected ✓.
Now, HTML meta description? Not needed but fine to add `<meta name="description">`. Optional; skip or include — harmless. I'll include brief.
Also: title text ✓.
Let me also think — is there ANY chance the screenshot at 3s shows something odd? Animations with negative delays start mid-cycle ✓ no blank period. Bolt: at t=3s with 5s cycle: bolt visible only ~64-80% (3.2-4.0s) — at exactly 3s bolt invisible, sky dim. Hmm — screenshot at ~3s: storm card might show no bolt! The storm flash begins at 64% = 3.2s. So at 3.0s the bolt is hidden; storm card shows just a dark cloud + rain — still looks fine, but I'd love the bolt in the screenshot. Adjust: make cycle 4.2s → visible from 2.69s–3.36s. At 3.0s: bolt opacity ~1 ✓ (flash at 68-73%: 2.86–3.07s bright) ✓. Or shift keyframes earlier: visible 40-60% of 5s = 2.0–3.0s — ends right at 3.0. Let me design the cycle so there's a flash around t≈2.6–3.4s AND another later: multiple flashes per cycle! Real lightning flashes 2-3 times per occurrence. Let me put flashes at ~45-60% and ~75-90% of a 5.2s cycle:
bolt-flash (5.2s):
0% 0
42% 0
45% 1
47% .12
49% .95
52% .3
54% 0 → first burst at 2.19–2.8s
70% 0
72% 1
74% .2
76% .9
79% .35
82% 0 → second burst at 3.7–4.26s
100% 0
At t≈3.0s: between bursts — bolt dim. Hmm. Add a third small burst at 55-62%? Bursts: 45-52, then 56-60, then 74-80... At 3.0s (57.7%): covered by 56-60 burst ✓. Let me define:
```
0%, 40% { opacity: 0 }
43% { opacity: 1 }
45% { opacity: .15 }
48% { opacity: .95 }
51% { opacity: .2 }
54% { opacity: 0 }
57% { opacity: .85 }
60% { opacity: .1 }
63% { opacity: 0 }
74% { opacity: 0 }
76% { opacity: 1 }
78% { opacity: .2 }
81% { opacity: .9 }
84% { opacity: .3 }
87% { opacity: 0 }
100% { opacity: 0 }
```
Cycle 5.2s: burst1 2.08–2.8s, burst2 2.96–3.28s, burst3 3.85–4.52s. Screenshot at ~3s → in burst2 ✓ likely bright ✓. And sky-flash similar slightly offset (+1%) for the "lightning then illumination" feel — actually sky flash should coincide; tiny offset for realism. sky-flash: same keyframe times, opacity values ~ same but max .9/.7.
That's a lot of keyframe stops — fine.
Simplify: I'll write both with ~15 stops. OK.
Also the ripple at t=3s ✓ visible (2.8s cycle, delays) ✓.
One more: the `.card:hover` lift — the storm bolt etc continue ✓.
Let me also add `cursor: default`? Cards aren't clickable; hover effects still fine. Keep default cursor.
Now — check for any use of things that might throw errors: none (no JS). CSS only.
Also requirement "made with HTML and CSS only" ✓.
Potential CSS syntax pitfalls to be careful about:
- `mask` shorthand with radial-gradient ✓.
- conic gradient `at 50% 50%` inside `from -8deg at 50% 50%` ✓ order: `repeating-conic-gradient([from <angle>] [at <position>], ...)` ✓.
- Multiple drop-shadows in one filter ✓ comma-separated ✓.
Now, let me also reconsider the `.drops span` base `transform: rotate(9deg)` — when animation runs, keyframes override transform; during the animation-delay (negative — no wait) fine ✓.
But careful — elements with `animation` and static `transform` declared: during animation, transform from keyframes ✓. With `animation: none` (reduced motion), static transform ✓.
Similarly `.flake` static: no transform declared → identity ✓ asterisk upright ✓.
`.sun-rays` static: identity ✓.
Storm `.drops span` static rotate(14deg) ✓.
Also — hmm, `.drops span` background gradient direction `to bottom`: transparent top → bright bottom: the streak is brightest at its bottom tip ✓ rain-like ✓. Actually real streaks brighter at bottom (leading edge falling) ✓.
Now the veil: `z-index: 1` — but `.card::before` shine z3 and `.card::after` veil z1; card-body z4 — body above shine ✓. Hmm wait: shine sweeps under the text: at z3 < z4 ✓. But shine over the scene ✓ nice.
Edge: `.scene` has z-index 2 — since `.scene` is a flex item with `position: relative` ✓.
Let me also make sure `.card-body` children don't overflow on narrow cards: chip + city on 280-40=240 width: city "Reykjavík" ~ 80px + chip "Snow" ~54px ✓ fits. At 375 single col: plenty ✓.
`temp` letter-spacing etc ✓.
The `.desc` texts ~70 chars → 3 lines at 240px? At .9rem, ~45 chars/line → 2 lines mostly, "Thunderstorms rumbling through, clearing by early evening." ~59 chars → 2 lines ✓. Fine.
Foot: "Hover a card to lift it · every icon is pure HTML + CSS · animations pause if you prefer reduced motion." Let me finalize: `Hover a card to lift it. Every icon above is built from plain elements and pseudo-elements — and animations switch off automatically under prefers-reduced-motion.` with `<code>prefers-reduced-motion</code>` ✓.
Eyebrow: `NO JS · NO IMAGES · NO SVG` and maybe append `· 100% HTML + CSS`.
H1: "Four skies, drawn in pure CSS". Lede: "Sun rays, raindrops, snowflakes and lightning — each card is built entirely from HTML elements and pseudo-elements, animated with nothing but @keyframes."
Good. Let me now also handle `h1` `text-wrap: balance`? Supported in Chrome 114+ ✓ nice touch: `text-wrap: balance` on h1 and lede? Add — harmless if unsupported.
Also `margin-inline: auto` etc.
Wait — one more important check: **`mask` on `.sun-rays`** — `radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 56%, #000 88%, transparent 94%)` — color stops with `transparent` keyword: interpolates alpha ✓ fine.
Ray inner radius 52% of 75 = 39px; core radius = (150 − 2×34)/2 = 41px. Core (41) slightly overlaps rays' start (39)? Mask black starts 56% = 42px ✓ core 41 + its box-shadow glow covers the 1px gap ✓ visually seamless ✓. Rays end black at 88% = 66px, fade to 94% = 70.5. Element corner distance = 106 — beyond 70.5 transparent ✓ no corner artifacts ✓.
Ray width 16° at 45° period ✓ 8 rays ✓.
Also, during rotation the conic background rotates with element ✓.
One concern: `from -8deg` + rotating animation — rays rotate continuously, alignment irrelevant ✓.
Let me write the sun glow stronger: box-shadow `0 0 30px 10px rgba(255,197,72,.55), 0 0 90px 30px rgba(255,170,60,.35)` — the second softer big glow. ✓ (box-shadow multiple ✓.)
Hmm — box-shadow on a circular element with radial-gradient ✓ fine.
Now cloud puff — one more check: `.cloud::before` at `top: -21px` — negative top OK (element overflow visible within scene; scene overflow hidden clips only outside scene bounds — cloud at top 30 → puff top at 9 ✓ inside ✓).
Storm cloud at top 26, puff −25 → 1 ✓ inside ✓ barely — fine.
Mini cloud top 22 −12 = 10 ✓.
Ripples at bottom 12, height 10+4 border... total ~14 → top at 176−12−14=150 ✓ below drops' end (166 fade) — overlapping fine.
Also drops travel: rain start 86-104 + 60 = ≤164 + fade ✓.
Flakes travel 64: start ≤98 + 64 = 162 ✓.
Storm drops ≤106 + 64 = 170 ✓ just at edge, fades by then ✓.
Bolt bottom 62+88 = 150 ✓.
Now, compile everything into final HTML. Also add a `<noscript>`? Not needed — no JS at all, works everywhere ✓.
Also — meta viewport ✓; add `color-scheme: dark`? `meta name="color-scheme" content="dark"` or CSS `color-scheme: dark` — helps form controls/scrollbars; add to :root ✓ minor nicety.
Let me write the complete file now, carefully.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Four Skies — Pure CSS Weather Cards</title>
<style>
/* ---------- base ---------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
color-scheme: dark;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}
html { -webkit-text-size-adjust: 100%; }
body {
font-family: var(--font);
background: #070b16;
color: #f4f7ff;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background:
radial-gradient(640px 420px at 12% -8%, rgba(88, 128, 255, .16), transparent 62%),
radial-gradient(720px 480px at 96% 110%, rgba(255, 138, 60, .12), transparent 62%);
pointer-events: none;
}
```
Wait — `body::before` with z-index -1 and body no stacking... body::before fixed z -1 goes behind body background? body has background #070b16 — the pseudo with z-index -1 will paint behind body's background? If body creates no stacking context, the fixed pseudo with negative z paints behind the body background → invisible! Classic issue: elements with negative z-index paint behind their parent's background only if... negative z-index children paint above the background of their stacking context ancestor — the nearest stacking context might be root; body's background is painted as part of the canvas... Actually: the root element's background covers the canvas; body background paints in the block background layer; a negatively-z-indexed child of body (body not a stacking context) belongs to root's stacking context, painting above ROOT background but below body? No — backgrounds of body paint in "in-flow" layer... negative z-index elements paint BELOW the background of the stacking context (root) but body's background is painted... hmm, body's background actually propagates? body background doesn't propagate to canvas (html does; if html has none, body's background IS propagated to the canvas per CSS spec!). Yes — when html has no background, body's background is propagated to the root/canvas. Then negative z children paint above the canvas background ✓ so the glow WOULD be visible. But this is fragile — simpler: make the glow a positive-layer: put it on `html` background directly:
```css
html {
background:
radial-gradient(640px 420px at 12% -8%, rgba(88,128,255,.16), transparent 62%),
radial-gradient(720px 480px at 96% 110%, rgba(255,138,60,.12), transparent 62%),
#070b16;
background-attachment: fixed?;
}
body { background: transparent; }
```
Multiple backgrounds on html ✓ radial gradients + final color ✓. background-attachment fixed makes glows stay on scroll — but gradients sized in px at % positions: with attachment fixed they position relative to viewport ✓. Minor; page likely fits ~800 anyway. Set `min-height: 100vh` on body not needed. I'll put the layered background on html with default attachment (scroll) — fine.
OK continue:
```css
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: clamp(30px, 5vh, 52px) clamp(16px, 4vw, 40px) 48px;
}
.head { text-align: center; margin-bottom: clamp(26px, 4.5vh, 44px); }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
font-weight: 600;
letter-spacing: .24em;
text-transform: uppercase;
color: #92a6cf;
margin-bottom: 14px;
}
.eyebrow b { color: #e8b04a; font-weight: 600; }?? maybe accent — skip.
h1 {
font-size: clamp(1.75rem, 3.4vw, 2.65rem);
font-weight: 800;
letter-spacing: -.025em;
line-height: 1.08;
color: #fbfcff;
margin-bottom: 13px;
text-wrap: balance;
}
.lede {
max-width: 640px;
margin: 0 auto;
font-size: clamp(.92rem, 1.3vw, 1rem);
line-height: 1.68;
color: rgba(190, 203, 230, .85);
text-wrap: pretty;? maybe balance — use pretty harmless
}
```
Grid, card, scenes, body as designed. Then the four card variants, keyframes, reduced-motion, media queries.
Let me assemble the whole thing in final form, being careful about every selector matching the markup.
Markup review — final:
Sunny scene children order: mini cloud then sun. But scene::before halo paints before both ✓.
Wait — actually I set `.sun { transform: translate(-50%,-50%) }` static; no animation on `.sun` itself ✓ centering persists ✓.
For the rain scene, the haze is scene::before, then cloud, drops, ripples ✓.
Note: `.card--rain .scene::before` — I must not also use `.scene::before` generically. All good per-card.
Also, mini cloud: is `aria-hidden` on scene ✓.
Full markup:
```html
<main class="wrap">
<header class="head">
<p class="eyebrow">No JavaScript · No images · No SVG — just HTML & CSS</p>
<h1>Four skies, drawn in pure CSS</h1>
<p class="lede">Every sun ray, raindrop, snowflake and bolt of lightning below is built from plain elements and pseudo-elements, animated with nothing but <code>@keyframes</code>.</p>
</header>
<section class="grid">
<article class="card card--sun">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--mini"></div>
<div class="sun">
<div class="sun-rays"></div>
<div class="sun-core"></div>
</div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">Lisbon</h2>
<span class="chip">Sunny</span>
</div>
<p class="temp">24°</p>
<p class="desc">Clear skies and golden light, with a soft breeze rolling in off the Atlantic.</p>
<p class="meta"><span>H 27°</span><span>L 18°</span><span>Wind 12 km/h</span></p>
</div>
</article>
<article class="card card--rain">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--rain"></div>
<div class="drops">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="ripple r1"></div>
<div class="ripple r2"></div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">London</h2>
<span class="chip">Rain</span>
</div>
<p class="temp">13°</p>
<p class="desc">Steady drizzle settling in for the afternoon — keep the umbrella close.</p>
<p class="meta"><span>H 15°</span><span>L 9°</span><span>Wind 20 km/h</span></p>
</div>
</article>
<article class="card card--snow">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--snow"></div>
<div class="flakes">
<span class="flake"></span><span class="flake"></span><span class="flake"></span><span class="flake"></span><span class="flake"></span><span class="flake"></span>
</div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">Reykjavík</h2>
<span class="chip">Snow</span>
</div>
<p class="temp">−2°</p>
<p class="desc">Light snow drifting down on a cold northerly wind.</p>
<p class="meta"><span>H 1°</span><span>L −8°</span><span>Wind 15 km/h</span></p>
</div>
</article>
<article class="card card--storm">
<div class="scene" aria-hidden="true">
<div class="cloud cloud--storm"></div>
<div class="drops">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="bolt-wrap"><div class="bolt"></div></div>
</div>
<div class="card-body">
<div class="body-top">
<h2 class="city">Singapore</h2>
<span class="chip">Thunder</span>
</div>
<p class="temp">28°</p>
<p class="desc">Thunderstorms rumbling through, clearing by early evening.</p>
<p class="meta"><span>H 30°</span><span>L 25°</span><span>Wind 38 km/h</span></p>
</div>
</article>
</section>
<p class="foot">Hover a card to lift it — icons pause automatically when your system prefers reduced motion (<code>prefers-reduced-motion</code>).</p>
</main>
```
`.lede code` inline styling needed ✓.
Now — since flakes all have class flake and nth-child selectors: `.flakes .flake:nth-child(1)` ✓ (they're the only children ✓).
Storm `.drops` reuses `.drops span` base styles ✓ — but base includes `animation: rainfall` — storm overrides with its own animation in `.card--storm .drops span` ✓ higher specificity ✓. And the rain nth-child positions are scoped `.card--rain .drops span:nth-child(n)` — wait, I planned generic `.drops span:nth-child()`. Scope properly: use `.card--rain .drops span:nth-child(n)` and `.card--storm .drops span:nth-child(n)` ✓ no bleed.
Also base `.drops span { transform: rotate(9deg); }` — storm overrides to 14deg ✓.
Now — hmm, one more idea for extra delight within requirements: a tiny CSS-only detail — maybe the card could show "updated 2 min ago"? Skip, keep clean.
Also consider `scrollbar` — page fits, no scroll at 800 ✓ (but with 4K/other heights fine).
Let me now also double check the chip `backdrop-filter` — inside card with overflow hidden ✓ works. Chrome ✓.
Write final CSS card body ink vars:
```css
.card {
--ink: #ffffff;
--ink-2: rgba(255,255,255,.86);
--ink-3: rgba(255,255,255,.62);
--line: rgba(255,255,255,.22);
--chip-bg: rgba(255,255,255,.16);
--chip-line: rgba(255,255,255,.32);
--tshadow: 0 1px 2px rgba(9,14,30,.28);
...
}
.card--sun {
--ink: #3b1d02;
--ink-2: rgba(59,29,2,.85);
--ink-3: rgba(59,29,2,.62);
--line: rgba(59,29,2,.25);
--chip-bg: rgba(59,29,2,.14);
--chip-line: rgba(59,29,2,.3);
--tshadow: none;
}
```
Body uses these ✓.
Let me also double-check the sun card chip text color: var(--ink) ✓ dark ✓ on translucent dark chip over amber ✓ readable ✓.
Temp `color: var(--ink)` ✓.
Now assemble everything. Also I should double check `.card-body` padding-bottom 22 and `.meta` bottom alignment.
One thing: `.card-body { flex: 1 }` requires `.card { display:flex; flex-direction: column }` ✓.
Scene `flex: none; height: 176px` ✓.
Also, I realize the `.card:hover .scene { transform: scale(1.05) }` — but on touch devices sticky hover — irrelevant.
Also add `@media (hover: none)`? skip.
Also — `.card { cursor: default }` fine default.
Let me also think about `transition` on `.scene` transform — under reduced motion `transition: none !important` ✓ instant scale — fine.
Also there's a subtle thing: `.card:hover` transform translateY(-10px) — combined with `:focus-within` not needed.
Finally, check there's no `position: fixed` issues on `html` background with multiple backgrounds — fine.
Let me also reconsider scene heights on mobile: 176 fixed OK.
Also: `<p class="temp">24°</p>` — use `font-size 3.3rem`. For `−2°` the minus renders in light weight ✓.
Possible improvement — add `letter-spacing: -.02em` to temp ✓.
Edge: `.temp` font-weight 200 — for `−2°` fine.
Now let me also verify: does anything require `border-radius: inherit` to work on ::after? `border-radius: inherit` — inherited from `.card` (24px) ✓ works since `.card::after` inherits from card ✓. Actually `inherit` keyword takes computed value of parent ✓ 24px ✓.
Also the shine ::before has no border-radius — it's clipped by card overflow hidden ✓ no need.
Now, "grid-template-columns: repeat(4, minmax(0, 1fr))" ✓.
Footer foot margin-top 28 ✓.
Now write out @keyframes list fully:
1. spin
2. sun-pulse
3. halo-breathe
4. cloud-bob
5. storm-bob
6. cloud-drift
7. rainfall
8. storm-fall
9. ripple
10. flake-drift
11. bolt-flash
12. sky-flash
All defined ✓ infinite ✓.
Sky flash opacity values ~ slightly dimmer than bolt ✓.
Let me make sure `.card--storm .scene::after` animation name sky-flash ✓.
Also — storm scene::after inset 0 covers whole scene ✓ radial ellipse at 50% 22% ✓.
One more check on the veil + scene + z-order: `.card--storm .scene::after` is inside scene (z2) ✓ above cloud/drops/bolt ✓ flashes over them ✓.
Also — storm cloud drop-shadow: `drop-shadow(0 16px 18px rgba(4,9,22,.55))` ✓.
Snow haze bottom white — flakes fall over it ✓.
Rain haze: darker blue bottom — ripples over it ✓.
I think the design is complete. Let me also add tiny "location dot" before city? A small `●`? That's a character, not an icon font — but it might read as an icon; skip.
Also — the `.eyebrow` middots: use `·` characters ✓.
Let me now write the complete final file carefully, then re-verify every selector against markup, checking for typos, and confirm requirement compliance. I'll write it cleanly with comments organizing sections.
Additionally — I want to double-check the `repeating-conic-gradient` + mask combo won't produce a visible seam: rays with mask ring — outer fade 88→94% gives soft outer tips ✓ inner 52→56% hard-ish ✓ fine.
Ray color `#ffe9a6` on amber bg `#ffb43e` (top area): lighter ✓ subtle ✓ plus the halo behind adds glow ✓. Maybe make rays a bit brighter: `#fff0bd`. Use `#ffefb8`.
Sun core inset 34px → diameter 82 ✓.
Mini cloud drift 36px at left 9% — on 280px card: 9% = 25px + 56 wide = 81; +36 = 117 < 280-? ✓ fine. On wide mobile card ✓ fine.
Halo breathe opacity .75↔1 ✓.
Also add slight `animation-delay` differences... fine as is.
Let me reconsider `.drops span` height 15px and width 3px with rotate 9deg ✓.
Ripple border 2px — with height 10 incl border? `box-sizing: border-box` global ✓ so 28×10 outer ✓.
Flake `height: 2px` might be too thin on hi-dpi? 2.5px rounds to 2.4? Use 3px for the big ones? Let me set base flake height 2.5px? Chrome handles fractional ✓. I'll use height: 2.5px? Hmm border-radius 2px on 2.5 height fine. Let me simplify: height: 3px? A 3px-thick asterisk arm reads chunky-cute ✓. With widths 8-14, arm thickness 3 ✓ good visibility ✓. Use 3px... but then the small 8px-wide flake with 3px arms — ratio ok. I'll go height 2.5px compromise? Non-integer renders fine. Use 2.5px.
Hmm, actually let me reconsider flake arm thickness relative to drop-shadow blur 3px — arms may look like glowing dots ✓ fine.
Now — writing time. I'll be meticulous.
Also decide: `.scene { transition: transform .45s cubic-bezier(.2,.7,.3,1.2)? }` overshoot on zoom subtle — use same easing as card for coherence: `.45s cubic-bezier(.22,.68,.34,1)`.
Card easing: `cubic-bezier(.22,.68,.34,1.08)` slight overshoot on lift — tasteful.
Final check on requirement "stacked on mobile": ✓ at ≤620px 1 column.
Also, I should double check that with `animation: none !important` under reduced-motion, the negative `animation-delay`s also irrelevant ✓.
And the universal reduced-motion rule also kills the halo breathe ✓ static halo ✓.
One question: `*::before, *::after { transition: none !important }` also affects `.card::before` shine → jumps instantly on hover; under reduced motion maybe better to hide the shine entirely? Instant band appears/disappears — looks like a glitch? On hover it appears at left and vanishes... it jumps to final offscreen position instantly, so actually nothing visible appears (it teleports offscreen) ✓ harmless.
Also `.card:hover` lift instant — fine.
Let me also guard: `@media (prefers-reduced-motion: reduce)` also add `.card:hover .scene { transform: none }`? Not necessary; instant zoom ok... but zoom = motion? It's instantaneous state change — acceptable, though purists might prefer none. I'll add transform:none for scene on hover under reduced motion? But `.card:hover` translateY(-10px) is also motion... "minimize non-essential motion" — instant repositioning is generally acceptable. I'll keep hover functional but instant. Hmm, actually to be maximally respectful, I could disable hover transforms too under reduced motion: `.card:hover { transform: none }` + scene none. Then the only feedback = shadow change (non-motion) ✓. That's classy. But then "lift up on hover" requirement — still satisfied globally (for users without the preference). Under the preference we intentionally disable ✓ that's the point of the media query. I'll disable transforms under reduced motion ✓.
Also `scroll-behavior` irrelevant.
OK — also one more accessibility touch: `:focus-visible` for... nothing focusable. Skip.
Writing the final code now.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Four Skies · Pure-CSS Weather Cards</title>
<style>
/* ============ base ============ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
color-scheme: dark;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}
html {
background:
radial-gradient(640px 420px at 10% -10%, rgba(90,130,255,.16), transparent 62%),
radial-gradient(760px 500px at 96% 112%, rgba(255,140,60,.13), transparent 62%),
#070b16;
}
body {
font-family: var(--font);
color: #f4f7ff;
-webkit-font-smoothing: antialiased;
}
```
Then wrap/head, grid, card, scenes, etc. Let me write all of it.
Also `.lede code`, `.foot code` share styling: `code { font-family: var(--mono); font-size: .82em; background: rgba(126,150,205,.14); border: 1px solid rgba(126,150,205,.2); padding: .1em .38em; border-radius: 6px; }` ✓.
Now, card CSS (full):
```css
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 20px;
}
.card {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 24px;
box-shadow:
0 16px 34px rgba(2, 6, 18, .5),
inset 0 1px 0 rgba(255,255,255,.25),
inset 0 0 0 1px rgba(255,255,255,.06);
transition: transform .45s cubic-bezier(.22,.68,.34,1.08), box-shadow .45s ease;
--ink: #fff;
--ink-2: rgba(255,255,255,.88);
--ink-3: rgba(255,255,255,.64);
--line: rgba(255,255,255,.22);
--chip-bg: rgba(255,255,255,.16);
--chip-line: rgba(255,255,255,.32);
--tshadow: 0 1px 2px rgba(9,14,30,.28);
}
.card:hover {
transform: translateY(-10px);
box-shadow:
0 32px 60px rgba(2,6,18,.62),
inset 0 1px 0 rgba(255,255,255,.3),
inset 0 0 0 1px rgba(255,255,255,.1);
}
.card--sun { background: linear-gradient(168deg, #ffb43e 0%, #ff8a00 48%, #ef5e0c 100%); }
.card--rain { background: linear-gradient(168deg, #4f9ede 0%, #33689c 56%, #26507e 100%); }
.card--snow { background: linear-gradient(168deg, #8cc2ea 0%, #6194c4 55%, #42739e 100%); }
.card--storm { background: linear-gradient(168deg, #47516b 0%, #2b3247 55%, #1a1f30 100%); }
.card--sun {
--ink: #3b1d02;
--ink-2: rgba(59,29,2,.85);
--ink-3: rgba(59,29,2,.66);
--line: rgba(59,29,2,.26);
--chip-bg: rgba(59,29,2,.15);
--chip-line: rgba(59,29,2,.32);
--tshadow: none;
}
/* readability veil behind text */
.card::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
background: linear-gradient(to top, rgba(8,12,26,.34), rgba(8,12,26,.08) 46%, rgba(8,12,26,0) 66%);
pointer-events: none;
}
/* hover shine sweep */
.card::before {
content: "";
position: absolute;
top: -12%;
bottom: -12%;
left: -80%;
width: 55%;
z-index: 3;
background: linear-gradient(105deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.28) 50%, rgba(255,255,255,0) 100%);
transform: skewX(-16deg);
transition: transform 1s ease;
pointer-events: none;
}
.card:hover::before { transform: skewX(-16deg) translateX(420%); }
```
Hmm wait — the veil z1 sits above the card background but below scene z2 — but the card's own `box-shadow` inset ring paints above backgrounds ✓ fine.
But hold on — for the sun card, veil dark over amber at bottom ✓ dark ink text benefits? Dark text over slightly darkened amber — contrast slightly reduced but still strong ✓.
Scene:
```css
.scene {
position: relative;
z-index: 2;
flex: none;
height: 178px;
overflow: hidden;
transition: transform .45s cubic-bezier(.22,.68,.34,1.08);
}
.card:hover .scene { transform: scale(1.05); }
```
Card body:
```css
.card-body {
position: relative;
z-index: 4;
flex: 1;
display: flex;
flex-direction: column;
padding: 18px 20px 20px;
text-shadow: var(--tshadow);
}
.body-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
}
.city { font-size: 1.1rem; font-weight: 750; color: var(--ink); letter-spacing: .01em; }
```
font-weight 750 — variable fonts? Segoe UI Variable on Win11 ✓ else rounds to 700 ✓ fine. Use 700 to be safe. OK 700.
```css
.chip {
flex: none;
font-size: 10px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--ink);
padding: 5px 10px;
border-radius: 999px;
background: var(--chip-bg);
border: 1px solid var(--chip-line);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
white-space: nowrap;
}
.temp {
font-size: 3.35rem;
font-weight: 200;
line-height: .95;
letter-spacing: -.03em;
color: var(--ink);
margin-bottom: 10px;
}
.desc {
font-size: .9rem;
line-height: 1.55;
color: var(--ink-2);
margin-bottom: 16px;
}
.meta {
margin-top: auto;
display: flex;
flex-wrap: wrap;
padding-top: 12px;
border-top: 1px solid var(--line);
font-size: .72rem;
font-weight: 600;
letter-spacing: .04em;
color: var(--ink-3);
}
.meta span + span {
border-left: 1px solid var(--line);
margin-left: 10px;
padding-left: 10px;
}
```
Icons:
```css
/* ---- sun ---- */
.card--sun .scene::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 330px;
height: 330px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: radial-gradient(circle, rgba(255,240,180,.62) 0%, rgba(255,215,120,.22) 45%, rgba(255,200,90,0) 68%);
animation: halo-breathe 6s ease-in-out infinite;
}
.sun {
position: absolute;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
transform: translate(-50%, -50%);
}
.sun-rays {
position: absolute;
inset: 0;
background: repeating-conic-gradient(from -8deg at 50% 50%,
#ffefb8 0deg 16deg, rgba(255,239,184,0) 16deg 45deg);
-webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 57%, #000 88%, transparent 94%);
mask: radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 57%, #000 88%, transparent 94%);
animation: spin 26s linear infinite;
}
.sun-core {
position: absolute;
inset: 34px;
border-radius: 50%;
background: radial-gradient(circle at 36% 32%, #fff7d2 0%, #ffdd55 48%, #ffb52e 100%);
box-shadow:
0 0 30px 8px rgba(255,199,72,.6),
0 0 90px 26px rgba(255,170,60,.35);
animation: sun-pulse 4.6s ease-in-out infinite;
}
```
Clouds:
```css
.cloud { position: absolute; }
.cloud::before, .cloud::after { content: ""; position: absolute; border-radius: 50%; }
.cloud--rain {
top: 30px;
left: calc(50% - 52px);
width: 104px;
height: 34px;
border-radius: 40px;
background: linear-gradient(to bottom, #e3edfa 0%, #b9cde8 100%);
filter: drop-shadow(0 14px 16px rgba(18,46,92,.35));
animation: cloud-bob 5.6s ease-in-out infinite;
}
.cloud--rain::before, .cloud--rain::after { background: #e3edfa; }
.cloud--rain::before { width: 44px; height: 44px; top: -21px; left: 15px; }
.cloud--rain::after { width: 31px; height: 31px; top: -14px; right: 15px; }
```
... snow, storm, mini similarly.
Rain field & positions & keyframes as planned.
Also `.card--rain .scene::before` haze.
Let me also finalize storm bolt timing keyframes (as designed above with three bursts).
sky-flash: match timings, lower opacities:
```css
@keyframes sky-flash {
0%, 40% { opacity: 0; }
43% { opacity: .85; }
45% { opacity: .06; }
48% { opacity: .7; }
51% { opacity: .12; }
54% { opacity: 0; }
57% { opacity: .6; }
60% { opacity: .05; }
63% { opacity: 0; }
74% { opacity: 0; }
76% { opacity: .9; }
78% { opacity: .12; }
81% { opacity: .65; }
84% { opacity: .2; }
87% { opacity: 0; }
100% { opacity: 0; }
}
```
bolt-flash:
```css
@keyframes bolt-flash {
0%, 40% { opacity: 0; }
43% { opacity: 1; }
45% { opacity: .12; }
48% { opacity: .95; }
51% { opacity: .2; }
54% { opacity: 0; }
57% { opacity: .85; }
60% { opacity: .08; }
63% { opacity: 0; }
74% { opacity: 0; }
76% { opacity: 1; }
78% { opacity: .18; }
81% { opacity: .9; }
84% { opacity: .3; }
87% { opacity: 0; }
100% { opacity: 0; }
}
```
duration 5.2s both bolt-wrap and scene::after ✓.
Hmm — wait: between 0% and 40% the bolt is invisible → bolt visible only ~47% of... no: visible 43-54 (11/5.2 ≈ 0.2-2.8... let me compute: 43% of 5.2 = 2.24s; bursts: 2.24–2.81s, 2.96–3.28s, 3.95–4.52s. Between-bursts gaps: 0–2.24 invisible (long dark), then flickers, then 4.52–5.2 invisible. Screenshot ~3.0s: burst2 at 2.96–3.28 ✓ bolt at 57% → 2.964s: opacity .85 ✓ bright ✓
But actually the exact screenshot time is unknown ("about 3 seconds"). 2.9–3.1 → burst2 ✓. Even if 2.2-2.8 → burst1 ✓. If it lands at 3.3-3.9 → dark (no bolt). Hmm risk remains. Broaden burst2: extend to 57–66%? Let me stretch: add stops: 57% .85, 60% .1, 63% .7, 66% 0 → burst2 spans 2.96–3.43s. Then dark gap 3.43–3.95 short. Screenshot at 3.0-3.4 mostly covered ✓. Also burst1 2.24–2.81 covers 2.3–2.8 ✓. So coverage 2.24–3.43s ≈ good for "about 3 seconds" ✓. And sky flash similarly. OK adjust both: replace 63% {opacity: 0} with 63% {opacity: .7} and 66% {opacity: 0}. Then next idle until 74%.
Updated bolt-flash:
```
0%, 40% { opacity: 0 }
43% { opacity: 1 }
45% { opacity: .12 }
48% { opacity: .95 }
51% { opacity: .2 }
54% { opacity: 0 }
57% { opacity: .85 }
60% { opacity: .1 }
63% { opacity: .7 }
66% { opacity: 0 }
74% { opacity: 0 }
76% { opacity: 1 }
78% { opacity: .18 }
81% { opacity: .9 }
84% { opacity: .3 }
87% { opacity: 0 }
100% { opacity: 0 }
```
sky-flash mirror with reduced opacities (max .75):
```
0%, 40% { opacity: 0 }
43% { opacity: .8 }
45% { opacity: .06 }
48% { opacity: .65 }
51% { opacity: .12 }
54% { opacity: 0 }
57% { opacity: .6 }
60% { opacity: .05 }
63% { opacity: .5 }
66% { opacity: 0 }
74% { opacity: 0 }
76% { opacity: .85 }
78% { opacity: .1 }
81% { opacity: .6 }
84% { opacity: .2 }
87% { opacity: 0 }
100% { opacity: 0 }
```
Good. 17 stops each — verbose but fine.
Storm duration: bolt-wrap animation `bolt-flash 5.2s linear infinite`; scene::after `sky-flash 5.2s linear infinite` ✓ synced ✓ (same start time — both start when applied at load ✓).
Wait — negative delays? none here ✓ they start at 0 together ✓ synced ✓.
Now reduced motion block:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation: none !important;
transition: none !important;
}
.card:hover { transform: none; }
.card:hover .scene { transform: none; }
}
```
Media queries:
```css
@media (max-width: 1020px) {
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
.grid { grid-template-columns: 1fr; gap: 18px; max-width: 460px; margin-inline: auto; }
}
```
Foot:
```css
.foot {
margin-top: 28px;
text-align: center;
font-size: .8rem;
line-height: 1.6;
color: rgba(163,178,207,.6);
}
```
`code` styling:
```css
code {
font-family: var(--mono);
font-size: .85em;
background: rgba(126,152,208,.13);
border: 1px solid rgba(126,152,208,.22);
border-radius: 6px;
padding: .08em .4em;
}
```
Now the remaining pieces: rain/snow/storm scene styles & positions; ripple; flakes; bolt.
Let me also decide snow scene haze color: white-ish at bottom over icy blue ✓ `rgba(235,246,255,.4)`.
Also: should snow scene have a couple of static "drift" mounds? skip.
Double-check flake nth-of-type selectors: `.card--snow .flake:nth-child(1)` — flakes are children of `.flakes` ✓ `nth-child` counts among all siblings — all are `.flake` spans ✓.
Let me write flake positions with `left/top` as planned.
Also — the `.flakes` container `position: absolute; inset: 0` ✓ pointer-events irrelevant.
Potential issue: `.flake` `filter: drop-shadow(...)` — 6 flakes each with filter ✓ cheap.
Also `.cloud` filter drop-shadows ✓.
Total animated elements: modest ✓ perf fine.
Now, storm drop colors `rgba(198,220,255,.9)` ✓ on dark bg ✓.
Also — storm bolt behind or in front of drops? Tree order in markup: cloud, drops, bolt-wrap → bolt last → on top ✓.
Everything seems settled. Let me also add `will-change: transform` on `.card`? Not necessary; transitions fine.
One more consideration — the `.card` overflow hidden + `border-radius: 24px` — the veil ::after inset 0 border-radius inherit ✓; scene top corners clipped by card ✓.
Also, `box-shadow` transition includes inset rings — fine.
**Sanity: any `position: absolute` element without a positioned ancestor?** `.ripple` — ancestor `.scene` relative ✓. `.drops` relative? `.drops` absolute inside scene ✓ its spans absolute inside `.drops` (which is positioned ✓).
`.sun-rays/core` absolute inside `.sun` (position absolute ✓ positioned).
`.bolt` inside `.bolt-wrap` (absolute ✓) — `.bolt` static, fills width/height 100% ✓ no positioning needed.
`.cloud::before/::after` absolute inside `.cloud` (absolute ✓).
OK. Also — `.scene` has `transition: transform` — under reduced motion removed ✓.
Also I should double check `text-wrap: balance` — fine.
Let me also add `overflow-x: hidden` on body? Not needed; nothing exceeds. Actually the html backgrounds don't create overflow ✓.
Also to be safe: `img`? none ✓.
Now — let me think about whether `backdrop-filter` on chip could interact with the veil... chip is above veil (body z4) ✓ blur picks card bg beneath ✓ subtle ✓.
Also the sun card chip with dark ink on translucent dark bg over amber → readable ✓.
Final: verify no `<script>`, no event handlers, no `on*` attributes ✓.
Also the requirement "four animated weather cards (sunny, rainy, snowy, stormy)" ✓.
Let me also reconsider: "rotating sun rays" — mine rotate ✓ 26s.
"falling rain drops" ✓.
"drifting snowflakes" ✓.
"flashing lightning" ✓ (bolt + sky flash).
Let me write out the complete final document now. I'll be careful with syntax. Also add comments to make the code readable (nice for judges).
One last idea: maybe give each card a unique accent for the chip? They use --chip vars; sun's differs ✓ fine.
Also — hmm, the `.body-top` chip: for storm "Thunder" — length ~7 chars ✓ fits.
Let me also handle potential text overflow of `.city` at very narrow (280px card): "Reykjavík" + "Snow" ✓ fine.
Also on 4-col 280px cards, `.meta` three spans: "H 27°" "L 18°" "Wind 12 km/h" — total width ≈ 40+40+80 + separators 40 = 200 ✓ fits 240 ✓. "Wind 38 km/h" slightly wider ✓ still fits. flex-wrap fallback ✓.
Now compose. Also — I realize for extra visual interest, maybe add a subtle CSS `background-size` texture... skip, done deliberating.
Writing final output.
Actually — one more thought on `.card:hover .scene { transform: scale(1.05) }`: the scene's overflow hidden means the zoom enlarges icons nicely ✓ and the veil (::after z1) doesn't scale (it's on card, not scene) ✓ fine.
Also `.scene::before` halo is inside scene → scales too ✓.
Double-check sun halo at scene top: halo 330px centered at scene center (89px) → extends from 89−165 = −76 (clipped top ✓) to 254 (clipped at 178 ✓). Radial center visible ✓ big soft glow ✓.
Mini cloud under halo (tree order: ::before, then .cloud--mini, then .sun) ✓ halo behind mini ✓.
Final structure done. Write it.
Let me make sure about `mask` fallback order: some browsers need `-webkit-mask`; put prefixed first then standard ✓.
Also — `repeating-conic-gradient` needs no prefix in Chrome ✓.
Also check: in `.sun-rays`, using mask shorthand `radial-gradient(...)` — shorthand `mask: <masking images>` ✓ radial-gradient allowed ✓.
Alright, also let me reconsider the sun rays inner radius: mask `transparent 52%` → 0.52×75 = 39px; `#000 57%` = 42.75px. The core r=41px with big glow — glow extends well past 42 → rays appear to emanate from the glow ✓ good.
Also — `.sun-rays` rotating: mask is on the element, rotates with it — mask is symmetric (circle) ✓ no wobble ✓.
Compose final answer now. Also ensure NOT to include any JS ✓ and the reply must be exactly one fenced code block ✓ (no text before/after — the rules say reply with exactly one fenced code block; I'll output only the code block).
Also let me pre-verify a couple of tricky CSS values:
`radial-gradient(circle closest-side at 50% 50%, transparent 52%, #000 57%, #000 88%, transparent 94%)` ✓ valid.
`repeating-conic-gradient(from -8deg at 50% 50%, #ffefb8 0deg 16deg, rgba(255,239,184,0) 16deg 45deg)` ✓ valid. Note: rays appear at -8..8°, then 37..53°, etc. — 8 rays ✓.
`clip-path: polygon(35% 0%, 95% 0%, 66% 45%, 90% 45%, 20% 100%, 28% 55%, 0% 55%)` ✓.
Gradient positions like `at 50% 22%` for ellipse: `radial-gradient(ellipse 75% 55% at 50% 22%, ...)` — syntax: `radial-gradient(ellipse 75% 55% at 50% 22%, rgba(255,255,255,.55), rgba(255,255,255,0) 70%)` — ellipse with two percentages = radii ✓ valid syntax (`ellipse <length-percentage>{2}`) ✓.
Hmm — safer: `radial-gradient(60% 50% at 50% 24%, ...)` ✓ same syntax family ✓ fine.
Storm flash overlay:
```css
.card--storm .scene::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(65% 55% at 52% 18%, rgba(255,255,255,.5), rgba(255,255,255,0) 70%);
opacity: 0;
animation: sky-flash 5.2s linear infinite;
}
```
✓.
Ripple keyframes with transform scale — base transform none static ✓.
`@keyframes ripple { 0% { transform: scale(.35); opacity: .8; } 70% { opacity: .3; } 100% { transform: scale(1.6); opacity: 0; } }` ✓.
Flake drift keyframes ✓ as planned.
Rainfall ✓.
cloud-bob, storm-bob, cloud-drift ✓.
spin ✓. sun-pulse ✓. halo-breathe ✓.
Now also `storm-fall` ✓.
Everything's consistent. Also I want `list` of spans for drops to not have layout side effects — spans are `position: absolute` ✓.
Let me also add `min-width: 0`? cards fine.
Also — `h1` margin, `.head` etc ✓.
One more: `.wrap` bottom padding 48 + foot ✓.
At 800 height: 30(top) + head(~150) + grid(~430) + foot(~45+28 margin) + bottom 48 = ~730 ✓.
Also, lede 2 lines at 640px width: text ~150 chars → at ~1rem, ~75 chars/line → 2 lines ✓.
Write it.
Also — I just want to triple-check the veil z-index vs scene: `.card::after { z-index: 1 }` — but `.card` doesn't create a stacking context unless... `.card` has no transform currently (only on hover) — its children with z-index participate in... z-index on positioned children works within the nearest stacking context — card isn't necessarily a stacking context, but positioned descendants with z-index still stack relative to the root context or nearest positioned ancestor forming a stacking context... For sibling comparison (::after vs .scene), both are positioned with z-index → they compare within the common parent stacking context; even if that context is the root, order: ::after z1 < .scene z2 ✓ works. On hover, `.card` gets a transform → becomes a stacking context → still consistent internally ✓. Actually — WAIT: if `.card` is NOT a stacking context, then its positioned children's z-index compete GLOBALLY with... e.g., other cards' children? Each card's ::after z1 vs another card's scene z2 — they don't overlap spatially (different grid cells) → no visual issue ✓. And body::before? I removed that (html background) ✓. OK fine. To be extra safe, add `isolation: isolate` on `.card` — cheap and correct. I'll add it ✓.
Also `.scene` `isolation`? not needed.
Let me also think — `.card { isolation: isolate }` ensures all stacking contained ✓.
Final answer code below. Let me carefully produce it in one pass with attention to matching braces.
Additional check — `.meta span + span` border-left with flex-wrap: if wrapped, a border appears at line start — minor. Widths should prevent wrap mostly ✓ fine.
Also `text-shadow: var(--tshadow)` — with `--tshadow: none`, `text-shadow: none` ✓ valid.
I'm also going to double check the storm scene: cloud top 26, height 40 → bottom 66; bolt top 62 → bolt starts inside cloud bottom ✓ visually attached ✓. Bolt left calc(50% - 17px), width 54 → spans center+10 to center+64... wait: left = 50%−17 → right edge = 50%−17+54 = 50%+37 → center of bolt at 50%+10 ✓ slight right ✓.
Storm drops lefts: 15%,26%,60%,72%,83% — bolt spans (on 280px card) 123–177px = 44–63% → drop at 60% overlaps bolt edge — behind bolt (bolt later in tree) ✓ fine.
Rain cloud top 30; drops top 84-104 under cloud (bottom 64) ✓ gap 20-40px — drops start a bit below cloud, fading in — with the -8px start they begin ~76 ✓ nice.
Snow cloud top 26 h 33 → bottom 59; flakes 70-98 ✓.
Sun: 150px centered in 178 scene ✓ mini cloud at top-left ✓.
Also — for the snow card, maybe add tiny static snow mounds at bottom? The white haze gradient covers ✓ enough.
Now writing the final file. I'll double check every `@keyframes` braces and semicolons as I go.
Also one more accessibility: `<article>` + h2 ✓; add `<h2 class="city">` ✓. And `aria-hidden` scenes ✓.
I'm confident. Final output — one code block, complete HTML.
Count nothing else needed. Write it.
Let me write the CSS in a tidy order:
1. reset & tokens
2. page shell (wrap, head, foot, code)
3. grid & card base (incl. veil/shine, hover)
4. card gradients + sun ink vars
5. scene base
6. card-body
7. sun icon
8. clouds (shared + variants)
9. rain (haze, drops, ripples)
10. snow (haze, flakes)
11. storm (haze, flash, drops, bolt)
12. keyframes
13. media queries (responsive, reduced motion)
Also — one detail: `.cloud` base class sets `position: absolute` ✓ and the mini cloud `left: 9%` top 22px ✓.
Also `.cloud--mini` needs `border-radius: 20px`? height 19 → radius 20 pill-ish ✓ use `border-radius: 20px`.
Wait, `.cloud--mini` height 19px, width 56 → radius 20 ✓.
Also mini-cloud puff `top:-12` → total height 31; top 22 → from 10 to 41 ✓.
Mini cloud drift 36px: from left 9% → moves to 9%+36px ✓ stays within ✓.
Also — animation `cloud-drift 9s ease-in-out infinite` — travels 0→36→0 ✓ smooth infinite ✓.
Done. Write.
One more possible nit: `.card:hover` for touch — fine.
Also — should I use `.card:active`? skip.
FINAL CODE:
(Also remember: reply with exactly one fenced code block; language `html`.)
Let me now also compute text: eyebrow "No JavaScript · No images · No SVG — just HTML & CSS" — the `&` needs escaping as `&` in HTML ✓.
Also — lede: "Every sun ray, raindrop, snowflake and bolt of lightning is built from plain elements and pseudo-elements — no images, no SVG, no scripts — just @keyframes." Wrap `@keyframes` in code ✓.
Writing final.
For safety with the `code` inside `.lede` on dark bg ✓ styling above.
Also careful: `.foot` says "Hover a card to lift it" ✓ and reduced-motion note ✓.
Now produce.
Also — grid `gap: 20px` desktop, 18 mobile ✓.
Also — one more possible issue: on the sun card, the veil `rgba(8,12,26,.34)` at bottom darkens amber making dark-brown text slightly lower contrast at the meta row (bottom). Dark text #3b1d02 on amber darkened 34% toward dark navy: bg luminance drops → dark text contrast decreases somewhat. #ff8a00 → mix with rgba(8,12,26,.34): result ≈ (0.66·255+0.34·8, ...) = (171, 96, 12)ish → luminance ~0.19 → contrast with #3b1d02 (lum ~0.012) = (0.19+0.05)/(0.012+0.05) ≈ 3.9 ✓ still fine for small meta text (borderline but with 600 weight ok). Alternatively reduce veil for sun card: override `.card--sun::after { background: linear-gradient(to top, rgba(8,12,26,.18), transparent 60%); }` — better: keep sun veil lighter ✓ I'll add that override.
Similarly snow veil .34 fine for white text ✓.
OK writing now, for real. I'll be careful with the storm keyframes and all values.
Also let me finalize raindrop nth-child list (7 drops), ensuring `animation-duration` variance:
```css
.card--rain .drops span:nth-child(1) { left: 26%; top: 84px; animation-delay: -0.20s; }
.card--rain .drops span:nth-child(2) { left: 37%; top: 96px; animation-delay: -0.75s; }
.card--rain .drops span:nth-child(3) { left: 48%; top: 86px; animation-delay: -1.10s; animation-duration: 1.45s; }
.card--rain .drops span:nth-child(4) { left: 60%; top: 99px; animation-delay: -0.40s; animation-duration: 1.7s; }
.card--rain .drops span:nth-child(5) { left: 71%; top: 90px; animation-delay: -0.95s; }
.card--rain .drops span:nth-child(6) { left: 81%; top: 97px; animation-delay: -1.35s; animation-duration: 1.5s; }
.card--rain .drops span:nth-child(7) { left: 32%; top: 103px; animation-delay: -0.60s; }
```
Storm:
```css
.card--storm .drops span:nth-child(1) { left: 14%; top: 92px; animation-delay: -0.10s; }
.card--storm .drops span:nth-child(2) { left: 25%; top: 104px; animation-delay: -0.55s; }
.card--storm .drops span:nth-child(3) { left: 59%; top: 95px; animation-delay: -0.90s; }
.card--storm .drops span:nth-child(4) { left: 71%; top: 105px; animation-delay: -0.35s; animation-duration: 1.05s; }
.card--storm .drops span:nth-child(5) { left: 83%; top: 90px; animation-delay: -0.70s; }
```
Flakes:
```css
.card--snow .flake:nth-child(1) { left: 23%; top: 74px; animation-duration: 6.8s; animation-delay: -1.2s; }
.card--snow .flake:nth-child(2) { left: 35%; top: 92px; width: 9px; animation-duration: 5.4s; animation-delay: -3.1s; }
.card--snow .flake:nth-child(3) { left: 49%; top: 78px; width: 14px; animation-duration: 7.6s; animation-delay: -4.6s; }
.card--snow .flake:nth-child(4) { left: 62%; top: 96px; width: 10px; animation-duration: 6.1s; animation-delay: -2.2s; }
.card--snow .flake:nth-child(5) { left: 74%; top: 82px; animation-duration: 7.0s; animation-delay: -5.4s; }
.card--snow .flake:nth-child(6) { left: 85%; top: 70px; width: 9px; animation-duration: 5.7s; animation-delay: -0.6s; }
```
Wait nth-child(6) delay -0.6 → begins at 0.6s into cycle → at t=0 it's at 89% progress — near end (falling to fade) — fine variety ✓.
Base flake: width 12px, height 2.5px.
Ripples:
```css
.ripple { position: absolute; bottom: 12px; width: 28px; height: 10px; border: 2px solid rgba(214,236,255,.65); border-radius: 50%; opacity: .55; animation: ripple 2.8s ease-out infinite; }
.ripple.r1 { left: 22%; }
.ripple.r2 { left: 60%; animation-delay: -1.4s; }
```
Snow haze and rain haze pseudo `content` ✓.
Now — very important: `.card--rain .scene::before` etc. — the sun card uses `.card--sun .scene::before` for the halo ✓ consistent ✓.
Ready. Also I'm going to double check `.drops span` base:
```css
.drops { position: absolute; inset: 0; }
.drops span {
position: absolute;
width: 3px;
height: 15px;
border-radius: 2px;
background: linear-gradient(to bottom, rgba(210,236,255,0), rgba(210,236,255,.95));
opacity: .9;
transform: rotate(9deg);
animation: rainfall 1.55s linear infinite;
}
.card--storm .drops span {
background: linear-gradient(to bottom, rgba(198,222,255,0), rgba(198,222,255,.92));
transform: rotate(14deg);
animation: storm-fall 1.15s linear infinite;
}
```
`.flakes { position: absolute; inset: 0; }` ✓.
OK writing the final file. Also the sun-card veil override:
```css
.card--sun::after {
background: linear-gradient(to top, rgba(8,12,26,.16), rgba(8,12,26,0) 55%);
}
```
Also I realize: should the veil apply at all for the sun card? dark ink needs BRIGHT bg; darkening hurts slightly — with .16 it's negligible ✓ keep for cohesion of the bottom edge.
Also the foot `code` ✓.
Also — one thing about `.card::before` (shine): it's `z-index: 3`, above veil ✓ below body ✓ above scene ✓ — sweeps across sky region ✓. But also above the scene's internal pseudos ✓ fine.
Also the base `transform: skewX(-16deg)` with `left: -80%` — the skew might cause it to peek into view at rest? Element spans x from -80% to -25% of card → fully offscreen left ✓ skew shifts vertically only (x unchanged for skewX) ✓.
`top: -12%; bottom: -12%` taller than card ✓ fine.
Great — produce final answer.
Also ensure the HTML contains NO `script`, no `onerror` etc ✓.
Also — quick re-check of the grid on 1280: `.wrap` padding 40 each side → content 1100? max-width 1180 → at 1280, wrap = 1180 (centered) minus padding 40×2 = 1100 content → cards (1100 − 60)/4 = 260 wide ✓ fine.
Hmm 260px cards — body-top city+chip ✓; desc wraps 3 lines maybe → equal heights ✓ fine.
Let me also reconsider gap 20 → cards 260. Scene 178 ✓ proportions good.
Mobile 375: wrap padding 16 → grid max-width 460 centered → cards 343 ✓.
Tablet 768: 2 cols → (768−32−20)/2 = 358 ✓.
All good. Final write-up now.
Wait — one more thing: `body` needs `min-height` for bg? html background covers ✓ fine.
Also `margin: 0 auto` on wrap ✓.
Also — since I use `html { background: ... }` with three layers including color last ✓ syntax: `background: radial-gradient(...), radial-gradient(...), #070b16;` — color allowed only in final layer ✓ correct as written (color after gradients in the shorthand, applies as background-color ✓).
Now, produce the answer.
Also double check `@media (prefers-reduced-motion: reduce)` placement at end ✓ and `!important` ✓.
One more consideration on reduced motion: `.card:hover { transform: none }` — but the base `.card` has no transform anyway ✓ and hover rule would apply — I set transform: none ✓ overriding the hover lift ✓.
Also — `.card:hover::before { transform: skewX(-16deg) translateX(420%) }` under reduced motion still applies (teleports offscreen ✓ invisible).
Done. Write final HTML.
Let me also add `<meta name="description" content="...">`? optional — include small.
Final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Four Skies · Pure-CSS Weather Cards</title>
<style>
/* ============ reset & tokens ============ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
color-scheme: dark;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}
html {
background:
radial-gradient(640px 420px at 10% -10%, rgba(90, 130, 255, .16), transparent 62%),
radial-gradient(760px 500px at 96% 112%, rgba(255, 140, 60, .13), transparent 62%),
#070b16;
}
body {
font-family: var(--font);
color: #f4f7ff;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
code {
font-family: var(--mono);
font-size: .85em;
background: rgba(126, 152, 208, .13);
border: 1px solid rgba(126, 152, 208, .22);
border-radius: 6px;
padding: .08em .4em;
}
/* ============ page shell ============ */
.wrap { max-width: 1180px; margin: 0 auto; padding: clamp(30px, 5vh, 52px) clamp(16px, 4vw, 40px) 48px; }
.head { text-align: center; margin-bottom: clamp(26px, 4.5vh, 44px); }
.eyebrow { font-family: var(--mono); font-size: 11px; font-weight: 600; letter-spacing: .24em; text-transform: uppercase; color: #92a6cf; margin-bottom: 14px; }
h1 { font-size: clamp(1.75rem, 3.4vw, 2.65rem); font-weight: 800; letter-spacing: -.025em; line-height: 1.08; color: #fbfcff; margin-bottom: 13px; text-wrap: balance; }
.lede { max-width: 660px; margin: 0 auto; font-size: clamp(.92rem, 1.3vw, 1rem); line-height: 1.68; color: rgba(190, 203, 230, .85); }
.foot { margin-top: 28px; text-align: center; font-size: .8rem; line-height: 1.7; color: rgba(163, 178, 207, .62); }
```
etc. Then grid/card... Let me write everything in the final message directly, carefully. I'll keep comments moderate.
Double-check `h1` element default margin removed by reset ✓.
One thing in the reset: `*::before/*::after margin padding 0` — that kills default paragraph margins ✓ all good since I control spacing.
Also `p` margins removed → `.lede` centered via margin auto ✓ set.
Final: also make sure `.head` is a `<header>` element ✓.
Write it all out now. I'll count braces mentally per rule block.
One more decision — should I keep `.card:focus-within`? No interactive descendants; skip.
Deliver.