ergod
Built with Ergod

Hill Country Skies — Central Texas Weather by Zone

Shared 2026-09-22 · 23 views
About this creation

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.

Interactive page
Open full page
Show source
<!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}`
    + `&current=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>