Create human-evolution.html

D David Veksler · 1 year ago 0f6ca568692ad2786b8756b2fdaa8eda2380c912
Parent: 795c64ff2

1 file changed +590 −0

Diff

diff --git a/human-evolution.html b/human-evolution.html
new file mode 100644
index 0000000..bcd8b66
--- /dev/null
+++ b/human-evolution.html
@@ -0,0 +1,590 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Human Evolution: A Journey Through Time (Styled v3)</title>
+
+    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
+    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"/>
+
+    <style>
+        :root {
+            --sidebar-width: 380px;
+            --header-height: 60px;
+            --primary-accent: #0d6efd;
+            --danger-accent: #dc3545; /* Bootstrap Danger Red */
+            --dark-bg: #161618; /* Slightly darker page bg */
+            --medium-bg: #212124; /* Sidebar, card body */
+            --light-bg: #303033; /* Card headers, borders */
+            --text-color: #e8e8e8; /* Main text - VERY light for high contrast */
+            --text-muted-color: #c0c0c0; /* Muted text - also lighter */
+            --timeline-period-color: #6cb2f7; /* Brighter blue for period name */
+        }
+        * { box-sizing: border-box; }
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            margin: 0;
+            overflow: hidden;
+            background: var(--dark-bg);
+            color: var(--text-color);
+            display: flex;
+            flex-direction: column;
+            height: 100vh;
+        }
+        header.app-header {
+            height: var(--header-height);
+            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
+            color: white;
+            display: flex; align-items: center; padding: 0 1.5rem;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
+            flex-shrink: 0;
+        }
+        header.app-header h1 { font-size: 1.4rem; font-weight: 300; margin: 0; letter-spacing: 0.5px; }
+        #main-content { display: flex; height: calc(100vh - var(--header-height)); flex-grow: 1; }
+        #controls-sidebar {
+            width: var(--sidebar-width);
+            background: var(--medium-bg);
+            border-right: 1px solid var(--light-bg);
+            overflow-y: auto;
+            padding: 0.75rem; /* Reduced padding */
+            display: flex; flex-direction: column; gap: 0.75rem; /* Reduced gap */
+        }
+        @media (max-width: 992px) {
+            #main-content { flex-direction: column; }
+            #controls-sidebar { width: 100%; max-height: 45vh; overflow-y: auto; border-right: none; border-bottom: 1px solid var(--light-bg); }
+            #map-container { flex-grow: 1; height: auto; }
+        }
+
+        #map-container { flex: 1; position: relative; background: #0a0a0a; }
+        #map { height: 100%; width: 100%; }
+
+        .control-card {
+            background-color: var(--medium-bg);
+            border: 1px solid var(--light-bg);
+            border-radius: 5px; /* Slightly less rounded */
+            box-shadow: 0 1px 2px rgba(0,0,0,0.15);
+        }
+        .control-card .card-header {
+            font-size: 0.85rem; /* Screenshot like header font */
+            font-weight: 500;
+            background-color: var(--light-bg);
+            color: var(--text-muted-color);
+            border-bottom: 1px solid var(--light-bg);
+            padding: 0.4rem 0.6rem; /* Tighter header padding */
+            display: flex; align-items: center;
+        }
+        .control-card .card-header i { margin-right: 0.4rem; font-size: 0.9em; }
+        .control-card .card-body { padding: 0.6rem; } /* Tighter body padding */
+
+        #current-period-display {
+            font-size: 1.15rem; /* Matching screenshot */
+            font-weight: bold;
+            margin-top: 0.1rem; margin-bottom: 0.05rem;
+            text-align: center; color: var(--timeline-period-color);
+        }
+        #timeline-value {
+            font-weight: normal; font-size: 0.7rem; /* Matching screenshot */
+            color: var(--text-muted-color);
+            display: block; text-align: center; margin-bottom: 0.5rem;
+        }
+        #timeline-scrubber {
+            -webkit-appearance: none; appearance: none;
+            width: 100%; height: 6px; border-radius: 3px;
+            background: #505050; outline: none; margin: 0.5rem 0;
+        }
+        #timeline-scrubber::-webkit-slider-thumb {
+            -webkit-appearance: none; appearance: none;
+            width: 16px; height: 16px; border-radius: 50%;
+            background: var(--primary-accent); cursor: pointer;
+            box-shadow: 0 0 4px rgba(var(--primary-accent), 0.6);
+        }
+        #timeline-scrubber::-moz-range-thumb {
+            width: 16px; height: 16px; border-radius: 50%;
+            background: var(--primary-accent); cursor: pointer; border: none;
+            box-shadow: 0 0 4px rgba(var(--primary-accent), 0.6);
+        }
+        .timeline-buttons .btn, .timeline-buttons .btn-group { margin-top: 0.25rem; }
+        .timeline-buttons .btn { padding: 0.25rem 0.6rem; font-size: 0.8rem; }
+        .timeline-buttons .btn i { margin-right: 0.25rem; }
+        #reset-button { background-color: var(--danger-accent); border-color: var(--danger-accent); color: white;}
+        #reset-button:hover { filter: brightness(90%);}
+
+
+        .quick-filter-buttons .btn { margin-right: 0.25rem; font-size:0.75rem; padding: 0.2rem 0.45rem;}
+        .btn-outline-light { color: var(--text-muted-color); border-color: var(--light-bg); }
+        .btn-outline-light:hover { color: var(--text-color); background-color: var(--light-bg); border-color: var(--light-bg); }
+        .btn-outline-light.active { color: var(--dark-bg); background-color: var(--primary-accent); border-color: var(--primary-accent); font-weight: 500; }
+
+
+        #species-filter-list {
+            max-height: 180px;
+            overflow-y: auto;
+            border: 1px solid var(--light-bg);
+            border-radius: 4px;
+            padding: 0.25rem; margin-top: 0.5rem;
+        }
+        #species-filter-list .form-check {
+            padding: .2rem .4rem;
+            border-bottom: 1px solid var(--light-bg);
+            display: flex; align-items: center;
+        }
+        #species-filter-list .form-check:last-child { border-bottom: none; }
+        #species-filter-list .form-check-label {
+            font-size: 0.8rem;
+            cursor: pointer; margin-left: 0.4rem;
+            color: var(--text-color); /* Ensure label text is light */
+        }
+        #species-filter-list .form-check-input {
+            cursor: pointer; margin-top:0;
+            width: 0.9em; height: 0.9em;
+            background-color: var(--medium-bg);
+            border: 1px solid var(--light-bg);
+        }
+        #species-filter-list .form-check-input:checked {
+            background-color: var(--primary-accent);
+            border-color: var(--primary-accent);
+        }
+        .species-color-indicator {
+            width: 10px; height: 10px; display: inline-block;
+            margin-left: 0.3rem;
+            border-radius: 2px; border: 1px solid rgba(255,255,255,0.1);
+        }
+
+        #narrative-panel .card-body {
+            font-size: 0.8rem; line-height: 1.5;
+            max-height: 150px; overflow-y: auto; color: var(--text-muted-color);
+        }
+        #narrative-panel .card-body strong { color: var(--text-color); }
+        .data-limitation-note { color: #ffc107; font-size: 0.75rem; margin-top:0.5rem; border-top: 1px dashed var(--light-bg); padding-top: 0.5rem;}
+        .data-limitation-note i { margin-right: 0.25rem;}
+
+        .legend {
+            position: absolute; bottom: 10px; right: 10px;
+            padding: 0.5rem 0.75rem; background: rgba(30, 30, 32, 0.92);
+            box-shadow: 0 1px 5px rgba(0,0,0,0.3); border-radius: 4px;
+            z-index: 1000; max-height: 180px; overflow-y: auto; border: 1px solid var(--light-bg);
+            color: var(--text-color);
+        }
+        .legend h5 {
+            margin-top: 0; margin-bottom: 5px; font-size: 0.85rem;
+            border-bottom: 1px solid var(--light-bg); padding-bottom: 3px;
+        }
+        .legend h5 i { font-size: 0.85em; margin-right: 0.25rem; }
+        .legend-item { display: flex; align-items: center; margin-bottom: 2px; font-size: 0.7rem; }
+        .legend-color-box {
+            width: 10px; height: 10px; margin-right: 5px;
+            border: 1px solid #777; opacity: 0.8; flex-shrink: 0; border-radius: 2px;
+        }
+
+        .loading-overlay {
+            position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+            background: rgba(0,0,0,0.9);
+            display: flex; align-items: center; justify-content: center;
+            color: white; font-size: 1.2rem; z-index: 9999;
+            flex-direction: column;
+        }
+        .loading-overlay .spinner-border { width: 2.5rem; height: 2.5rem; margin-bottom: 0.75rem;}
+
+        .climate-toggle-container {
+            position: absolute; top: 10px; right: 10px;
+            background: var(--medium-bg); padding: 0.4rem 0.6rem;
+            border-radius: 5px; z-index: 1000; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+            border: 1px solid var(--light-bg);
+        }
+         .climate-toggle-container .form-check-label {font-size: 0.75rem; color: var(--text-muted-color);}
+         .climate-toggle-container .form-check-input { border-color: var(--primary-accent);}
+         .climate-toggle-container .form-check-input:checked { background-color: var(--primary-accent); border-color: var(--primary-accent);}
+
+        .leaflet-control-zoom-in, .leaflet-control-zoom-out {
+            background-color: var(--medium-bg) !important; color: var(--text-color) !important;
+            border: 1px solid var(--light-bg) !important; border-radius: 4px !important;
+        }
+        .leaflet-control-zoom-in:hover, .leaflet-control-zoom-out:hover { background-color: var(--light-bg) !important; }
+        .leaflet-popup-content-wrapper, .leaflet-popup-tip {
+            background: var(--medium-bg); color: var(--text-color);
+            box-shadow: 0 3px 14px rgba(0,0,0,0.4); border: 1px solid var(--light-bg);
+        }
+        .leaflet-popup-content h5 { margin-top: 0; margin-bottom: 8px; font-size: 1.05em; border-bottom: 1px solid var(--light-bg); padding-bottom: 5px;}
+        .leaflet-container a.leaflet-popup-close-button { color: var(--text-muted-color); }
+    </style>
+</head>
+<body>
+    <div id="loading-screen" class="loading-overlay">
+        <div class="spinner-border text-light" role="status"></div>
+        <p class="mt-2 mb-0">Loading Evolution Data...</p>
+    </div>
+
+    <header class="app-header">
+        <h1><i class="bi bi-globe-americas"></i> Human Evolution: A Journey Through Time</h1>
+    </header>
+
+    <div id="main-content">
+        <aside id="controls-sidebar">
+            <div id="controls-content" class="d-none h-100 d-flex flex-column">
+                <div class="control-card card">
+                    <div class="card-header"><i class="bi bi-clock-history"></i> Timeline Control</div>
+                    <div class="card-body">
+                        <div id="current-period-display">Data not loaded.</div>
+                        <span id="timeline-value"></span>
+                        <input type="range" class="form-range" id="timeline-scrubber" min="0" value="0" disabled>
+                        <div class="d-flex justify-content-between align-items-center mt-2 timeline-buttons">
+                            <button id="play-pause-button" class="btn btn-primary btn-sm" title="Play or Pause animation" disabled><i class="bi bi-play-fill"></i> <span>Play</span></button>
+                            <div class="btn-group btn-group-sm" role="group">
+                                <button type="button" class="btn btn-outline-light speed-btn" data-speed="1">1x</button>
+                                <button type="button" class="btn btn-outline-light speed-btn" data-speed="2">2x</button>
+                                <button type="button" class="btn btn-outline-light speed-btn" data-speed="5">5x</button>
+                            </div>
+                            <button id="reset-button" class="btn btn-danger btn-sm" title="Reset to initial state" disabled><i class="bi bi-arrow-clockwise"></i> Reset</button>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="control-card card">
+                    <div class="card-header"><i class="bi bi-people-fill"></i> Species Filter</div>
+                    <div class="card-body">
+                        <div class="btn-group btn-group-sm mb-2 quick-filter-buttons" role="group">
+                            <button class="btn btn-outline-light" id="filter-all">All</button>
+                            <button class="btn btn-outline-light" id="filter-none">None</button>
+                            <button class="btn btn-outline-light" id="filter-homo">Homo</button>
+                        </div>
+                        <div id="species-filter-list">
+                            <p class="text-muted p-2 small">No species data loaded.</p>
+                        </div>
+                    </div>
+                </div>
+
+                <div id="narrative-panel" class="control-card card flex-grow-1 mb-0">
+                    <div class="card-header"><i class="bi bi-book-half"></i> Period Overview</div>
+                    <div class="card-body">
+                        <p id="narrative-text" class="text-muted">Select a time period to see an overview.</p>
+                         <p class="data-limitation-note"><i class="bi bi-exclamation-triangle-fill"></i> Note: The current dataset may not include geographic data for all species. Species without this data will not be drawn on the map. The legend lists species active in the current period.</p>
+                    </div>
+                </div>
+            </div>
+        </aside>
+
+        <main id="map-container">
+            <div id="map"></div>
+            <div class="climate-toggle-container">
+                <div class="form-check form-switch">
+                    <input class="form-check-input" type="checkbox" role="switch" id="climate-toggle-switch">
+                    <label class="form-check-label" for="climate-toggle-switch">Ice Sheets</label>
+                </div>
+            </div>
+            <div id="legend" class="legend">
+                <h5><i class="bi bi-palette"></i> Legend</h5>
+                <div id="legend-items-container">
+                    <small class="text-muted">Data not loaded.</small>
+                </div>
+            </div>
+        </main>
+    </div>
+
+    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
+
+    <script>
+    // --- State & Config ---
+    let timePeriodsData = [];
+    const map = L.map('map', { worldCopyJump: true, zoomControl: false }).setView([20, 40], 2);
+    L.control.zoom({ position: 'topleft' }).addTo(map);
+    L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
+        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
+        subdomains: 'abcd', maxZoom: 19
+    }).addTo(map);
+
+    let iceSheetLayerGroup = L.layerGroup();
+    let speciesGeoJsonLayerGroup = L.layerGroup().addTo(map); // Layer for species ranges/migrations
+
+    let currentPeriodIndex = 0;
+    let selectedSpecies = {};
+    let allSpeciesList = [];
+    let isPlaying = false;
+    let animationFrameId;
+    let lastFrameTime = 0;
+    let playbackSpeed = 1;
+    const baseAnimationInterval = 3500;
+
+    const domElements = {
+        timelineScrubber: document.getElementById('timeline-scrubber'),
+        currentPeriodDisplay: document.getElementById('current-period-display'),
+        timelineValueDisplay: document.getElementById('timeline-value'),
+        speciesFilterList: document.getElementById('species-filter-list'),
+        legendItemsContainer: document.getElementById('legend-items-container'),
+        playPauseButton: document.getElementById('play-pause-button'),
+        playPauseIcon: document.getElementById('play-pause-button').querySelector('i'),
+        playPauseText: document.getElementById('play-pause-button').querySelector('span'),
+        resetButton: document.getElementById('reset-button'),
+        narrativeTextElement: document.getElementById('narrative-text'),
+        loadingScreen: document.getElementById('loading-screen'),
+        controlsContentDiv: document.getElementById('controls-content'),
+        climateToggle: document.getElementById('climate-toggle-switch')
+    };
+
+    const iceSheetGeoJson = {
+        lgm_laurentide: {"type":"Polygon","coordinates":[[[-105,40],[-55,40],[-55,75],[-105,75],[-105,40]]]},
+        lgm_fennoscandian: {"type":"Polygon","coordinates":[[[0,50],[45,50],[45,70],[0,70],[0,50]]]}
+    };
+
+    async function fetchEvolutionData() {
+        try {
+            const response = await fetch('https://cheatsheets.davidveksler.com/evolution_data.json');
+            if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+            timePeriodsData = await response.json();
+            if (!Array.isArray(timePeriodsData) || timePeriodsData.length === 0) {
+                 throw new Error("Fetched data is not valid or empty.");
+            }
+            initializeApplication();
+        } catch (error) {
+            console.error("Could not fetch evolution data:", error);
+            domElements.currentPeriodDisplay.innerHTML = "<strong class='text-danger'>Error loading data.</strong>";
+            domElements.loadingScreen.innerHTML = `<div class='p-3 text-center'><i class='bi bi-exclamation-triangle-fill text-danger h1'></i><p>Failed to load evolution data.<br><small>${error.message}</small></p></div>`;
+        }
+    }
+
+    function initializeApplication() {
+        domElements.loadingScreen.style.display = 'none';
+        domElements.controlsContentDiv.classList.remove('d-none');
+        domElements.timelineScrubber.disabled = false;
+        domElements.playPauseButton.disabled = false;
+        domElements.resetButton.disabled = false;
+
+        domElements.timelineScrubber.max = timePeriodsData.length - 1;
+        populateAllSpeciesList();
+        populateSpeciesFilter();
+        currentPeriodIndex = 0;
+        updateUIForCurrentPeriod();
+        updateSpeedButtonsActiveState();
+    }
+
+    function populateAllSpeciesList() {
+        const speciesSet = new Map();
+        timePeriodsData.forEach(period => {
+            if (period.species && Array.isArray(period.species)) {
+                period.species.forEach(s => {
+                    if (s && s.name && !speciesSet.has(s.name)) {
+                        speciesSet.set(s.name, { color: s.color || '#cccccc' });
+                    }
+                });
+            }
+        });
+        allSpeciesList = Array.from(speciesSet, ([name, data]) => ({ name, ...data }));
+        allSpeciesList.sort((a, b) => a.name.localeCompare(b.name));
+    }
+
+    function populateSpeciesFilter() {
+        domElements.speciesFilterList.innerHTML = '';
+        if (allSpeciesList.length === 0) {
+            domElements.speciesFilterList.innerHTML = '<p class="text-muted p-2 small">No species found.</p>'; return;
+        }
+        allSpeciesList.forEach(s => {
+            selectedSpecies[s.name] = true;
+            const itemDiv = document.createElement('div'); itemDiv.className = 'form-check';
+            const input = document.createElement('input');
+            input.type = 'checkbox'; input.className = 'form-check-input';
+            input.id = `filter-${s.name.replace(/\W/g, '_')}`; input.value = s.name; input.checked = true;
+            input.addEventListener('change', (event) => {
+                selectedSpecies[event.target.value] = event.target.checked;
+                updateUIForCurrentPeriod();
+            });
+            const colorIndicator = document.createElement('span');
+            colorIndicator.className = 'species-color-indicator';
+            colorIndicator.style.backgroundColor = s.color;
+
+            const label = document.createElement('label');
+            label.className = 'form-check-label'; label.htmlFor = input.id;
+            label.style.color = domElements.textMutedColor; // Use a light color for the text
+            label.textContent = s.name;
+
+            itemDiv.appendChild(input);
+            itemDiv.appendChild(colorIndicator);
+            itemDiv.appendChild(label);
+            domElements.speciesFilterList.appendChild(itemDiv);
+        });
+    }
+
+    function updateUIForCurrentPeriod() {
+        speciesGeoJsonLayerGroup.clearLayers(); // Clear previous species' geographic data
+
+        if (!timePeriodsData || timePeriodsData.length === 0 || currentPeriodIndex < 0 || currentPeriodIndex >= timePeriodsData.length) {
+            domElements.currentPeriodDisplay.textContent = `Timeline End`;
+            domElements.timelineValueDisplay.textContent = "(Drag or Reset)";
+            domElements.narrativeTextElement.innerHTML = "<p class='text-muted'>End of data.</p>";
+            domElements.legendItemsContainer.innerHTML = '<small class="text-muted">No active period.</small>';
+            return;
+        }
+
+        const period = timePeriodsData[currentPeriodIndex];
+        domElements.currentPeriodDisplay.textContent = period.periodName || "Unknown Period";
+        domElements.timelineValueDisplay.textContent = period.timeRange || `Period ${currentPeriodIndex + 1}`;
+        domElements.timelineScrubber.value = currentPeriodIndex;
+        domElements.narrativeTextElement.innerHTML = period.narrative || "<p class='text-muted'>No narrative for this period.</p>";
+
+        let speciesInCurrentPeriod = period.species || [];
+        let visibleSpeciesInLegend = [];
+
+        speciesInCurrentPeriod.forEach(s => {
+            if (s && s.name && selectedSpecies[s.name]) {
+                visibleSpeciesInLegend.push(s); // Add to legend regardless of map drawing
+
+                // Attempt to draw on map if geographic data exists
+                let popupContent = `<h5>${s.name}</h5>
+                                    <p class="mb-1 small"><strong>Timeline:</strong> ${s.timeline || 'N/A'}</p>
+                                    <p class="mb-0 small">${s.notes || 'No specific notes.'}</p>`;
+
+                if (s.estimatedRangeGeoJson && Object.keys(s.estimatedRangeGeoJson).length > 0) {
+                    try {
+                        L.geoJSON(s.estimatedRangeGeoJson, {
+                            style: { fillColor: s.color || '#888888', weight: 1, opacity: 1, color: '#ccc', fillOpacity: 0.4 }
+                        }).bindPopup(popupContent).addTo(speciesGeoJsonLayerGroup);
+                    } catch (e) { console.warn("Error drawing range for", s.name, e); }
+                }
+
+                if (s.migrationPaths && Array.isArray(s.migrationPaths)) {
+                    s.migrationPaths.forEach(path => {
+                        if (path && Object.keys(path).length > 0) {
+                           try {
+                                L.geoJSON(path, {
+                                    style: { color: s.migrationColor || s.color || '#888888', weight: 2.5, opacity: 0.65, dashArray: '5, 5' }
+                                }).bindPopup(popupContent).addTo(speciesGeoJsonLayerGroup);
+                            } catch (e) { console.warn("Error drawing migration for", s.name, e); }
+                        }
+                    });
+                }
+            }
+        });
+        updateLegend(visibleSpeciesInLegend);
+        updateClimateOverlay(period.timeRange);
+    }
+
+    function updateLegend(visibleSpecies) {
+        domElements.legendItemsContainer.innerHTML = '';
+        if (visibleSpecies.length === 0) {
+            domElements.legendItemsContainer.innerHTML = '<small class="text-muted">No species selected or in period.</small>'; return;
+        }
+        visibleSpecies.sort((a,b) => a.name.localeCompare(b.name));
+        visibleSpecies.forEach(s => {
+            const itemDiv = document.createElement('div'); itemDiv.className = 'legend-item';
+            const colorBox = document.createElement('span');
+            colorBox.className = 'legend-color-box'; colorBox.style.backgroundColor = s.color || '#cccccc';
+            const nameSpan = document.createElement('span'); nameSpan.textContent = s.name;
+            // Use a light color for legend text for readability
+            nameSpan.style.color = domElements.textMutedColor;
+            itemDiv.appendChild(colorBox); itemDiv.appendChild(nameSpan);
+            domElements.legendItemsContainer.appendChild(itemDiv);
+        });
+    }
+
+    function parseYearFromRange(rangeString) { /* Same as before */
+        if (!rangeString) return { start: null, end: null };
+        const cleaned = rangeString.toLowerCase().replace(/c\.|ago|years|million|kya|mya/g, '').trim();
+        const parts = cleaned.split(/\s*-\s*|\s+to\s+/);
+        function parsePart(part) {
+            part = part.trim(); let num = parseFloat(part.replace(/,/g, '')); // Remove commas before parsing
+            if (rangeString.toLowerCase().includes("million") || rangeString.toLowerCase().includes("mya")) num *= 1000000;
+            else if (rangeString.toLowerCase().includes("kya")) num *= 1000;
+            return isNaN(num) ? null : -Math.abs(num);
+        }
+        let startYear = parsePart(parts[0]); let endYear = parts.length > 1 ? parsePart(parts[1]) : startYear;
+        if (startYear && endYear === null) endYear = startYear; if (endYear && startYear === null) startYear = endYear;
+        if (startYear && endYear && startYear > endYear) [startYear, endYear] = [endYear, startYear];
+        return { start: startYear, end: endYear };
+    }
+
+    function updateClimateOverlay(periodTimeRange) { /* Same as before */
+        iceSheetLayerGroup.clearLayers();
+        if (domElements.climateToggle.checked && periodTimeRange) {
+            const { start, end } = parseYearFromRange(periodTimeRange);
+            const lgmStart = -26500; const lgmEnd = -19000;
+            if (start && end && Math.max(start, lgmStart) <= Math.min(end, lgmEnd)) {
+                Object.values(iceSheetGeoJson).forEach(sheet => {
+                    L.geoJSON(sheet, { style: { color: '#ADC8E6', fillColor: '#ADD8E6', fillOpacity: 0.3, weight: 1 }})
+                     .addTo(iceSheetLayerGroup);
+                });
+            }
+        }
+        if (!map.hasLayer(iceSheetLayerGroup) && domElements.climateToggle.checked && iceSheetLayerGroup.getLayers().length > 0) map.addLayer(iceSheetLayerGroup);
+        else if (map.hasLayer(iceSheetLayerGroup) && (!domElements.climateToggle.checked || iceSheetLayerGroup.getLayers().length === 0)) map.removeLayer(iceSheetLayerGroup);
+    }
+
+    function animationLoop(timestamp) { /* Same as before */
+        if (!isPlaying) return;
+        if (timestamp - lastFrameTime >= (baseAnimationInterval / playbackSpeed)) {
+            lastFrameTime = timestamp; currentPeriodIndex++;
+            if (currentPeriodIndex >= timePeriodsData.length) {
+                currentPeriodIndex = timePeriodsData.length - 1; pauseAnimation();
+            }
+            updateUIForCurrentPeriod();
+        }
+        animationFrameId = requestAnimationFrame(animationLoop);
+    }
+    function playAnimation() { /* Same as before */
+        if (!timePeriodsData || timePeriodsData.length === 0) return;
+        isPlaying = true; domElements.playPauseIcon.className = 'bi bi-pause-fill'; domElements.playPauseText.textContent = "Pause";
+        lastFrameTime = performance.now();
+        if (currentPeriodIndex >= timePeriodsData.length - 1 && timePeriodsData.length > 0) currentPeriodIndex = 0;
+        updateUIForCurrentPeriod(); animationFrameId = requestAnimationFrame(animationLoop);
+    }
+    function pauseAnimation() { /* Same as before */
+        isPlaying = false; domElements.playPauseIcon.className = 'bi bi-play-fill'; domElements.playPauseText.textContent = "Play";
+        if (animationFrameId) cancelAnimationFrame(animationFrameId);
+    }
+    function togglePlayPause() { if (isPlaying) pauseAnimation(); else playAnimation(); }
+    function resetAnimation() { /* Same as before */
+        pauseAnimation(); currentPeriodIndex = 0; domElements.timelineScrubber.value = 0;
+        playbackSpeed = 1; updateSpeedButtonsActiveState();
+        if (allSpeciesList && allSpeciesList.length > 0) {
+             Object.keys(selectedSpecies).forEach(sName => selectedSpecies[sName] = true);
+             document.querySelectorAll('#species-filter-list input[type="checkbox"]').forEach(cb => cb.checked = true);
+        }
+        updateUIForCurrentPeriod();
+    }
+    function updateSpeedButtonsActiveState() { /* Same as before */
+        document.querySelectorAll('.speed-btn').forEach(btn => {
+            btn.classList.remove('active', 'btn-primary'); btn.classList.add('btn-outline-light');
+            if (parseInt(btn.dataset.speed) === playbackSpeed) {
+                btn.classList.add('active', 'btn-primary'); btn.classList.remove('btn-outline-light');
+            }
+        });
+    }
+
+    // --- Event Listeners ---
+    domElements.timelineScrubber.addEventListener('input', (event) => {
+        currentPeriodIndex = parseInt(event.target.value); pauseAnimation(); updateUIForCurrentPeriod();
+    });
+    domElements.playPauseButton.addEventListener('click', togglePlayPause);
+    domElements.resetButton.addEventListener('click', resetAnimation);
+    document.querySelectorAll('.speed-btn').forEach(button => {
+        button.addEventListener('click', () => {
+            playbackSpeed = parseInt(button.dataset.speed); updateSpeedButtonsActiveState();
+        });
+    });
+    document.getElementById('filter-all').addEventListener('click', () => {
+        allSpeciesList.forEach(s => selectedSpecies[s.name] = true);
+        document.querySelectorAll('#species-filter-list input[type="checkbox"]').forEach(cb => cb.checked = true);
+        updateUIForCurrentPeriod();
+    });
+    document.getElementById('filter-none').addEventListener('click', () => {
+        allSpeciesList.forEach(s => selectedSpecies[s.name] = false);
+        document.querySelectorAll('#species-filter-list input[type="checkbox"]').forEach(cb => cb.checked = false);
+        updateUIForCurrentPeriod();
+    });
+    document.getElementById('filter-homo').addEventListener('click', () => {
+        allSpeciesList.forEach(s => {
+            const isHomo = s.name.toLowerCase().includes('homo'); selectedSpecies[s.name] = isHomo;
+            const cb = document.getElementById(`filter-${s.name.replace(/\W/g, '_')}`);
+            if(cb) cb.checked = isHomo;
+        });
+        updateUIForCurrentPeriod();
+    });
+    domElements.climateToggle.addEventListener('change', () => {
+        if (timePeriodsData.length > 0) updateUIForCurrentPeriod(); // Re-render to apply climate change
+    });
+
+    // --- Initial Load ---
+    document.addEventListener('DOMContentLoaded', fetchEvolutionData);
+    </script>
+</body>
+</html>
\ No newline at end of file