<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Atlantic Hurricane Dashboard — Mobile (FL-first)</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="color-scheme" content="dark light" />
<style>
:root{
--bg:#0b1220;--card:#121b2d;--fg:#eaf2ff;--muted:#9fb4d7;--accent:#5fd1ff;
--ok:#ffd166;--bad:#ff6b6b;--gap:14px;--r:14px
}
@media (prefers-color-scheme: light){
:root{--bg:#f7fbff;--card:#ffffff;--fg:#0a2540;--muted:#456;--accent:#0077ff}
}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--fg);font-family:system-ui,-apple-system,Roboto,Inter,Segoe UI,Arial,sans-serif}
header{position:sticky;top:0;z-index:10;background:linear-gradient(180deg,rgba(0,0,0,.35),transparent);backdrop-filter:blur(6px)}
.wrap{max-width:860px;margin:0 auto;padding:16px}
h1{margin:0;font-size:1.25rem}
p.small{color:var(--muted);margin:.25rem 0 0}
.grid{display:grid;grid-template-columns:1fr;gap:var(--gap)}
.card{background:var(--card);border-radius:var(--r);box-shadow:0 8px 24px rgba(0,0,0,.18);overflow:hidden}
.card h3{font-size:1rem;margin:0;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.06)}
.body{padding:12px 14px}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.btn{appearance:none;border:1px solid #2b4266;background:#15223a;color:#eaf2ff;
padding:12px 14px;border-radius:12px;font-size:.95rem;line-height:1;cursor:pointer;flex:1;min-width:120px;text-align:center}
.btn.accent{background:var(--accent);color:#012038;border-color:transparent;font-weight:600}
.btn.ghost{background:transparent}
.btn.warn{background:var(--bad);border-color:transparent;color:#2b0000}
.pill{padding:10px 12px;border-radius:999px;background:#162641;border:1px solid #2b4266;color:#eaf2ff;min-width:120px}
.hint{color:var(--muted);font-size:.9rem}
.imgbox{display:grid;place-items:center;background:#0f1728}
.imgbox img{display:block;max-width:100%;height:auto}
.frame{aspect-ratio:16/9;background:#0f1728;border:0;width:100%}
.toggles{display:flex;gap:8px;flex-wrap:wrap}
.toggle{padding:10px 12px;border-radius:999px;border:1px solid #2b4266;background:#0f1728;color:#eaf2ff;font-size:.9rem}
.toggle.active{background:#1f3358;border-color:#4a6aa0}
.switch{display:flex;align-items:center;gap:8px}
.switch input{appearance:none;width:48px;height:28px;border-radius:999px;background:#1a2a45;position:relative;border:1px solid #2b4266}
.switch input:checked{background:#0c663f;border-color:#0c663f}
.switch input::after{content:"";position:absolute;width:22px;height:22px;border-radius:50%;background:#cfe5ff;top:2px;left:2px;transition:all .2s}
.switch input:checked::after{left:24px;background:#eafff7}
footer{color:var(--muted);font-size:.9rem;margin:10px 0 24px}
.toolbar{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media (min-width:620px){ .toolbar{grid-template-columns:repeat(4,1fr)} }
/* preset chips */
.chip{padding:10px 12px;border:1px solid #2b4266;border-radius:999px;background:#0f1728;color:#eaf2ff;font-size:.9rem;cursor:pointer}
.chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
/* touch-friendly */
.btn, .toggle, .chip {touch-action:manipulation}
</style>
</head>
<body>
<header>
<div class="wrap">
<h1>Atlantic Hurricane Dashboard</h1>
<p class="small">Florida-first view • NHC outlooks • Live map • Silent satellite • Quick resources</p>
<div class="row" style="margin-top:10px">
<input id="lat" class="pill" inputmode="decimal" placeholder="Lat e.g. 27.95" />
<input id="lon" class="pill" inputmode="decimal" placeholder="Lon e.g. -82.46" />
<button class="btn accent" id="center">Center Map</button>
<button class="btn" id="geo">My Location</button>
</div>
<div class="row" style="margin-top:8px">
<label class="switch">
🔇 Silent Mode
<input type="checkbox" id="silent" />
</label>
<button class="btn" id="zoomEarth">🛰️ Zoom Earth</button>
<button class="btn ghost" id="saveHome" title="Save current center as Home">⭐ Set Home</button>
<button class="btn" id="goHome" title="Go to saved Home">🏠 Home</button>
</div>
<!-- Florida & nearby presets -->
<div class="chips" id="presets">
<button class="chip" data-lat="27.95" data-lon="-82.46" data-zoom="7">Tampa Bay</button>
<button class="chip" data-lat="24" data-lon="-90" data-zoom="5">Gulf of Mexico</button>
<button class="chip" data-lat="27.0" data-lon="-81.5" data-zoom="6">Florida Peninsula</button>
<button class="chip" data-lat="25.6" data-lon="-80.2" data-zoom="7">SE Florida</button>
<button class="chip" data-lat="30.3" data-lon="-86.6" data-zoom="7">FL Panhandle</button>
<button class="chip" data-lat="16" data-lon="-72" data-zoom="5">Caribbean</button>
</div>
</div>
</header>
<main class="wrap">
<!-- NHC Outlooks -->
<section class="grid">
<article class="card">
<h3>NHC Graphical Tropical Weather Outlook — 2-Day (Atlantic)</h3>
<div class="imgbox">
<img id="twoDay" alt="NHC 2-day outlook (Atlantic)" loading="lazy"
src="https://www.nhc.noaa.gov/xgtwo/two_atl_2d0.png" />
</div>
<div class="body toolbar">
<button class="btn" data-open="https://www.nhc.noaa.gov/gtwo.php?basin=atlc">Open Outlook</button>
<button class="btn ghost" id="r2">↻ Refresh</button>
<button class="btn" data-open="https://www.nhc.noaa.gov/">NHC Active</button>
<button class="btn" data-open="https://www.nhc.noaa.gov/text/refresh/MIATWOAT+shtml/">Text Outlook</button>
</div>
</article>
<article class="card">
<h3>NHC Graphical Tropical Weather Outlook — 7-Day (Atlantic)</h3>
<div class="imgbox">
<img id="sevenDay" alt="NHC 7-day outlook (Atlantic)" loading="lazy"
src="https://www.nhc.noaa.gov/xgtwo/two_atl_7d0.png" />
</div>
<div class="body toolbar">
<button class="btn" data-open="https://www.nhc.noaa.gov/gtwo.php?basin=atlc&fdays=7">Open 7-Day</button>
<button class="btn ghost" id="r7">↻ Refresh</button>
<button class="btn" data-open="https://www.nhc.noaa.gov/graphics_at1.shtml">AL Basin Graphics</button>
<button class="btn" data-open="https://www.nhc.noaa.gov/archive/">Advisory Archive</button>
</div>
</article>
</section>
<!-- Live Map + Silent Satellite -->
<section class="grid">
<article class="card">
<h3>Live Map — Windy (interactive) / NOAA (silent)</h3>
<div class="body">
<div class="toggles" id="layers">
<button class="toggle active" data-layer="satellite">🛰️ Satellite</button>
<button class="toggle" data-layer="radar">🌧️ Radar</button>
<button class="toggle" data-layer="wind">💨 Wind</button>
<button class="toggle" data-layer="pressure">🌀 Pressure</button>
</div>
<p class="hint" style="margin-top:6px">Tip: Turn on 🔇 Silent Mode to switch to a noise-free GOES image.</p>
</div>
<!-- Windy iframe (default) -->
<iframe id="windy" class="frame" title="Windy Map" loading="lazy" referrerpolicy="no-referrer"></iframe>
<!-- NOAA silent image (shown when Silent Mode is on) -->
<div class="imgbox" id="noaaBox" style="display:none">
<img id="goes" alt="GOES-East GeoColor (latest)" loading="lazy"
src="https://cdn.star.nesdis.noaa.gov/GOES16/ABI/FD/GEOCOLOR/latest.jpg"
style="width:100%;height:100%;object-fit:contain" />
</div>
<div class="body row">
<button class="btn" id="reset">↺ Center Florida</button>
<button class="btn" data-open="https://www.noaa.gov/hurricane-season">NOAA Season Hub</button>
<button class="btn" data-open="https://tropical.colostate.edu/forecasting.html">CSU Forecasts</button>
<button class="btn" data-open="https://alerts.weather.gov/">NWS Alerts Map</button>
</div>
</article>
</section>
<!-- Tips -->
<section class="grid">
<article class="card">
<h3>Quick Tips</h3>
<div class="body">
<ul style="margin:8px 0 0 18px; line-height:1.35">
<li>Use 7-day outlook to spot early “areas to watch.”</li>
<li>Tap Zoom Earth for tracks/winds; NHC pages for cones & hazards.</li>
<li><b>Safety first:</b> Follow local NWS guidance over any third-party map.</li>
</ul>
</div>
</article>
</section>
<footer class="wrap">
Sources: NOAA/NHC, Windy, NOAA STAR (GOES-East), CSU. Built for phones—fast taps, low bandwidth.
</footer>
</main>
<script>
// ---------- helpers ----------
function openExt(url){ window.open(url,'_blank','noopener'); }
document.querySelectorAll('[data-open]').forEach(b=>b.onclick=()=>openExt(b.dataset.open));
function getParam(name){ return new URLSearchParams(location.search).get(name); }
// refresh images with cache-buster
function refresh(id){
const el = document.getElementById(id);
const u = new URL(el.src); u.searchParams.set('t', Date.now()); el.src = u.toString();
}
document.getElementById('r2').onclick = ()=>refresh('twoDay');
document.getElementById('r7').onclick = ()=>refresh('sevenDay');
// ---------- Map state ----------
const windy = document.getElementById('windy');
const layers = document.getElementById('layers');
const silent = document.getElementById('silent');
const noaaBox = document.getElementById('noaaBox');
// Florida-centric default (good Tampa Bay vicinity)
const flDefault = { lat:27.6, lon:-82.6, zoom:6 }; // west/central FL view
const basinDefault = { lat:24.0, lon:-60.0, zoom:4 };
let state = { ...flDefault, layer:'satellite' };
// URL params override defaults
const pLat = parseFloat(getParam('lat'));
const pLon = parseFloat(getParam('lon'));
const pZoom = parseInt(getParam('zoom')||'',10);
if(Number.isFinite(pLat) && Number.isFinite(pLon)){ state.lat = pLat; state.lon = pLon; }
if(Number.isFinite(pZoom)) state.zoom = pZoom;
// Restore Home if present (and no URL center)
const homeJSON = localStorage.getItem('wt_home_center');
if(!(Number.isFinite(pLat) && Number.isFinite(pLon)) && homeJSON){
try{
const h = JSON.parse(homeJSON);
if(Number.isFinite(h.lat) && Number.isFinite(h.lon)){
state.lat = h.lat; state.lon = h.lon; state.zoom = h.zoom ?? state.zoom;
}
}catch(e){}
}
// Build Windy URL
function windySrc(){
const overlay = ({satellite:'satellite',radar:'radar',wind:'wind',pressure:'pressure'})[state.layer] || 'satellite';
return `https://embed.windy.com/embed2.html?lat=${state.lat.toFixed(3)}&lon=${state.lon.toFixed(3)}&zoom=${state.zoom}`+
`&level=surface&overlay=${overlay}&product=ecmwf&menu=&message=true&marker=&calendar=now&pressure=true`+
`&type=map&location=coordinates&detail=&detailLat=${state.lat.toFixed(3)}&detailLon=${state.lon.toFixed(3)}`+
`&metricWind=kt&metricTemp=%C2%B0F&sound=false`;
}
function loadWindy(){ windy.src = windySrc(); }
// Try to auto-center if geolocation already granted (no surprise prompts)
async function tryAutoGeo(){
try{
if(!navigator.permissions || !navigator.geolocation) { loadWindy(); return; }
const perm = await navigator.permissions.query({ name:'geolocation' });
if(perm.state === 'granted'){
navigator.geolocation.getCurrentPosition(pos=>{
state.lat = pos.coords.latitude; state.lon = pos.coords.longitude; state.zoom = 7;
loadWindy();
// also prefill inputs
document.getElementById('lat').value = state.lat.toFixed(4);
document.getElementById('lon').value = state.lon.toFixed(4);
}, ()=>loadWindy());
} else {
// keep Florida default without prompting
loadWindy();
}
} catch(e){ loadWindy(); }
}
tryAutoGeo();
// ---------- layer toggles ----------
layers.addEventListener('click', e=>{
const btn = e.target.closest('.toggle'); if(!btn) return;
layers.querySelectorAll('.toggle').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
state.layer = btn.dataset.layer;
if(!silent.checked) loadWindy();
});
// ---------- controls ----------
document.getElementById('reset').onclick = ()=>{
state = { ...state, ...flDefault };
if(!silent.checked) loadWindy();
};
document.getElementById('center').onclick = ()=>{
const lat = parseFloat(document.getElementById('lat').value);
const lon = parseFloat(document.getElementById('lon').value);
if(Number.isFinite(lat) && Number.isFinite(lon)){
state.lat = lat; state.lon = lon; state.zoom = 7;
if(!silent.checked) loadWindy();
}else alert('Enter valid numbers for lat/lon (e.g. 27.95 and -82.46).');
};
document.getElementById('geo').onclick = ()=>{
if(!navigator.geolocation) return alert('Geolocation not supported.');
navigator.geolocation.getCurrentPosition(pos=>{
state.lat = pos.coords.latitude; state.lon = pos.coords.longitude; state.zoom = 7;
document.getElementById('lat').value = state.lat.toFixed(4);
document.getElementById('lon').value = state.lon.toFixed(4);
if(!silent.checked) loadWindy();
}, ()=>alert('Could not get your location. You can type lat/lon manually.'));
};
document.getElementById('zoomEarth').onclick = ()=>{
const url = Number.isFinite(state.lat) && Number.isFinite(state.lon)
? `https://zoom.earth/#view=${state.lat},${state.lon},7z,now,labels:off`
: 'https://zoom.earth/hurricanes/';
openExt(url);
};
// ---------- Silent Mode (image only, auto-refresh) ----------
let goesTimer=null;
function startGOES(){ stopGOES(); refresh('goes'); goesTimer=setInterval(()=>refresh('goes'), 5*60*1000); }
function stopGOES(){ if(goesTimer){ clearInterval(goesTimer); goesTimer=null; } }
silent.addEventListener('change', ()=>{
if(silent.checked){
windy.style.display='none';
noaaBox.style.display='grid';
startGOES();
}else{
stopGOES();
noaaBox.style.display='none';
windy.style.display='block';
loadWindy();
}
});
// ---------- Presets + Home ----------
document.getElementById('presets').addEventListener('click', (e)=>{
const chip = e.target.closest('.chip'); if(!chip) return;
state.lat = parseFloat(chip.dataset.lat);
state.lon = parseFloat(chip.dataset.lon);
state.zoom = parseInt(chip.dataset.zoom,10) || 6;
if(!silent.checked) loadWindy();
document.getElementById('lat').value = state.lat.toFixed(4);
document.getElementById('lon').value = state.lon.toFixed(4);
});
document.getElementById('saveHome').onclick = ()=>{
const home = { lat: state.lat, lon: state.lon, zoom: state.zoom };
localStorage.setItem('wt_home_center', JSON.stringify(home));
alert('Saved current center as Home.');
};
document.getElementById('goHome').onclick = ()=>{
const j = localStorage.getItem('wt_home_center');
if(!j) return alert('No Home saved yet. Tap ⭐ Set Home first.');
try{
const h = JSON.parse(j);
if(Number.isFinite(h.lat) && Number.isFinite(h.lon)){
state.lat = h.lat; state.lon = h.lon; state.zoom = h.zoom ?? 7;
if(!silent.checked) loadWindy();
document.getElementById('lat').value = state.lat.toFixed(4);
document.getElementById('lon').value = state.lon.toFixed(4);
}
}catch(e){ alert('Home data corrupted; please set it again.'); }
};
// initial NHC refresh (deferred for snappier first paint)
window.addEventListener('load', ()=> setTimeout(()=>{ refresh('twoDay'); refresh('sevenDay'); }, 700));
</script>
</body>
</html>