A creation shared by its maker. The page below runs in a sandbox that cannot read anything of yours or talk to any server. Read the source before you run it anywhere else.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hill Country Skies — Central Texas Weather by Zone</title>
<style>
:root {
--bg: #1a1206;
--bg-soft: #2a1d0c;
--card: #2b2114;
--card-hi: #352817;
--accent: #f4a83a;
--accent-2: #e0662b;
--text: #fbf3e4;
--muted: #c9b48f;
--shadow: 0 18px 50px rgba(0,0,0,.45);
--radius: 18px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
background: radial-gradient(1200px 700px at 78% -8%, #3a2810 0%, var(--bg) 55%);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
nav {
display: flex; align-items: center; justify-content: space-between;
padding: 20px clamp(20px, 5vw, 64px);
position: sticky; top: 0; z-index: 50;
backdrop-filter: blur(8px);
background: linear-gradient(180deg, rgba(26,18,6,.88), rgba(26,18,6,.4));
}
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 20px; letter-spacing: .3px; }
.brand .dot { width: 12px; height: 12px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--accent-2)); box-shadow: 0 0 14px var(--accent); }
.nav-links { display: flex; gap: 26px; color: var(--muted); font-size: 14px; }
.nav-links a:hover { color: var(--text); }
.nav-cta {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: #2a1604; font-weight: 700; padding: 9px 18px; border-radius: 12px; font-size: 14px;
transition: transform .15s ease, box-shadow .15s ease;
}
.nav-cta:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(244,168,58,.35); }
.unit-toggle {
display: flex; align-items: center; border: 1px solid rgba(255,255,255,.12);
border-radius: 999px; overflow: hidden; font-size: 13px; font-weight: 700; background: var(--card);
}
.unit-toggle span { padding: 7px 12px; cursor: pointer; color: var(--muted); transition: all .15s ease; }
.unit-toggle span.active { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #2a1604; }
.hero { padding: clamp(40px, 9vw, 110px) clamp(20px, 5vw, 64px) 30px; text-align: center; max-width: 980px; margin: 0 auto; }
.pill {
display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--accent);
border: 1px solid rgba(244,168,58,.35); background: rgba(244,168,58,.08);
padding: 6px 14px; border-radius: 999px; margin-bottom: 22px;
}
.pill .pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1.8s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
h1 { font-size: clamp(34px, 6vw, 62px); line-height: 1.05; font-weight: 800; letter-spacing: -.5px; }
h1 .grad { background: linear-gradient(120deg, var(--accent), var(--accent-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.sub { color: var(--muted); font-size: clamp(15px, 2.2vw, 19px); margin: 20px auto 0; max-width: 640px; }
.search {
margin: 30px auto 0; max-width: 620px; display: flex; gap: 10px;
background: var(--card); padding: 8px; border-radius: 16px; border: 1px solid rgba(255,255,255,.06);
box-shadow: var(--shadow);
}
.search input {
flex: 1; background: transparent; border: none; outline: none; color: var(--text);
font-size: 16px; padding: 12px 14px;
}
.search input::placeholder { color: var(--muted); }
.search button {
background: linear-gradient(135deg, var(--accent), var(--accent-2)); border: none; cursor: pointer;
color: #2a1604; font-weight: 700; padding: 0 22px; border-radius: 11px; font-size: 15px;
transition: transform .15s ease;
}
.search button:hover { transform: translateY(-1px); }
.chips { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 18px auto 0; max-width: 680px; }
.chip {
font-size: 13px; color: var(--muted); background: var(--card); border: 1px solid rgba(255,255,255,.07);
padding: 6px 13px; border-radius: 999px; cursor: pointer; transition: border-color .15s ease, color .15s ease;
}
.chip:hover { border-color: var(--accent); color: var(--text); }
.hint { color: var(--muted); font-size: 13px; margin-top: 12px; }
/* Zones */
#zones { max-width: 980px; margin: 10px auto 0; padding: 0 clamp(20px,5vw,64px); }
.zonebar { display: flex; gap: 8px; flex-wrap: wrap; }
.zonechip {
font-size: 13px; font-weight: 600; color: var(--muted); background: var(--card);
border: 1px solid rgba(255,255,255,.07); padding: 8px 15px; border-radius: 999px; cursor: pointer;
transition: all .15s ease;
}
.zonechip:hover { border-color: var(--accent); color: var(--text); }
.zonechip.active { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #2a1604; border-color: transparent; }
.zonehead { font-size: 16px; font-weight: 700; margin: 26px 0 12px; display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.zonehead .zs { color: var(--muted); font-size: 13px; font-weight: 500; }
.zones-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px,1fr)); gap: 12px; }
.zcard {
background: var(--card); border: 1px solid rgba(255,255,255,.05); border-radius: 14px; padding: 14px;
cursor: pointer; transition: transform .15s ease, border-color .15s ease;
}
.zcard:hover { transform: translateY(-3px); border-color: rgba(244,168,58,.4); }
.zcard .zt { font-weight: 700; font-size: 15px; }
.zcard .ze { font-size: 28px; margin: 4px 0; }
.zcard .ztemp { font-size: 26px; font-weight: 800; }
.zcard .zc { color: var(--muted); font-size: 13px; }
.zcard.loading { opacity: .55; }
#result { max-width: 980px; margin: 26px auto 0; display: none; padding: 0 clamp(20px,5vw,64px); }
#result.show { display: block; animation: rise .4s ease; }
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.back-btn { font-size: 13px; color: var(--muted); cursor: pointer; margin-bottom: 12px; display: inline-block; }
.back-btn:hover { color: var(--accent); }
.track-btn {
display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; font-weight: 600;
background: var(--card-hi); color: var(--text); border: 1px solid rgba(255,255,255,.08);
padding: 9px 16px; border-radius: 12px; margin-bottom: 16px; transition: border-color .15s ease;
}
.track-btn:hover { border-color: var(--accent); }
.track-btn.tracked { background: rgba(244,168,58,.14); border-color: var(--accent); color: var(--accent); }
.current {
background: linear-gradient(135deg, var(--card), var(--card-hi));
border-radius: var(--radius); padding: 28px; box-shadow: var(--shadow);
display: grid; grid-template-columns: auto 1fr; gap: 26px; align-items: center;
}
.current .icon { font-size: 64px; line-height: 1; }
.current .loc { font-size: 22px; font-weight: 700; }
.current .cond { color: var(--muted); font-size: 15px; margin-top: 2px; }
.current .temp { font-size: 56px; font-weight: 800; letter-spacing: -1px; }
.current .details { display: flex; gap: 26px; margin-top: 14px; flex-wrap: wrap; }
.current .details div { font-size: 14px; color: var(--muted); }
.current .details span { display: block; color: var(--text); font-size: 18px; font-weight: 700; margin-top: 2px; }
.forecast { display: grid; grid-template-columns: repeat(7, 1fr); gap: 10px; margin-top: 16px; }
.day {
background: var(--card); border-radius: 14px; padding: 14px 8px; text-align: center;
border: 1px solid rgba(255,255,255,.05);
}
.day .d { font-size: 12px; color: var(--muted); }
.day .e { font-size: 26px; margin: 6px 0; }
.day .hi { font-weight: 700; font-size: 14px; }
.day .lo { color: var(--muted); font-size: 13px; }
#tracked-wrap { max-width: 980px; margin: 30px auto 0; padding: 0 clamp(20px,5vw,64px); }
#tracked-wrap h2 { font-size: 18px; margin-bottom: 14px; color: var(--text); }
#tracked-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px,1fr)); gap: 14px; }
.mini {
background: var(--card); border-radius: 14px; padding: 16px; border: 1px solid rgba(255,255,255,.05);
position: relative; cursor: pointer; transition: transform .15s ease, border-color .15s ease;
}
.mini:hover { transform: translateY(-3px); border-color: rgba(244,168,58,.4); }
.mini .mloc { font-weight: 700; font-size: 15px; }
.mini .mtemp { font-size: 30px; font-weight: 800; margin-top: 6px; }
.mini .mcond { color: var(--muted); font-size: 13px; }
.mini .x { position: absolute; top: 8px; right: 10px; color: var(--muted); font-size: 16px; }
.mini .x:hover { color: #ff7b7b; }
.features { max-width: 980px; margin: 70px auto 0; padding: 0 clamp(20px,5vw,64px); }
.features h2 { text-align: center; font-size: clamp(24px,4vw,34px); font-weight: 800; }
.features .lead { text-align: center; color: var(--muted); margin: 12px auto 36px; max-width: 560px; }
.grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap: 18px; }
.feature {
background: var(--card); border-radius: var(--radius); padding: 24px; border: 1px solid rgba(255,255,255,.05);
}
.feature .fi { font-size: 30px; }
.feature h3 { margin: 12px 0 8px; font-size: 18px; }
.feature p { color: var(--muted); font-size: 14px; }
footer { text-align: center; color: var(--muted); font-size: 13px; padding: 60px 20px 40px; }
footer a { color: var(--accent); }
.err { color: #ffb27b; font-size: 14px; margin-top: 10px; text-align: center; }
@media (max-width: 720px) {
.forecast { grid-template-columns: repeat(4, 1fr); }
.current { grid-template-columns: 1fr; text-align: center; justify-items: center; }
.nav-links { display: none; }
}
</style>
</head>
<body>
<nav>
<div class="brand"><span class="dot"></span> Hill Country Skies</div>
<div class="nav-links">
<a href="#zones">Zones</a>
<a href="#tracked-wrap">My Towns</a>
<a href="#how">About</a>
</div>
<a class="nav-cta" href="#search">Check my town</a>
<div class="unit-toggle" id="unit" title="Switch temperature units"><span data-u="f">°F</span><span data-u="c">°C</span></div>
</nav>
<section class="hero" id="search">
<div class="pill"><span class="pulse"></span> Central Texas · Live data · No API key</div>
<h1>Weather for the<br><span class="grad">heart of Texas</span></h1>
<p class="sub">From the Hill Country to the Brazos Valley — browse live conditions by geographic zone, or search any town. Track the places that matter to you.</p>
<div class="search">
<input id="q" type="text" placeholder="Search a Central Texas town…" autocomplete="off" />
<button id="go">Search</button>
</div>
<div class="chips" id="chips"></div>
<div class="hint">Tap a town or search any place. Saved towns stay in this browser.</div>
<div class="err" id="err"></div>
</section>
<section id="zones">
<div class="zonebar" id="zonebar"></div>
<div id="zone-body"></div>
</section>
<section id="result">
<span class="back-btn" id="back">← Back to zones</span>
<div class="track-btn" id="track">★ Track this town</div>
<div class="current">
<div class="icon" id="c-icon">⛅</div>
<div>
<div class="loc" id="c-loc">—</div>
<div class="cond" id="c-cond">—</div>
<div class="temp" id="c-temp">—</div>
<div class="details">
<div>Feels like<span id="c-feels">—</span></div>
<div>Humidity<span id="c-hum">—</span></div>
<div>Wind<span id="c-wind">—</span></div>
<div>Rain<span id="c-rain">—</span></div>
</div>
</div>
</div>
<div class="forecast" id="forecast"></div>
</section>
<section id="tracked-wrap">
<h2>★ My Towns</h2>
<div id="tracked-grid"></div>
</section>
<section class="features" id="features">
<h2>Built for Central Texans</h2>
<p class="lead">Hill Country heat, flash floods, blue northers — we know the weather here doesn't play. Keep an eye on every zone at once.</p>
<div class="grid3">
<div class="feature"><div class="fi">🗺️</div><h3>Browse by zone</h3><p>Flip between the Hill Country, I-35 Corridor, Greater San Antonio, Brazos Valley and Blackland Prairie — all live at once.</p></div>
<div class="feature"><div class="fi">🌳</div><h3>Every town, not just the big ones</h3><p>Geocoded lookup across the region — Fredericksburg, Bastrop, Taylor, Lockhart and beyond.</p></div>
<div class="feature"><div class="fi">📌</div><h3>Pin your towns</h3><p>Track home, work, and the in-laws' place. They reload instantly from your browser — no account.</p></div>
<div class="feature"><div class="fi">📅</div><h3>7-day forecast</h3><p>Highs, lows, and conditions for the week ahead, updated every time you check.</p></div>
<div class="feature"><div class="fi">⚡</div><h3>Zero config</h3><p>No signup, no API key, no credit card. Open it and start tracking.</p></div>
<div class="feature"><div class="fi">🔒</div><h3>Private by default</h3><p>Your tracked towns never leave your device. What you watch stays yours.</p></div>
</div>
</section>
<footer id="how">
Weather data via <a href="https://open-meteo.com" target="_blank" rel="noreferrer">Open-Meteo</a>. A Central Texas demo landing page. 🤠🌤️
</footer>
<script>
const $ = (id) => document.getElementById(id);
const LS_KEY = "hillcountry.tracked";
// Safe storage: falls back to in-memory when localStorage is blocked
// (sandboxed iframe, file://, or privacy mode). Wrapped so access never throws.
const Store = (() => {
let mem = {};
let ok = true;
try {
const k = "__hc_probe__";
window.localStorage.setItem(k, "1");
window.localStorage.removeItem(k);
} catch { ok = false; }
return {
get: (k) => {
try { return ok ? window.localStorage.getItem(k) : (k in mem ? mem[k] : null); }
catch { return k in mem ? mem[k] : null; }
},
set: (k, v) => {
try { if (ok) window.localStorage.setItem(k, v); else mem[k] = v; }
catch { mem[k] = v; }
}
};
})();
// Preset Central Texas towns
const CITIES = [
{ name: "Austin", lat: 30.2672, lon: -97.7431 },
{ name: "San Antonio", lat: 29.4241, lon: -98.4936 },
{ name: "Waco", lat: 31.5493, lon: -97.1467 },
{ name: "Round Rock", lat: 30.5083, lon: -97.6789 },
{ name: "Temple", lat: 31.0982, lon: -97.3428 },
{ name: "Killeen", lat: 31.1171, lon: -97.7278 },
{ name: "Georgetown", lat: 30.6324, lon: -97.6770 },
{ name: "New Braunfels", lat: 29.7030, lon: -98.1245 },
{ name: "Fredericksburg",lat: 30.2755, lon: -98.8713 },
{ name: "Bastrop", lat: 30.1105, lon: -97.3211 },
{ name: "Taylor", lat: 30.5705, lon: -97.4094 },
{ name: "Lockhart", lat: 29.8852, lon: -97.6694 }
];
// Geographic zones across Central Texas
const ZONES = [
{ id: "hillcountry", name: "Hill Country", towns: ["Fredericksburg","Austin","New Braunfels","Georgetown","San Antonio"] },
{ id: "i35", name: "I-35 Corridor", towns: ["Waco","Temple","Killeen","Round Rock","Austin"] },
{ id: "sanantonio", name: "Greater San Antonio", towns: ["San Antonio","New Braunfels","Lockhart","Austin"] },
{ id: "brazos", name: "Brazos Valley", towns: ["Waco","Temple","Taylor","Killeen"] },
{ id: "blackland", name: "Blackland Prairie", towns: ["Waco","Taylor","Round Rock","Georgetown","Temple"] }
];
const WMO = {
0:["Clear sky","☀️"],1:["Mainly clear","🌤️"],2:["Partly cloudy","⛅"],3:["Overcast","☁️"],
45:["Fog","🌫️"],48:["Rime fog","🌫️"],51:["Light drizzle","🌦️"],53:["Drizzle","🌦️"],55:["Heavy drizzle","🌧️"],
61:["Light rain","🌦️"],63:["Rain","🌧️"],65:["Heavy rain","🌧️"],66:["Freezing rain","🌧️"],67:["Freezing rain","🌧️"],
71:["Light snow","🌨️"],73:["Snow","🌨️"],75:["Heavy snow","❄️"],77:["Snow grains","🌨️"],
80:["Rain showers","🌦️"],81:["Rain showers","🌧️"],82:["Violent showers","⛈️"],
85:["Snow showers","🌨️"],86:["Snow showers","🌨️"],
95:["Thunderstorm","⛈️"],96:["Thunderstorm + hail","⛈️"],99:["Severe thunderstorm","⛈️"]
};
const wmo = (c) => WMO[c] || ["—","🌡️"];
// Unit preference (defaults to Fahrenheit for Texas)
function getUnit() { return Store.get("hillcountry.unit") || "f"; }
function setUnit(u) { Store.set("hillcountry.unit", u); }
const T = (c) => Math.round(getUnit() === "f" ? c*9/5+32 : c) + "°";
const W = (k) => Math.round(getUnit() === "f" ? k*0.621371 : k) + (getUnit() === "f" ? " mph" : " km/h");
const R = (m) => (getUnit() === "f" ? (m*0.0393701).toFixed(2) : m.toFixed(1)) + (getUnit() === "f" ? " in" : " mm");
let current = null;
function loadTracked() {
let list;
try { list = JSON.parse(Store.get(LS_KEY)) || []; }
catch { return []; }
// Migrate old entries that stored a Fahrenheit value instead of Celsius
// (or were already double-converted). Central Texas in °C is ~ -5..45, so
// any temp above 50 was saved wrong — convert it back once.
let fixed = false;
list.forEach(t => {
if (typeof t.temp === "number" && t.temp > 50) { t.temp = (t.temp - 32) * 5 / 9; fixed = true; }
});
if (fixed) saveTracked(list);
return list;
}
function saveTracked(list) { Store.set(LS_KEY, JSON.stringify(list)); }
async function geocode(q) {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(q)}&count=1&language=en&format=json`;
const r = await fetch(url);
const j = await r.json();
if (!j.results || !j.results.length) throw new Error("No town found. Try another spelling.");
return j.results[0];
}
async function getWeather(lat, lon) {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}`
+ `¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,precipitation`
+ `&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto`;
const r = await fetch(url);
return r.json();
}
function render(geo, wx) {
current = { name: geo.name, country: geo.country_code || geo.country || "US", lat: geo.latitude ?? geo.lat, lon: geo.longitude ?? geo.lon, tempC: c.temperature_2m };
const c = wx.current, d = wx.daily;
const [cond, icon] = wmo(c.weather_code);
$("c-icon").textContent = icon;
$("c-loc").textContent = geo.name + (geo.country_code && geo.country_code !== "US" ? `, ${geo.country_code}` : (geo.state ? `, ${geo.state}` : ""));
$("c-cond").textContent = cond;
$("c-temp").textContent = T(c.temperature_2m);
$("c-feels").textContent = T(c.apparent_temperature);
$("c-hum").textContent = `${c.relative_humidity_2m}%`;
$("c-wind").textContent = W(c.wind_speed_10m);
$("c-rain").textContent = R(c.precipitation);
const days = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
let html = "";
for (let i = 0; i < d.time.length && i < 7; i++) {
const dt = new Date(d.time[i] + "T00:00");
const [, di] = wmo(d.weather_code[i]);
html += `<div class="day"><div class="d">${days[dt.getDay()]}</div>`
+ `<div class="e">${di}</div>`
+ `<div class="hi">${T(d.temperature_2m_max[i])}</div>`
+ `<div class="lo">${T(d.temperature_2m_min[i])}</div></div>`;
}
$("forecast").innerHTML = html;
updateTrackBtn();
$("zones").style.display = "none";
$("result").classList.add("show");
}
function renderTracked() {
const list = loadTracked();
const grid = $("tracked-grid");
if (!list.length) { grid.innerHTML = `<p style="color:var(--muted);font-size:14px">No towns yet — search a town and tap “Track this town”.</p>`; return; }
grid.innerHTML = list.map((t, i) =>
`<div class="mini" data-i="${i}"><span class="x" data-x="${i}">✕</span>`
+ `<div class="mloc">${t.name}${t.state ? ", "+t.state : ""}</div>`
+ `<div class="mtemp">${T(t.temp)}</div>`
+ `<div class="mcond">${t.cond}</div></div>`
).join("");
}
function updateTrackBtn() {
const tracked = loadTracked();
const isTracked = current && tracked.some(t => t.name === current.name && t.lat === current.lat);
const tb = $("track");
tb.classList.toggle("tracked", !!isTracked);
tb.textContent = isTracked ? "✓ Tracked" : "★ Track this town";
}
async function search(q) {
$("err").textContent = "";
if (!q.trim()) return;
try {
const preset = CITIES.find(ct => ct.name.toLowerCase() === q.trim().toLowerCase());
if (preset) {
const wx = await getWeather(preset.lat, preset.lon);
render({ name: preset.name, country_code: "US", state: "TX", latitude: preset.lat, longitude: preset.lon }, wx);
} else {
const geo = await geocode(q);
const wx = await getWeather(geo.latitude, geo.longitude);
render(geo, wx);
}
window.scrollTo({ top: $("result").offsetTop - 80, behavior: "smooth" });
} catch (e) {
$("err").textContent = e.message || "Something went wrong fetching the weather.";
}
}
// Build quick-pick chips (first 8 cities)
$("chips").innerHTML = CITIES.slice(0, 8).map(c => `<span class="chip" data-n="${c.name}">${c.name}</span>`).join("");
$("chips").addEventListener("click", (e) => { if (e.target.dataset.n) search(e.target.dataset.n); });
// Zones
const cityByName = (n) => CITIES.find(c => c.name.toLowerCase() === n.toLowerCase());
let activeZone = "hillcountry";
function loadZone(id) {
activeZone = id;
const z = ZONES.find(z => z.id === id);
const bar = $("zonebar");
bar.querySelectorAll(".zonechip").forEach(c => c.classList.toggle("active", c.dataset.z === id));
const body = $("zone-body");
body.innerHTML = `<div class="zonehead"><span>${z.name}</span><span class="zs">loading live conditions…</span></div>`
+ `<div class="zones-grid">` + z.towns.map(t =>
`<div class="zcard loading" data-t="${t}"><div class="zt">${t}</div><div class="ze">⏳</div><div class="ztemp">—</div><div class="zc">—</div></div>`
).join("") + `</div>`;
Promise.all(z.towns.map(t => {
const c = cityByName(t);
return getWeather(c.lat, c.lon).then(wx => ({ t, wx })).catch(() => ({ t, wx: null }));
})).then(results => {
const temps = [];
results.forEach(({ t, wx }) => {
const card = body.querySelector(`.zcard[data-t="${CSS.escape(t)}"]`);
if (!wx || !wx.current) { card.querySelector(".zc").textContent = "unavailable"; card.classList.remove("loading"); return; }
const c = wx.current;
const [cond, icon] = wmo(c.weather_code);
card.querySelector(".ze").textContent = icon;
card.querySelector(".ztemp").textContent = T(c.temperature_2m);
card.querySelector(".zc").textContent = cond;
card.classList.remove("loading");
temps.push(c.temperature_2m);
});
const span = body.querySelector(".zs");
if (temps.length) {
span.textContent = `now ${T(Math.min(...temps))}–${T(Math.max(...temps))} across the zone`;
} else span.textContent = "";
});
}
function buildZoneBar() {
$("zonebar").innerHTML = ZONES.map(z => `<span class="zonechip" data-z="${z.id}">${z.name}</span>`).join("");
$("zonebar").addEventListener("click", (e) => { if (e.target.dataset.z) loadZone(e.target.dataset.z); });
}
$("zone-body").addEventListener("click", (e) => {
const card = e.target.closest(".zcard");
if (card) search(card.dataset.t);
});
$("back").addEventListener("click", () => {
$("result").classList.remove("show");
$("zones").style.display = "block";
window.scrollTo({ top: $("zones").offsetTop - 70, behavior: "smooth" });
});
$("go").addEventListener("click", () => search($("q").value));
$("q").addEventListener("keydown", (e) => { if (e.key === "Enter") search($("q").value); });
$("track").addEventListener("click", () => {
if (!current) return;
const list = loadTracked();
const idx = list.findIndex(t => t.name === current.name && t.lat === current.lat);
if (idx >= 0) list.splice(idx, 1);
else list.push({ name: current.name, state: "TX", lat: current.lat, lon: current.lon,
temp: current.tempC, unit: "C", cond: $("c-cond").textContent });
saveTracked(list);
updateTrackBtn();
renderTracked();
});
$("tracked-grid").addEventListener("click", async (e) => {
if (e.target.dataset.x !== undefined) {
const list = loadTracked();
list.splice(+e.target.dataset.x, 1);
saveTracked(list); renderTracked(); return;
}
const card = e.target.closest(".mini");
if (card) {
const t = loadTracked()[+card.dataset.i];
if (t) {
try { const wx = await getWeather(t.lat, t.lon); render({ name: t.name, country_code: "US", state: t.state, latitude: t.lat, longitude: t.lon }, wx); }
catch (err) { $("err").textContent = "Couldn't refresh that town."; }
}
}
});
// Unit toggle
function paintUnit() {
$("unit").querySelectorAll("span").forEach(s => s.classList.toggle("active", s.dataset.u === getUnit()));
}
$("unit").addEventListener("click", (e) => {
if (!e.target.dataset.u) return;
setUnit(e.target.dataset.u);
paintUnit();
loadZone(activeZone); // refresh zone cards
renderTracked(); // refresh tracked cards
if (current && $("result").classList.contains("show")) search(current.name); // refresh detail view
});
// Boot
buildZoneBar();
paintUnit();
loadZone("hillcountry");
renderTracked();
</script>
</body>
</html>