Explorar o código

Change to open-meteo and update the display.

Lee Morgan hai 3 meses
pai
achega
b54220ddcd
Modificáronse 5 ficheiros con 474 adicións e 145 borrados
  1. 108 119
      index.css
  2. 4 0
      index.html
  3. 31 26
      index.js
  4. 293 0
      index_old.css
  5. 38 0
      weatherCodes.js

+ 108 - 119
index.css

@@ -1,10 +1,12 @@
 :root{
-  --bg: #0b0f16;
-  --border: rgba(255,255,255,.1);
-  --text: rgba(255,255,255,.92);
-  --muted: rgba(255,255,255,.68);
-  --accent: #4cc9f0;
-  --accent-2: #a78bfa;
+  --bg: #05020f;
+  --glass: rgba(12,8,35,.82);
+  --border: rgba(0,255,240,.38);
+  --text: #f8f9ff;
+  --muted: #9ed4ff;
+  --accent: #00ffea;
+  --accent-2: #ff00c8;
+  --glow: #00ffea;
 
   --outer-pad: 16px;
   --gap: 14px;
@@ -24,20 +26,19 @@
 }
 
 *{box-sizing:border-box}
-html,body{height:100%;width:100%}
+html,body{height:100%;width:100%;margin:0;padding:0}
 
 body{
-  margin:0;
   font-family:var(--font);
   color:var(--text);
   background:
-    radial-gradient(1200px 600px at 20% 10%,rgba(76,201,240,.12),transparent 60%),
-    radial-gradient(900px 500px at 85% 20%,rgba(167,139,250,.1),transparent 55%),
-    radial-gradient(800px 600px at 50% 100%,rgba(255,255,255,.06),transparent 55%),
+    radial-gradient(1200px 600px at 20% 10%, rgba(0,255,234,.18), transparent 60%),
+    radial-gradient(900px 500px at 85% 20%, rgba(255,0,200,.14), transparent 55%),
+    radial-gradient(800px 600px at 50% 100%, rgba(120,0,255,.08), transparent 55%),
     var(--bg);
   overflow:hidden;
   -webkit-font-smoothing:antialiased;
-  text-rendering:geometricPrecision
+  text-rendering:geometricPrecision;
 }
 
 .grid{
@@ -48,7 +49,7 @@ body{
   grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);
   grid-template-rows:minmax(0,1fr) minmax(0,1fr);
   gap:var(--gap);
-  filter:brightness(.98)
+  filter:brightness(.98);
 }
 .grid>*{min-width:0;min-height:0}
 
@@ -57,77 +58,85 @@ body{
   min-width:0;
   min-height:0;
   border-radius:var(--radius);
-  background:linear-gradient(180deg,#ffffff0f,#ffffff08);
+  background:var(--glass);
   border:1px solid var(--border);
-  box-shadow:0 10px 30px #00000059,inset 0 1px #ffffff0f;
-  overflow:hidden
+  box-shadow:0 10px 30px #00000059, inset 0 1px #ffffff0f, 0 0 0 1px rgba(0,255,234,.12);
+  overflow:hidden;
+  backdrop-filter:blur(16px);
 }
 
-/* -------- Top Left -------- */
+/* -------- Top Left - weather description/logo NOW SAFELY to the RIGHT of date/time -------- */
 .topLeft{
   display:flex;
   flex-direction:column;
-  min-width:0;
-  min-height:0;
   padding:12px;
-  gap:10px
+  gap:10px;
 }
 
 .dateTime{
   flex:0 0 auto;
   min-width:0;
-  display:flex;
-  flex-direction:column;
-  gap:4px;
-  padding:10px 12px;
+  display: grid;
+  grid-template-columns: 1fr 38%;   /* fully responsive % — no more fixed px clipping */
+  gap: 8px 24px;
+  padding:14px 18px;
   border-radius:var(--radius-sm);
-  background:#00000038;
-  border:1px solid rgba(255,255,255,.08);
+  background:rgba(8,4,28,.85);
+  border:1px solid rgba(0,255,234,.35);
   position: relative;
+  align-items: start;
+  min-height: auto;                 /* prevents collapse */
 }
 
 #date{
+  grid-column: 1;
   margin:0;
   font-size:var(--date-size);
   letter-spacing:.35px;
-  color:var(--muted)
+  color:var(--muted);
+  text-transform:uppercase;
 }
 
 #time{
-  margin:0;
+  grid-column: 1;
+  margin:2px 0 0 0;
   font-size:var(--time-size);
   font-weight:760;
   letter-spacing:.8px;
   line-height:1.02;
   color:var(--text);
-  overflow:hidden;
-  text-overflow:ellipsis;
-  white-space:nowrap
+  text-shadow:0 0 20px var(--glow);
 }
 
-.weather{
-  flex:1 1 auto;
+/* weather description + logo moved to right column (flex column so they stack nicely) */
+.dateTime > div {
+  grid-column: 2;
+  grid-row: 1 / span 2;
+  display:flex;
+  flex-direction:column;
+  align-items: flex-end;
+  justify-content: center;
+  gap: 6px;
+  height: 100%;
   min-width:0;
-  min-height:0;
-  display:flex
 }
 
-.weatherBlock{
-  flex:1 1 auto;
-  min-width:0;
-  min-height:0;
-  padding:10px;
-  grid-template-rows:auto auto minmax(0,1fr);
-  gap:8px;
+#weatherDescription{
+  font-size:clamp(21px, 1.85vh, 29px);
+  font-weight:700;
+  color:#fff;
+  text-shadow:0 0 15px var(--accent);
+  margin:0;
+  text-align:right;
+  line-height:1.05;
+  white-space:nowrap;
 }
 
-.weatherBlock h2:before{
-  content:"";
-  width:10px;
-  height:10px;
-  border-radius:50%;
-  background:linear-gradient(135deg,var(--accent),var(--accent-2));
-  box-shadow:0 0 18px #4cc9f059
+#currentWeatherLogo{
+  font-size:clamp(46px, 3.9vh, 60px);
+  line-height:1;
+  filter:drop-shadow(0 0 22px var(--accent-2));
+  margin:0;
 }
 
 #conditionLogo{
@@ -135,44 +144,55 @@ body{
     position: absolute;
     top: 0;
     right: 0;
+    filter:drop-shadow(0 0 25px var(--glow));
+    z-index: 1;
+    pointer-events: none;
+}
+
+/* -------- weather block below (unchanged proportions) -------- */
+.weather{
+  flex:1 1 auto;
+  min-width:0;
+  min-height:0;
+  display:flex;
+}
+
+.weatherBlock{
+  flex:1 1 auto;
+  min-width:0;
+  min-height:0;
+  padding:10px;
 }
 
-/* FIX 1: make weatherInfo a strict 2x2 grid so the last two rows can't get clipped */
 .weatherInfo{
   height: 100%;
-
   display:grid;
   grid-template-columns:minmax(0,1fr) minmax(0,1fr);
   grid-template-rows:minmax(0,1fr) minmax(0,1fr);
   gap:6px 8px;
-
   align-content:stretch;
   align-items:stretch;
 }
 
 .weatherItem{
   margin:0;
-  padding:6px 9px;             /* slightly tighter */
+  padding:6px 9px;
   border-radius:14px;
-  background:#ffffff09;
-  border:1px solid rgba(255,255,255,.06);
+  background:rgba(255,255,255,.09);
+  border:1px solid rgba(0,255,234,.25);
   font-size:var(--info-size);
-  line-height:1.00;            /* tighter to stop bottom clipping */
+  line-height:1.00;
   color:var(--muted);
-
   display:flex;
   align-items:center;
   justify-content:center;
-
-  min-width:0;
-  min-height:0;
-
-  overflow:hidden;
-  text-overflow:ellipsis;
-  white-space:nowrap
+  box-shadow:0 4px 12px rgba(0,0,0,.4);
 }
 
-#temp,#feels,#wind,#humidity{color:var(--text);font-weight:650}
+#temp,#feels,#wind,#humidity{
+  color:var(--text);
+  font-weight:700;
+}
 
 /* -------- Top Right (Radar) -------- */
 .topRight{
@@ -180,39 +200,20 @@ body{
   display:flex;
   align-items:center;
   justify-content:center;
-  background:radial-gradient(800px 520px at 70% 10%,rgba(167,139,250,.09),transparent 55%),#0000001a
+  background:radial-gradient(800px 520px at 70% 10%, rgba(255,0,200,.12), transparent 55%), rgba(8,4,28,.85);
 }
 
-/* FIX 2: slight zoom, clip a bit top/bottom */
 #radar{
   width:100%;
   height:100%;
-
-  object-fit:scale-down;             /* crop instead of letterbox */
-  object-position:center 55%;   /* trims a touch more top/bottom */
-  transform:scale(1.5);        /* very slight zoom */
-  transform-origin:center;
-
+  object-fit:scale-down;
+  object-position:center 52%;
+  transform:scale(1.08);
   border-radius:calc(var(--radius) - 6px);
-  border:1px solid rgba(255,255,255,.08);
-  box-shadow:0 14px 40px #00000073;
+  border:1px solid rgba(0,255,234,.45);
+  box-shadow:0 14px 40px #00000073, 0 0 25px rgba(0,255,234,.3);
   background:#00000038;
-  display:block
-}
-
-.topRight:after{
-  content:"Radar";
-  position:absolute;
-  top:12px;
-  left:14px;
-  padding:6px 10px;
-  font-size:14px;
-  letter-spacing:.6px;
-  text-transform:uppercase;
-  color:#ffffffb8;
-  background:#0006;
-  border:1px solid rgba(255,255,255,.08);
-  border-radius:999px
+  display:block;
 }
 
 /* -------- Bottom Left (Forecast) -------- */
@@ -225,23 +226,20 @@ table{
   border-spacing:0;
   overflow:hidden;
   border-radius:calc(var(--radius) - 6px);
-  background:#0000002e;
-  border:1px solid rgba(255,255,255,.08);
-  table-layout:fixed
+  background:rgba(8,4,28,.75);
+  border:1px solid rgba(0,255,234,.3);
+  table-layout:fixed;
 }
 
 thead th{
   padding:8px;
   font-size:var(--table-head);
   line-height:1.1;
-  color:#ffffffd1;
+  color:#fff;
   letter-spacing:.4px;
   text-transform:uppercase;
-  background:linear-gradient(90deg,#4cc9f01a,#a78bfa14);
-  border-bottom:1px solid rgba(255,255,255,.1);
-  overflow:hidden;
-  text-overflow:ellipsis;
-  white-space:nowrap
+  background:linear-gradient(90deg, rgba(0,255,234,.25), rgba(255,0,200,.2));
+  border-bottom:1px solid rgba(0,255,234,.35);
 }
 
 tbody td{
@@ -250,44 +248,35 @@ tbody td{
   line-height:1.1;
   color:var(--muted);
   border-bottom:1px solid rgba(255,255,255,.06);
-  overflow:hidden;
-  text-overflow:ellipsis;
-  white-space:nowrap
 }
 
 tbody td:first-child{
   width:44%;
-  color:#ffffffe0;
+  color:#e0f8ff;
   font-weight:760;
-  letter-spacing:.15px;
-  background:#ffffff08;
-  border-right:1px solid rgba(255,255,255,.06)
+  background:rgba(0,255,234,.1);
+  border-right:1px solid rgba(0,255,234,.25);
 }
 
 tbody td:not(:first-child),thead th:not(:first-child){text-align:center}
-tbody tr:nth-child(odd) td{background:#ffffff05}
-tbody tr:nth-child(2n) td{background:#ffffff07}
-tbody tr:nth-child(odd) td:first-child{background:#ffffff0a}
-tbody tr:nth-child(2n) td:first-child{background:#ffffff0e}
+tbody tr:nth-child(odd) td{background:rgba(255,255,255,.04)}
+tbody tr:nth-child(2n) td{background:rgba(255,255,255,.07)}
 tbody tr:last-child td{border-bottom:none}
 
 /* -------- Bottom Right -------- */
 .bottomRight{
   padding:12px;
-  background:radial-gradient(900px 600px at 20% 20%,rgba(76,201,240,.08),transparent 55%),#0000001a;
-  min-width:0;
-  min-height:0
+  background:radial-gradient(900px 600px at 20% 20%, rgba(0,255,234,.1), transparent 55%), rgba(8,4,28,.85);
 }
 
 .bottomRight iframe{
   width:100%;
   height:100%;
-  border:1px solid rgba(255,255,255,.1);
+  border:1px solid rgba(0,255,234,.35);
   border-radius:calc(var(--radius) - 6px);
-  box-shadow:0 18px 50px #0000008c;
+  box-shadow:0 18px 50px #0000008c, 0 0 30px rgba(255,0,200,.3);
   background:#000;
-  display:block
+  display:block;
 }
 
-:focus{outline:2px solid rgba(76,201,240,.55);outline-offset:2px}
-
+:focus{outline:3px solid var(--accent);outline-offset:3px;border-radius:12px}

+ 4 - 0
index.html

@@ -11,6 +11,10 @@
                 <div class="dateTime">
                     <p id="date"></p>
                     <p id="time"></p>
+                    <div>
+                        <p id="weatherDescription"></p>
+                        <p id="currentWeatherLogo"></p>
+                    </div>
                     <img id="conditionLogo">
                 </div>
                 <div class="weather">

+ 31 - 26
index.js

@@ -1,3 +1,5 @@
+import getWeatherInfo from "./weatherCodes.js";
+
 const setDateTime = ()=>{
     const dateTime = new Date();
     document.getElementById("date").textContent = dateTime.toLocaleDateString("en-US", {
@@ -17,56 +19,59 @@ const renderForecast = (f)=>{
     const head = document.getElementById("forecastHead").children[0];
     const tbody = document.getElementById("forecastBody");
 
-    for(let i = 0; i < f.length; i++){
-        const [y, m, d] = f[i].date.split("-").map(Number);
-        const date = new Date(y, m-1, d);
-        head.children[i+1].textContent = date.toLocaleDateString("en-US", {
+    for(let i = 0; i < 3; i++){
+        const sunrise = new Date(f.sunrise[i]);
+        const sunset = new Date(f.sunset[i]);
+        const timeOptions = {
+            hour: "2-digit",
+            minute: "2-digit",
+            hour12: true
+        };
+
+        head.children[i+1].textContent = new Date(f.time[i]).toLocaleDateString("en-US", {
             month: "short",
             day: "numeric",
             year: "2-digit"
         });
-
-        tbody.children[0].children[i+1].textContent = `${f[i].day.mintemp_f} - ${f[i].day.maxtemp_f}`;
-        tbody.children[1].children[i+1].textContent = `${f[i].day.avghumidity}%`;
-        tbody.children[2].children[i+1].textContent = `${f[i].day.daily_chance_of_rain}%`;
-        tbody.children[3].children[i+1].textContent = f[i].day.totalprecip_in;
-        tbody.children[4].children[i+1].textContent = f[i].day.maxwind_mph;
-        tbody.children[5].children[i+1].textContent = f[i].astro.sunrise;
-        tbody.children[6].children[i+1].textContent = f[i].astro.sunset;
+        tbody.children[0].children[i+1].textContent = `${f.apparent_temperature_min[i]} - ${f.apparent_temperature_max[i]}`;
+        tbody.children[1].children[i+1].textContent = `${f.relative_humidity_2m_mean[i]}%`;
+        tbody.children[2].children[i+1].textContent = `${f.precipitation_probability_max[i]}%`;
+        tbody.children[3].children[i+1].textContent = f.precipitation_sum[i];
+        tbody.children[4].children[i+1].textContent = f.wind_speed_10m_max[i];
+        tbody.children[5].children[i+1].textContent = sunrise.toLocaleTimeString("en-US", timeOptions);
+        tbody.children[6].children[i+1].textContent = sunset.toLocaleTimeString("en-US", timeOptions);
     }
 }
 
 const getWeather = ()=>{
-    fetch("https://api.weatherapi.com/v1/forecast.json?key=519f51763fa8477b864184849251502&q=29576&days=3&aqi=no", {
-        method: "get"
-    })
+    fetch("https://api.open-meteo.com/v1/forecast?latitude=33.54&longitude=-79.05&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,rain_sum,precipitation_sum,precipitation_probability_max,wind_speed_10m_max,wind_gusts_10m_max,wind_direction_10m_dominant,relative_humidity_2m_mean,weather_code&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m,is_day&timezone=America%2FNew_York&forecast_days=3&temperature_unit=fahrenheit", {method: "GET"})
         .then(r=>r.json())
         .then((response)=>{
-            const tempDisplay = `${response.current.temp_f}\u00B0F`;
-            document.getElementById("temp").textContent = tempDisplay;
+            const tempDisplay = `${response.current.temperature_2m}\u00B0F`;
+            document.getElementById("temp").textContent= tempDisplay;
 
-            const feelsLike = `Feels Like ${response.current.feelslike_f}\u00B0F`;
+            const feelsLike = `Feels Like ${response.current.apparent_temperature}\u00B0F`;
             document.getElementById("feels").textContent = feelsLike;
 
-            const currentWeatherImage = document.getElementById("conditionLogo");
-            currentWeatherImage.src = `https:${response.current.condition.icon}`;
-            currentWeatherImage.alt = response.current.condition.text;
+            const weatherInfo = getWeatherInfo(response.current.weather_code, response.current.is_day);
+            document.getElementById("weatherDescription").textContent = weatherInfo.desc;
+            document.getElementById("currentWeatherLogo").textContent = weatherInfo.icon;
 
-            const currentWind = `Winds ${response.current.wind_mph}mph ${response.current.wind_dir}`;
+            const currentWind = `Wind ${response.current.wind_speed_10m}mph`;
             const currentElem = document.querySelector("#wind p");
             const newElem = document.createElement("p");
             newElem.textContent = currentWind;
             currentElem.replaceWith(newElem);
 
-            document.querySelector("#wind svg").style.transform = `rotate(${response.current.wind_degree}deg)`;
+            document.querySelector("#wind svg").style.transform = `rotate(${response.current.wind_direction_10m}deg)`;
 
-            const humidity = `${response.current.humidity}% humidity`;
+            const humidity = `${response.current.relative_humidity_2m}% humidity`;
             document.getElementById("humidity").textContent = humidity;
 
-            renderForecast(response.forecast.forecastday);
+            renderForecast(response.daily);
         })
         .catch((err)=>{
-            console.log(err);
+            console.error(err);
         });
 }
 

+ 293 - 0
index_old.css

@@ -0,0 +1,293 @@
+:root{
+  --bg: #0b0f16;
+  --border: rgba(255,255,255,.1);
+  --text: rgba(255,255,255,.92);
+  --muted: rgba(255,255,255,.68);
+  --accent: #4cc9f0;
+  --accent-2: #a78bfa;
+
+  --outer-pad: 16px;
+  --gap: 14px;
+  --radius: 18px;
+  --radius-sm: 12px;
+
+  --font: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Noto Sans", Arial, sans-serif;
+
+  --time-size: clamp(40px, 2.55vh, 62px);
+  --date-size: clamp(16px, 1.25vh, 22px);
+  --h2-size: clamp(15px, 1.05vh, 19px);
+  --cond-size: clamp(16px, 1.25vh, 21px);
+  --info-size: clamp(12px, 1vh, 17px);
+
+  --table-head: clamp(12px, 1vh, 15px);
+  --table-cell: clamp(12px, 1.05vh, 16px);
+}
+
+*{box-sizing:border-box}
+html,body{height:100%;width:100%}
+
+body{
+  margin:0;
+  font-family:var(--font);
+  color:var(--text);
+  background:
+    radial-gradient(1200px 600px at 20% 10%,rgba(76,201,240,.12),transparent 60%),
+    radial-gradient(900px 500px at 85% 20%,rgba(167,139,250,.1),transparent 55%),
+    radial-gradient(800px 600px at 50% 100%,rgba(255,255,255,.06),transparent 55%),
+    var(--bg);
+  overflow:hidden;
+  -webkit-font-smoothing:antialiased;
+  text-rendering:geometricPrecision
+}
+
+.grid{
+  height:100vh;
+  width:100vw;
+  padding:var(--outer-pad);
+  display:grid;
+  grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);
+  grid-template-rows:minmax(0,1fr) minmax(0,1fr);
+  gap:var(--gap);
+  filter:brightness(.98)
+}
+.grid>*{min-width:0;min-height:0}
+
+.topLeft,.topRight,.bottomLeft,.bottomRight{
+  position:relative;
+  min-width:0;
+  min-height:0;
+  border-radius:var(--radius);
+  background:linear-gradient(180deg,#ffffff0f,#ffffff08);
+  border:1px solid var(--border);
+  box-shadow:0 10px 30px #00000059,inset 0 1px #ffffff0f;
+  overflow:hidden
+}
+
+/* -------- Top Left -------- */
+.topLeft{
+  display:flex;
+  flex-direction:column;
+  min-width:0;
+  min-height:0;
+  padding:12px;
+  gap:10px
+}
+
+.dateTime{
+  flex:0 0 auto;
+  min-width:0;
+  display:flex;
+  flex-direction:column;
+  gap:4px;
+  padding:10px 12px;
+  border-radius:var(--radius-sm);
+  background:#00000038;
+  border:1px solid rgba(255,255,255,.08);
+  position: relative;
+}
+
+#date{
+  margin:0;
+  font-size:var(--date-size);
+  letter-spacing:.35px;
+  color:var(--muted)
+}
+
+#time{
+  margin:0;
+  font-size:var(--time-size);
+  font-weight:760;
+  letter-spacing:.8px;
+  line-height:1.02;
+  color:var(--text);
+  overflow:hidden;
+  text-overflow:ellipsis;
+  white-space:nowrap
+}
+
+.weather{
+  flex:1 1 auto;
+  min-width:0;
+  min-height:0;
+  display:flex
+}
+
+.weatherBlock{
+  flex:1 1 auto;
+  min-width:0;
+  min-height:0;
+  padding:10px;
+  grid-template-rows:auto auto minmax(0,1fr);
+  gap:8px;
+}
+
+.weatherBlock h2:before{
+  content:"";
+  width:10px;
+  height:10px;
+  border-radius:50%;
+  background:linear-gradient(135deg,var(--accent),var(--accent-2));
+  box-shadow:0 0 18px #4cc9f059
+}
+
+#conditionLogo{
+    height: 100%;
+    position: absolute;
+    top: 0;
+    right: 0;
+}
+
+/* FIX 1: make weatherInfo a strict 2x2 grid so the last two rows can't get clipped */
+.weatherInfo{
+  height: 100%;
+
+  display:grid;
+  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
+  grid-template-rows:minmax(0,1fr) minmax(0,1fr);
+  gap:6px 8px;
+
+  align-content:stretch;
+  align-items:stretch;
+}
+
+.weatherItem{
+  margin:0;
+  padding:6px 9px;             /* slightly tighter */
+  border-radius:14px;
+  background:#ffffff09;
+  border:1px solid rgba(255,255,255,.06);
+  font-size:var(--info-size);
+  line-height:1.00;            /* tighter to stop bottom clipping */
+  color:var(--muted);
+
+  display:flex;
+  align-items:center;
+  justify-content:center;
+
+  min-width:0;
+  min-height:0;
+
+  overflow:hidden;
+  text-overflow:ellipsis;
+  white-space:nowrap
+}
+
+#temp,#feels,#wind,#humidity{color:var(--text);font-weight:650}
+
+/* -------- Top Right (Radar) -------- */
+.topRight{
+  padding:12px;
+  display:flex;
+  align-items:center;
+  justify-content:center;
+  background:radial-gradient(800px 520px at 70% 10%,rgba(167,139,250,.09),transparent 55%),#0000001a
+}
+
+/* FIX 2: slight zoom, clip a bit top/bottom */
+#radar{
+  width:100%;
+  height:100%;
+
+  object-fit:scale-down;             /* crop instead of letterbox */
+  object-position:center 55%;   /* trims a touch more top/bottom */
+  transform:scale(1.5);        /* very slight zoom */
+  transform-origin:center;
+
+  border-radius:calc(var(--radius) - 6px);
+  border:1px solid rgba(255,255,255,.08);
+  box-shadow:0 14px 40px #00000073;
+  background:#00000038;
+  display:block
+}
+
+.topRight:after{
+  content:"Radar";
+  position:absolute;
+  top:12px;
+  left:14px;
+  padding:6px 10px;
+  font-size:14px;
+  letter-spacing:.6px;
+  text-transform:uppercase;
+  color:#ffffffb8;
+  background:#0006;
+  border:1px solid rgba(255,255,255,.08);
+  border-radius:999px
+}
+
+/* -------- Bottom Left (Forecast) -------- */
+.bottomLeft{padding:12px;display:flex;min-width:0;min-height:0}
+
+table{
+  width:100%;
+  height:100%;
+  border-collapse:separate;
+  border-spacing:0;
+  overflow:hidden;
+  border-radius:calc(var(--radius) - 6px);
+  background:#0000002e;
+  border:1px solid rgba(255,255,255,.08);
+  table-layout:fixed
+}
+
+thead th{
+  padding:8px;
+  font-size:var(--table-head);
+  line-height:1.1;
+  color:#ffffffd1;
+  letter-spacing:.4px;
+  text-transform:uppercase;
+  background:linear-gradient(90deg,#4cc9f01a,#a78bfa14);
+  border-bottom:1px solid rgba(255,255,255,.1);
+  overflow:hidden;
+  text-overflow:ellipsis;
+  white-space:nowrap
+}
+
+tbody td{
+  padding:6px 8px;
+  font-size:var(--table-cell);
+  line-height:1.1;
+  color:var(--muted);
+  border-bottom:1px solid rgba(255,255,255,.06);
+  overflow:hidden;
+  text-overflow:ellipsis;
+  white-space:nowrap
+}
+
+tbody td:first-child{
+  width:44%;
+  color:#ffffffe0;
+  font-weight:760;
+  letter-spacing:.15px;
+  background:#ffffff08;
+  border-right:1px solid rgba(255,255,255,.06)
+}
+
+tbody td:not(:first-child),thead th:not(:first-child){text-align:center}
+tbody tr:nth-child(odd) td{background:#ffffff05}
+tbody tr:nth-child(2n) td{background:#ffffff07}
+tbody tr:nth-child(odd) td:first-child{background:#ffffff0a}
+tbody tr:nth-child(2n) td:first-child{background:#ffffff0e}
+tbody tr:last-child td{border-bottom:none}
+
+/* -------- Bottom Right -------- */
+.bottomRight{
+  padding:12px;
+  background:radial-gradient(900px 600px at 20% 20%,rgba(76,201,240,.08),transparent 55%),#0000001a;
+  min-width:0;
+  min-height:0
+}
+
+.bottomRight iframe{
+  width:100%;
+  height:100%;
+  border:1px solid rgba(255,255,255,.1);
+  border-radius:calc(var(--radius) - 6px);
+  box-shadow:0 18px 50px #0000008c;
+  background:#000;
+  display:block
+}
+
+:focus{outline:2px solid rgba(76,201,240,.55);outline-offset:2px}
+

+ 38 - 0
weatherCodes.js

@@ -0,0 +1,38 @@
+/**
+ * Get weather icon + description from Open-Meteo weather_code
+ * @param {number} code - weather_code from the API
+ * @param {number|boolean} isDay - 1 = daytime, 0 = nighttime (use response.current.is_day or hourly.is_day)
+ * @returns {{icon: string, description: string}}
+ */
+export default getWeatherInfo = (code, isDay = 1) => {
+    const isDaytime = isDay === 1 || isDay === true;
+
+    const map = {
+        0:   { icon: isDaytime ? "☀️" : "🌙",          desc: "Clear sky" },
+        1:   { icon: isDaytime ? "🌤️" : "🌙",          desc: "Mainly clear" },
+        2:   { icon: isDaytime ? "⛅" : "🌥️",          desc: "Partly cloudy" },
+        3:   { icon: "☁️",                              desc: "Overcast" },
+        45:  { icon: "🌫️",                              desc: "Fog" },
+        48:  { icon: "🌫️",                              desc: "Depositing rime fog" },
+        51:  { icon: "🌧️",                              desc: "Light drizzle" },
+        53:  { icon: "🌧️",                              desc: "Moderate drizzle" },
+        55:  { icon: "🌧️",                              desc: "Dense drizzle" },
+        61:  { icon: "🌧️",                              desc: "Light rain" },
+        63:  { icon: "🌧️",                              desc: "Moderate rain" },
+        65:  { icon: "🌧️",                              desc: "Heavy rain" },
+        71:  { icon: "❄️",                              desc: "Light snow" },
+        73:  { icon: "❄️",                              desc: "Moderate snow" },
+        75:  { icon: "❄️",                              desc: "Heavy snow" },
+        77:  { icon: "❄️",                              desc: "Snow grains" },
+        80:  { icon: "🌦️",                              desc: "Light rain showers" },
+        81:  { icon: "🌧️",                              desc: "Moderate rain showers" },
+        82:  { icon: "🌧️",                              desc: "Violent rain showers" },
+        85:  { icon: "❄️",                              desc: "Light snow showers" },
+        86:  { icon: "❄️",                              desc: "Heavy snow showers" },
+        95:  { icon: "⛈️",                              desc: "Thunderstorm" },
+        96:  { icon: "⛈️",                              desc: "Thunderstorm with slight hail" },
+        99:  { icon: "⛈️",                              desc: "Thunderstorm with heavy hail" }
+    };
+
+    return map[code] || { icon: "❓", desc: "Unknown" };
+};