Root
>
ISS.html
Download
Delete
ISS.html
32,7 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 ISS Tracker</title> <style> :root { --bg: #060914; --panel: #111827; --panel-soft: #172033; --panel-strong: #202a3d; --text: #f8fafc; --muted: #aab4c5; --line: rgba(255, 255, 255, 0.12); --accent: #8ab4ff; --accent-2: #fbbf24; --danger: #fca5a5; --shadow: rgba(0, 0, 0, 0.38); 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; } .stars { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; } .star { position: absolute; width: 2px; height: 2px; border-radius: 999px; background: rgba(255, 255, 255, 0.68); animation: twinkle 4s ease-in-out infinite; } @keyframes twinkle { 0%, 100% { opacity: 0.18; transform: scale(0.7); } 50% { opacity: 0.92; transform: scale(1.3); } } .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(--danger); } .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; } .settings-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .settings-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; } .settings-button.active { color: #08111f; background: var(--accent); border-color: transparent; } .hero { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr); gap: clamp(20px, 4vw, 56px); align-items: center; } .map-card { position: relative; min-height: 0; height: min(62vh, 640px); 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-grid { position: absolute; inset: 0; pointer-events: none; opacity: 0.55; } .map-grid span { position: absolute; background: rgba(255, 255, 255, 0.055); } .map-grid .v { top: 0; bottom: 0; width: 1px; } .map-grid .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; } .equator { position: absolute; left: 4%; right: 4%; top: 50%; height: 1px; background: rgba(255, 255, 255, 0.13); } .orbit-line { position: absolute; left: -10%; right: -10%; top: 50%; height: 1px; border-top: 2px dashed rgba(138, 180, 255, 0.28); transform: rotate(-23deg); transform-origin: center; pointer-events: none; } .trail-dot { position: absolute; width: 6px; height: 6px; border-radius: 999px; background: rgba(138, 180, 255, 0.38); transform: translate(-50%, -50%); pointer-events: none; } .iss { position: absolute; width: 58px; height: 58px; transform: translate(-50%, -50%); border: 0; border-radius: 22px; background: var(--accent); color: #08111f; display: grid; place-items: center; box-shadow: 0 0 42px rgba(138, 180, 255, 0.48); z-index: 5; } .iss::after { content: ""; position: absolute; inset: -14px; border: 1px solid var(--accent); border-radius: 28px; opacity: 0.42; animation: ping 2.5s ease-out infinite; } .iss svg { width: 34px; height: 34px; position: relative; z-index: 1; } @keyframes ping { 0% { transform: scale(0.65); opacity: 0.45; } 100% { transform: scale(1.7); opacity: 0; } } .map-label { position: absolute; left: 18px; bottom: 18px; width: min(430px, 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); } .map-label-title { font-weight: 950; letter-spacing: -0.04em; line-height: 1.05; } .map-label-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.1rem, 7vw, 6.8rem); font-weight: 950; line-height: 0.82; 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.55rem, 3vw, 2.4rem); font-weight: 950; line-height: 1; letter-spacing: -0.06em; word-break: break-word; } .mini-note { margin-top: 8px; color: var(--muted); font-size: 0.88rem; } .bottom { display: grid; grid-template-columns: 1fr; min-height: 150px; } .telemetry-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; } .telemetry-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 13px; } .telemetry-title { font-weight: 950; letter-spacing: -0.04em; } .telemetry-note { color: var(--muted); font-size: 0.86rem; text-align: right; } .telemetry-grid { display: grid; grid-template-columns: repeat(6, minmax(160px, 1fr)); gap: 10px; overflow: auto; padding-bottom: 3px; } .telemetry-item { min-width: 160px; border: 1px solid var(--line); border-radius: 20px; background: var(--panel-soft); padding: 12px; } .telemetry-key { color: var(--muted); font-size: 0.76rem; font-weight: 850; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 8px; } .telemetry-value { font-weight: 950; font-size: 1.35rem; letter-spacing: -0.05em; } @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); } .telemetry-grid { grid-template-columns: repeat(6, 170px); } } @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)); } .map-label { position: static; width: auto; margin: 14px; } .app { overflow: hidden !important; } } </style> </head> <body> <main class="app" style="width: 100%"> <div id="stars" class="stars"></div> <section class="stage" aria-live="polite"> <header class="topbar"> <div> <div class="eyebrow">Live satellite telemetry</div> <div class="title">International Space Station</div> <div id="updated" class="updated">Loading ISS position...</div> </div> <div class="settings"> <button id="settingsToggle" class="gear" type="button" aria-label="Open tracker 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">Tracker settings</div> <div class="settings-copy"> No storage, no geolocation, no forms. Trail exists only in memory while this page is open. </div> <div class="settings-grid"> <button id="pauseButton" class="settings-button" type="button">Pause updates</button> <button id="refreshButton" class="settings-button" type="button">Refresh now</button> <button id="kmButton" class="settings-button active" type="button">Kilometers</button> <button id="miButton" class="settings-button" type="button">Miles</button> </div> </div> </div> </header> <section class="hero"> <article class="map-card" id="map"> <div class="map-grid" 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 class="equator"></div> <div class="orbit-line"></div> <div id="trailLayer"></div> <button id="issMarker" class="iss" type="button" aria-label="ISS position"> <svg viewBox="0 0 64 64" fill="none" aria-hidden="true"> <path d="M26 24h12v16H26z" stroke="currentColor" stroke-width="4" stroke-linejoin="round" /> <path d="M6 20h16v24H6zM42 20h16v24H42z" stroke="currentColor" stroke-width="4" stroke-linejoin="round" /> <path d="M22 32h4M38 32h4M32 14v10M32 40v10" stroke="currentColor" stroke-width="4" stroke-linecap="round" /> </svg> </button> <div class="map-label"> <div id="mapTitle" class="map-label-title">Waiting for telemetry</div> <div id="mapMeta" class="map-label-meta">The ISS marker will update every 8 seconds.</div> </div> </article> <aside class="side"> <article class="big-stat"> <div class="stat-label">Velocity</div> <div id="velocity" class="stat-value">--</div> <div id="velocityNote" class="stat-note">Orbital speed from live telemetry.</div> </article> <div class="mini-grid"> <article class="mini"> <div class="stat-label">Altitude</div> <div id="altitude" class="mini-value">--</div> <div id="altitudeNote" class="mini-note">Above Earth</div> </article> <article class="mini"> <div class="stat-label">Visibility</div> <div id="visibility" class="mini-value">--</div> <div class="mini-note">Daylight / eclipse</div> </article> <article class="mini"> <div class="stat-label">Latitude</div> <div id="latitude" class="mini-value">--</div> <div class="mini-note">North / south</div> </article> <article class="mini"> <div class="stat-label">Longitude</div> <div id="longitude" class="mini-value">--</div> <div class="mini-note">East / west</div> </article> </div> </aside> </section> <section class="bottom"> <article class="telemetry-card"> <div class="telemetry-head"> <div class="telemetry-title">Live telemetry</div> <div id="telemetryNote" class="telemetry-note">Updates every 8 seconds</div> </div> <div class="telemetry-grid"> <div class="telemetry-item"> <div class="telemetry-key">Satellite ID</div> <div id="satId" class="telemetry-value">25544</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Footprint</div> <div id="footprint" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Timestamp</div> <div id="timestamp" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Solar lat</div> <div id="solarLat" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Solar lon</div> <div id="solarLon" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Trail points</div> <div id="trailCount" class="telemetry-value">0</div> </div> </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 SATELLITE_ID = 25544; const POLL_MS = 8000; const MAX_TRAIL_POINTS = 48; let useMiles = false; let paused = false; let timer = null; let trail = []; const els = { stars: document.getElementById("stars"), updated: document.getElementById("updated"), settingsToggle: document.getElementById("settingsToggle"), settingsPanel: document.getElementById("settingsPanel"), pauseButton: document.getElementById("pauseButton"), refreshButton: document.getElementById("refreshButton"), kmButton: document.getElementById("kmButton"), miButton: document.getElementById("miButton"), trailLayer: document.getElementById("trailLayer"), issMarker: document.getElementById("issMarker"), mapTitle: document.getElementById("mapTitle"), mapMeta: document.getElementById("mapMeta"), velocity: document.getElementById("velocity"), velocityNote: document.getElementById("velocityNote"), altitude: document.getElementById("altitude"), altitudeNote: document.getElementById("altitudeNote"), visibility: document.getElementById("visibility"), latitude: document.getElementById("latitude"), longitude: document.getElementById("longitude"), satId: document.getElementById("satId"), footprint: document.getElementById("footprint"), timestamp: document.getElementById("timestamp"), solarLat: document.getElementById("solarLat"), solarLon: document.getElementById("solarLon"), trailCount: document.getElementById("trailCount"), telemetryNote: document.getElementById("telemetryNote") }; function createStars() { els.stars.textContent = ""; for (let i = 0; i < 120; i += 1) { const star = document.createElement("span"); star.className = "star"; star.style.left = `${Math.random() * 100}%`; star.style.top = `${Math.random() * 100}%`; star.style.animationDuration = `${2.5 + Math.random() * 5}s`; star.style.animationDelay = `${Math.random() * -5}s`; els.stars.appendChild(star); } } function setStatus(text, isError = false) { els.updated.textContent = text; els.updated.classList.toggle("error", isError); } function formatTimeFromUnix(seconds) { const date = new Date(seconds * 1000); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function formatDateTimeFromUnix(seconds) { const date = new Date(seconds * 1000); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function formatCoord(value, axis) { if (!Number.isFinite(value)) return "--"; const direction = axis === "lat" ? value >= 0 ? "N" : "S" : value >= 0 ? "E" : "W"; return `${Math.abs(value).toFixed(2)}° ${direction}`; } function formatDistance(value) { if (!Number.isFinite(value)) return "--"; return `${Math.round(value).toLocaleString()} ${useMiles ? "mi" : "km"}`; } function formatSpeed(value) { if (!Number.isFinite(value)) return "--"; return `${Math.round(value).toLocaleString()}`; } function speedUnit() { return useMiles ? "mph" : "km/h"; } function project(lon, lat) { return { x: ((lon + 180) / 360) * 100, y: ((90 - lat) / 180) * 100 }; } function renderTrail() { els.trailLayer.textContent = ""; trail.forEach((point, index) => { const projected = project(point.longitude, point.latitude); const dot = document.createElement("span"); const opacity = (index + 1) / trail.length; dot.className = "trail-dot"; dot.style.left = `${projected.x}%`; dot.style.top = `${projected.y}%`; dot.style.opacity = String(Math.max(0.16, opacity * 0.62)); dot.style.width = `${4 + opacity * 5}px`; dot.style.height = `${4 + opacity * 5}px`; els.trailLayer.appendChild(dot); }); els.trailCount.textContent = String(trail.length); } function setMarkerPosition(latitude, longitude) { const projected = project(longitude, latitude); els.issMarker.style.left = `${projected.x}%`; els.issMarker.style.top = `${projected.y}%`; } function normalizeVisibility(value) { if (!value) return "--"; if (value === "daylight") return "Daylight"; if (value === "eclipsed") return "Eclipse"; return String(value); } function renderTelemetry(data) { const latitude = Number(data.latitude); const longitude = Number(data.longitude); const altitude = Number(data.altitude); const velocity = Number(data.velocity); const footprint = Number(data.footprint); const solarLat = Number(data.solar_lat); const solarLon = Number(data.solar_lon); if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { throw new Error("ISS position data was incomplete."); } trail.push({ latitude, longitude, timestamp: data.timestamp }); trail = trail.slice(-MAX_TRAIL_POINTS); setMarkerPosition(latitude, longitude); renderTrail(); els.velocity.textContent = formatSpeed(velocity); els.velocityNote.textContent = speedUnit(); els.altitude.textContent = formatDistance(altitude); els.altitudeNote.textContent = "Above Earth"; els.visibility.textContent = normalizeVisibility(data.visibility); els.latitude.textContent = formatCoord(latitude, "lat"); els.longitude.textContent = formatCoord(longitude, "lon"); els.satId.textContent = String(data.id || SATELLITE_ID); els.footprint.textContent = formatDistance(footprint); els.timestamp.textContent = formatDateTimeFromUnix(data.timestamp); els.solarLat.textContent = formatCoord(solarLat, "lat"); els.solarLon.textContent = formatCoord(solarLon, "lon"); els.mapTitle.textContent = `ISS over ${formatCoord(latitude, "lat")}, ${formatCoord(longitude, "lon")}`; els.mapMeta.textContent = `${formatDistance(altitude)} altitude · ${formatSpeed(velocity)} ${speedUnit()} · ${normalizeVisibility(data.visibility)}`; setStatus(`Updated ${formatTimeFromUnix(data.timestamp)}`); } async function fetchIss() { if (paused) return; try { setStatus("Fetching ISS telemetry..."); const units = useMiles ? "miles" : "kilometers"; const url = `https://api.wheretheiss.at/v1/satellites/${SATELLITE_ID}?units=${units}`; const response = await fetch(url, { cache: "no-store" }); if (!response.ok) { throw new Error("ISS telemetry request failed."); } const data = await response.json(); if (!data || typeof data !== "object") { throw new Error("Unexpected ISS telemetry format."); } renderTelemetry(data); } catch (error) { setStatus(error.message || "Could not load ISS telemetry.", true); } } function startPolling() { clearInterval(timer); timer = setInterval(fetchIss, POLL_MS); fetchIss(); } function setUnit(nextUseMiles) { useMiles = nextUseMiles; els.kmButton.classList.toggle("active", !useMiles); els.miButton.classList.toggle("active", useMiles); trail = []; renderTrail(); fetchIss(); } els.settingsToggle.addEventListener("click", () => { const isOpen = !els.settingsPanel.hidden; els.settingsPanel.hidden = isOpen; els.settingsToggle.setAttribute("aria-expanded", String(!isOpen)); }); els.pauseButton.addEventListener("click", () => { paused = !paused; els.pauseButton.textContent = paused ? "Resume updates" : "Pause updates"; els.pauseButton.classList.toggle("active", paused); els.telemetryNote.textContent = paused ? "Updates paused" : "Updates every 8 seconds"; if (!paused) { fetchIss(); } else { setStatus("Updates paused."); } }); els.refreshButton.addEventListener("click", fetchIss); els.kmButton.addEventListener("click", () => setUnit(false)); els.miButton.addEventListener("click", () => setUnit(true)); document.addEventListener("click", event => { if (event.target.closest(".settings")) return; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); }); createStars(); startPolling(); </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 ISS Tracker</title> <style> :root { --bg: #060914; --panel: #111827; --panel-soft: #172033; --panel-strong: #202a3d; --text: #f8fafc; --muted: #aab4c5; --line: rgba(255, 255, 255, 0.12); --accent: #8ab4ff; --accent-2: #fbbf24; --danger: #fca5a5; --shadow: rgba(0, 0, 0, 0.38); 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; } .stars { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; } .star { position: absolute; width: 2px; height: 2px; border-radius: 999px; background: rgba(255, 255, 255, 0.68); animation: twinkle 4s ease-in-out infinite; } @keyframes twinkle { 0%, 100% { opacity: 0.18; transform: scale(0.7); } 50% { opacity: 0.92; transform: scale(1.3); } } .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(--danger); } .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; } .settings-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .settings-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; } .settings-button.active { color: #08111f; background: var(--accent); border-color: transparent; } .hero { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr); gap: clamp(20px, 4vw, 56px); align-items: center; } .map-card { position: relative; min-height: 0; height: min(62vh, 640px); 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-grid { position: absolute; inset: 0; pointer-events: none; opacity: 0.55; } .map-grid span { position: absolute; background: rgba(255, 255, 255, 0.055); } .map-grid .v { top: 0; bottom: 0; width: 1px; } .map-grid .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; } .equator { position: absolute; left: 4%; right: 4%; top: 50%; height: 1px; background: rgba(255, 255, 255, 0.13); } .orbit-line { position: absolute; left: -10%; right: -10%; top: 50%; height: 1px; border-top: 2px dashed rgba(138, 180, 255, 0.28); transform: rotate(-23deg); transform-origin: center; pointer-events: none; } .trail-dot { position: absolute; width: 6px; height: 6px; border-radius: 999px; background: rgba(138, 180, 255, 0.38); transform: translate(-50%, -50%); pointer-events: none; } .iss { position: absolute; width: 58px; height: 58px; transform: translate(-50%, -50%); border: 0; border-radius: 22px; background: var(--accent); color: #08111f; display: grid; place-items: center; box-shadow: 0 0 42px rgba(138, 180, 255, 0.48); z-index: 5; } .iss::after { content: ""; position: absolute; inset: -14px; border: 1px solid var(--accent); border-radius: 28px; opacity: 0.42; animation: ping 2.5s ease-out infinite; } .iss svg { width: 34px; height: 34px; position: relative; z-index: 1; } @keyframes ping { 0% { transform: scale(0.65); opacity: 0.45; } 100% { transform: scale(1.7); opacity: 0; } } .map-label { position: absolute; left: 18px; bottom: 18px; width: min(430px, 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); } .map-label-title { font-weight: 950; letter-spacing: -0.04em; line-height: 1.05; } .map-label-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.1rem, 7vw, 6.8rem); font-weight: 950; line-height: 0.82; 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.55rem, 3vw, 2.4rem); font-weight: 950; line-height: 1; letter-spacing: -0.06em; word-break: break-word; } .mini-note { margin-top: 8px; color: var(--muted); font-size: 0.88rem; } .bottom { display: grid; grid-template-columns: 1fr; min-height: 150px; } .telemetry-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; } .telemetry-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 13px; } .telemetry-title { font-weight: 950; letter-spacing: -0.04em; } .telemetry-note { color: var(--muted); font-size: 0.86rem; text-align: right; } .telemetry-grid { display: grid; grid-template-columns: repeat(6, minmax(160px, 1fr)); gap: 10px; overflow: auto; padding-bottom: 3px; } .telemetry-item { min-width: 160px; border: 1px solid var(--line); border-radius: 20px; background: var(--panel-soft); padding: 12px; } .telemetry-key { color: var(--muted); font-size: 0.76rem; font-weight: 850; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 8px; } .telemetry-value { font-weight: 950; font-size: 1.35rem; letter-spacing: -0.05em; } @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); } .telemetry-grid { grid-template-columns: repeat(6, 170px); } } @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)); } .map-label { position: static; width: auto; margin: 14px; } .app { overflow: hidden !important; } } </style> </head> <body> <main class="app" style="width: 100%"> <div id="stars" class="stars"></div> <section class="stage" aria-live="polite"> <header class="topbar"> <div> <div class="eyebrow">Live satellite telemetry</div> <div class="title">International Space Station</div> <div id="updated" class="updated">Loading ISS position...</div> </div> <div class="settings"> <button id="settingsToggle" class="gear" type="button" aria-label="Open tracker 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">Tracker settings</div> <div class="settings-copy"> No storage, no geolocation, no forms. Trail exists only in memory while this page is open. </div> <div class="settings-grid"> <button id="pauseButton" class="settings-button" type="button">Pause updates</button> <button id="refreshButton" class="settings-button" type="button">Refresh now</button> <button id="kmButton" class="settings-button active" type="button">Kilometers</button> <button id="miButton" class="settings-button" type="button">Miles</button> </div> </div> </div> </header> <section class="hero"> <article class="map-card" id="map"> <div class="map-grid" 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 class="equator"></div> <div class="orbit-line"></div> <div id="trailLayer"></div> <button id="issMarker" class="iss" type="button" aria-label="ISS position"> <svg viewBox="0 0 64 64" fill="none" aria-hidden="true"> <path d="M26 24h12v16H26z" stroke="currentColor" stroke-width="4" stroke-linejoin="round" /> <path d="M6 20h16v24H6zM42 20h16v24H42z" stroke="currentColor" stroke-width="4" stroke-linejoin="round" /> <path d="M22 32h4M38 32h4M32 14v10M32 40v10" stroke="currentColor" stroke-width="4" stroke-linecap="round" /> </svg> </button> <div class="map-label"> <div id="mapTitle" class="map-label-title">Waiting for telemetry</div> <div id="mapMeta" class="map-label-meta">The ISS marker will update every 8 seconds.</div> </div> </article> <aside class="side"> <article class="big-stat"> <div class="stat-label">Velocity</div> <div id="velocity" class="stat-value">--</div> <div id="velocityNote" class="stat-note">Orbital speed from live telemetry.</div> </article> <div class="mini-grid"> <article class="mini"> <div class="stat-label">Altitude</div> <div id="altitude" class="mini-value">--</div> <div id="altitudeNote" class="mini-note">Above Earth</div> </article> <article class="mini"> <div class="stat-label">Visibility</div> <div id="visibility" class="mini-value">--</div> <div class="mini-note">Daylight / eclipse</div> </article> <article class="mini"> <div class="stat-label">Latitude</div> <div id="latitude" class="mini-value">--</div> <div class="mini-note">North / south</div> </article> <article class="mini"> <div class="stat-label">Longitude</div> <div id="longitude" class="mini-value">--</div> <div class="mini-note">East / west</div> </article> </div> </aside> </section> <section class="bottom"> <article class="telemetry-card"> <div class="telemetry-head"> <div class="telemetry-title">Live telemetry</div> <div id="telemetryNote" class="telemetry-note">Updates every 8 seconds</div> </div> <div class="telemetry-grid"> <div class="telemetry-item"> <div class="telemetry-key">Satellite ID</div> <div id="satId" class="telemetry-value">25544</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Footprint</div> <div id="footprint" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Timestamp</div> <div id="timestamp" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Solar lat</div> <div id="solarLat" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Solar lon</div> <div id="solarLon" class="telemetry-value">--</div> </div> <div class="telemetry-item"> <div class="telemetry-key">Trail points</div> <div id="trailCount" class="telemetry-value">0</div> </div> </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 SATELLITE_ID = 25544; const POLL_MS = 8000; const MAX_TRAIL_POINTS = 48; let useMiles = false; let paused = false; let timer = null; let trail = []; const els = { stars: document.getElementById("stars"), updated: document.getElementById("updated"), settingsToggle: document.getElementById("settingsToggle"), settingsPanel: document.getElementById("settingsPanel"), pauseButton: document.getElementById("pauseButton"), refreshButton: document.getElementById("refreshButton"), kmButton: document.getElementById("kmButton"), miButton: document.getElementById("miButton"), trailLayer: document.getElementById("trailLayer"), issMarker: document.getElementById("issMarker"), mapTitle: document.getElementById("mapTitle"), mapMeta: document.getElementById("mapMeta"), velocity: document.getElementById("velocity"), velocityNote: document.getElementById("velocityNote"), altitude: document.getElementById("altitude"), altitudeNote: document.getElementById("altitudeNote"), visibility: document.getElementById("visibility"), latitude: document.getElementById("latitude"), longitude: document.getElementById("longitude"), satId: document.getElementById("satId"), footprint: document.getElementById("footprint"), timestamp: document.getElementById("timestamp"), solarLat: document.getElementById("solarLat"), solarLon: document.getElementById("solarLon"), trailCount: document.getElementById("trailCount"), telemetryNote: document.getElementById("telemetryNote") }; function createStars() { els.stars.textContent = ""; for (let i = 0; i < 120; i += 1) { const star = document.createElement("span"); star.className = "star"; star.style.left = `${Math.random() * 100}%`; star.style.top = `${Math.random() * 100}%`; star.style.animationDuration = `${2.5 + Math.random() * 5}s`; star.style.animationDelay = `${Math.random() * -5}s`; els.stars.appendChild(star); } } function setStatus(text, isError = false) { els.updated.textContent = text; els.updated.classList.toggle("error", isError); } function formatTimeFromUnix(seconds) { const date = new Date(seconds * 1000); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function formatDateTimeFromUnix(seconds) { const date = new Date(seconds * 1000); if (Number.isNaN(date.getTime())) return "--"; return date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function formatCoord(value, axis) { if (!Number.isFinite(value)) return "--"; const direction = axis === "lat" ? value >= 0 ? "N" : "S" : value >= 0 ? "E" : "W"; return `${Math.abs(value).toFixed(2)}° ${direction}`; } function formatDistance(value) { if (!Number.isFinite(value)) return "--"; return `${Math.round(value).toLocaleString()} ${useMiles ? "mi" : "km"}`; } function formatSpeed(value) { if (!Number.isFinite(value)) return "--"; return `${Math.round(value).toLocaleString()}`; } function speedUnit() { return useMiles ? "mph" : "km/h"; } function project(lon, lat) { return { x: ((lon + 180) / 360) * 100, y: ((90 - lat) / 180) * 100 }; } function renderTrail() { els.trailLayer.textContent = ""; trail.forEach((point, index) => { const projected = project(point.longitude, point.latitude); const dot = document.createElement("span"); const opacity = (index + 1) / trail.length; dot.className = "trail-dot"; dot.style.left = `${projected.x}%`; dot.style.top = `${projected.y}%`; dot.style.opacity = String(Math.max(0.16, opacity * 0.62)); dot.style.width = `${4 + opacity * 5}px`; dot.style.height = `${4 + opacity * 5}px`; els.trailLayer.appendChild(dot); }); els.trailCount.textContent = String(trail.length); } function setMarkerPosition(latitude, longitude) { const projected = project(longitude, latitude); els.issMarker.style.left = `${projected.x}%`; els.issMarker.style.top = `${projected.y}%`; } function normalizeVisibility(value) { if (!value) return "--"; if (value === "daylight") return "Daylight"; if (value === "eclipsed") return "Eclipse"; return String(value); } function renderTelemetry(data) { const latitude = Number(data.latitude); const longitude = Number(data.longitude); const altitude = Number(data.altitude); const velocity = Number(data.velocity); const footprint = Number(data.footprint); const solarLat = Number(data.solar_lat); const solarLon = Number(data.solar_lon); if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { throw new Error("ISS position data was incomplete."); } trail.push({ latitude, longitude, timestamp: data.timestamp }); trail = trail.slice(-MAX_TRAIL_POINTS); setMarkerPosition(latitude, longitude); renderTrail(); els.velocity.textContent = formatSpeed(velocity); els.velocityNote.textContent = speedUnit(); els.altitude.textContent = formatDistance(altitude); els.altitudeNote.textContent = "Above Earth"; els.visibility.textContent = normalizeVisibility(data.visibility); els.latitude.textContent = formatCoord(latitude, "lat"); els.longitude.textContent = formatCoord(longitude, "lon"); els.satId.textContent = String(data.id || SATELLITE_ID); els.footprint.textContent = formatDistance(footprint); els.timestamp.textContent = formatDateTimeFromUnix(data.timestamp); els.solarLat.textContent = formatCoord(solarLat, "lat"); els.solarLon.textContent = formatCoord(solarLon, "lon"); els.mapTitle.textContent = `ISS over ${formatCoord(latitude, "lat")}, ${formatCoord(longitude, "lon")}`; els.mapMeta.textContent = `${formatDistance(altitude)} altitude · ${formatSpeed(velocity)} ${speedUnit()} · ${normalizeVisibility(data.visibility)}`; setStatus(`Updated ${formatTimeFromUnix(data.timestamp)}`); } async function fetchIss() { if (paused) return; try { setStatus("Fetching ISS telemetry..."); const units = useMiles ? "miles" : "kilometers"; const url = `https://api.wheretheiss.at/v1/satellites/${SATELLITE_ID}?units=${units}`; const response = await fetch(url, { cache: "no-store" }); if (!response.ok) { throw new Error("ISS telemetry request failed."); } const data = await response.json(); if (!data || typeof data !== "object") { throw new Error("Unexpected ISS telemetry format."); } renderTelemetry(data); } catch (error) { setStatus(error.message || "Could not load ISS telemetry.", true); } } function startPolling() { clearInterval(timer); timer = setInterval(fetchIss, POLL_MS); fetchIss(); } function setUnit(nextUseMiles) { useMiles = nextUseMiles; els.kmButton.classList.toggle("active", !useMiles); els.miButton.classList.toggle("active", useMiles); trail = []; renderTrail(); fetchIss(); } els.settingsToggle.addEventListener("click", () => { const isOpen = !els.settingsPanel.hidden; els.settingsPanel.hidden = isOpen; els.settingsToggle.setAttribute("aria-expanded", String(!isOpen)); }); els.pauseButton.addEventListener("click", () => { paused = !paused; els.pauseButton.textContent = paused ? "Resume updates" : "Pause updates"; els.pauseButton.classList.toggle("active", paused); els.telemetryNote.textContent = paused ? "Updates paused" : "Updates every 8 seconds"; if (!paused) { fetchIss(); } else { setStatus("Updates paused."); } }); els.refreshButton.addEventListener("click", fetchIss); els.kmButton.addEventListener("click", () => setUnit(false)); els.miButton.addEventListener("click", () => setUnit(true)); document.addEventListener("click", event => { if (event.target.closest(".settings")) return; els.settingsPanel.hidden = true; els.settingsToggle.setAttribute("aria-expanded", "false"); }); createStars(); startPolling(); </script> </body> </html>
Move to Deleted?
will be moved to Deleted.
Cancel
Delete
Rename
Name
Cancel
Rename