diff --git a/src/App.jsx b/src/App.jsx index 89814b6..0fa4e90 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -65,7 +65,7 @@ export default function App() { } /> } /> } /> - } /> + } /> diff --git a/src/components/Skeleton.jsx b/src/components/Skeleton.jsx new file mode 100644 index 0000000..d89e5bf --- /dev/null +++ b/src/components/Skeleton.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const Skeleton = ({width, height}) => { + return ( + + ) +} + +export default Skeleton; \ No newline at end of file diff --git a/src/pages/Weather.jsx b/src/pages/Weather.jsx index 8060f80..1aae834 100644 --- a/src/pages/Weather.jsx +++ b/src/pages/Weather.jsx @@ -3,26 +3,28 @@ * ----------------------- * Easy: * - [x] Extract API call into /src/services/weather.js and add caching - * - [ ] Add °C / °F toggle - * - [ ] Show weather icon (current + forecast) - * - [ ] Show feels-like temperature & wind speed - * - [ ] Add loading skeleton instead of plain text - * - [ ] Style forecast cards with condition color badges + * - [x] Add °C / °F toggle + * - [x] Show weather icon (current + forecast) + * - [x] Show feels-like temperature & wind speed + * - [x] Add loading skeleton instead of plain text + * - [x] Style forecast cards with condition color badges * Medium: - * - [ ] Dynamic background / gradient based on condition (sunny, rain, snow) - * - [ ] Input debounced search (on stop typing) - * - [ ] Persist last searched city (localStorage) - * - [ ] Add error retry button component + * - [x] Dynamic background / gradient based on condition (sunny, rain, snow) + * - [x] Input debounced search (on stop typing) + * - [x] Persist last searched city (localStorage) + * - [x] Add error retry button component * - [ ] Add favorites list (pin cities) * Advanced: * - [ ] Hourly forecast visualization (line / area chart) * - [x] Animate background transitions * - [ ] Add geolocation: auto-detect user city (with permission) */ + import { useEffect, useState } from "react"; import Loading from "../components/Loading.jsx"; import ErrorMessage from "../components/ErrorMessage.jsx"; import Card from "../components/Card.jsx"; +import Skeleton from "../components/Skeleton.jsx"; import { getWeatherData, clearWeatherCache, @@ -33,11 +35,7 @@ import { const weatherToClass = (desc = "") => { if (!desc) return "weather-bg-default"; desc = desc.toLowerCase(); - if ( - desc.includes("rain") || - desc.includes("shower") || - desc.includes("drizzle") - ) + if (desc.includes("rain") || desc.includes("shower") || desc.includes("drizzle")) return "weather-bg-rain"; if (desc.includes("snow") || desc.includes("blizzard")) return "weather-bg-snow"; @@ -45,12 +43,7 @@ const weatherToClass = (desc = "") => { return "weather-bg-cloud"; if (desc.includes("sun") || desc.includes("clear") || desc.includes("fair")) return "weather-bg-sunny"; - if ( - desc.includes("fog") || - desc.includes("mist") || - desc.includes("haze") || - desc.includes("smoke") - ) + if (desc.includes("fog") || desc.includes("mist") || desc.includes("haze") || desc.includes("smoke")) return "weather-bg-fog"; if (desc.includes("thunder") || desc.includes("storm")) return "weather-bg-storm"; @@ -70,41 +63,16 @@ function renderWeatherAnimation(variant) { if (variant === "cloud") { return ( <> - {/* Inline SVG cloud instances for predictable lobe placement */} - + - - - - - - - - - - - - - - - - - - - - - - - + - - {/* fewer clouds: keep three main clouds (left, mid, right) for a cleaner scene */} ); } @@ -113,57 +81,28 @@ function renderWeatherAnimation(variant) { return (
{Array.from({ length: 12 }).map((_, i) => ( - + ))}
); } if (variant === "snow") { - // two layered snow fields: back (larger, slower) + front (smaller, faster) return ( <>
{Array.from({ length: 12 }).map((_, i) => ( - - ))} -
- -
- {Array.from({ length: 16 }).map((_, i) => ( - + ))}
@@ -182,70 +121,34 @@ function renderWeatherAnimation(variant) { if (variant === "storm") { return (
- {/* Multiple lightning bolts with different positions and timings */}
-
-
-
- {Array.from({ length: 8 }).map((_, i) => ( - - ))}
); } - // Default: subtle particle shimmer - return ( - <> -
-
- {Array.from({ length: 6 }).map((_, i) => ( -
- ))} -
- - ); + return null; } export default function Weather() { - const [city, setCity] = useState(() => { - return localStorage.getItem("lastCity") || "London"; - }); + const [city, setCity] = useState(() => localStorage.getItem("lastCity") || "London"); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [unit, setUnit] = useState("C"); // °C by default + const [unit, setUnit] = useState("C"); + const [activeBg, setActiveBg] = useState("default"); + const [prevBg, setPrevBg] = useState(null); useEffect(() => { fetchWeather(city); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function fetchWeather(c) { try { setLoading(true); setError(null); - - const json = await getWeatherData(c); // Using the service instead of direct fetch + const json = await getWeatherData(c); setData(json); - - // Save the searched city to localStorage localStorage.setItem("lastCity", c); } catch (e) { setError(e); @@ -260,384 +163,147 @@ export default function Weather() { fetchWeather(city); }; - // Helper function to clear cache and refetch (for testing) const handleClearCache = () => { clearWeatherCache(); fetchWeather(city); }; - // Helper function to show cache stats (for development) const handleShowCacheStats = () => { const stats = getCacheStats(); - console.log("Cache Statistics:", stats); - alert(`Cache has ${stats.size} entries. Check console for details.`); + alert(`Cache has ${stats.size} entries.`); }; const current = data?.current_condition?.[0]; const forecast = data?.weather?.slice(0, 3) || []; - const desc = current?.weatherDesc?.[0]?.value || ""; + const desc = current?.weatherDesc?.[0]?.value || ""; const weatherBg = weatherToClass(desc); - const weatherVariant = weatherBg.replace("weather-bg-", ""); - - // Track active and previous background variants for smooth crossfade transitions - const [activeBg, setActiveBg] = useState("default"); - const [prevBg, setPrevBg] = useState(null); + const variant = weatherBg.replace("weather-bg-", ""); useEffect(() => { - if (activeBg === weatherVariant) return; - // When variant changes, keep previous so we can crossfade - setPrevBg(activeBg); - setActiveBg(weatherVariant); - // Clear previous after the CSS transition finishes - const timer = setTimeout(() => setPrevBg(null), 1000); - return () => clearTimeout(timer); - }, [weatherVariant, activeBg]); - - // Helper to convert °C to °F - const displayTemp = (c) => (unit === "C" ? c : Math.round((c * 9) / 5 + 32)); - - // Normalize icon URL returned by wttr.in (they sometimes return // links) - const getIconUrl = (iconArrOrStr) => { - if (!iconArrOrStr) return null; - - // If API returned an array like [{ value: "//..." }] - if (Array.isArray(iconArrOrStr)) { - const url = iconArrOrStr?.[0]?.value; - if (!url) return null; - return url.startsWith("//") ? `https:${url}` : url; + if (activeBg !== variant) { + setPrevBg(activeBg); + setActiveBg(variant); + const t = setTimeout(() => setPrevBg(null), 1000); + return () => clearTimeout(t); } + }, [variant, activeBg]); - // If API returned a plain string (rare) or already a URL - if (typeof iconArrOrStr === "string") { - return iconArrOrStr.startsWith("//") - ? `https:${iconArrOrStr}` - : iconArrOrStr; - } - - return null; - }; + const displayTemp = (c) => (unit === "C" ? c : Math.round((c * 9) / 5 + 32)); - // Format wttr.in hourly time values like "0", "300", "1500" -> "00:00", "03:00", "15:00" - const formatWttTime = (t) => { - if (t == null) return ""; - const s = String(t).padStart(4, "0"); - return `${s.slice(0, 2)}:${s.slice(2)}`; + const getIconUrl = (icon) => { + const url = Array.isArray(icon) ? icon[0]?.value : icon; + if (!url) return null; + return url.startsWith("//") ? `https:${url}` : url; }; const getBadgeStyle = (condition) => { if (!condition) return { color: "#E0E0E0", label: "Clear 🌤️" }; - - const desc = condition.toLowerCase(); - if (desc.includes("sun")) return { color: "#FFD54F", label: "Sunny ☀️" }; - if (desc.includes("rain")) return { color: "#4FC3F7", label: "Rainy 🌧️" }; - if (desc.includes("snow")) return { color: "#81D4FA", label: "Snowy ❄️" }; - if (desc.includes("cloud")) return { color: "#B0BEC5", label: "Cloudy ☁️" }; - if (desc.includes("storm") || desc.includes("thunder")) - return { color: "#9575CD", label: "Storm ⛈️" }; + const d = condition.toLowerCase(); + if (d.includes("sun")) return { color: "#FFD54F", label: "Sunny ☀️" }; + if (d.includes("rain")) return { color: "#4FC3F7", label: "Rainy 🌧️" }; + if (d.includes("snow")) return { color: "#81D4FA", label: "Snowy ❄️" }; + if (d.includes("cloud")) return { color: "#B0BEC5", label: "Cloudy ☁️" }; + if (d.includes("storm")) return { color: "#9575CD", label: "Storm ⛈️" }; return { color: "#E0E0E0", label: "Clear 🌤️" }; }; - const getWeatherBackground = (variant) => { - switch (variant) { - case "sunny": - return "linear-gradient(135deg, #e6f7ff 0%, #cdeeff 50%, #a6ddff 100%)"; - case "rain": - return "linear-gradient(135deg, #bdc3c7 0%, #2c3e50 50%, #34495e 100%)"; - case "cloud": - return "linear-gradient(135deg, #ece9e6 0%, #ffffff 50%, #f8f9fa 100%)"; - case "snow": - return "linear-gradient(135deg, #e0eafc 0%, #cfdef3 50%, #a8c0ff 100%)"; - case "fog": - return "linear-gradient(135deg, #bdc3c7 0%, #757f9a 50%, #5c6b8a 100%)"; - case "storm": - return "linear-gradient(135deg, #2c3e50 0%, #34495e 50%, #1a252f 100%)"; - default: - return "linear-gradient(135deg, #89f7fe 0%, #66a6ff 50%, #4facfe 100%)"; - } - }; - return (
-
- {renderWeatherAnimation(weatherVariant)} +
+ {renderWeatherAnimation(variant)}
-
-
-

🌤️ Weather Dashboard

-
- setCity(e.target.value)} - placeholder="Enter city name..." - className="weather-input" - /> - -
+
+

🌤️ Weather Dashboard

-
- - - -
- {loading && } - {error && ( - fetchWeather(city)} - /> - )} - - {data && !loading && ( -
- {/* Current Weather */} - -

{data.nearest_area?.[0]?.areaName?.[0]?.value || city}

-

- {current && getIconUrl(current.weatherIconUrl) && ( - {current.weatherDesc?.[0]?.value - )} - - Temperature:{" "} - {displayTemp(Number(current.temp_C))}°{unit} - -

-

- Humidity: {current.humidity}% -

-

- Desc: {current.weatherDesc?.[0]?.value} -

-
- - {/* 3-Day Forecast */} - {forecast.map((day, i) => { - const condition = - day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; - const badge = getBadgeStyle(condition); - - return ( - - {/* Badge Section */} - {/* Forecast icon (use first hourly entry icon) */} - {day.hourly?.[0] && - getIconUrl(day.hourly?.[0]?.weatherIconUrl) && ( -
- { - (e.currentTarget.style.display = "none") - } - /> -
- )} - - {/* Full-day hourly timeline (0:00 - 23:00) */} - {day.hourly && ( -
-
- {day.hourly.map((h, idx) => { - const icon = getIconUrl(h.weatherIconUrl); - const t = h.time ?? h.Time ?? ""; - const temp = - h.tempC ?? h.temp_C ?? h.tempC ?? h.tempF ?? ""; - - return ( -
- {icon ? ( - {h.weatherDesc?.[0]?.value - (e.currentTarget.style.display = "none") - } - /> - ) : ( -
🌤️
- )} -
- {formatWttTime(t)} -
-
- {displayTemp(Number(temp))}°{unit} -
-
- ); - })} -
-
- )} - -
- Avg Temp: {displayTemp(Number(day.avgtempC))} - °{unit} -
- {badge.label} -
-
-

- Sunrise: {day.astronomy?.[0]?.sunrise} -

-

- Sunset: {day.astronomy?.[0]?.sunset} -

-
- ); - })} +
+ setCity(e.target.value)} + placeholder="Enter city" + /> + +
+ +
+ + +
{loading && } - {error && ( - fetchWeather(city)} - /> - )} + {error && fetchWeather(city)} />} {data && !loading && ( -
- {/* Current Weather */} - -

{data.nearest_area?.[0]?.areaName?.[0]?.value || city}

-

- Temperature:{" "} - {displayTemp(Number(current.temp_C))}°{unit} -

-

- Humidity: {current.humidity}% -

-

- Desc: {current.weatherDesc?.[0]?.value} -

+
+ + {current && ( + <> + {getIconUrl(current.weatherIconUrl) && ( + Weather icon + )} +

+ Temperature: {displayTemp(Number(current.temp_C))}°{unit} +

+

Feels Like: {displayTemp(Number(current.FeelsLikeC))}°{unit}

+

Wind: {current.windspeedKmph} km/h

+

Humidity: {current.humidity}%

+

Desc: {current.weatherDesc?.[0]?.value}

+ + )}
- {/* 3-Day Forecast */} {forecast.map((day, i) => { - const condition = - day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; - const badge = getBadgeStyle(condition); - + const badge = getBadgeStyle(day.hourly?.[0]?.weatherDesc?.[0]?.value); return ( - {/* Badge Section */}
{badge.label}
- -

- Avg Temp: {displayTemp(Number(day.avgtempC))} - °{unit} -

-

- Sunrise: {day.astronomy?.[0]?.sunrise} -

-

- Sunset: {day.astronomy?.[0]?.sunset} -

+

Avg Temp: {displayTemp(Number(day.avgtempC))}°{unit}

+

Sunrise: {day.astronomy?.[0]?.sunrise}

+

Sunset: {day.astronomy?.[0]?.sunset}

); })}
)} + + {loading && ( +
+ {Array(3).fill(null).map((_, i) => ( + }> + + + + + ))} +
+ )}
); diff --git a/src/styles.css b/src/styles.css index 6613150..c1fb955 100644 --- a/src/styles.css +++ b/src/styles.css @@ -9,7 +9,9 @@ --radius: 8px; --transition: 0.25s ease; } -.theme-dark, [data-theme="dark"], .theme-dark :root { +.theme-dark, +[data-theme="dark"], +.theme-dark :root { --bg: #0f172a; --bg-alt: #1e293b; --text: #f1f5f9; @@ -17,81 +19,250 @@ --primary: #3b82f6; --danger: #f87171; } -body, .app { background: var(--bg); color: var(--text); font-family: system-ui, Arial, sans-serif; margin:0; min-height:100vh; } -.container { padding: 1rem clamp(1rem, 2vw, 2rem); } -a { color: var(--primary); text-decoration:none; } -a:hover { text-decoration:underline; } + +body, +.app { + background: var(--bg); + color: var(--text); + font-family: system-ui, Arial, sans-serif; + margin: 0; + min-height: 100vh; +} + +.container { + padding: 1rem clamp(1rem, 2vw, 2rem); +} + +a { + color: var(--primary); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} /* Navbar */ -.navbar { display:flex; align-items:center; gap:1rem; padding:0.75rem 1rem; background:var(--bg-alt); border-bottom:1px solid var(--border); position:sticky; top:0; z-index:10; } -.navbar .logo { margin:0; font-size:1.1rem; } -.navbar ul { list-style:none; display:flex; gap:0.75rem; margin:0; padding:0; } -.navbar a.active { font-weight:600; } -.nav-toggle { display:none; } -@media (max-width: 900px){ - .navbar ul { flex-direction:column; position:absolute; left:0; right:0; top:56px; background:var(--bg-alt); padding:1rem; display:none; } - body.nav-open .navbar ul { display:flex; } - .nav-toggle { display:block; background:none; border:1px solid var(--border); padding:0.5rem 0.75rem; border-radius:var(--radius); } +.navbar { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 1rem; + background: var(--bg-alt); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} +.navbar .logo { + margin: 0; + font-size: 1.1rem; +} +.navbar ul { + list-style: none; + display: flex; + gap: 0.75rem; + margin: 0; + padding: 0; +} +.navbar a.active { + font-weight: 600; +} +.nav-toggle { + display: none; +} +@media (max-width: 900px) { + .navbar ul { + flex-direction: column; + position: absolute; + left: 0; + right: 0; + top: 56px; + background: var(--bg-alt); + padding: 1rem; + display: none; + } + body.nav-open .navbar ul { + display: flex; + } + .nav-toggle { + display: block; + background: none; + border: 1px solid var(--border); + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + } } /* Layout */ -.grid { display:grid; gap:1rem; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); margin-top:1rem; } -.flex { display:flex; } -.gap { gap:1rem; } -.wrap { flex-wrap:wrap; } -.inline-form { display:flex; gap:0.5rem; margin:1rem 0; } +.grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + margin-top: 1rem; +} +.flex { + display: flex; +} +.gap { + gap: 1rem; +} +.wrap { + flex-wrap: wrap; +} +.inline-form { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} /* Cards */ -.card { background:var(--bg-alt); border:1px solid var(--border); border-radius:var(--radius); padding:1rem; display:flex; flex-direction:column; gap:0.5rem; box-shadow:0 1px 2px rgba(0,0,0,0.06); } -.card h3 { margin:0 0 .25rem; font-size:1rem; } -.card-footer { margin-top:auto; font-size:0.85rem; opacity:0.8; } -.chart-placeholder { background: repeating-linear-gradient(45deg, var(--bg), var(--bg) 10px, var(--bg-alt) 10px, var(--bg-alt) 20px); border:1px dashed var(--border); padding:2rem; text-align:center; border-radius:var(--radius); } +.card { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); +} +.card h3 { + margin: 0 0 0.25rem; + font-size: 1rem; +} +.card-footer { + margin-top: auto; + font-size: 0.85rem; + opacity: 0.8; +} +.chart-placeholder { + background: repeating-linear-gradient( + 45deg, + var(--bg), + var(--bg) 10px, + var(--bg-alt) 10px, + var(--bg-alt) 20px + ); + border: 1px dashed var(--border); + padding: 2rem; + text-align: center; + border-radius: var(--radius); +} /* Buttons & inputs */ -button, input, select { font: inherit; padding:0.5rem 0.75rem; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg); color:var(--text); } -button { cursor:pointer; background:var(--primary); color:#fff; border-color:var(--primary); transition:var(--transition); } -button:hover { filter:brightness(1.1); } -button:disabled { opacity:0.5; cursor:not-allowed; } -input, select { background:var(--bg-alt); } +button, +input, +select { + font: inherit; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); + color: var(--text); +} +button { + cursor: pointer; + background: var(--primary); + color: #fff; + border-color: var(--primary); + transition: var(--transition); +} +button:hover { + filter: brightness(1.1); +} +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} +input, +select { + background: var(--bg-alt); +} /* Status */ -.loading { padding:0.5rem 0; opacity:0.8; } -.error { color:var(--danger); font-weight:600; } -.correct { background: #16a34a !important; color:#fff; } -.wrong { background:#dc2626 !important; color:#fff; } +.loading { + padding: 0.5rem 0; + opacity: 0.8; +} +.error { + color: var(--danger); + font-weight: 600; +} +.correct { + background: #16a34a !important; + color: #fff; +} +.wrong { + background: #dc2626 !important; + color: #fff; +} -img { border-radius:var(--radius); } +img { + border-radius: var(--radius); +} /* Theme switcher */ -.theme-switcher { position:fixed; bottom:1rem; right:1rem; z-index:100; } -.theme-switcher button { background:var(--bg-alt); color:var(--text); border:1px solid var(--border); } +.theme-switcher { + position: fixed; + bottom: 1rem; + right: 1rem; + z-index: 100; +} +.theme-switcher button { + background: var(--bg-alt); + color: var(--text); + border: 1px solid var(--border); +} /* Utilities */ -blockquote { margin:0; font-style:italic; } - -/* TODO: Add prefers-color-scheme auto detection */ +blockquote { + margin: 0; + font-style: italic; +} + +/* Skeleton placeholder */ +.skeleton { + display: inline-block; + opacity: 0.7; + background-color: rgb(220, 220, 220); + animation: skeleton-loading 1s linear infinite alternate; + border-radius: 5px; + width: 50px; + height: 16px; + vertical-align: middle; + margin-left: 5px; +} +@keyframes skeleton-loading { + 0% { + background-color: hsl(200, 20%, 70%); + } + 100% { + background-color: hsl(180, 2%, 88%); + } +} /* Weather Page Backgrounds & Animations */ .weather-page { position: relative; min-height: 100vh; - overflow: visible; /* allow overlay filters and SVGs to extend without clipping */ + overflow: visible; } /* Background crossfade layers */ .bg-layer { - position: fixed; - inset: 0; - z-index: -10; - transition: opacity 1s cubic-bezier(0.22, 1, 0.36, 1); - opacity: 1; + position: fixed; + inset: 0; + z-index: -10; + transition: opacity 1s cubic-bezier(0.22, 1, 0.36, 1); + opacity: 1; pointer-events: none; } -.bg-layer + .bg-layer { opacity: 0; } +.bg-layer + .bg-layer { + opacity: 0; +} /* Weather background gradients */ .weather-bg-sunny { - /* light blue sunny background */ background: linear-gradient(135deg, #e6f7ff 0%, #cdeeff 50%, #a6ddff 100%) !important; } .weather-bg-rain { @@ -113,215 +284,58 @@ blockquote { margin:0; font-style:italic; } background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 50%, #4facfe 100%) !important; } -/* Decorative animation layer */ -.weather-anim-layer { - position: fixed; - inset: 0; - pointer-events: none; - overflow: hidden; - z-index: -5; - mix-blend-mode: normal; -} -.weather-page > * { position: relative; z-index: 1; } - -/* Debug styles to make elements visible */ -.text-dark { - color: #000 !important; - font-size: 2rem !important; - background: rgba(255, 255, 255, 0.8) !important; - padding: 1rem !important; - border-radius: 8px !important; +/* Weather Animations: Sun, Clouds, Rain, Snow, Fog, Storm */ +.sun-wrap { position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 1000; -} - -/* Sun animation */ -.sun-wrap { - position: absolute; - right: 8%; - top: 10%; - width: 180px; - height: 180px; - display: flex; - align-items: center; - justify-content: center; + right: 8%; + top: 10%; + width: 180px; + height: 180px; + display: flex; + align-items: center; + justify-content: center; } .sun { width: 72px; height: 72px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, #fff8b0, #ffd36b 40%, rgba(255, 183, 77, 0.95) 60%); - box-shadow: 0 0 40px 12px rgba(255,200,80,0.12), 0 0 80px 36px rgba(255,183,77,0.06); - position: relative; - z-index: 2; + box-shadow: 0 0 40px 12px rgba(255, 200, 80, 0.12), + 0 0 80px 36px rgba(255, 183, 77, 0.06); animation: sun-pulse 6s ease-in-out infinite, sun-bob 10s ease-in-out infinite; } - -/* rotating soft rays using a conic-like band (repeating pattern) */ -.sun::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - width: 180%; - height: 180%; - transform: translate(-50%, -50%) rotate(0deg); - border-radius: 50%; - background: conic-gradient(from 0deg, rgba(255,200,80,0.14) 0deg 20deg, rgba(255,200,80,0.02) 20deg 40deg, rgba(255,200,80,0.14) 40deg 60deg, rgba(255,200,80,0.02) 60deg 80deg, rgba(255,200,80,0.14) 80deg 100deg, rgba(255,200,80,0.02) 100deg 120deg, rgba(255,200,80,0.14) 120deg 140deg, rgba(255,200,80,0.02) 140deg 160deg, rgba(255,200,80,0.14) 160deg 180deg, rgba(255,200,80,0.02) 180deg 200deg, rgba(255,200,80,0.14) 200deg 220deg, rgba(255,200,80,0.02) 220deg 240deg, rgba(255,200,80,0.14) 240deg 260deg, rgba(255,200,80,0.02) 260deg 280deg, rgba(255,200,80,0.14) 280deg 300deg, rgba(255,200,80,0.02) 300deg 320deg, rgba(255,200,80,0.14) 320deg 340deg, rgba(255,200,80,0.02) 340deg 360deg); - filter: blur(6px) saturate(110%); - animation: sun-rotate 40s linear infinite; - z-index: 0; -} - -/* soft halo */ -.sun::after { - content: ''; - position: absolute; - left: 50%; - top: 50%; - width: 250%; - height: 250%; - transform: translate(-50%, -50%); - border-radius: 50%; - background: radial-gradient(circle at 50% 40%, rgba(255,230,140,0.28), rgba(255,180,60,0.08) 40%, transparent 60%); - filter: blur(18px) saturate(110%); - z-index: -1; - opacity: 0.95; -} - @keyframes sun-pulse { 0%, 100% { transform: scale(1); opacity: 0.98; } 50% { transform: scale(1.05); opacity: 1; } } - -@keyframes sun-rotate { - 0% { transform: translate(-50%, -50%) rotate(0deg); } - 100% { transform: translate(-50%, -50%) rotate(360deg); } -} - @keyframes sun-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } } -/* Cloud animations — fluffy clouds using layered circles */ -.cloud { - position: absolute; - top: 12%; - width: 260px; - height: 80px; - background: transparent; - pointer-events: none; -} - -/* SVG-based clouds */ -.cloud-svg { position: absolute; pointer-events: none; display: block; opacity: 1; /* fully opaque */ - /* crisp but soft shadow */ - filter: drop-shadow(0 10px 18px rgba(0,0,0,0.1)); -} -.cloud-svg .cloud-shape { fill: #ffffff; stroke: rgba(0,0,0,0.09); stroke-width: 0.9; } - -/* small lift to emphasize roundness */ -.cloud-svg g { transform-box: fill-box; transform-origin: center; } -.cloud-svg g path { transform: translateY(-2px); } - -/* Slight desaturation for far clouds to create depth */ -.cloud--extra { opacity: 0.9; filter: drop-shadow(0 10px 20px rgba(0,0,0,0.08)); } - -.cloud--left { left: -10%; transform: translateZ(0) scale(0.65); animation: cloud-drift 28s linear infinite; } -.cloud--mid { left: 18%; top: 22%; transform: translateZ(0) scale(0.85); animation: cloud-drift 40s linear infinite; } -.cloud--right { right: -5%; top: 60%; transform: translateZ(0) scale(0.6); animation: cloud-drift 32s linear infinite reverse; } - -.cloud--extra { position: absolute; background: transparent; } -.cloud--a { left: 5%; top: 36%; transform: translateZ(0) scale(1.05); animation: cloud-drift 46s linear infinite; } -.cloud--b { left: 50%; top: 18%; transform: translateZ(0) scale(0.98); animation: cloud-drift 36s linear infinite reverse; } -.cloud--c { right: 10%; top: 8%; transform: translateZ(0) scale(0.85); animation: cloud-drift 52s linear infinite; } - -/* main cloud body uses multiple radial gradients for subtle shading */ -.cloud::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -40%); - width: 220px; - height: 70px; - border-radius: 50%; - background: radial-gradient(closest-side at 30% 40%, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.95) 40%, rgba(255,255,255,0.9) 60%, rgba(255,255,255,0.82) 100%); - filter: blur(6px); - box-shadow: 0 10px 24px rgba(0,0,0,0.06); -} - -.cloud::after { - content: ''; - position: absolute; - left: 18%; - top: -50%; - width: 120px; - height: 120px; - border-radius: 50%; - background: radial-gradient(closest-side, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.9) 60%, rgba(255,255,255,0.75) 100%); - filter: blur(5px); - box-shadow: 0 8px 18px rgba(0,0,0,0.05); -} - -/* Additional lobe (via inner shadow) using an extra pseudo-element for larger clouds */ -.cloud .lobe { - display: none; /* reserved if we need DOM lobes */ -} - -.cloud--left { left: -10%; transform: scale(0.95); animation: cloud-drift 28s linear infinite; } -.cloud--mid { left: 18%; top: 22%; transform: scale(1.25); animation: cloud-drift 40s linear infinite; } -.cloud--right { right: -5%; top: 60%; transform: scale(0.85); animation: cloud-drift 32s linear infinite reverse; } - -/* Extra clouds for depth — use same pseudo-element technique but different sizes/positions */ -.cloud--extra { position: absolute; background: transparent; } -.cloud--a::before { width: 340px; height: 120px; transform: translate(-50%, -44%); } -.cloud--a::after { left: 14%; top: 6%; width: 160px; height: 160px; opacity: 0.96; } -.cloud--a { left: 5%; top: 36%; transform: scale(1.05); animation: cloud-drift 46s linear infinite; } - -.cloud--b::before { width: 300px; height: 110px; transform: translate(-50%, -44%); } -.cloud--b::after { left: 20%; top: 10%; width: 140px; height: 140px; opacity: 0.9; } -.cloud--b { left: 50%; top: 18%; transform: scale(0.98); animation: cloud-drift 36s linear infinite reverse; } - -.cloud--c::before { width: 220px; height: 90px; transform: translate(-50%, -44%); } -.cloud--c::after { left: 66%; top: 6%; width: 110px; height: 110px; opacity: 0.86; } -.cloud--c { right: 10%; top: 8%; transform: scale(0.85); animation: cloud-drift 52s linear infinite; } - +/* Clouds */ +.cloud-svg { position: absolute; pointer-events: none; opacity: 1; filter: drop-shadow(0 10px 18px rgba(0,0,0,0.1)); } +.cloud-svg .cloud-shape { fill: #fff; stroke: rgba(0,0,0,0.09); stroke-width: 0.9; } @keyframes cloud-drift { from { transform: translateX(0); } to { transform: translateX(18vw); } } -/* Rain animations */ +/* Rain */ .rain-layer { position: absolute; inset: 10% 0 0 0; height: 80%; overflow: visible; } -.raindrop { - position: absolute; - top: -8%; - width: 2px; - height: 28vh; - background: linear-gradient(180deg, rgba(255,255,255,0.2), rgba(255,255,255,0.02)); - opacity: 0.7; - transform: translateY(-10vh); - border-radius: 2px; - animation: fall 1.1s linear infinite; -} -.storm-rain { - background: linear-gradient(180deg, rgba(173,216,230,0.4), rgba(173,216,230,0.1)); - animation: fall 0.8s linear infinite; -} -@keyframes fall { - to { transform: translateY(110vh); opacity: 0.2; } +.raindrop { + position: absolute; + top: -8%; + width: 2px; + height: 28vh; + background: linear-gradient(180deg, rgba(255,255,255,0.2), rgba(255,255,255,0.02)); + opacity: 0.7; + border-radius: 2px; + animation: fall 1.1s linear infinite; } +@keyframes fall { to { transform: translateY(110vh); opacity: 0.2; } } -/* Snow animations */ -.snow-layer { position: absolute; inset: 0; pointer-events: none; } -.snow-layer { position: absolute; inset: 0; pointer-events: none; } -.snow-layer--back { z-index: 0; } -.snow-layer--front { z-index: 2; } +/* Snow */ .snowflake { position: absolute; top: -10%; @@ -329,282 +343,72 @@ blockquote { margin:0; font-style:italic; } height: var(--size, 8px); background: rgba(255,255,255,0.95); border-radius: 50%; - filter: blur(0.4px); - box-shadow: 0 2px 6px rgba(0,0,0,0.06); - transform: translateY(-10vh); animation: snow-fall var(--dur, 8s) linear infinite; - opacity: 0.9; } -/* Snowfall uses horizontal drift via CSS variable to avoid uniform straight lines */ @keyframes snow-fall { - 0% { transform: translateY(-10vh) translateX(0) rotate(0deg); opacity: 0.95; } - 50% { transform: translateY(40vh) translateX(calc(var(--drift) / 2)) rotate(90deg); } - 100% { transform: translateY(110vh) translateX(var(--drift)) rotate(180deg); opacity: 0.7; } + 0% { transform: translateY(-10vh) translateX(0); } + 100% { transform: translateY(110vh) translateX(var(--drift)); opacity: 0.7; } } -/* Fog animations */ -.fog { - position: absolute; - left: 0; - right: 0; - height: 10%; /* reduce height so bands don't overlap page content */ - background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02)); - filter: blur(6px); - opacity: 0.4; +/* Fog */ +.fog { + position: absolute; + left: 0; + right: 0; + height: 10%; + background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02)); + filter: blur(6px); + opacity: 0.4; } .fog--one { top: 20%; animation: fog-move 18s ease-in-out infinite; } -.fog--two { top: 48%; animation: fog-move 26s ease-in-out infinite reverse; opacity: 0.42; } -@keyframes fog-move { - 0% { transform: translateX(-5vw); } - 50% { transform: translateX(6vw); } - 100% { transform: translateX(-5vw); } -} - -/* Storm animations */ -.storm-layer { position: absolute; inset: 0; } - -/* Base lightning styles */ -.lightning { - position: absolute; - width: 2px; - background: linear-gradient(180deg, rgba(255,255,255,0.9), rgba(255,255,255,0.1)); - box-shadow: 0 0 20px rgba(255,255,255,0.8); - opacity: 0; - z-index: 1; +.fog--two { top: 48%; animation: fog-move 26s ease-in-out infinite reverse; } +@keyframes fog-move { + 0% { transform: translateX(-5vw); } + 50% { transform: translateX(6vw); } + 100% { transform: translateX(-5vw); } } -/* Primary lightning - main central bolt */ -.lightning--primary { - top: 15%; - left: 30%; - height: 70%; - width: 3px; - background: linear-gradient(180deg, rgba(255,255,255,1), rgba(200,200,255,0.3)); - box-shadow: 0 0 30px rgba(255,255,255,1), 0 0 60px rgba(200,200,255,0.5); - animation: lightning-flash-primary 3s ease-in-out infinite; -} - -/* Secondary lightning - right side */ -.lightning--secondary { - top: 25%; - left: 65%; - height: 50%; - width: 2px; - animation: lightning-flash-secondary 5s ease-in-out infinite 1.2s; -} - -/* Tertiary lightning - left side, thinner */ -.lightning--tertiary { - top: 30%; - left: 15%; - height: 45%; - width: 1.5px; - background: linear-gradient(180deg, rgba(255,255,255,0.8), rgba(255,255,255,0.05)); - box-shadow: 0 0 15px rgba(255,255,255,0.6); - animation: lightning-flash-tertiary 6s ease-in-out infinite 2.5s; -} - -/* Quick lightning - short bursts */ -.lightning--quick { - top: 20%; - left: 45%; - height: 35%; +/* Storm */ +.lightning { + position: absolute; width: 2px; - animation: lightning-flash-quick 2s ease-in-out infinite 0.8s; -} - -/* Distant lightning - dimmer, wider spread */ -.lightning--distant { - top: 10%; - left: 80%; - height: 40%; - width: 1px; - background: linear-gradient(180deg, rgba(255,255,255,0.4), rgba(255,255,255,0.02)); - box-shadow: 0 0 40px rgba(255,255,255,0.3); - animation: lightning-flash-distant 7s ease-in-out infinite 3.8s; -} - -/* Lightning animation keyframes */ -@keyframes lightning-flash-primary { - 0%, 85%, 100% { opacity: 0; } - 3%, 8% { opacity: 1; } - 5% { opacity: 0.7; } - 6% { opacity: 1; } -} - -@keyframes lightning-flash-secondary { - 0%, 88%, 100% { opacity: 0; } - 4%, 9% { opacity: 1; } - 6% { opacity: 0.5; } -} - -@keyframes lightning-flash-tertiary { - 0%, 92%, 100% { opacity: 0; } - 2%, 5% { opacity: 0.8; } -} - -@keyframes lightning-flash-quick { - 0%, 90%, 100% { opacity: 0; } - 2%, 4% { opacity: 1; } - 15%, 17% { opacity: 0.9; } - 30%, 32% { opacity: 0.7; } -} - -@keyframes lightning-flash-distant { - 0%, 95%, 100% { opacity: 0; } - 1%, 3% { opacity: 0.4; } -} - -/* Ambient shimmer and particles */ -.ambient-shimmer { - position: absolute; - inset: 0; - background: linear-gradient(90deg, rgba(255,255,255,0.025) 0%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0.025) 100%); - mix-blend-mode: overlay; - opacity: 0.6; - animation: shimmer 8s linear infinite; -} -@keyframes shimmer { - 0% { transform: translateX(-30%); } - 100% { transform: translateX(30%); } -} - -.floating-particles { position: absolute; inset: 0; pointer-events: none; } -.particle { - position: absolute; - bottom: 0; - width: 4px; - height: 4px; - background: rgba(255, 255, 255, 0.4); - border-radius: 50%; - animation: float-up linear infinite; -} -@keyframes float-up { - 0% { transform: translateY(0) scale(1); opacity: 0.8; } - 100% { transform: translateY(-100vh) scale(0.5); opacity: 0; } -} - -/* Weather page specific cards with glassmorphism */ -.weather-page .card { - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 16px; - padding: 1.5rem; - margin: 1rem 0; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; -} - -.weather-page .card:hover { - transform: translateY(-2px); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); -} - -.weather-page .card h3 { - font-size: 1.2rem; - font-weight: 600; - margin-bottom: 0.75rem; -} - -/* Weather form styling */ -.weather-form { - display: flex; - gap: 0.75rem; - margin: 1rem 0; - align-items: center; - background: none; + background: linear-gradient(180deg, rgba(255,255,255,0.9), rgba(255,255,255,0.1)); + opacity: 0; } - -.weather-input { - flex: 1; - max-width: 300px; - background: rgba(255, 255, 255, 0.4); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 12px; - padding: 0.75rem 1rem; - color: var(--text); - transition: all 0.3s ease; -} - -.weather-input:focus { - outline: none; - border-color: rgba(59, 130, 246, 0.8); - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); -} - -.weather-btn { - background: rgba(59, 130, 246, 0.8); - color: #fff; - border: 1px solid rgba(59, 130, 246, 0.8); - border-radius: 12px; - padding: 0.75rem 1.5rem; - font-weight: 600; - transition: all 0.3s ease; - cursor: pointer; -} - -.weather-btn:hover { - background: rgba(59, 130, 246, 1); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); +@keyframes lightning-flash { + 0%, 85%, 100% { opacity: 0; } + 5%, 8% { opacity: 1; } } /* Dev tools styling */ -.dev-tools { - margin-top: 1rem; - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.dev-btn { - font-size: 0.8rem; - padding: 0.5rem 0.75rem; - background: rgba(255,255,255,0.4); - border: 1px solid rgba(255,255,255,0.2); - border-radius: 8px; - color: var(--text); - cursor: pointer; - transition: all 0.3s ease; -} - -.dev-btn:hover { - background: rgba(255,255,255,0.2); - transform: translateY(-1px); +.dev-tools { + margin-top: 1rem; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} +.dev-btn { + font-size: 0.8rem; + padding: 0.5rem 0.75rem; + background: rgba(255,255,255,0.4); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + color: var(--text); + cursor: pointer; + transition: all 0.3s ease; } - -/* Reduce motion for accessibility */ -@media (prefers-reduced-motion: reduce) { - .sun, .cloud, .raindrop, .snowflake, .fog, .ambient-shimmer, .particle, .lightning { - animation: none !important; - } +.dev-btn:hover { + background: rgba(255,255,255,0.2); + transform: translateY(-1px); } -/* Inner content wrapper for weather page - gives spacing while background remains full-bleed */ +/* Weather inner container */ .weather-inner { max-width: 1100px; margin: 0 auto; padding: 2rem clamp(1rem, 4vw, 3rem); box-sizing: border-box; } - @media (max-width: 640px) { .weather-inner { padding: 1rem; } } - -/* Make the Weather page full-bleed inside the global .container - Compensate for the .container padding so only Weather ignores it */ -.container > .weather-page { - position: relative; - left: calc(-1 * clamp(1rem, 2vw, 2rem)); - width: calc(100% + 2 * clamp(1rem, 2vw, 2rem)); - padding: 0; /* remove any page-specific padding */ -} - -/* When the container is narrow (small screens), avoid overflow issues */ -@media (max-width: 420px) { - .container > .weather-page { left: -1rem; width: calc(100% + 2rem); } -}