Root
>
Earthquake.html
Download
Delete
Earthquake.html
31,6 KB • HTML • jun. 18, 2026 06:54
Render
Source
Live Edit
Copy
Save
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Sandbox Earthquake Radar</title> <style> :root { --bg: #070a12; --panel: #111827; --panel-soft: #172033; --panel-strong: #202a3d; --text: #f8fafc; --muted: #aab4c5; --line: rgba(255, 255, 255, 0.12); --accent: #ffb86b; --low: #7dd3fc; --mid: #facc15; --high: #fb923c; --severe: #f87171; --shadow: rgba(0, 0, 0, 0.38); --error: #fca5a5; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; color: var(--text); background: var(--bg); } body { min-height: 100svh; } button { font: inherit; cursor: pointer; } .app { position: relative; width: 100vw; height: 100svh; min-height: 560px; overflow: hidden; background: var(--bg); isolation: isolate; } .pulse-field { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 0; } .wave { position: absolute; width: 180px; height: 180px; border: 1px solid var(--accent); border-radius: 50%; opacity: 0; animation: wave 4.8s ease-out infinite; } @keyframes wave { 0% { transform: translate(-50%, -50%) scale(0.15); opacity: 0; } 12% { opacity: 0.28; } 100% { transform: translate(-50%, -50%) scale(1.8); opacity: 0; } } .stage { position: relative; z-index: 1; width: 100%; height: 100%; padding: clamp(18px, 3vw, 42px); display: grid; grid-template-rows: auto 1fr auto; gap: 22px; } .topbar { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; z-index: 10; } .eyebrow { color: var(--muted); font-size: 0.78rem; font-weight: 850; text-transform: uppercase; letter-spacing: 0.14em; } .title { margin-top: 7px; font-size: clamp(1.8rem, 4.2vw, 4rem); font-weight: 950; line-height: 0.92; letter-spacing: -0.07em; max-width: 980px; } .updated { margin-top: 10px; color: var(--muted); font-size: 0.95rem; } .updated.error { color: var(--error); } .settings { position: relative; z-index: 20; } .gear { width: 50px; height: 50px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 17px; color: var(--text); background: rgba(17, 24, 39, 0.9); box-shadow: 0 18px 52px var(--shadow); } .gear svg { width: 23px; height: 23px; } .settings-panel { position: absolute; top: 62px; right: 0; width: min(390px, calc(100vw - 36px)); padding: 16px; border: 1px solid var(--line); border-radius: 24px; background: rgba(17, 24, 39, 0.97); box-shadow: 0 24px 80px var(--shadow); } .settings-panel[hidden] { display: none; } .settings-title { font-weight: 950; letter-spacing: -0.03em; margin-bottom: 8px; } .settings-copy { color: var(--muted); font-size: 0.88rem; line-height: 1.4; margin-bottom: 14px; } .feed-buttons { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .feed-button { border: 1px solid var(--line); border-radius: 15px; padding: 11px 10px; color: var(--text); background: var(--panel-soft); font-weight: 850; text-align: left; } .feed-button.active { color: #111827; background: var(--accent); border-color: transparent; } .hero { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr); gap: clamp(20px, 4vw, 54px); align-items: center; } .map-card { position: relative; min-height: 0; height: min(62vh, 620px); border: 1px solid var(--line); border-radius: 34px; background: rgba(17, 24, 39, 0.78); box-shadow: 0 28px 90px var(--shadow); overflow: hidden; } .map-card::before { content: ""; position: absolute; inset: 9%; border: 1px solid rgba(255, 255, 255, 0.07); border-radius: 50%; pointer-events: none; } .map-card::after { content: ""; position: absolute; left: 4%; right: 4%; top: 50%; height: 1px; background: rgba(255, 255, 255, 0.08); pointer-events: none; } .grid-lines { position: absolute; inset: 0; pointer-events: none; opacity: 0.5; } .grid-lines span { position: absolute; background: rgba(255, 255, 255, 0.055); } .grid-lines .v { top: 0; bottom: 0; width: 1px; } .grid-lines .h { left: 0; right: 0; height: 1px; } .continent { position: absolute; border: 1px solid rgba(255, 255, 255, 0.09); background: rgba(255, 255, 255, 0.035); border-radius: 999px; pointer-events: none; } .quake { position: absolute; transform: translate(-50%, -50%); border-radius: 50%; background: var(--accent); box-shadow: 0 0 28px currentColor; border: 1px solid rgba(255, 255, 255, 0.65); } .quake::after { content: ""; position: absolute; inset: -8px; border: 1px solid currentColor; border-radius: 50%; opacity: 0.4; animation: ping 2.6s ease-out infinite; } @keyframes ping { 0% { transform: scale(0.45); opacity: 0.45; } 100% { transform: scale(2.4); opacity: 0; } } .tooltip { position: absolute; left: 18px; bottom: 18px; width: min(420px, calc(100% - 36px)); border: 1px solid var(--line); border-radius: 24px; background: rgba(7, 10, 18, 0.9); padding: 14px; box-shadow: 0 18px 52px var(--shadow); } .tooltip-title { font-weight: 950; letter-spacing: -0.04em; line-height: 1.05; } .tooltip-meta { color: var(--muted); font-size: 0.86rem; margin-top: 7px; line-height: 1.35; } .side { display: grid; gap: 13px; min-width: 0; } .big-stat { border: 1px solid var(--line); border-radius: 30px; background: rgba(17, 24, 39, 0.8); box-shadow: 0 18px 52px var(--shadow); padding: 20px; } .stat-label { color: var(--muted); font-size: 0.78rem; font-weight: 850; letter-spacing: 0.13em; text-transform: uppercase; margin-bottom: 12px; } .stat-value { font-size: clamp(3.3rem, 8vw, 7rem); font-weight: 950; line-height: 0.78; letter-spacing: -0.09em; } .stat-note { margin-top: 14px; color: var(--muted); line-height: 1.4; } .mini-grid { display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 13px; } .mini { min-height: 122px; border: 1px solid var(--line); border-radius: 25px; background: rgba(17, 24, 39, 0.8); box-shadow: 0 18px 52px var(--shadow); padding: 16px; } .mini-value { font-size: clamp(1.7rem, 3vw, 2.6rem); font-weight: 950; line-height: 1; letter-spacing: -0.06em; } .mini-note { margin-top: 8px; color: var(--muted); font-size: 0.88rem; } .bottom { display: grid; grid-template-columns: 1fr; min-height: 158px; } .list-card { border: 1px solid var(--line); border-radius: 29px; background: rgba(17, 24, 39, 0.82); box-shadow: 0 18px 52px var(--shadow); padding: 16px; min-width: 0; } .list-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 13px; } .list-title { font-weight: 950; letter-spacing: -0.04em; } .list-note { color: var(--muted); font-size: 0.86rem; text-align: right; } .quake-list { display: grid; grid-template-columns: repeat(6, minmax(170px, 1fr)); gap: 10px; overflow: auto; padding-bottom: 3px; } .quake-row { min-width: 170px; border: 1px solid var(--line); border-radius: 20px; background: var(--panel-soft); padding: 12px; } .row-top { display: flex; justify-content: space-between; gap: 8px; align-items: center; margin-bottom: 8px; } .mag-pill { border-radius: 999px; padding: 4px 8px; color: #111827; background: var(--accent); font-weight: 950; font-size: 0.82rem; } .depth { color: var(--muted); font-size: 0.78rem; font-weight: 800; } .place { font-weight: 900; line-height: 1.15; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .time { margin-top: 8px; color: var(--muted); font-size: 0.8rem; } @media (max-width: 980px) { .app { overflow: auto; } .stage { min-height: 100svh; height: auto; } .hero { grid-template-columns: 1fr; align-items: start; } .map-card { height: min(62vh, 520px); } .quake-list { grid-template-columns: repeat(6, 180px); } } @media (max-width: 680px) { .stage { gap: 18px; } .title { font-size: 2rem; } .map-card { height: 420px; } .mini-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .tooltip { position: static; width: auto; margin: 14px; } .app { overflow: hidden !important; } } </style> </head> <body> <main class="app" style="width: 100%"> <div id="pulseField" class="pulse-field"></div> <section class="stage" aria-live="polite"> <header class="topbar"> <div> <div class="eyebrow">USGS seismic feed</div> <div id="feedTitle" class="title">Earthquake radar</div> <div id="updated" class="updated">Loading recent earthquakes...</div> </div> <div class="settings"> <button id="settingsToggle" class="gear" type="button" aria-label="Open feed settings" aria-expanded="false" aria-controls="settingsPanel"> <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"> <path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Z" stroke="currentColor" stroke-width="1.8" /> <path d="M19.4 15a1.8 1.8 0 0 0 .36 1.98l.05.05a2.1 2.1 0 0 1-2.97 2.97l-.05-.05a1.8 1.8 0 0 0-1.98-.36 1.8 1.8 0 0 0-1.1 1.65V21.4a2.1 2.1 0 0 1-4.2 0v-.08a1.8 1.8 0 0 0-1.1-1.65 1.8 1.8 0 0 0-1.98.36l-.05.05a2.1 2.1 0 0 1-2.97-2.97l.05-.05A1.8 1.8 0 0 0 4.6 15a1.8 1.8 0 0 0-1.65-1.1H2.9a2.1 2.1 0 0 1 0-4.2h.08A1.8 1.8 0 0 0 4.6 8a1.8 1.8 0 0 0-.36-1.98l-.05-.05A2.1 2.1 0 0 1 7.16 3l.05.05A1.8 1.8 0 0 0 9.2 3.4 1.8 1.8 0 0 0 10.3 1.75V1.7a2.1 2.1 0 0 1 4.2 0v.08A1.8 1.8 0 0 0 15.6 3.4a1.8 1.8 0 0 0 1.98-.36l.05-.05A2.1 2.1 0 0 1 20.6 6l-.05.05A1.8 1.8 0 0 0 20.2 8c.26.66.9 1.1 1.6 1.1h.1a2.1 2.1 0 0 1 0 4.2h-.08A1.8 1.8 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="1.25" /> </svg> </button> <div id="settingsPanel" class="settings-panel" hidden> <div class="settings-title">Change feed</div> <div class="settings-copy"> No storage, no geolocation, no form submission. Data is fetched directly from the selected USGS GeoJSON feed. </div> <div id="feedButtons" class="feed-buttons"></div> </div> </div> </header> <section class="hero"> <article class="map-card" id="map"> <div class="grid-lines" aria-hidden="true"> <span class="v" style="left: 20%;"></span> <span class="v" style="left: 40%;"></span> <span class="v" style="left: 60%;"></span> <span class="v" style="left: 80%;"></span> <span class="h" style="top: 25%;"></span> <span class="h" style="top: 50%;"></span> <span class="h" style="top: 75%;"></span> </div> <div class="continent" style="left: 9%; top: 22%; width: 20%; height: 26%; transform: rotate(-18deg);"></div> <div class="continent" style="left: 22%; top: 51%; width: 14%; height: 25%; transform: rotate(22deg);"></div> <div class="continent" style="left: 43%; top: 22%; width: 14%; height: 21%; transform: rotate(13deg);"></div> <div class="continent" style="left: 54%; top: 25%; width: 28%; height: 31%; transform: rotate(-8deg);"></div> <div class="continent" style="left: 72%; top: 58%; width: 13%; height: 14%; transform: rotate(10deg);"></div> <div id="quakeLayer"></div> <div class="tooltip"> <div id="tooltipTitle" class="tooltip-title">Hover an event</div> <div id="tooltipMeta" class="tooltip-meta">Recent earthquakes appear as live points on the global projection.</div> </div> </article> <aside class="side"> <article class="big-stat"> <div class="stat-label">Strongest magnitude</div> <div id="maxMag" class="stat-value">--</div> <div id="maxMagNote" class="stat-note">Waiting for USGS feed.</div> </article> <div class="mini-grid"> <article class="mini"> <div class="stat-label">Events</div> <div id="eventCount" class="mini-value">--</div> <div class="mini-note">Feed total</div> </article> <article class="mini"> <div class="stat-label">Average depth</div> <div id="avgDepth" class="mini-value">--</div> <div class="mini-note">Kilometers</div> </article> <article class="mini"> <div class="stat-label">M 4.5+</div> <div id="bigCount" class="mini-value">--</div> <div class="mini-note">Stronger events</div> </article> <article class="mini"> <div class="stat-label">Tsunami flags</div> <div id="tsunamiCount" class="mini-value">--</div> <div class="mini-note">Reported by feed</div> </article> </div> </aside> </section> <section class="bottom"> <article class="list-card"> <div class="list-head"> <div class="list-title">Latest strongest events</div> <div id="listNote" class="list-note">Sorted by magnitude</div> </div> <div id="quakeList" class="quake-list"></div> </article> </section> </section> </main> <script> // Sandbox-safe: // - no localStorage // - no sessionStorage // - no IndexedDB // - no cookies // - no geolocation // - no forms // - no external libraries, fonts, CSS, or images const feeds = [ { id: "2.5_day", label: "M2.5+ day", title: "M2.5+ earthquakes, past day", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson" }, { id: "all_day", label: "All day", title: "All earthquakes, past day", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson" }, { id: "4.5_week", label: "M4.5+ week", title: "M4.5+ earthquakes, past week", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson" }, { id: "significant_week", label: "Significant", title: "Significant earthquakes, past week", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/significant_week.geojson" }, { id: "2.5_week", label: "M2.5+ week", title: "M2.5+ earthquakes, past week", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_week.geojson" }, { id: "4.5_month", label: "M4.5+ month", title: "M4.5+ earthquakes, past month", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_month.geojson" } ]; let activeFeed = feeds[0]; const els = { pulseField: document.getElementById("pulseField"), feedTitle: document.getElementById("feedTitle"), updated: document.getElementById("updated"), settingsToggle: document.getElementById("settingsToggle"), settingsPanel: document.getElementById("settingsPanel"), feedButtons: document.getElementById("feedButtons"), quakeLayer: document.getElementById("quakeLayer"), tooltipTitle: document.getElementById("tooltipTitle"), tooltipMeta: document.getElementById("tooltipMeta"), maxMag: document.getElementById("maxMag"), maxMagNote: document.getElementById("maxMagNote"), eventCount: document.getElementById("eventCount"), avgDepth: document.getElementById("avgDepth"), bigCount: document.getElementById("bigCount"), tsunamiCount: document.getElementById("tsunamiCount"), quakeList: document.getElementById("quakeList"), listNote: document.getElementById("listNote") }; function setStatus(text, isError = false) { els.updated.textContent = text; els.updated.classList.toggle("error", isError); } function formatTime(ms) { const date = new Date(ms); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); } function formatGenerated(ms) { const date = new Date(ms); if (Number.isNaN(date.getTime())) return "Updated from USGS"; return `USGS generated ${date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}`; } function magnitudeColor(mag) { if (!Number.isFinite(mag)) return "var(--low)"; if (mag >= 6) return "var(--severe)"; if (mag >= 4.5) return "var(--high)"; if (mag >= 3) return "var(--mid)"; return "var(--low)"; } function magnitudeSize(mag) { const safe = Number.isFinite(mag) ? mag : 1; return Math.max(7, Math.min(34, 5 + safe * safe * 0.9)); } function project(lon, lat) { const x = ((lon + 180) / 360) * 100; const y = ((90 - lat) / 180) * 100; return { x, y }; } function cleanPlace(place) { return place || "Unknown location"; } function getDepth(feature) { const depth = feature?.geometry?.coordinates?.[2]; return Number.isFinite(depth) ? depth : null; } function renderFeedButtons() { els.feedButtons.textContent = ""; feeds.forEach(feed => { const button = document.createElement("button"); button.type = "button"; button.className = `feed-button${feed.id === activeFeed.id ? " active" : ""}`; button.textContent = feed.label; button.addEventListener("click", () => { activeFeed = feed; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); renderFeedButtons(); loadFeed(feed); }); els.feedButtons.appendChild(button); }); } function renderWaves(features) { els.pulseField.textContent = ""; features.slice(0, 16).forEach((feature, index) => { const coords = feature.geometry?.coordinates; if (!coords || coords.length < 2) return; const [lon, lat] = coords; const projected = project(lon, lat); const mag = feature.properties?.mag; const wave = document.createElement("span"); wave.className = "wave"; wave.style.left = `${projected.x}%`; wave.style.top = `${projected.y}%`; wave.style.borderColor = magnitudeColor(mag); wave.style.animationDelay = `${index * 0.22}s`; els.pulseField.appendChild(wave); }); } function renderMap(features) { els.quakeLayer.textContent = ""; features.slice(0, 220).forEach(feature => { const coords = feature.geometry?.coordinates; if (!coords || coords.length < 2) return; const [lon, lat] = coords; const depth = getDepth(feature); const mag = feature.properties?.mag; const place = cleanPlace(feature.properties?.place); const time = feature.properties?.time; const { x, y } = project(lon, lat); const point = document.createElement("button"); point.type = "button"; point.className = "quake"; point.style.left = `${x}%`; point.style.top = `${y}%`; point.style.width = `${magnitudeSize(mag)}px`; point.style.height = `${magnitudeSize(mag)}px`; point.style.color = magnitudeColor(mag); point.style.background = magnitudeColor(mag); point.setAttribute("aria-label", `${place}, magnitude ${mag || "unknown"}`); point.addEventListener("mouseenter", () => { els.tooltipTitle.textContent = `${Number.isFinite(mag) ? "M" + mag.toFixed(1) : "M --"} · ${place}`; els.tooltipMeta.textContent = `${formatTime(time)} · ${ depth === null ? "Depth unknown" : Math.round(depth) + " km depth" } · ${lat.toFixed(2)}°, ${lon.toFixed(2)}°`; }); point.addEventListener("focus", () => { point.dispatchEvent(new Event("mouseenter")); }); els.quakeLayer.appendChild(point); }); } function renderStats(features) { const magnitudes = features .map(feature => feature.properties?.mag) .filter(Number.isFinite); const depths = features .map(getDepth) .filter(Number.isFinite); const strongest = features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag)[0]; const maxMag = strongest?.properties?.mag; const avgDepth = depths.length ? depths.reduce((sum, value) => sum + value, 0) / depths.length : null; const bigCount = magnitudes.filter(mag => mag >= 4.5).length; const tsunamiCount = features.filter(feature => feature.properties?.tsunami === 1).length; els.maxMag.textContent = Number.isFinite(maxMag) ? maxMag.toFixed(1) : "--"; els.maxMag.style.color = magnitudeColor(maxMag); els.maxMagNote.textContent = strongest ? cleanPlace(strongest.properties.place) : "No magnitude data in this feed."; els.eventCount.textContent = String(features.length); els.avgDepth.textContent = Number.isFinite(avgDepth) ? `${Math.round(avgDepth)}` : "--"; els.bigCount.textContent = String(bigCount); els.tsunamiCount.textContent = String(tsunamiCount); } function renderList(features) { els.quakeList.textContent = ""; const sorted = features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag) .slice(0, 12); sorted.forEach(feature => { const mag = feature.properties.mag; const depth = getDepth(feature); const place = cleanPlace(feature.properties.place); const time = feature.properties.time; const row = document.createElement("article"); row.className = "quake-row"; row.innerHTML = ` <div class="row-top"> <div class="mag-pill" style="background: ${magnitudeColor(mag)};">M${mag.toFixed(1)}</div> <div class="depth">${depth === null ? "--" : Math.round(depth)} km</div> </div> <div class="place">${place}</div> <div class="time">${formatTime(time)}</div> `; els.quakeList.appendChild(row); }); els.listNote.textContent = sorted.length ? "Sorted by magnitude" : "No events"; } function renderData(feed, data) { const features = Array.isArray(data.features) ? data.features : []; els.feedTitle.textContent = feed.title; renderStats(features); renderMap(features); renderList(features); renderWaves( features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag) ); if (features.length) { const first = features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag)[0]; if (first) { const coords = first.geometry?.coordinates || []; const depth = getDepth(first); els.tooltipTitle.textContent = `Strongest: M${first.properties.mag.toFixed(1)} · ${cleanPlace(first.properties.place)}`; els.tooltipMeta.textContent = `${formatTime(first.properties.time)} · ${ depth === null ? "Depth unknown" : Math.round(depth) + " km depth" } · ${Number(coords[1]).toFixed(2)}°, ${Number(coords[0]).toFixed(2)}°`; } } else { els.tooltipTitle.textContent = "No events in this feed"; els.tooltipMeta.textContent = "Try another feed from the settings panel."; } setStatus(formatGenerated(data.metadata?.generated)); } async function loadFeed(feed) { setStatus(`Loading ${feed.title}...`); try { const response = await fetch(feed.url, { cache: "no-store" }); if (!response.ok) { throw new Error("USGS feed request failed."); } const data = await response.json(); if (!data || !Array.isArray(data.features)) { throw new Error("Unexpected USGS feed format."); } renderData(feed, data); } catch (error) { setStatus(error.message || "Could not load earthquake feed.", true); } } els.settingsToggle.addEventListener("click", () => { const isOpen = !els.settingsPanel.hidden; els.settingsPanel.hidden = isOpen; els.settingsToggle.setAttribute("aria-expanded", String(!isOpen)); }); document.addEventListener("click", event => { if (event.target.closest(".settings")) return; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); }); renderFeedButtons(); loadFeed(activeFeed); </script> </body> </html>
Copy
Save
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Sandbox Earthquake Radar</title> <style> :root { --bg: #070a12; --panel: #111827; --panel-soft: #172033; --panel-strong: #202a3d; --text: #f8fafc; --muted: #aab4c5; --line: rgba(255, 255, 255, 0.12); --accent: #ffb86b; --low: #7dd3fc; --mid: #facc15; --high: #fb923c; --severe: #f87171; --shadow: rgba(0, 0, 0, 0.38); --error: #fca5a5; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; color: var(--text); background: var(--bg); } body { min-height: 100svh; } button { font: inherit; cursor: pointer; } .app { position: relative; width: 100vw; height: 100svh; min-height: 560px; overflow: hidden; background: var(--bg); isolation: isolate; } .pulse-field { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 0; } .wave { position: absolute; width: 180px; height: 180px; border: 1px solid var(--accent); border-radius: 50%; opacity: 0; animation: wave 4.8s ease-out infinite; } @keyframes wave { 0% { transform: translate(-50%, -50%) scale(0.15); opacity: 0; } 12% { opacity: 0.28; } 100% { transform: translate(-50%, -50%) scale(1.8); opacity: 0; } } .stage { position: relative; z-index: 1; width: 100%; height: 100%; padding: clamp(18px, 3vw, 42px); display: grid; grid-template-rows: auto 1fr auto; gap: 22px; } .topbar { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; z-index: 10; } .eyebrow { color: var(--muted); font-size: 0.78rem; font-weight: 850; text-transform: uppercase; letter-spacing: 0.14em; } .title { margin-top: 7px; font-size: clamp(1.8rem, 4.2vw, 4rem); font-weight: 950; line-height: 0.92; letter-spacing: -0.07em; max-width: 980px; } .updated { margin-top: 10px; color: var(--muted); font-size: 0.95rem; } .updated.error { color: var(--error); } .settings { position: relative; z-index: 20; } .gear { width: 50px; height: 50px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 17px; color: var(--text); background: rgba(17, 24, 39, 0.9); box-shadow: 0 18px 52px var(--shadow); } .gear svg { width: 23px; height: 23px; } .settings-panel { position: absolute; top: 62px; right: 0; width: min(390px, calc(100vw - 36px)); padding: 16px; border: 1px solid var(--line); border-radius: 24px; background: rgba(17, 24, 39, 0.97); box-shadow: 0 24px 80px var(--shadow); } .settings-panel[hidden] { display: none; } .settings-title { font-weight: 950; letter-spacing: -0.03em; margin-bottom: 8px; } .settings-copy { color: var(--muted); font-size: 0.88rem; line-height: 1.4; margin-bottom: 14px; } .feed-buttons { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .feed-button { border: 1px solid var(--line); border-radius: 15px; padding: 11px 10px; color: var(--text); background: var(--panel-soft); font-weight: 850; text-align: left; } .feed-button.active { color: #111827; background: var(--accent); border-color: transparent; } .hero { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr); gap: clamp(20px, 4vw, 54px); align-items: center; } .map-card { position: relative; min-height: 0; height: min(62vh, 620px); border: 1px solid var(--line); border-radius: 34px; background: rgba(17, 24, 39, 0.78); box-shadow: 0 28px 90px var(--shadow); overflow: hidden; } .map-card::before { content: ""; position: absolute; inset: 9%; border: 1px solid rgba(255, 255, 255, 0.07); border-radius: 50%; pointer-events: none; } .map-card::after { content: ""; position: absolute; left: 4%; right: 4%; top: 50%; height: 1px; background: rgba(255, 255, 255, 0.08); pointer-events: none; } .grid-lines { position: absolute; inset: 0; pointer-events: none; opacity: 0.5; } .grid-lines span { position: absolute; background: rgba(255, 255, 255, 0.055); } .grid-lines .v { top: 0; bottom: 0; width: 1px; } .grid-lines .h { left: 0; right: 0; height: 1px; } .continent { position: absolute; border: 1px solid rgba(255, 255, 255, 0.09); background: rgba(255, 255, 255, 0.035); border-radius: 999px; pointer-events: none; } .quake { position: absolute; transform: translate(-50%, -50%); border-radius: 50%; background: var(--accent); box-shadow: 0 0 28px currentColor; border: 1px solid rgba(255, 255, 255, 0.65); } .quake::after { content: ""; position: absolute; inset: -8px; border: 1px solid currentColor; border-radius: 50%; opacity: 0.4; animation: ping 2.6s ease-out infinite; } @keyframes ping { 0% { transform: scale(0.45); opacity: 0.45; } 100% { transform: scale(2.4); opacity: 0; } } .tooltip { position: absolute; left: 18px; bottom: 18px; width: min(420px, calc(100% - 36px)); border: 1px solid var(--line); border-radius: 24px; background: rgba(7, 10, 18, 0.9); padding: 14px; box-shadow: 0 18px 52px var(--shadow); } .tooltip-title { font-weight: 950; letter-spacing: -0.04em; line-height: 1.05; } .tooltip-meta { color: var(--muted); font-size: 0.86rem; margin-top: 7px; line-height: 1.35; } .side { display: grid; gap: 13px; min-width: 0; } .big-stat { border: 1px solid var(--line); border-radius: 30px; background: rgba(17, 24, 39, 0.8); box-shadow: 0 18px 52px var(--shadow); padding: 20px; } .stat-label { color: var(--muted); font-size: 0.78rem; font-weight: 850; letter-spacing: 0.13em; text-transform: uppercase; margin-bottom: 12px; } .stat-value { font-size: clamp(3.3rem, 8vw, 7rem); font-weight: 950; line-height: 0.78; letter-spacing: -0.09em; } .stat-note { margin-top: 14px; color: var(--muted); line-height: 1.4; } .mini-grid { display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 13px; } .mini { min-height: 122px; border: 1px solid var(--line); border-radius: 25px; background: rgba(17, 24, 39, 0.8); box-shadow: 0 18px 52px var(--shadow); padding: 16px; } .mini-value { font-size: clamp(1.7rem, 3vw, 2.6rem); font-weight: 950; line-height: 1; letter-spacing: -0.06em; } .mini-note { margin-top: 8px; color: var(--muted); font-size: 0.88rem; } .bottom { display: grid; grid-template-columns: 1fr; min-height: 158px; } .list-card { border: 1px solid var(--line); border-radius: 29px; background: rgba(17, 24, 39, 0.82); box-shadow: 0 18px 52px var(--shadow); padding: 16px; min-width: 0; } .list-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 13px; } .list-title { font-weight: 950; letter-spacing: -0.04em; } .list-note { color: var(--muted); font-size: 0.86rem; text-align: right; } .quake-list { display: grid; grid-template-columns: repeat(6, minmax(170px, 1fr)); gap: 10px; overflow: auto; padding-bottom: 3px; } .quake-row { min-width: 170px; border: 1px solid var(--line); border-radius: 20px; background: var(--panel-soft); padding: 12px; } .row-top { display: flex; justify-content: space-between; gap: 8px; align-items: center; margin-bottom: 8px; } .mag-pill { border-radius: 999px; padding: 4px 8px; color: #111827; background: var(--accent); font-weight: 950; font-size: 0.82rem; } .depth { color: var(--muted); font-size: 0.78rem; font-weight: 800; } .place { font-weight: 900; line-height: 1.15; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .time { margin-top: 8px; color: var(--muted); font-size: 0.8rem; } @media (max-width: 980px) { .app { overflow: auto; } .stage { min-height: 100svh; height: auto; } .hero { grid-template-columns: 1fr; align-items: start; } .map-card { height: min(62vh, 520px); } .quake-list { grid-template-columns: repeat(6, 180px); } } @media (max-width: 680px) { .stage { gap: 18px; } .title { font-size: 2rem; } .map-card { height: 420px; } .mini-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .tooltip { position: static; width: auto; margin: 14px; } .app { overflow: hidden !important; } } </style> </head> <body> <main class="app" style="width: 100%"> <div id="pulseField" class="pulse-field"></div> <section class="stage" aria-live="polite"> <header class="topbar"> <div> <div class="eyebrow">USGS seismic feed</div> <div id="feedTitle" class="title">Earthquake radar</div> <div id="updated" class="updated">Loading recent earthquakes...</div> </div> <div class="settings"> <button id="settingsToggle" class="gear" type="button" aria-label="Open feed settings" aria-expanded="false" aria-controls="settingsPanel"> <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"> <path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Z" stroke="currentColor" stroke-width="1.8" /> <path d="M19.4 15a1.8 1.8 0 0 0 .36 1.98l.05.05a2.1 2.1 0 0 1-2.97 2.97l-.05-.05a1.8 1.8 0 0 0-1.98-.36 1.8 1.8 0 0 0-1.1 1.65V21.4a2.1 2.1 0 0 1-4.2 0v-.08a1.8 1.8 0 0 0-1.1-1.65 1.8 1.8 0 0 0-1.98.36l-.05.05a2.1 2.1 0 0 1-2.97-2.97l.05-.05A1.8 1.8 0 0 0 4.6 15a1.8 1.8 0 0 0-1.65-1.1H2.9a2.1 2.1 0 0 1 0-4.2h.08A1.8 1.8 0 0 0 4.6 8a1.8 1.8 0 0 0-.36-1.98l-.05-.05A2.1 2.1 0 0 1 7.16 3l.05.05A1.8 1.8 0 0 0 9.2 3.4 1.8 1.8 0 0 0 10.3 1.75V1.7a2.1 2.1 0 0 1 4.2 0v.08A1.8 1.8 0 0 0 15.6 3.4a1.8 1.8 0 0 0 1.98-.36l.05-.05A2.1 2.1 0 0 1 20.6 6l-.05.05A1.8 1.8 0 0 0 20.2 8c.26.66.9 1.1 1.6 1.1h.1a2.1 2.1 0 0 1 0 4.2h-.08A1.8 1.8 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="1.25" /> </svg> </button> <div id="settingsPanel" class="settings-panel" hidden> <div class="settings-title">Change feed</div> <div class="settings-copy"> No storage, no geolocation, no form submission. Data is fetched directly from the selected USGS GeoJSON feed. </div> <div id="feedButtons" class="feed-buttons"></div> </div> </div> </header> <section class="hero"> <article class="map-card" id="map"> <div class="grid-lines" aria-hidden="true"> <span class="v" style="left: 20%;"></span> <span class="v" style="left: 40%;"></span> <span class="v" style="left: 60%;"></span> <span class="v" style="left: 80%;"></span> <span class="h" style="top: 25%;"></span> <span class="h" style="top: 50%;"></span> <span class="h" style="top: 75%;"></span> </div> <div class="continent" style="left: 9%; top: 22%; width: 20%; height: 26%; transform: rotate(-18deg);"></div> <div class="continent" style="left: 22%; top: 51%; width: 14%; height: 25%; transform: rotate(22deg);"></div> <div class="continent" style="left: 43%; top: 22%; width: 14%; height: 21%; transform: rotate(13deg);"></div> <div class="continent" style="left: 54%; top: 25%; width: 28%; height: 31%; transform: rotate(-8deg);"></div> <div class="continent" style="left: 72%; top: 58%; width: 13%; height: 14%; transform: rotate(10deg);"></div> <div id="quakeLayer"></div> <div class="tooltip"> <div id="tooltipTitle" class="tooltip-title">Hover an event</div> <div id="tooltipMeta" class="tooltip-meta">Recent earthquakes appear as live points on the global projection.</div> </div> </article> <aside class="side"> <article class="big-stat"> <div class="stat-label">Strongest magnitude</div> <div id="maxMag" class="stat-value">--</div> <div id="maxMagNote" class="stat-note">Waiting for USGS feed.</div> </article> <div class="mini-grid"> <article class="mini"> <div class="stat-label">Events</div> <div id="eventCount" class="mini-value">--</div> <div class="mini-note">Feed total</div> </article> <article class="mini"> <div class="stat-label">Average depth</div> <div id="avgDepth" class="mini-value">--</div> <div class="mini-note">Kilometers</div> </article> <article class="mini"> <div class="stat-label">M 4.5+</div> <div id="bigCount" class="mini-value">--</div> <div class="mini-note">Stronger events</div> </article> <article class="mini"> <div class="stat-label">Tsunami flags</div> <div id="tsunamiCount" class="mini-value">--</div> <div class="mini-note">Reported by feed</div> </article> </div> </aside> </section> <section class="bottom"> <article class="list-card"> <div class="list-head"> <div class="list-title">Latest strongest events</div> <div id="listNote" class="list-note">Sorted by magnitude</div> </div> <div id="quakeList" class="quake-list"></div> </article> </section> </section> </main> <script> // Sandbox-safe: // - no localStorage // - no sessionStorage // - no IndexedDB // - no cookies // - no geolocation // - no forms // - no external libraries, fonts, CSS, or images const feeds = [ { id: "2.5_day", label: "M2.5+ day", title: "M2.5+ earthquakes, past day", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson" }, { id: "all_day", label: "All day", title: "All earthquakes, past day", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson" }, { id: "4.5_week", label: "M4.5+ week", title: "M4.5+ earthquakes, past week", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson" }, { id: "significant_week", label: "Significant", title: "Significant earthquakes, past week", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/significant_week.geojson" }, { id: "2.5_week", label: "M2.5+ week", title: "M2.5+ earthquakes, past week", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_week.geojson" }, { id: "4.5_month", label: "M4.5+ month", title: "M4.5+ earthquakes, past month", url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_month.geojson" } ]; let activeFeed = feeds[0]; const els = { pulseField: document.getElementById("pulseField"), feedTitle: document.getElementById("feedTitle"), updated: document.getElementById("updated"), settingsToggle: document.getElementById("settingsToggle"), settingsPanel: document.getElementById("settingsPanel"), feedButtons: document.getElementById("feedButtons"), quakeLayer: document.getElementById("quakeLayer"), tooltipTitle: document.getElementById("tooltipTitle"), tooltipMeta: document.getElementById("tooltipMeta"), maxMag: document.getElementById("maxMag"), maxMagNote: document.getElementById("maxMagNote"), eventCount: document.getElementById("eventCount"), avgDepth: document.getElementById("avgDepth"), bigCount: document.getElementById("bigCount"), tsunamiCount: document.getElementById("tsunamiCount"), quakeList: document.getElementById("quakeList"), listNote: document.getElementById("listNote") }; function setStatus(text, isError = false) { els.updated.textContent = text; els.updated.classList.toggle("error", isError); } function formatTime(ms) { const date = new Date(ms); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); } function formatGenerated(ms) { const date = new Date(ms); if (Number.isNaN(date.getTime())) return "Updated from USGS"; return `USGS generated ${date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}`; } function magnitudeColor(mag) { if (!Number.isFinite(mag)) return "var(--low)"; if (mag >= 6) return "var(--severe)"; if (mag >= 4.5) return "var(--high)"; if (mag >= 3) return "var(--mid)"; return "var(--low)"; } function magnitudeSize(mag) { const safe = Number.isFinite(mag) ? mag : 1; return Math.max(7, Math.min(34, 5 + safe * safe * 0.9)); } function project(lon, lat) { const x = ((lon + 180) / 360) * 100; const y = ((90 - lat) / 180) * 100; return { x, y }; } function cleanPlace(place) { return place || "Unknown location"; } function getDepth(feature) { const depth = feature?.geometry?.coordinates?.[2]; return Number.isFinite(depth) ? depth : null; } function renderFeedButtons() { els.feedButtons.textContent = ""; feeds.forEach(feed => { const button = document.createElement("button"); button.type = "button"; button.className = `feed-button${feed.id === activeFeed.id ? " active" : ""}`; button.textContent = feed.label; button.addEventListener("click", () => { activeFeed = feed; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); renderFeedButtons(); loadFeed(feed); }); els.feedButtons.appendChild(button); }); } function renderWaves(features) { els.pulseField.textContent = ""; features.slice(0, 16).forEach((feature, index) => { const coords = feature.geometry?.coordinates; if (!coords || coords.length < 2) return; const [lon, lat] = coords; const projected = project(lon, lat); const mag = feature.properties?.mag; const wave = document.createElement("span"); wave.className = "wave"; wave.style.left = `${projected.x}%`; wave.style.top = `${projected.y}%`; wave.style.borderColor = magnitudeColor(mag); wave.style.animationDelay = `${index * 0.22}s`; els.pulseField.appendChild(wave); }); } function renderMap(features) { els.quakeLayer.textContent = ""; features.slice(0, 220).forEach(feature => { const coords = feature.geometry?.coordinates; if (!coords || coords.length < 2) return; const [lon, lat] = coords; const depth = getDepth(feature); const mag = feature.properties?.mag; const place = cleanPlace(feature.properties?.place); const time = feature.properties?.time; const { x, y } = project(lon, lat); const point = document.createElement("button"); point.type = "button"; point.className = "quake"; point.style.left = `${x}%`; point.style.top = `${y}%`; point.style.width = `${magnitudeSize(mag)}px`; point.style.height = `${magnitudeSize(mag)}px`; point.style.color = magnitudeColor(mag); point.style.background = magnitudeColor(mag); point.setAttribute("aria-label", `${place}, magnitude ${mag || "unknown"}`); point.addEventListener("mouseenter", () => { els.tooltipTitle.textContent = `${Number.isFinite(mag) ? "M" + mag.toFixed(1) : "M --"} · ${place}`; els.tooltipMeta.textContent = `${formatTime(time)} · ${ depth === null ? "Depth unknown" : Math.round(depth) + " km depth" } · ${lat.toFixed(2)}°, ${lon.toFixed(2)}°`; }); point.addEventListener("focus", () => { point.dispatchEvent(new Event("mouseenter")); }); els.quakeLayer.appendChild(point); }); } function renderStats(features) { const magnitudes = features .map(feature => feature.properties?.mag) .filter(Number.isFinite); const depths = features .map(getDepth) .filter(Number.isFinite); const strongest = features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag)[0]; const maxMag = strongest?.properties?.mag; const avgDepth = depths.length ? depths.reduce((sum, value) => sum + value, 0) / depths.length : null; const bigCount = magnitudes.filter(mag => mag >= 4.5).length; const tsunamiCount = features.filter(feature => feature.properties?.tsunami === 1).length; els.maxMag.textContent = Number.isFinite(maxMag) ? maxMag.toFixed(1) : "--"; els.maxMag.style.color = magnitudeColor(maxMag); els.maxMagNote.textContent = strongest ? cleanPlace(strongest.properties.place) : "No magnitude data in this feed."; els.eventCount.textContent = String(features.length); els.avgDepth.textContent = Number.isFinite(avgDepth) ? `${Math.round(avgDepth)}` : "--"; els.bigCount.textContent = String(bigCount); els.tsunamiCount.textContent = String(tsunamiCount); } function renderList(features) { els.quakeList.textContent = ""; const sorted = features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag) .slice(0, 12); sorted.forEach(feature => { const mag = feature.properties.mag; const depth = getDepth(feature); const place = cleanPlace(feature.properties.place); const time = feature.properties.time; const row = document.createElement("article"); row.className = "quake-row"; row.innerHTML = ` <div class="row-top"> <div class="mag-pill" style="background: ${magnitudeColor(mag)};">M${mag.toFixed(1)}</div> <div class="depth">${depth === null ? "--" : Math.round(depth)} km</div> </div> <div class="place">${place}</div> <div class="time">${formatTime(time)}</div> `; els.quakeList.appendChild(row); }); els.listNote.textContent = sorted.length ? "Sorted by magnitude" : "No events"; } function renderData(feed, data) { const features = Array.isArray(data.features) ? data.features : []; els.feedTitle.textContent = feed.title; renderStats(features); renderMap(features); renderList(features); renderWaves( features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag) ); if (features.length) { const first = features .filter(feature => Number.isFinite(feature.properties?.mag)) .sort((a, b) => b.properties.mag - a.properties.mag)[0]; if (first) { const coords = first.geometry?.coordinates || []; const depth = getDepth(first); els.tooltipTitle.textContent = `Strongest: M${first.properties.mag.toFixed(1)} · ${cleanPlace(first.properties.place)}`; els.tooltipMeta.textContent = `${formatTime(first.properties.time)} · ${ depth === null ? "Depth unknown" : Math.round(depth) + " km depth" } · ${Number(coords[1]).toFixed(2)}°, ${Number(coords[0]).toFixed(2)}°`; } } else { els.tooltipTitle.textContent = "No events in this feed"; els.tooltipMeta.textContent = "Try another feed from the settings panel."; } setStatus(formatGenerated(data.metadata?.generated)); } async function loadFeed(feed) { setStatus(`Loading ${feed.title}...`); try { const response = await fetch(feed.url, { cache: "no-store" }); if (!response.ok) { throw new Error("USGS feed request failed."); } const data = await response.json(); if (!data || !Array.isArray(data.features)) { throw new Error("Unexpected USGS feed format."); } renderData(feed, data); } catch (error) { setStatus(error.message || "Could not load earthquake feed.", true); } } els.settingsToggle.addEventListener("click", () => { const isOpen = !els.settingsPanel.hidden; els.settingsPanel.hidden = isOpen; els.settingsToggle.setAttribute("aria-expanded", String(!isOpen)); }); document.addEventListener("click", event => { if (event.target.closest(".settings")) return; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); }); renderFeedButtons(); loadFeed(activeFeed); </script> </body> </html>
Move to Deleted?
will be moved to Deleted.
Cancel
Delete
Rename
Name
Cancel
Rename