• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

GEO DB - INTERACTIVE APP SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>City Data Explorer</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        :root {
            color-scheme: light dark;
            --bg-color: transparent;
            --text-main: #ffffff;
            --text-muted: #aaaaaa;
            --text-subtle: #888888;
            
            --surface: transparent;
            --surface-raised: #111111;
            --surface-overlay: #222222;
            --surface-hover: #2a2a2a;
            --surface-active: #1a3a5f;
            
            --border: #333333;
            --border-light: #444444;
            
            --primary: #4a90e2;
            --primary-hover: #357ae8;
            
            --danger-bg: rgba(239, 68, 68, 0.1);
            --danger-border: rgba(239, 68, 68, 0.3);
            --danger-text: #fca5a5;
        }

        html:not(.dark) {
            --bg-color: transparent;
            --text-main: #111111;
            --text-muted: #555555;
            --text-subtle: #666666;
            
            --surface: transparent;
            --surface-raised: #f9f9f9;
            --surface-overlay: #ffffff;
            --surface-hover: #f0f0f0;
            --surface-active: #e6f0fa;
            
            --border: #cccccc;
            --border-light: #bbbbbb;
            
            --primary: #4a90e2;
            --primary-hover: #357ae8;
            
            --danger-bg: rgba(239, 68, 68, 0.1);
            --danger-border: rgba(239, 68, 68, 0.3);
            --danger-text: #dc2626;
        }


        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: var(--bg-color);
            color: var(--text-main);
            line-height: 1.5;
            padding: 0;
            margin: 0;
            height: 100vh;
            overflow: hidden;
            -webkit-font-smoothing: antialiased;
        }

        .container {
            width: 100%;
            height: 100vh;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }

        header {
            margin-bottom: 24px;
            padding-bottom: 16px;
            border-bottom: 1px solid var(--border);
        }

        h1 {
            font-size: 22px;
            font-weight: 600;
            margin-bottom: 4px;
        }

        .subtitle {
            color: var(--text-muted);
            font-size: 14px;
        }

        .search-area {
            background: transparent;
            padding: 20px;
            margin-bottom: 0;
            border-bottom: 1px solid var(--border);
        }

        .search-row {
            display: flex;
            gap: 10px;
            margin-bottom: 15px;
        }

        input {
            flex: 1;
            padding: 12px 15px;
            background: var(--surface-overlay);
            border: 1px solid var(--border-light);
            border-radius: 6px;
            color: var(--text-main);
            font-size: 16px; /* Better for mobile */
            -webkit-appearance: none;
        }

        input:focus {
            outline: none;
            border-color: var(--primary);
        }

        button {
            padding: 12px 20px;
            background: var(--primary);
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 16px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
            -webkit-tap-highlight-color: transparent;
        }

        button:hover {
            background: var(--primary-hover);
            transform: translateY(-2px);
        }

        button:disabled {
            background: var(--border-light);
            cursor: not-allowed;
        }

        .quick-search {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }

        .quick-search button {
            padding: 8px 12px;
            background: var(--surface-overlay);
            font-size: 14px;
            flex: 1;
            min-width: calc(50% - 4px); /* 2 buttons per row on mobile */
            color: var(--text-main);
            border: 1px solid var(--border-light);
            border-radius: 6px;
            transition: all 0.2s;
        }

        .quick-search button:hover {
            background: var(--surface-hover);
            border-color: var(--primary);
            transform: translateY(-2px);
        }

        /* Custom scrollbar styling */
        ::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }

        ::-webkit-scrollbar-track {
            background: var(--surface-raised);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb {
            background: var(--border-light);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: #555;
        }

        /* Firefox scrollbar */
        * {
            scrollbar-width: thin;
            scrollbar-color: var(--border-light) var(--surface-raised);
        }

        .alert {
            padding: 12px 15px;
            border-radius: 6px;
            margin-bottom: 15px;
            font-size: 14px;
            display: none;
        }

        .alert.error {
            background: var(--danger-bg);
            border: 1px solid var(--danger-border);
            color: var(--danger-text);
        }

        .loading {
            text-align: center;
            padding: 40px;
            color: var(--text-subtle);
            display: none;
        }

        .results-container {
            display: grid;
            grid-template-columns: 1fr 1.5fr;
            gap: 20px;
            flex: 1;
            min-height: 0;
        }

        .results-side {
            background: var(--surface-raised);
            border-radius: 8px;
            padding: 20px;
            border: 1px solid var(--border);
            display: flex;
            flex-direction: column;
        }

        .results-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .results-count {
            font-size: 14px;
            color: var(--text-subtle);
        }

        .city-list-container {
            flex: 1;
            overflow-y: auto;
            min-height: 0;
        }

        .city-list {
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        .details-side {
            background: var(--surface-raised);
            border-radius: 8px;
            padding: 20px;
            border: 1px solid var(--border);
            display: flex;
            flex-direction: column;
        }

        .details-content-container {
            flex: 1;
            overflow-y: auto;
            min-height: 0;
        }

        .section-title {
            font-size: 16px;
            font-weight: 600;
            color: var(--text-main);
        }

        .city-item {
            padding: 12px;
            background: var(--surface-overlay);
            border-radius: 6px;
            border: 1px solid var(--border);
            cursor: pointer;
            transition: all 0.2s;
            -webkit-tap-highlight-color: transparent;
        }

        .city-item:hover {
            background: var(--surface-hover);
            border-color: var(--border-light);
        }

        .city-item.active {
            background: var(--surface-active);
            border-color: var(--primary);
        }

        .city-name {
            font-weight: 500;
            margin-bottom: 4px;
            color: var(--text-main);
            font-size: 15px;
        }

        .city-location {
            font-size: 13px;
            color: var(--text-muted);
            margin-bottom: 6px;
        }

        .city-stats {
            display: flex;
            gap: 15px;
            font-size: 12px;
            color: var(--text-subtle);
        }

        .no-results {
            text-align: center;
            padding: 30px;
            color: var(--text-subtle);
        }

        .details-content {
            min-height: 400px;
        }

        .details-header {
            margin-bottom: 20px;
        }

        .details-title {
            font-size: 20px;
            font-weight: 600;
            margin-bottom: 5px;
            color: var(--text-main);
        }

        .details-subtitle {
            font-size: 14px;
            color: var(--text-muted);
            margin-bottom: 10px;
        }

        .details-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 15px;
            margin-bottom: 20px;
        }

        .detail-box {
            background: var(--surface-overlay);
            border-radius: 6px;
            padding: 15px;
            border: 1px solid var(--border);
        }

        .detail-label {
            font-size: 12px;
            color: var(--text-subtle);
            margin-bottom: 8px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .detail-value {
            font-size: 18px;
            font-weight: 500;
            color: var(--text-main);
        }

        .detail-sub {
            font-size: 13px;
            color: var(--text-muted);
            margin-top: 4px;
        }

        .empty-details {
            text-align: center;
            padding: 60px 20px;
            color: var(--text-subtle);
        }

        .info-row {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            margin-top: 15px;
            font-size: 13px;
        }

        .info-item {
            color: var(--text-subtle);
        }

        .info-value {
            color: var(--text-main);
            font-weight: 500;
        }

        /* Mobile layout - tabs instead of side-by-side */
        .mobile-tabs {
            display: none;
            flex-direction: column;
            flex: 1;
            min-height: 0;
            margin-top: 20px;
        }

        .tab-buttons {
            display: flex;
            background: var(--surface-raised);
            border-radius: 8px 8px 0 0;
            overflow: hidden;
            border-bottom: 1px solid var(--border);
        }

        .tab-button {
            flex: 1;
            padding: 14px;
            background: transparent;
            color: var(--text-subtle);
            border: none;
            text-align: center;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
        }

        .tab-button:hover {
            color: var(--text-main);
            background: rgba(128, 128, 128, 0.1);
        }

        .tab-button.active {
            background: var(--surface-overlay);
            color: var(--text-main);
            border-bottom: 2px solid var(--primary);
        }

        .tab-content {
            background: var(--surface-raised);
            border-radius: 0 0 8px 8px;
            padding: 20px;
            border: 1px solid var(--border);
            border-top: none;
            flex: 1;
            min-height: 0;
            overflow-y: auto;
        }

        .desktop-layout {
            display: flex;
            flex-direction: column;
            flex: 1;
            min-height: 0;
            margin-top: 20px;
        }

        /* Desktop styles */
        @media (min-width: 1025px) {
            .mobile-tabs {
                display: none !important;
            }
        }

        /* Tablet and mobile styles */
        @media (max-width: 1024px) {
            .desktop-layout {
                display: none;
            }

            .mobile-tabs {
                display: block;
            }

            .results-container {
                grid-template-columns: 1fr;
            }

            .city-list-container,
            .details-content-container {
                max-height: none;
                overflow-y: visible;
            }

            .quick-search button {
                min-width: calc(25% - 6px); /* 4 buttons per row on tablet */
            }

            .details-grid {
                grid-template-columns: 1fr;
            }
        }

        /* Small mobile styles */
        @media (max-width: 640px) {
            

            

            .search-row {
                flex-direction: column;
            }

            button, input {
                width: 100%;
            }

            .quick-search button {
                min-width: calc(50% - 4px); /* 2 buttons per row on mobile */
                font-size: 13px;
                padding: 10px 8px;
            }

            .tab-content {
                padding: 16px;
                min-height: 400px;
            }

            .city-item {
                padding: 10px;
            }

            .city-name {
                font-size: 14px;
            }

            .detail-box {
                padding: 12px;
            }

            .detail-value {
                font-size: 16px;
            }
        }

        /* Very small screens */
        @media (max-width: 375px) {
            h1 {
                font-size: 20px;
            }

            .quick-search button {
                min-width: 100%; /* 1 button per row */
            }

            .tab-button {
                font-size: 13px;
                padding: 12px 8px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="search-area">
            <div class="search-row">
                <input type="text" id="cityInput" placeholder="Enter city name (e.g., Paris, Tokyo)" autocomplete="off">
                <button id="searchBtn">Search</button>
            </div>
            <div class="quick-search">
                <button data-city="Paris">Paris</button>
                <button data-city="London">London</button>
                <button data-city="Tokyo">Tokyo</button>
                <button data-city="New York">New York</button>
                <button data-city="Berlin">Berlin</button>
                <button data-city="Sydney">Sydney</button>
                <button data-city="Dubai">Dubai</button>
                <button data-city="Singapore">Singapore</button>
            </div>
        </div>

        <div id="alert" class="alert"></div>

        <div id="loading" class="loading">
            Loading city data...
        </div>

        <!-- Desktop Layout (for larger screens) -->
        <div id="desktopResults" class="desktop-layout" style="display: none;">
            <div class="results-container">
                <div class="results-side">
                    <div class="results-header">
                        <div class="section-title">Search Results</div>
                        <div id="resultsCount" class="results-count"></div>
                    </div>
                    <div class="city-list-container">
                        <div id="cityList" class="city-list"></div>
                    </div>
                </div>

                <div class="details-side">
                    <div class="section-title">City Details</div>
                    <div class="details-content-container">
                        <div id="detailsContent" class="details-content">
                            <div class="empty-details">
                                Select a city to view detailed information
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Mobile Layout (for smaller screens) -->
        <div id="mobileTabs" class="mobile-tabs" style="display: none;">
            <div class="tab-buttons">
                <button class="tab-button active" data-tab="cities">Cities</button>
                <button class="tab-button" data-tab="details">Details</button>
            </div>

            <div class="tab-content" id="citiesTab">
                <div class="results-header" style="margin-bottom: 15px;">
                    <div class="section-title">Search Results</div>
                    <div id="mobileResultsCount" class="results-count"></div>
                </div>
                <div id="mobileCityList" class="city-list"></div>
            </div>

            <div class="tab-content" id="detailsTab" style="display: none;">
                <div class="section-title" style="margin-bottom: 15px;">City Details</div>
                <div id="mobileDetailsContent">
                    <div class="empty-details">
                        Select a city to view detailed information
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        const PROXY_ENDPOINT = '/.netlify/functions/proxy-request';
        const GEO_DB_NOTE_ID = '53d3b57b-137e-483c-b546-b0193f446d7c';
        const DEFAULT_LIMIT = 10;

        // DOM elements
        const cityInput = document.getElementById('cityInput');
        const searchBtn = document.getElementById('searchBtn');
        const alertDiv = document.getElementById('alert');
        const loadingDiv = document.getElementById('loading');
        const desktopResults = document.getElementById('desktopResults');
        const mobileTabs = document.getElementById('mobileTabs');
        const cityList = document.getElementById('cityList');
        const mobileCityList = document.getElementById('mobileCityList');
        const detailsContent = document.getElementById('detailsContent');
        const mobileDetailsContent = document.getElementById('mobileDetailsContent');
        const resultsCount = document.getElementById('resultsCount');
        const mobileResultsCount = document.getElementById('mobileResultsCount');
        const quickButtons = document.querySelectorAll('.quick-search button');
        const tabButtons = document.querySelectorAll('.tab-button');

        let isSearching = false;
        let currentCityId = null;
        let currentCities = [];

        function showAlert(message, type = 'error') {
            alertDiv.textContent = message;
            alertDiv.className = `alert ${type}`;
            alertDiv.style.display = 'block';

            if (type !== 'error') {
                setTimeout(() => {
                    alertDiv.style.display = 'none';
                }, 3000);
            }
        }

        function hideAlert() {
            alertDiv.style.display = 'none';
        }

        function switchTab(tabName) {
            // Update tab buttons
            tabButtons.forEach(btn => {
                btn.classList.remove('active');
                if (btn.dataset.tab === tabName) {
                    btn.classList.add('active');
                }
            });

            // Show selected tab content
            document.getElementById('citiesTab').style.display = tabName === 'cities' ? 'block' : 'none';
            document.getElementById('detailsTab').style.display = tabName === 'details' ? 'block' : 'none';
        }

        async function makeRequest(url, noteId = GEO_DB_NOTE_ID) {
            try {
                const response = await fetch(PROXY_ENDPOINT, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Accept': 'application/json'
                    },
                    body: JSON.stringify({
                        noteId: noteId,
                        targetUrl: url,
                        method: 'GET',
                        options: {
                            headers: {
                                'x-rapidapi-host': 'wft-geo-db.p.rapidapi.com',
                                'x-rapidapi-key': '{{RAPIDAPI_KEY}}'
                            }
                        },
                        debug: false
                    })
                });

                if (!response.ok) {
                    const errorText = await response.text();
                    console.error('Proxy response:', response.status, errorText);

                    let errorMessage = `API Error (${response.status})`;
                    try {
                        const errorData = JSON.parse(errorText);
                        if (errorData.errors && errorData.errors.length > 0) {
                            errorMessage = errorData.errors[0].message || errorMessage;
                        } else if (errorData.message) {
                            errorMessage = errorData.message;
                        }
                    } catch {
                        // If not JSON, keep original message
                    }

                    throw new Error(errorMessage);
                }

                return await response.json();
            } catch (error) {
                console.error('Request failed:', error);
                throw error;
            }
        }

        async function searchCity(cityName = null) {
            if (isSearching) return;

            const city = cityName || cityInput.value.trim();
            if (!city) {
                showAlert('Please enter a city name');
                return;
            }

            if (cityName && !cityInput.value) {
                cityInput.value = cityName;
            }

            isSearching = true;
            searchBtn.disabled = true;
            quickButtons.forEach(btn => btn.disabled = true);

            hideAlert();
            loadingDiv.style.display = 'block';
            desktopResults.style.display = 'none';
            mobileTabs.style.display = 'none';

            try {
                const searchUrl = `https://wft-geo-db.p.rapidapi.com/v1/geo/cities?namePrefix=${encodeURIComponent(city)}&limit=${DEFAULT_LIMIT}&sort=-population`;
                const data = await makeRequest(searchUrl);

                currentCities = data.data || [];

                // Show appropriate layout based on screen size
                if (window.innerWidth >= 1025) {
                    desktopResults.style.display = 'flex';
                    mobileTabs.style.display = 'none';
                    displaySearchResults(currentCities, city, cityList);
                    resultsCount.textContent = `${currentCities.length} cities`;
                } else {
                    desktopResults.style.display = 'none';
                    mobileTabs.style.display = 'flex';
                    displaySearchResults(currentCities, city, mobileCityList);
                    mobileResultsCount.textContent = `${currentCities.length} cities`;
                    switchTab('cities'); // Always show cities tab first on mobile
                }

                if (currentCities.length > 0) {
                    currentCityId = currentCities[0].id;
                    fetchCityDetails(currentCityId);
                }

            } catch (error) {
                console.error('Search error:', error);
                showAlert(`Search failed: ${error.message}`);
            } finally {
                isSearching = false;
                searchBtn.disabled = false;
                quickButtons.forEach(btn => btn.disabled = false);
                loadingDiv.style.display = 'none';
            }
        }

        async function fetchCityDetails(cityId) {
            if (!cityId) return;

            // Update active city in both desktop and mobile lists
            updateActiveCity(cityId);

            try {
                const detailUrl = `https://wft-geo-db.p.rapidapi.com/v1/geo/cities/${cityId}`;
                const data = await makeRequest(detailUrl);
                const city = data.data || data;

                // Update both desktop and mobile details
                displayCityDetails(city, detailsContent);
                displayCityDetails(city, mobileDetailsContent);

                currentCityId = cityId;

                // On mobile, switch to details tab after selection
                if (window.innerWidth < 1025) {
                    switchTab('details');
                }

            } catch (error) {
                console.error('Details error:', error);
                const errorHtml = `
                    <div class="empty-details">
                        Error loading city details
                    </div>
                `;
                detailsContent.innerHTML = errorHtml;
                mobileDetailsContent.innerHTML = errorHtml;
            }
        }

        function updateActiveCity(cityId) {
            // Update desktop city list
            document.querySelectorAll('#cityList .city-item').forEach(item => {
                item.classList.remove('active');
                if (parseInt(item.dataset.cityId) === cityId) {
                    item.classList.add('active');
                }
            });

            // Update mobile city list
            document.querySelectorAll('#mobileCityList .city-item').forEach(item => {
                item.classList.remove('active');
                if (parseInt(item.dataset.cityId) === cityId) {
                    item.classList.add('active');
                }
            });
        }

        function displaySearchResults(cities, searchTerm, targetElement) {
            targetElement.innerHTML = '';

            if (!cities || cities.length === 0) {
                targetElement.innerHTML = '<div class="no-results">No cities found</div>';
                return;
            }

            cities.forEach((city, index) => {
                const item = document.createElement('div');
                item.className = 'city-item';
                item.dataset.cityId = city.id;

                if (index === 0) {
                    item.classList.add('active');
                }

                item.innerHTML = `
                    <div class="city-name">${city.name}, ${city.countryCode}</div>
                    <div class="city-location">${city.region || 'Unknown region'}</div>
                    <div class="city-stats">
                        <span>👥 ${formatNumber(city.population)}</span>
                        <span>📍 ${city.latitude?.toFixed(4)}, ${city.longitude?.toFixed(4)}</span>
                    </div>
                `;

                item.addEventListener('click', () => fetchCityDetails(city.id));
                targetElement.appendChild(item);
            });
        }

        function displayCityDetails(city, targetElement) {
            const elevation = city.elevationMeters ?
                `${formatNumber(city.elevationMeters)}m (${formatNumber(Math.round(city.elevationMeters * 3.28084))}ft)` :
                'N/A';

            targetElement.innerHTML = `
                <div class="details-header">
                    <div class="details-title">${city.name}, ${city.countryCode}</div>
                    <div class="details-subtitle">${city.region || ''} • ${city.country || ''}</div>
                </div>

                <div class="details-grid">
                    <div class="detail-box">
                        <div class="detail-label">Population</div>
                        <div class="detail-value">${formatNumber(city.population)}</div>
                        <div class="detail-sub">Total residents</div>
                    </div>

                    <div class="detail-box">
                        <div class="detail-label">Elevation</div>
                        <div class="detail-value">${elevation.split(' ')[0]}</div>
                        <div class="detail-sub">Above sea level</div>
                    </div>

                    <div class="detail-box">
                        <div class="detail-label">Timezone</div>
                        <div class="detail-value">${city.timezone || 'N/A'}</div>
                        <div class="detail-sub">Local time zone</div>
                    </div>

                    <div class="detail-box">
                        <div class="detail-label">Coordinates</div>
                        <div class="detail-value">${city.latitude?.toFixed(4)}, ${city.longitude?.toFixed(4)}</div>
                        <div class="detail-sub">Latitude, Longitude</div>
                    </div>
                </div>

                <div class="info-row">
                    <div class="info-item">
                        <span class="info-label">Country Code: </span>
                        <span class="info-value">${city.countryCode || 'N/A'}</span>
                    </div>
                    <div class="info-item">
                        <span class="info-label">Region Code: </span>
                        <span class="info-value">${city.regionCode || 'N/A'}</span>
                    </div>
                    ${city.wikiDataId ? `
                    <div class="info-item">
                        <span class="info-label">WikiData ID: </span>
                        <span class="info-value">${city.wikiDataId}</span>
                    </div>
                    ` : ''}
                </div>
            `;
        }

        function formatNumber(num) {
            if (!num) return 'N/A';
            return num.toLocaleString();
        }

        // Event listeners
        searchBtn.addEventListener('click', () => searchCity());

        cityInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') searchCity();
        });

        quickButtons.forEach(button => {
            button.addEventListener('click', () => {
                const city = button.getAttribute('data-city');
                searchCity(city);
            });
        });

        tabButtons.forEach(button => {
            button.addEventListener('click', () => {
                switchTab(button.dataset.tab);
            });
        });

        // Handle window resize
        let resizeTimeout;
        window.addEventListener('resize', () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(() => {
                if (currentCities.length > 0) {
                    if (window.innerWidth >= 1025) {
                        desktopResults.style.display = 'flex';
                        mobileTabs.style.display = 'none';
                        displaySearchResults(currentCities, '', cityList);
                        resultsCount.textContent = `${currentCities.length} cities`;
                    } else {
                        desktopResults.style.display = 'none';
                        mobileTabs.style.display = 'flex';
                        displaySearchResults(currentCities, '', mobileCityList);
                        mobileResultsCount.textContent = `${currentCities.length} cities`;
                    }
                }
            }, 250);
        });

        // Focus input on load
        cityInput.focus();

        // Initialize with a tip (only once)
        if (!localStorage.getItem('cityExplorerTipShown')) {
            setTimeout(() => {
                showAlert('💡 Tip: Search for any city or use the quick buttons to get started', '');
                localStorage.setItem('cityExplorerTipShown', 'true');
            }, 1500);
        }
    </script>

    <script>
        (function syncTheme() {
            try {
                if (window.parent && window.parent !== window) {
                    const parentHtml = window.parent.document.documentElement;
                    const localHtml = document.documentElement;
                    
                    const updateTheme = () => {
                        if (parentHtml.classList.contains('dark')) {
                            localHtml.classList.add('dark');
                        } else {
                            localHtml.classList.remove('dark');
                        }
                    };
                    
                    updateTheme();
                    
                    const observer = new MutationObserver((mutations) => {
                        mutations.forEach((mutation) => {
                            if (mutation.attributeName === 'class') {
                                updateTheme();
                            }
                        });
                    });
                    observer.observe(parentHtml, { attributes: true });
                    return;
                }
            } catch (e) {
                console.warn('Could not sync theme with parent window.', e);
            }
            
            if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                document.documentElement.classList.add('dark');
            }
        })();
    </script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.