diff --git a/app.js b/app.js
new file mode 100644
index 0000000..8ae0590
--- /dev/null
+++ b/app.js
@@ -0,0 +1,411 @@
+/*
+ * PlanetPulse architecture (client-only, no build step)
+ * ----------------------------------------------------
+ * data.js section: hard-coded dataset of cities with multiple metrics; METRICS config describes how to read & display each metric.
+ * scene.js section: builds Three.js renderer, camera, controls, and the ThreeGlobe instance; handles resize, spin loop, and user interaction.
+ * ui.js section: wires controls (metric select, spin toggle, reset, time slider/playback), legend rendering, status messaging, and detail panel updates.
+ * resilience: wraps globe creation in try/catch; graceful message if WebGL or assets fail; keeps UI usable with static content.
+ * trade-offs: uses CDN textures & three-globe for speed; dataset is small for clarity; point-based overlay (no choropleth) to stay performant without build tools.
+ */
+
+import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js';
+
+// ---------------------- data.js ----------------------
+const TIMELINE_YEARS = [2010, 2015, 2020, 2024];
+
+const DATA_POINTS = [
+ { name: 'New York, USA', lat: 40.7128, lon: -74.006, co2Series: [15.2, 14.8, 14.1, 13.4], renewableSeries: [24, 26, 32, 38], gdp: 82500, population: 19.6 },
+ { name: 'London, UK', lat: 51.5072, lon: -0.1276, co2Series: [6.3, 6.0, 5.8, 5.5], renewableSeries: [30, 35, 44, 52], gdp: 56500, population: 14.3 },
+ { name: 'Tokyo, Japan', lat: 35.6764, lon: 139.65, co2Series: [9.8, 9.4, 9.2, 9.1], renewableSeries: [12, 15, 18, 24], gdp: 60500, population: 37.5 },
+ { name: 'São Paulo, Brazil', lat: -23.55, lon: -46.633, co2Series: [2.9, 2.8, 2.7, 2.6], renewableSeries: [78, 80, 82, 83], gdp: 21500, population: 22 },
+ { name: 'Lagos, Nigeria', lat: 6.5244, lon: 3.3792, co2Series: [0.7, 0.65, 0.62, 0.6], renewableSeries: [30, 32, 34, 36], gdp: 5200, population: 21 },
+ { name: 'Cairo, Egypt', lat: 30.0444, lon: 31.2357, co2Series: [2.7, 2.6, 2.55, 2.5], renewableSeries: [9, 10, 11, 12], gdp: 13100, population: 20.9 },
+ { name: 'Sydney, Australia', lat: -33.8688, lon: 151.2093, co2Series: [16.2, 15.8, 15.5, 15.2], renewableSeries: [18, 23, 28, 32], gdp: 57500, population: 5.3 },
+ { name: 'Toronto, Canada', lat: 43.6532, lon: -79.3832, co2Series: [13.1, 12.9, 12.6, 12.3], renewableSeries: [54, 58, 61, 65], gdp: 63000, population: 6.3 },
+ { name: 'Delhi, India', lat: 28.7041, lon: 77.1025, co2Series: [1.6, 1.7, 1.8, 1.9], renewableSeries: [17, 19, 21, 23], gdp: 9300, population: 30.3 },
+ { name: 'Paris, France', lat: 48.8566, lon: 2.3522, co2Series: [5.1, 4.9, 4.7, 4.5], renewableSeries: [43, 45, 49, 51], gdp: 56500, population: 11.2 },
+ { name: 'Johannesburg, South Africa', lat: -26.2041, lon: 28.0473, co2Series: [8.4, 8.2, 8.0, 7.8], renewableSeries: [7, 8, 10, 11], gdp: 15200, population: 5.8 },
+ { name: 'Beijing, China', lat: 39.9042, lon: 116.4074, co2Series: [9.7, 9.4, 9.1, 8.9], renewableSeries: [18, 21, 25, 28], gdp: 33000, population: 21.5 },
+ { name: 'Reykjavík, Iceland', lat: 64.1466, lon: -21.9426, co2Series: [7.2, 7.0, 6.9, 6.8], renewableSeries: [97, 98, 99, 99], gdp: 70400, population: 0.13 },
+ { name: 'Dubai, UAE', lat: 25.2048, lon: 55.2708, co2Series: [24.2, 24.0, 23.7, 23.5], renewableSeries: [5, 6, 8, 9], gdp: 43000, population: 3.6 }
+];
+
+const METRICS = {
+ co2: {
+ label: 'CO₂ per capita',
+ unit: 't',
+ accessor: d => d.co2Series,
+ description: 'Tons of CO₂ emitted per person annually.',
+ palette: ['#3b82f6', '#f97316', '#ef4444'],
+ seriesKey: 'co2Series'
+ },
+ renewable: {
+ label: 'Renewable share',
+ unit: '%',
+ accessor: d => d.renewableSeries,
+ description: 'Percent of electricity from renewables.',
+ palette: ['#0ea5e9', '#22c55e', '#84cc16'],
+ seriesKey: 'renewableSeries'
+ },
+ gdp: {
+ label: 'GDP per capita',
+ unit: '$',
+ accessor: d => d.gdp,
+ description: 'GDP per person (USD, nominal).',
+ palette: ['#a855f7', '#f59e0b', '#f97316']
+ }
+};
+
+// ---------------------- utilities ----------------------
+const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
+
+function computeExtent(arr, accessor) {
+ let min = Infinity, max = -Infinity;
+ arr.forEach(item => {
+ const val = accessor(item);
+ if (Number.isFinite(val)) {
+ min = Math.min(min, val);
+ max = Math.max(max, val);
+ }
+ });
+ if (min === Infinity || max === -Infinity) return [0, 1];
+ if (min === max) return [min - 0.5, max + 0.5];
+ return [min, max];
+}
+
+function lerpColor(a, b, t) {
+ const ca = parseInt(a.slice(1), 16);
+ const cb = parseInt(b.slice(1), 16);
+ const ar = (ca >> 16) & 255, ag = (ca >> 8) & 255, ab = ca & 255;
+ const br = (cb >> 16) & 255, bg = (cb >> 8) & 255, bb = cb & 255;
+ const r = Math.round(ar + (br - ar) * t);
+ const g = Math.round(ag + (bg - ag) * t);
+ const b = Math.round(ab + (bb - ab) * t);
+ return `rgb(${r}, ${g}, ${b})`;
+}
+
+function makeColorScale(palette) {
+ return value => {
+ if (!Number.isFinite(value)) return '#ffffff';
+ const [a, b, c] = palette;
+ const mid = 0.5;
+ const t = clamp(value, 0, 1);
+ if (t < mid) return lerpColor(a, b, t / mid);
+ return lerpColor(b, c, (t - mid) / (1 - mid));
+ };
+}
+
+// ---------------------- scene.js ----------------------
+let renderer, camera, scene, controls, globe;
+let autoRotate = true;
+let isUserInteracting = false;
+let currentMetricKey = 'co2';
+let colorScale = makeColorScale(METRICS[currentMetricKey].palette);
+let valueExtent = [0, 1];
+let currentYearIndex = TIMELINE_YEARS.length - 1;
+let playTimer = null;
+
+const globeContainer = document.getElementById('globeContainer');
+const legendEl = document.getElementById('legend');
+const statusEl = document.getElementById('status');
+const selectEl = document.getElementById('metricSelect');
+const spinBtn = document.getElementById('spinToggle');
+const resetBtn = document.getElementById('resetView');
+const playBtn = document.getElementById('timePlay');
+const yearSlider = document.getElementById('yearSlider');
+const yearDisplay = document.getElementById('yearDisplay');
+const detailEl = document.getElementById('detailCard');
+const summaryEl = document.getElementById('metricSummary');
+
+yearSlider.max = TIMELINE_YEARS.length - 1;
+yearSlider.value = currentYearIndex;
+
+init();
+
+function init() {
+ populateMetricSelect();
+ try {
+ if (typeof THREE === 'undefined' || typeof ThreeGlobe === 'undefined') {
+ throw new Error('Three.js or ThreeGlobe not loaded');
+ }
+ buildScene();
+ applyMetric(currentMetricKey);
+ animate();
+ setStatus('Globe ready.');
+ } catch (err) {
+ console.error(err);
+ setStatus('Unable to start 3D globe; showing fallback.');
+ globeContainer.innerHTML = '
WebGL not available.
';
+ }
+ wireUI();
+ updateYearUI();
+ window.addEventListener('resize', handleResize);
+}
+
+function buildScene() {
+ const width = globeContainer.clientWidth || globeContainer.parentElement.clientWidth;
+ const height = globeContainer.clientHeight || 520;
+
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
+ renderer.setSize(width, height);
+ renderer.setPixelRatio(window.devicePixelRatio);
+ globeContainer.appendChild(renderer.domElement);
+
+ scene = new THREE.Scene();
+
+ camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 2000);
+ camera.position.set(0, 120, 320);
+
+ controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.dampingFactor = 0.08;
+ controls.minDistance = 120;
+ controls.maxDistance = 480;
+ controls.addEventListener('start', () => { isUserInteracting = true; autoRotate = false; updateSpinButton(); });
+ controls.addEventListener('end', () => { isUserInteracting = false; });
+
+ const ambient = new THREE.AmbientLight(0xffffff, 0.8);
+ scene.add(ambient);
+ const dir = new THREE.DirectionalLight(0xffffff, 0.6);
+ dir.position.set(1, 1, 1);
+ scene.add(dir);
+
+ globe = new ThreeGlobe({ animateIn: true })
+ .globeImageUrl('https://cdn.jsdelivr.net/npm/three-globe/example/img/earth-dark.jpg')
+ .bumpImageUrl('https://cdn.jsdelivr.net/npm/three-globe/example/img/earth-topology.png')
+ .atmosphereColor('#6ee7ff')
+ .atmosphereAltitude(0.18);
+
+ globe
+ .pointAltitude(d => sizeForValue(getMetricValue(d, METRICS[currentMetricKey])))
+ .pointColor(d => colorScale(normalizeValue(getMetricValue(d, METRICS[currentMetricKey]))))
+ .pointLabel(d => labelForPoint(d));
+
+ globe.onPointHover(handleHover);
+ globe.onPointClick(handleClick);
+
+ scene.add(globe);
+}
+
+function animate() {
+ requestAnimationFrame(animate);
+ if (autoRotate && globe) globe.rotation.y += 0.0008;
+ controls?.update();
+ renderer?.render(scene, camera);
+}
+
+function handleResize() {
+ if (!renderer || !camera) return;
+ const width = globeContainer.clientWidth;
+ const height = globeContainer.clientHeight || 520;
+ renderer.setSize(width, height);
+ camera.aspect = width / height;
+ camera.updateProjectionMatrix();
+}
+
+// ---------------------- ui.js ----------------------
+function populateMetricSelect() {
+ Object.entries(METRICS).forEach(([key, cfg]) => {
+ const opt = document.createElement('option');
+ opt.value = key;
+ opt.textContent = cfg.label;
+ selectEl.appendChild(opt);
+ });
+ selectEl.value = currentMetricKey;
+}
+
+function wireUI() {
+ selectEl.addEventListener('change', e => {
+ applyMetric(e.target.value);
+ });
+ spinBtn.addEventListener('click', () => {
+ autoRotate = !autoRotate;
+ updateSpinButton();
+ });
+ resetBtn.addEventListener('click', resetView);
+ playBtn.addEventListener('click', toggleTimelinePlay);
+ yearSlider.addEventListener('input', () => {
+ currentYearIndex = Number(yearSlider.value);
+ stopTimelinePlay();
+ updateYearUI();
+ refreshPoints();
+ });
+}
+
+function updateSpinButton() {
+ spinBtn.textContent = autoRotate ? 'Pause Spin' : 'Resume Spin';
+}
+
+function updateYearUI() {
+ const year = TIMELINE_YEARS[currentYearIndex];
+ yearDisplay.textContent = year;
+ yearSlider.value = currentYearIndex;
+ playBtn.textContent = playTimer ? 'Pause' : 'Play';
+ const metricCfg = METRICS[currentMetricKey];
+ const timelineEl = document.querySelector('.timeline');
+ const isTemporal = Boolean(metricCfg.seriesKey);
+ if (timelineEl) timelineEl.style.display = isTemporal ? 'flex' : 'none';
+ yearSlider.disabled = !isTemporal;
+ playBtn.disabled = !isTemporal;
+}
+
+function toggleTimelinePlay() {
+ if (playTimer) {
+ stopTimelinePlay();
+ return;
+ }
+ playTimer = setInterval(() => {
+ currentYearIndex = (currentYearIndex + 1) % TIMELINE_YEARS.length;
+ updateYearUI();
+ refreshPoints();
+ }, 1300);
+ updateYearUI();
+}
+
+function stopTimelinePlay() {
+ if (!playTimer) return;
+ clearInterval(playTimer);
+ playTimer = null;
+ updateYearUI();
+}
+
+function resetView() {
+ controls.reset();
+ camera.position.set(0, 120, 320);
+ autoRotate = true;
+ updateSpinButton();
+}
+
+function applyMetric(metricKey) {
+ currentMetricKey = metricKey;
+ const cfg = METRICS[metricKey];
+ valueExtent = computeExtent(DATA_POINTS, d => getMetricValue(d, cfg));
+ colorScale = makeColorScale(cfg.palette);
+
+ globe
+ ?.pointsData(DATA_POINTS)
+ .pointColor(d => colorScale(normalizeValue(getMetricValue(d, cfg))))
+ .pointAltitude(d => sizeForValue(getMetricValue(d, cfg)));
+
+ updateLegend();
+ updateSummary(cfg);
+ setStatus(`${cfg.label} loaded.`);
+ updateYearUI();
+ if (!cfg.seriesKey) stopTimelinePlay();
+}
+
+function refreshPoints() {
+ const cfg = METRICS[currentMetricKey];
+ valueExtent = computeExtent(DATA_POINTS, d => getMetricValue(d, cfg));
+ globe
+ ?.pointColor(d => colorScale(normalizeValue(getMetricValue(d, cfg))))
+ .pointAltitude(d => sizeForValue(getMetricValue(d, cfg)))
+ .pointsData(DATA_POINTS);
+ updateLegend();
+ updateSummary(cfg);
+ if (cfg.seriesKey) setStatus(`${cfg.label}: ${TIMELINE_YEARS[currentYearIndex]}`);
+}
+
+function normalizeValue(value) {
+ const [min, max] = valueExtent;
+ if (!Number.isFinite(value)) return 0;
+ if (max === min) return 0.5;
+ return clamp((value - min) / (max - min), 0, 1);
+}
+
+function sizeForValue(val) {
+ if (!Number.isFinite(val)) return 0.02;
+ const t = normalizeValue(val);
+ return 0.04 + t * 0.08; // keeps points visible but varied
+}
+
+function getMetricValue(d, cfg) {
+ if (cfg.seriesKey) {
+ const series = Array.isArray(d[cfg.seriesKey]) ? d[cfg.seriesKey] : [];
+ return series[currentYearIndex] ?? series[series.length - 1];
+ }
+ return cfg.accessor(d);
+}
+
+function labelForPoint(d) {
+ const cfg = METRICS[currentMetricKey];
+ const val = getMetricValue(d, cfg);
+ const formatted = cfg.unit === '$'
+ ? `$${val.toLocaleString('en-US')}`
+ : `${val}${cfg.unit}`;
+ const yearPart = cfg.seriesKey ? ` (${TIMELINE_YEARS[currentYearIndex]})` : '';
+ return `${d.name}
${cfg.label}${yearPart}: ${formatted}
Population: ${d.population}M`;
+}
+
+function handleHover(point) {
+ if (!point) {
+ detailEl.classList.add('muted');
+ detailEl.innerHTML = 'Hover or tap a city to inspect.';
+ return;
+ }
+ setDetail(point, false);
+}
+
+function handleClick(point) {
+ if (!point) return;
+ setDetail(point, true);
+}
+
+function setDetail(point, pinned) {
+ const cfg = METRICS[currentMetricKey];
+ const val = getMetricValue(point, cfg);
+ const formatted = cfg.unit === '$'
+ ? `$${val.toLocaleString('en-US')}`
+ : `${val}${cfg.unit}`;
+ const co2Now = getMetricValue(point, METRICS.co2);
+ const renNow = getMetricValue(point, METRICS.renewable);
+
+ detailEl.classList.remove('muted');
+ detailEl.innerHTML = `
+
+
+
${pinned ? 'Pinned' : 'Hover'} city
+
${point.name}
+
+
${cfg.label}
+
+ Value${cfg.seriesKey ? ' (' + TIMELINE_YEARS[currentYearIndex] + ')' : ''}${formatted}
+ Population${point.population}M
+ Renewables (${TIMELINE_YEARS[currentYearIndex]})${renNow}%
+ CO₂ (${TIMELINE_YEARS[currentYearIndex]})${co2Now}t
+ GDP per capita$${point.gdp.toLocaleString('en-US')}
+ `;
+}
+
+function updateLegend() {
+ const [min, max] = valueExtent;
+ const mid = (min + max) / 2;
+ const cfg = METRICS[currentMetricKey];
+ const year = cfg.seriesKey ? ` (${TIMELINE_YEARS[currentYearIndex]})` : '';
+ legendEl.innerHTML = `
+ Legend
+ ${cfg.label}${year}
+
+
${formatValue(min, cfg.unit)}
+
+
${formatValue(max, cfg.unit)}
+
+ Median ~ ${formatValue(mid, cfg.unit)}
+ `;
+}
+
+function updateSummary(cfg) {
+ const [min, max] = valueExtent;
+ const year = cfg.seriesKey ? ` (${TIMELINE_YEARS[currentYearIndex]})` : '';
+ summaryEl.innerHTML = `${cfg.description}${year}
Range: ${formatValue(min, cfg.unit)} – ${formatValue(max, cfg.unit)}`;
+}
+
+function formatValue(val, unit) {
+ if (!Number.isFinite(val)) return 'n/a';
+ if (unit === '$') return `$${Math.round(val).toLocaleString('en-US')}`;
+ return `${Math.round(val * 10) / 10}${unit}`;
+}
+
+function setStatus(msg) {
+ statusEl.textContent = msg;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..67a9e81
--- /dev/null
+++ b/index.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+ PlanetPulse – Global Metrics Globe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2010201520202024
+
+
+
2024
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..bdf1117
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,201 @@
+:root {
+ --bg: #0c1628;
+ --panel: #0f1f36;
+ --card: #132742;
+ --accent: #6ee7ff;
+ --accent-2: #ff9f1c;
+ --text: #e6eef7;
+ --muted: #9bb0cb;
+ --border: rgba(255, 255, 255, 0.08);
+ --shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
+ --radius: 16px;
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ background: radial-gradient(circle at 20% 20%, rgba(110, 231, 255, 0.08), transparent 35%),
+ radial-gradient(circle at 80% 10%, rgba(255, 159, 28, 0.08), transparent 40%),
+ var(--bg);
+ color: var(--text);
+ font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.topbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 24px;
+ background: rgba(12, 22, 40, 0.6);
+ backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--border);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.brand { display: flex; gap: 12px; align-items: center; }
+.glow-dot {
+ width: 14px; height: 14px; border-radius: 50%;
+ background: radial-gradient(circle, var(--accent) 0%, #0c1628 70%);
+ box-shadow: 0 0 12px var(--accent);
+}
+.eyebrow { margin: 0; color: var(--muted); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; }
+h1 { margin: 2px 0 0; font-size: 22px; letter-spacing: -0.02em; }
+
+.controls { display: flex; gap: 12px; align-items: center; }
+.control { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
+.control select {
+ background: var(--card);
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 8px 12px;
+ border-radius: 10px;
+ font-size: 14px;
+}
+.control-btn, .ghost-btn {
+ border-radius: 10px;
+ padding: 10px 14px;
+ border: 1px solid var(--border);
+ color: var(--text);
+ cursor: pointer;
+ background: linear-gradient(120deg, rgba(110, 231, 255, 0.2), rgba(110, 231, 255, 0.05));
+ transition: transform 160ms ease, box-shadow 160ms ease;
+}
+.control-btn:disabled, .ghost-btn:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+.control-btn.sm { padding: 8px 10px; font-size: 13px; }
+.ghost-btn { background: transparent; }
+.control-btn:hover, .ghost-btn:hover { transform: translateY(-1px); box-shadow: 0 10px 20px rgba(0,0,0,0.25); }
+
+.layout {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 16px;
+ padding: 16px 24px 32px;
+ flex: 1;
+ min-height: 0;
+}
+
+.globe-panel {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ position: relative;
+ overflow: hidden;
+ box-shadow: var(--shadow);
+ min-height: 480px;
+}
+#globeContainer { width: 100%; height: 100%; }
+.fallback {
+ width: 100%; height: 100%; display: grid; place-items: center;
+ color: var(--muted); font-size: 14px;
+}
+
+.timeline {
+ position: absolute;
+ left: 16px;
+ top: 16px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ background: rgba(12,22,40,0.8);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ backdrop-filter: blur(6px);
+ font-size: 13px;
+}
+.slider-wrap { display: flex; flex-direction: column; gap: 6px; }
+.slider-wrap input[type=range] {
+ width: 160px;
+ accent-color: var(--accent);
+}
+.year-labels { display: flex; justify-content: space-between; color: var(--muted); font-size: 11px; }
+
+
+.legend {
+ position: absolute;
+ left: 16px;
+ bottom: 16px;
+ padding: 10px 12px;
+ background: rgba(12, 22, 40, 0.85);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ font-size: 12px;
+ color: var(--muted);
+ backdrop-filter: blur(6px);
+}
+.legend-scale { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
+.legend-bar {
+ height: 10px;
+ width: 140px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #3b82f6, #f97316, #ef4444);
+ border: 1px solid var(--border);
+}
+
+.status {
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ background: rgba(0,0,0,0.45);
+ color: var(--muted);
+ padding: 8px 10px;
+ border-radius: 10px;
+ font-size: 12px;
+ border: 1px solid var(--border);
+}
+
+.info-panel {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 16px;
+ box-shadow: var(--shadow);
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.info-panel h2 { margin: 0 0 4px; }
+
+.card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 12px 14px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
+}
+.muted { color: var(--muted); }
+.label { color: var(--muted); font-size: 12px; letter-spacing: 0.05em; text-transform: uppercase; margin: 0 0 6px; }
+.metric-summary { font-size: 14px; line-height: 1.5; }
+.list { margin: 0; padding-left: 16px; color: var(--text); line-height: 1.5; }
+
+.mini-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; }
+
+.detail-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
+.pill { padding: 4px 8px; background: rgba(110, 231, 255, 0.12); border-radius: 999px; font-size: 12px; color: var(--accent); border: 1px solid var(--border); }
+.data-row { display: flex; justify-content: space-between; margin: 6px 0; color: var(--muted); }
+.data-row span:last-child { color: var(--text); }
+
+@media (max-width: 900px) {
+ .layout { grid-template-columns: 1fr; }
+ .globe-panel { min-height: 360px; }
+ .topbar { flex-direction: column; align-items: flex-start; gap: 12px; }
+ .controls { width: 100%; flex-wrap: wrap; }
+ .timeline { top: auto; bottom: 12px; left: 12px; right: 12px; justify-content: space-between; }
+ .slider-wrap input[type=range] { width: 120px; }
+}
+
+@media (max-width: 480px) {
+ body { padding-bottom: 16px; }
+ .topbar { position: sticky; }
+ .legend { font-size: 11px; }
+}