diff --git a/docs/adamaps-index-preview.html b/docs/adamaps-index-preview.html
new file mode 100644
index 0000000..495e946
--- /dev/null
+++ b/docs/adamaps-index-preview.html
@@ -0,0 +1,545 @@
+
+
+
+
+
+ AdaMaps — Verified Sign Map
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+// AQI color gradient: green→yellow→orange→red→purple
+const AQI_GRADIENT = {
+ 0.0: '#00e400', // Good (0-50)
+ 0.17: '#ffff00', // Moderate (51-100)
+ 0.33: '#ff7e00', // Unhealthy for sensitive (101-150)
+ 0.50: '#ff0000', // Unhealthy (151-200)
+ 0.67: '#8f3f97', // Very unhealthy (201-300)
+ 1.0: '#7e0023', // Hazardous (301+)
+};
+
+const PM25_GRADIENT = {
+ 0.0: '#00e5ff',
+ 0.25: '#00ff88',
+ 0.50: '#ffff00',
+ 0.75: '#ff7e00',
+ 1.0: '#ff0000',
+};
+
+function aqiLabel(aqi) {
+ if (aqi == null) return '—';
+ if (aqi <= 50) return `${aqi} Good`;
+ if (aqi <= 100) return `${aqi} Moderate`;
+ if (aqi <= 150) return `${aqi} Unhealthy (Sensitive)`;
+ if (aqi <= 200) return `${aqi} Unhealthy`;
+ if (aqi <= 300) return `${aqi} Very Unhealthy`;
+ return `${aqi} Hazardous`;
+}
+
+function aqiColor(aqi) {
+ if (aqi == null) return '#888';
+ if (aqi <= 50) return '#00e400';
+ if (aqi <= 100) return '#ffff00';
+ if (aqi <= 150) return '#ff7e00';
+ if (aqi <= 200) return '#ff0000';
+ if (aqi <= 300) return '#8f3f97';
+ return '#7e0023';
+}
+
+let airHeatAqi = null;
+let airHeatPm25 = null;
+let airMode = null; // null | 'aqi' | 'pm25'
+
+async function fetchAirOverlay(metric) {
+ try {
+ const res = await fetch(
+ `https://api.adamaps.org/api/air/heatmap?metric=${metric}&hours=24`,
+ { cache: 'no-store' }
+ );
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ return await res.json(); // [[lat, lon, intensity], ...]
+ } catch (e) {
+ console.warn('Air overlay fetch failed:', e);
+ return [];
+ }
+}
+
+async function toggleAirOverlay(metric) {
+ // Clear both layers first
+ if (airHeatAqi) { map.removeLayer(airHeatAqi); airHeatAqi = null; }
+ if (airHeatPm25) { map.removeLayer(airHeatPm25); airHeatPm25 = null; }
+
+ // Toggle off if same metric clicked again
+ if (airMode === metric) {
+ airMode = null;
+ btnAqi.classList.remove('active');
+ btnPm25.classList.remove('active');
+ statusEl.textContent = 'live · ' + new Date().toLocaleTimeString();
+ return;
+ }
+
+ airMode = metric;
+ btnAqi.classList.toggle('active', metric === 'aqi');
+ btnPm25.classList.toggle('active', metric === 'pm25');
+ statusEl.textContent = `loading ${metric.toUpperCase()} overlay...`;
+
+ const points = await fetchAirOverlay(metric);
+ if (!points.length) {
+ statusEl.textContent = 'no air quality data yet';
+ airMode = null;
+ btnAqi.classList.remove('active');
+ btnPm25.classList.remove('active');
+ return;
+ }
+
+ const gradient = metric === 'aqi' ? AQI_GRADIENT : PM25_GRADIENT;
+ const layer = L.heatLayer(points, {
+ radius: 25,
+ blur: 20,
+ maxZoom: 17,
+ gradient,
+ minOpacity: 0.3,
+ });
+
+ if (metric === 'aqi') airHeatAqi = layer;
+ else airHeatPm25 = layer;
+
+ layer.addTo(map);
+ statusEl.textContent = `${metric.toUpperCase()} overlay · ${points.length.toLocaleString()} pts`;
+}
+
+// ── Legend ────────────────────────────────────────────────────────────────────
+function buildAirLegend() {
+ const legend = L.control({ position: 'bottomright' });
+ legend.onAdd = () => {
+ const div = L.DomUtil.create('div', 'air-legend');
+ div.innerHTML = `
+ AQI / PM2.5
+ Good (0-50)
+ Moderate (51-100)
+ Unhealthy* (101-150)
+ Unhealthy (151-200)
+ Very Unhealthy (201-300)
+ Hazardous (301+)
+ `;
+ return div;
+ };
+ return legend;
+}
+
+// Call this after map init — wires up buttons and legend
+function initAirOverlay() {
+ buildAirLegend().addTo(map);
+ btnAqi.addEventListener('click', () => toggleAirOverlay('aqi'));
+ btnPm25.addEventListener('click', () => toggleAirOverlay('pm25'));
+}
+
+
+ // Init air overlay buttons + legend after map is ready
+ initAirOverlay();
+
+
+