<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RampGen 2.0 | Modern Color Ramp Generator</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/lucide@0.263.0/dist/umd/lucide.min.js"></script>
    <!-- Coloris for better color picking -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.css"/>
    <script src="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.4.2/chroma.min.js"></script>
    <link rel="stylesheet" href="assets/css/highlight-darcula.css">
    <script src="assets/js/highlight.js"></script>
    <script>
        tailwind.config = {
            darkMode: 'class',
            theme: {
                extend: {
                    fontFamily: {
                        sans: ['Inter', 'sans-serif'],
                    },
                },
            },
        }
    </script>
    <style>
        body { font-family: 'Inter', sans-serif; }
        .clr-field button { width: 100%; height: 100%; border-radius: 8px; }
        .ramp-preview { height: 64px; border-radius: 12px; overflow: hidden; display: flex; transition: filter 0.3s ease; }
        .stop-item:hover .remove-stop { opacity: 1; }
        [v-cloak] { display: none; }
        /* Custom scrollbar */
        ::-webkit-scrollbar { width: 8px; }
        ::-webkit-scrollbar-track { background: #f1f1f1; }
        .dark ::-webkit-scrollbar-track { background: #1e293b; }
        ::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
        ::-webkit-scrollbar-thumb:hover { background: #555; }
        .dark ::-webkit-scrollbar-thumb { background: #475569; }
        .dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
        #map { height: 400px; border-radius: 12px; transition: filter 0.3s ease; }
        .dark #map { background: #000; }
        
        /* Coloris custom overrides */
        .clr-picker { background-color: white; border-radius: 12px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); border: 1px solid #e2e8f0; }
        .dark .clr-picker { background-color: #1e293b; border-color: #334155; color: white; }
        .clr-field button { border: none !important; }

        /* Vision Simulation Filters */
        .vision-protanopia { filter: url('#protanopia'); }
        .vision-deuteranopia { filter: url('#deuteranopia'); }
        .vision-tritanopia { filter: url('#tritanopia'); }
        .vision-achromatopsia { filter: grayscale(100%); }
        
        /* Ensure tile layer has correct z-index and visibility */
        .leaflet-tile-pane { z-index: 2 !important; }
        .leaflet-pane { z-index: 400; }
        .leaflet-top, .leaflet-bottom { z-index: 1000; }
        .leaflet-layer { opacity: 1 !important; visibility: visible !important; }
        .maplibregl-map { font: inherit; }
        .maplibregl-canvas { outline: none; }
        .ramp-fullscreen-button {
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
        }
        .ramp-fullscreen-button:focus {
            outline: none;
            box-shadow: none;
        }
        .ramp-fullscreen-control .ramp-fullscreen-button {
            font-size: 16px;
            font-weight: 700;
            line-height: 1;
        }
        .dark .leaflet-bar.ramp-fullscreen-control a {
            background: #0f172a;
            color: #e2e8f0;
            border-color: #334155;
        }
        .dark .leaflet-bar.ramp-fullscreen-control a:hover {
            background: #1e293b;
            color: #f8fafc;
        }
        .maplibregl-ctrl-group.ramp-fullscreen-control .maplibregl-ctrl-icon {
            color: #334155;
            background: #ffffff;
        }
        .maplibregl-ctrl-group.ramp-fullscreen-control .maplibregl-ctrl-icon:hover {
            color: #0f172a;
            background: #f8fafc;
        }
        .dark .maplibregl-ctrl-group.ramp-fullscreen-control .maplibregl-ctrl-icon {
            color: #e2e8f0;
            background: #0f172a;
        }
        .dark .maplibregl-ctrl-group.ramp-fullscreen-control .maplibregl-ctrl-icon:hover {
            color: #f8fafc;
            background: #1e293b;
        }
        .dark .maplibregl-ctrl-group.ramp-fullscreen-control {
            border-color: #334155;
        }

        .map-container { position: relative; }
        .map-overlay { position: absolute; top: 10px; right: 10px; z-index: 1000; pointer-events: none; }

        /* Legend styles */
        .info { padding: 12px; font-size: 14px; background: rgba(255, 255, 255, 0.95); box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); border-radius: 8px; min-width: 80px; border: 1px solid #e2e8f0; }
        .dark .info { background: rgba(15, 23, 42, 0.95); box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); color: #f1f5f9; border: 1px solid #334155; }
        .info h4 { margin: 0 0 5px; color: #777; }
        .dark .info h4 { color: #94a3b8; }
        .legend { line-height: 1.5; color: #555; }
        .dark .legend { color: #cbd5e1; }
        .legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.8; }
        .value-tooltip-popup .leaflet-popup-content-wrapper,
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-content {
            border-radius: 8px;
            font-size: 12px;
            line-height: 1.4;
            opacity: 1 !important;
            filter: none !important;
            mix-blend-mode: normal !important;
        }
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-content {
            padding: 10px 30px 10px 10px !important;
            box-shadow: 0 10px 25px rgba(2, 6, 23, 0.25) !important;
        }
        .value-tooltip-popup .leaflet-popup-tip,
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-tip {
            opacity: 1 !important;
            filter: none !important;
        }
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-tip {
            background: transparent !important;
            border-left-color: transparent !important;
            border-right-color: transparent !important;
        }
        .value-tooltip-popup .leaflet-popup-content-wrapper *,
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-content * {
            opacity: 1 !important;
            text-shadow: none !important;
            filter: none !important;
            mix-blend-mode: normal !important;
        }
        .value-tooltip-content { font-size: 12px; line-height: 1.4; color: inherit; font-weight: 600; }
        .value-tooltip-content strong,
        .value-tooltip-content span { color: inherit; }
        .maplibregl-popup.value-tooltip-popup,
        .maplibregl-popup.value-tooltip-popup * {
            opacity: 1 !important;
            filter: none !important;
        }
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-close-button {
            right: 6px;
            top: 4px;
            width: 20px;
            height: 20px;
            font-size: 18px;
            font-weight: 700;
            line-height: 18px;
            opacity: 1 !important;
            text-shadow: none !important;
            border: 0 !important;
            outline: none !important;
            box-shadow: none !important;
            background: transparent !important;
            appearance: none !important;
            -webkit-appearance: none !important;
        }
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-close-button:focus,
        .maplibregl-popup.value-tooltip-popup .maplibregl-popup-close-button:focus-visible {
            border: 0 !important;
            outline: none !important;
            box-shadow: none !important;
        }
        .value-tooltip-popup.value-tooltip-light .leaflet-popup-content-wrapper,
        .maplibregl-popup.value-tooltip-popup.value-tooltip-light .maplibregl-popup-content {
            background: #ffffff !important;
            color: #0f172a !important;
            border: 1px solid #cbd5e1 !important;
        }
        .value-tooltip-popup.value-tooltip-light .leaflet-popup-tip,
        .maplibregl-popup.value-tooltip-popup.value-tooltip-light .maplibregl-popup-tip {
            border-top-color: #ffffff !important;
            border-bottom-color: #ffffff !important;
        }
        .value-tooltip-popup.value-tooltip-light .leaflet-popup-content-wrapper *,
        .maplibregl-popup.value-tooltip-popup.value-tooltip-light .maplibregl-popup-content * {
            color: #0f172a !important;
        }
        .maplibregl-popup.value-tooltip-popup.value-tooltip-light .maplibregl-popup-close-button {
            color: #0f172a !important;
        }
        .maplibregl-popup.value-tooltip-popup.value-tooltip-light .maplibregl-popup-close-button:hover {
            color: #0f172a !important;
            background: rgba(15, 23, 42, 0.08) !important;
        }
        .value-tooltip-popup.value-tooltip-dark .leaflet-popup-content-wrapper,
        .maplibregl-popup.value-tooltip-popup.value-tooltip-dark .maplibregl-popup-content {
            background: rgba(15, 23, 42, 0.98) !important;
            color: #f8fafc !important;
            border: 1px solid #334155 !important;
        }
        .value-tooltip-popup.value-tooltip-dark .leaflet-popup-tip,
        .maplibregl-popup.value-tooltip-popup.value-tooltip-dark .maplibregl-popup-tip {
            border-top-color: rgba(15, 23, 42, 0.98) !important;
            border-bottom-color: rgba(15, 23, 42, 0.98) !important;
        }
        .value-tooltip-popup.value-tooltip-dark .leaflet-popup-content-wrapper *,
        .maplibregl-popup.value-tooltip-popup.value-tooltip-dark .maplibregl-popup-content * {
            color: #f8fafc !important;
        }
        .maplibregl-popup.value-tooltip-popup.value-tooltip-dark .maplibregl-popup-close-button {
            color: #f8fafc !important;
        }
        .maplibregl-popup.value-tooltip-popup.value-tooltip-dark .maplibregl-popup-close-button:hover {
            color: #f8fafc !important;
            background: rgba(148, 163, 184, 0.18) !important;
        }

        /* Modal styling */
        .modal { transition: opacity 0.25s ease; }
        body.modal-active { overflow-x: hidden; overflow-y: visible !important; }
        
        /* Dropdown styling */
        .dropdown:hover .dropdown-menu { display: block; }
        .dropdown-menu::before {
            content: "";
            position: absolute;
            top: -10px;
            left: 0;
            width: 100%;
            height: 10px;
        }

        /* Modal Code Scrollbar */
        .custom-scrollbar::-webkit-scrollbar {
            width: 6px;
            height: 6px;
        }
        .custom-scrollbar::-webkit-scrollbar-track {
            background: #1e293b;
            border-radius: 3px;
        }
        .custom-scrollbar::-webkit-scrollbar-thumb {
            background: #475569;
            border-radius: 3px;
        }
        .custom-scrollbar::-webkit-scrollbar-thumb:hover {
            background: #64748b;
        }
    </style>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
    <link href="https://unpkg.com/maplibre-gl@5.6.2/dist/maplibre-gl.css" rel="stylesheet" />
    <script src="https://unpkg.com/maplibre-gl@5.6.2/dist/maplibre-gl.js"></script>
</head>
<body class="bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 min-h-screen flex flex-col transition-colors duration-300">
    <script>
        // Check for dark mode preference
        // Default to dark mode if no preference is saved
        if (localStorage.getItem('darkMode') === 'true' || !('darkMode' in localStorage)) {
            document.documentElement.classList.add('dark');
        } else {
            document.documentElement.classList.remove('dark');
        }
    </script>
    <!-- Header -->
    <header class="bg-slate-900 text-white p-4 shadow-lg border-b border-slate-700">
        <!-- SVG Filters for Color Blindness -->
        <svg style="display: none">
            <defs>
                <filter id="protanopia">
                    <feColorMatrix type="matrix" values="0.567, 0.433, 0, 0, 0 0.558, 0.442, 0, 0, 0 0, 0.242, 0.758, 0, 0 0, 0, 0, 1, 0" />
                </filter>
                <filter id="deuteranopia">
                    <feColorMatrix type="matrix" values="0.625, 0.375, 0, 0, 0 0.7, 0.3, 0, 0, 0 0, 0.3, 0.7, 0, 0 0, 0, 0, 1, 0" />
                </filter>
                <filter id="tritanopia">
                    <feColorMatrix type="matrix" values="0.95, 0.05, 0, 0, 0 0, 0.433, 0.567, 0, 0 0, 0.475, 0.525, 0, 0 0, 0, 0, 1, 0" />
                </filter>
            </defs>
        </svg>
        <div class="container mx-auto flex flex-col sm:flex-row gap-3 sm:gap-0 justify-between sm:items-center">
            <a href="https://rampgenerator.com" class="flex items-center gap-3">
                <div class="relative w-10 h-10 group">
                    <div class="absolute inset-0 bg-gradient-to-tr from-blue-600 via-purple-600 to-pink-600 rounded-xl rotate-6 group-hover:rotate-12 transition-transform duration-300 opacity-50 blur-sm"></div>
                    <div class="relative w-10 h-10 bg-slate-800 rounded-xl flex items-center justify-center shadow-2xl border border-slate-700 overflow-hidden">
                        <svg viewBox="0 0 24 24" fill="none" class="w-7 h-7 transform -rotate-12 group-hover:rotate-0 transition-transform duration-500">
                            <rect x="3" y="4" width="3" height="16" rx="1.5" fill="#60a5fa" />
                            <rect x="8.5" y="4" width="3" height="16" rx="1.5" fill="#a855f7" />
                            <rect x="14" y="4" width="3" height="16" rx="1.5" fill="#f472b6" />
                            <rect x="19.5" y="4" width="1.5" height="16" rx="0.75" fill="#fb7185" />
                        </svg>
                        <div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
                    </div>
                </div>
                <h1 class="text-xl sm:text-2xl font-bold tracking-tighter">RAMPGEN<span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">2.0</span></h1>
            </a>
            <div class="flex flex-wrap items-center gap-2 sm:gap-4">
                <button id="themeToggle" class="p-2 text-slate-400 hover:text-white transition-colors rounded-lg hover:bg-slate-800" title="Toggle Light/Dark Mode">
                    <i data-lucide="sun" class="w-5 h-5 hidden dark:block"></i>
                    <i data-lucide="moon" class="w-5 h-5 block dark:hidden"></i>
                </button>
                <button id="changelogBtn" class="flex items-center gap-2 text-sm text-slate-400 hover:text-white transition-colors" title="View Changelog">
                    <i data-lucide="history" class="w-4 h-4"></i> Changelog
                </button>
                <a href="/account" class="flex items-center gap-2 text-sm text-slate-400 hover:text-white transition-colors" title="Account and Projects">
                    <i data-lucide="user-circle-2" class="w-4 h-4"></i> Account
                </a>
                <a href="original.php" class="text-sm text-slate-400 hover:text-white transition-colors">Classic Version</a>
                <button id="shareBtn" class="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded-lg text-sm font-medium transition-all shadow-md">
                    <i data-lucide="share-2" class="w-4 h-4"></i> Share
                </button>
            </div>
        </div>
    </header>

    <main class="flex-1 container mx-auto p-4 lg:p-8 grid grid-cols-1 lg:grid-cols-12 gap-8">
        <!-- Sidebar Controls -->
        <aside class="lg:col-span-4 space-y-6">

            <section id="accountPanel" class="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800">
                <h2 class="text-lg font-semibold mb-4 flex items-center gap-2 dark:text-white">
                    <i data-lucide="folder-heart" class="w-5 h-5 text-emerald-500"></i> Saved Ramps
                </h2>
                <p id="accountStatus" class="text-xs text-slate-500 dark:text-slate-400 mb-4">Checking login...</p>

                <div class="space-y-3">
                    <div id="quickAuthActions" class="hidden">
                        <a href="/login?next=%2F" class="w-full inline-flex items-center justify-center px-3 py-2 rounded-lg bg-blue-600 text-white text-sm font-semibold hover:bg-blue-500">Sign In to Save Ramps</a>
                    </div>

                    <div id="quickSavedActions" class="hidden space-y-3">
                        <select id="quickProjectSelect" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg text-sm outline-none"></select>
                        <select id="quickSavedRampSelect" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg text-sm outline-none"></select>
                        <div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
                            <button id="quickLoadBtn" class="px-3 py-2 bg-emerald-600 text-white text-sm rounded-lg hover:bg-emerald-500">Load Saved</button>
                            <button id="quickUpdateBtn" class="px-3 py-2 bg-amber-600 text-white text-sm rounded-lg hover:bg-amber-500">Update Selected</button>
                            <button id="quickSaveBtn" class="px-3 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-500">Save As New</button>
                        </div>
                        <a href="/account" class="text-xs text-blue-500 hover:text-blue-400">Manage projects and API keys</a>
                    </div>
                </div>
            </section>

            <section class="bg-white dark:bg-slate-900 p-6 rounded-2xl   shadow-sm border border-slate-200 dark:border-slate-800">
                <div class="flex justify-between items-center mb-4">
                    <h2 class="text-lg font-semibold flex items-center gap-2 dark:text-white">
                        <i data-lucide="paint-brush" class="w-5 h-5 text-blue-500"></i> Colors & Stops
                    </h2>
                    <div class="flex gap-2">
                        <button id="reverseBtn" class="p-2 text-slate-500 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-slate-800 rounded-lg transition-all" title="Reverse Ramp">
                            <i data-lucide="arrow-up-down" class="w-4 h-4"></i>
                        </button>
                    </div>
                </div>
                
                <div id="stopsContainer" class="space-y-3 max-h-[400px] overflow-y-auto pr-2">
                    <!-- Stops will be injected here -->
                </div>

                <button id="addStopBtn" class="w-full mt-4 flex items-center justify-center gap-2 py-3 border-2 border-dashed border-slate-200 dark:border-slate-700 rounded-xl text-slate-500 hover:text-blue-600 hover:border-blue-400 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-slate-800 transition-all font-medium">
                    <i data-lucide="plus-circle" class="w-5 h-5"></i> Add Stop
                </button>
            </section>

            <!-- Presets Section -->
            <section class="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800">
                <div class="flex justify-between items-center mb-4">
                    <h2 class="text-lg font-semibold flex items-center gap-2 dark:text-white">
                        <i data-lucide="library" class="w-5 h-5 text-purple-500"></i> Map Presets
                    </h2>
                    <div>
                        <label class="flex items-center gap-2 cursor-pointer">
                            <input type="checkbox" id="continuousToggle" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500 dark:bg-slate-800 dark:border-slate-700">
                            <span class="text-[11px] font-bold text-slate-400 uppercase tracking-wider">Continuous</span>
                        </label>
                    </div>
                </div>
                <div class="space-y-4">
                    <div>
                        <span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-2">Sequential</span>
                        <div class="grid grid-cols-4 gap-2 mb-4" id="sequentialPresets"></div>
                    </div>
                    <div>
                        <span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-2">Diverging</span>
                        <div class="grid grid-cols-4 gap-2 mb-4" id="divergingPresets"></div>
                    </div>
                    <div>
                        <span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-2">Qualitative</span>
                        <div class="grid grid-cols-4 gap-2" id="qualitativePresets"></div>
                    </div>
                    <div>
                        <span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-2">Colorblind-Safe</span>
                        <div class="grid grid-cols-4 gap-2" id="colorblindPresets"></div>
                    </div>
                </div>
            </section>

            <section class="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800">
                <h2 class="text-lg font-semibold mb-4 flex items-center gap-2 dark:text-white">
                    <i data-lucide="settings" class="w-5 h-5 text-slate-500"></i> Settings
                </h2>
                <div class="space-y-4">
                    <div>
                        <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Number of Steps</label>
                        <input type="number" id="stepsInput" value="10" min="2" max="100" class="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
                    </div>
                    <div class="grid grid-cols-2 gap-4">
                        <div>
                            <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Min Value</label>
                            <input type="number" id="minValueInput" value="0" class="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
                        </div>
                        <div>
                            <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Max Value</label>
                            <input type="number" id="maxValueInput" value="100" class="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
                        </div>
                    </div>
                    <div>
                        <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Blending Mode</label>
                        <select id="modeSelect" class="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
                            <option value="rgb">RGB (Linear)</option>
                            <option value="lch">LCH (Perceptual)</option>
                            <option value="lab">Lab</option>
                            <option value="hsl">HSL</option>
                        </select>
                        <p class="text-[10px] text-slate-400 mt-1 leading-tight">RGB provides standard linear color transitions.</p>
                    </div>
                    <div>
                        <label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Decimals</label>
                        <input type="number" id="decimalsInput" value="2" min="0" max="10" class="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
                    </div>
                </div>
            </section>



        </aside>

        <!-- Main Content -->
        <div class="lg:col-span-8 space-y-8">
            <!-- Preview Section -->
            <section class="bg-white dark:bg-slate-900 p-8 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800">
                <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
                    <div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 w-full sm:w-auto">
                        <div class="flex items-center gap-2">
                            <i data-lucide="eye" class="w-5 h-5 text-orange-500"></i>
                            <span class="text-sm font-bold text-slate-400 uppercase tracking-wider whitespace-nowrap">Color Blindness:</span>
                        </div>
                        <div class="flex items-center">
                            <select id="visionSelect" class="bg-transparent text-lg font-bold dark:text-white outline-none cursor-pointer focus:ring-0 appearance-none pr-6 relative">
                                <option value="none">Normal Vision</option>
                                <option value="protanopia">Protanopia (Red weak)</option>
                                <option value="deuteranopia">Deuteranopia (Green weak)</option>
                                <option value="tritanopia">Tritanopia (Blue weak)</option>
                                <option value="achromatopsia">Achromatopsia (No color)</option>
                            </select>
                            <i data-lucide="chevron-down" class="w-4 h-4 text-slate-400 -ml-5 pointer-events-none"></i>
                        </div>
                    </div>
                    <div id="exportDropdown" class="relative inline-block text-left dropdown w-full sm:w-auto z-[1200]">
                        <button class="inline-flex justify-center items-center gap-2 w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-500 focus:outline-none transition-all shadow-md">
                            <i data-lucide="download" class="w-4 h-4"></i> Export <i data-lucide="chevron-down" class="w-4 h-4"></i>
                        </button>
                        <div class="dropdown-menu absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white dark:bg-slate-800 ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 dark:divide-slate-700 focus:outline-none hidden z-[1300] transition-all">
                            <div class="py-1">
                                <button type="button" onclick="openExportModal('css')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="palette" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> CSS Gradient
                                </button>
                                <button type="button" onclick="openExportModal('json-colors')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="list" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> JSON Colors
                                </button>
                                <button type="button" onclick="openExportModal('json')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="file-json" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> Export JSON (Full)
                                </button>
                                <button type="button" onclick="openExportModal('javascript')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="code" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> Export JavaScript
                                </button>
                                <button type="button" onclick="openExportModal('leaflet')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="map" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> Export Leaflet
                                </button>
                                <button type="button" onclick="openExportModal('maplibre')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="box" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> Export MapLibre
                                </button>
                                <button type="button" onclick="openExportModal('image-url')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="image-plus" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> Export Image URL
                                </button>
                                <button type="button" onclick="openExportModal('api')" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="server" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> API Endpoint
                                </button>
                                <button type="button" id="exportSvgBtn" class="group flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-slate-700 w-full text-left">
                                    <i data-lucide="image" class="w-4 h-4 mr-3 text-gray-400 group-hover:text-blue-500"></i> Download SVG
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
                <div id="rampPreview" class="ramp-preview mb-8 shadow-inner ring-1 ring-slate-200 dark:ring-slate-700"></div>
                
                <div class="border-t border-slate-100 dark:border-slate-800 pt-8">
                    <h3 class="text-lg font-semibold mb-4 flex items-center gap-2 dark:text-white">
                        <i data-lucide="map" class="w-5 h-5 text-green-500"></i> Map Preview
                    </h3>
                    <div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
                        <div>
                            <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Map Engine</label>
                            <select id="mapEngineSelect" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
                                <option value="leaflet">Leaflet</option>
                                <option value="maplibre">MapLibre</option>
                            </select>
                        </div>
                        <div>
                            <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">GeoJSON Sample</label>
                            <select id="geoJsonSampleSelect" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
                                <option value="us_states">US States (Demo)</option>
                                <option value="us_counties">US Counties (Demo)</option>
                            </select>
                        </div>
                        <div>
                            <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Custom GeoJSON</label>
                            <button id="openGeoJsonPickerBtn" type="button" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg text-left hover:border-blue-300 dark:hover:border-blue-500 transition-colors">
                                Choose 
                            </button>
                        </div>
                        <div>
                            <div class="relative mb-1 pr-7">
                                <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider">Choropleth Value Field</label>
                                <button id="choroplethSettingsBtn" type="button" class="absolute right-0 top-1/2 -translate-y-1/2 p-1 rounded-md border border-slate-200 text-slate-500 hover:text-blue-600 hover:border-blue-300 dark:border-slate-700 dark:text-slate-400 dark:hover:text-blue-300 transition-colors" title="Choropleth field settings">
                                    <i data-lucide="settings-2" class="w-3.5 h-3.5"></i>
                                </button>
                            </div>
                            <select id="choroplethPropertySelect" disabled class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all disabled:opacity-60 disabled:cursor-not-allowed">
                                <option value="__auto__">Auto detect</option>
                            </select>
                        </div>
                    </div>
                    <div id="strokeControlsWrap" class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-2.5 mb-3 items-start rounded-xl border border-slate-200/80 dark:border-slate-700/70 bg-slate-50/70 dark:bg-slate-900/55 px-3 py-2.5 sm:px-4 sm:py-3">
                        <div>
                            <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1.5">Border Color</label>
                            <div id="strokeColorTrigger" class="h-[40px] rounded-lg border border-slate-200 dark:border-slate-700 px-3 flex items-center cursor-pointer transition-all">
                                <span id="strokeColorHex" class="font-mono text-sm font-semibold select-none">#FFFFFF</span>
                            </div>
                            <input id="strokeColorInput" type="text" value="#FFFFFF" data-coloris class="stroke-color-picker-input absolute opacity-0 w-0 h-0 pointer-events-none">
                        </div>
                        <div>
                            <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1.5">Border Width</label>
                            <div class="flex items-center gap-2">
                                <input id="strokeWidthSlider" type="range" min="0" max="5" step="0.10" value="0" class="flex-1 accent-blue-600">
                                <input id="strokeWidthInput" type="number" min="0" max="20" step="0.01" value="0" class="w-20 px-2.5 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
                            </div>
                        </div>
                        <div>
                            <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1.5">Border Opacity <span id="strokeOpacityValue" class="ml-1">1.00</span></label>
                            <input id="strokeOpacityInput" type="range" min="0" max="1" step="0.01" value="1" class="w-full accent-blue-600 mt-1">
                        </div>
                    </div>
                    <div id="geoJsonNotice" class="hidden text-xs px-3 py-2 mb-3 rounded-lg bg-amber-50 text-amber-700 border border-amber-200 dark:bg-amber-950/40 dark:text-amber-200 dark:border-amber-900"></div>
                    <div class="map-container">
                        <div id="map" class="shadow-md border border-slate-200 dark:border-slate-800"></div>
                        <div id="geoJsonMapLoading" class="hidden absolute inset-0 z-[1150] flex items-center justify-center rounded-[12px] bg-slate-900/20 dark:bg-slate-950/45 backdrop-blur-[1px]">
                            <div class="px-4 py-3 rounded-xl border border-slate-200 bg-white/95 text-slate-700 shadow-lg dark:border-slate-700 dark:bg-slate-900/95 dark:text-slate-200 flex items-center gap-2">
                                <span class="inline-block w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></span>
                                <span id="geoJsonMapLoadingText" class="text-sm font-medium">Loading GeoJSON...</span>
                            </div>
                        </div>
                        <div id="map3dControls" class="hidden absolute bottom-3 left-3 z-[1100] p-2 rounded-lg border border-slate-300 bg-slate-50/95 shadow-md backdrop-blur-sm dark:border-slate-700 dark:bg-slate-900/90">
                            <button id="map3dToggle" class="min-w-[44px] text-center px-3 py-1.5 text-xs font-semibold rounded-md border border-slate-300 bg-white text-slate-800 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800">3D</button>
                            <div id="map3dHeightWrap" class="hidden mt-2">
                                <label class="block text-[10px] font-bold tracking-wider uppercase text-slate-500 dark:text-slate-300 mb-1">
                                    Height (km) <span id="map3dHeightValue">1.0km</span>
                                </label>
                                <input id="map3dHeightSlider" type="range" min="0" max="1000" step="1" value="1" class="w-40 accent-blue-600">
                            </div>
                        </div>
                        <div id="mapLegend" class="info legend map-overlay"></div>
                    </div>
                </div>

                <div class="border-t border-slate-100 dark:border-slate-800 pt-8">
                    <h3 class="text-lg font-semibold mb-4 flex items-center gap-2 dark:text-white">
                        <i data-lucide="table" class="w-5 h-5 text-indigo-500"></i> Generated Ramp
                    </h3>
                    <div class="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
                        <table class="w-full text-left text-sm">
                            <thead class="bg-slate-50 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
                                <tr>
                                    <th class="px-4 py-3 font-semibold text-slate-700 dark:text-slate-300">#</th>
                                    <th class="px-4 py-3 font-semibold text-slate-700 dark:text-slate-300">Hex Code</th>
                                    <th class="px-4 py-3 font-semibold text-slate-700 dark:text-slate-300 text-right">Value</th>
                                </tr>
                            </thead>
                            <tbody id="rampTableBody" class="dark:text-slate-300">
                                <!-- Table rows injected here -->
                            </tbody>
                        </table>
                    </div>
                </div>
            </section>
        </div>
    </main>

    <footer class="bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 py-6 mt-12">
        <div class="container mx-auto px-4 text-center text-slate-500 dark:text-slate-400 text-sm">
            <div class="mb-2">
                &copy; 2026 <a href="https://www.capitalcoders.ca" target="_blank" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">Capital Coders Inc.</a>
            </div>
            <div class="flex items-center justify-center gap-1">
                Designed & Built with <i data-lucide="heart" class="w-3.5 h-3.5 text-red-500 fill-current"></i> by Jordan Harding
            </div>
        </div>
    </footer>

    <!-- GeoJSON Picker Modal -->
    <div id="geoJsonPickerModal" class="modal fixed w-full h-full top-0 left-0 flex items-center justify-center opacity-0 pointer-events-none z-[2050] p-4 sm:p-6 overflow-y-auto">
        <div class="modal-overlay-geojson-picker absolute w-full h-full bg-slate-900 opacity-50"></div>
        <div class="modal-container bg-white dark:bg-slate-900 w-full max-w-lg mx-auto rounded-2xl shadow-2xl z-50 border border-slate-200 dark:border-slate-800 my-8">
            <div class="py-5 sm:py-6 px-4 sm:px-6">
                <div class="flex justify-between items-center pb-4 border-b border-slate-100 dark:border-slate-800">
                    <h3 class="text-lg font-bold dark:text-white flex items-center gap-2">
                        <i data-lucide="map" class="w-5 h-5 text-blue-500"></i> Custom GeoJSON
                    </h3>
                    <button id="geoJsonPickerCloseBtn" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
                        <i data-lucide="x" class="w-5 h-5 text-slate-400"></i>
                    </button>
                </div>
                <div class="pt-4 space-y-4">
                    <div class="rounded-lg border border-slate-200 dark:border-slate-700 p-3">
                        <p class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Use Local File (Not Uploaded)</p>
                        <input id="geoJsonLocalFileInput" type="file" accept=".geojson,.json,application/geo+json,application/json" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg file:mr-3 file:py-1 file:px-2 file:border-0 file:text-xs file:font-semibold file:bg-blue-100 file:text-blue-700 dark:file:bg-blue-900/50 dark:file:text-blue-300">
                        <p class="mt-1 text-[11px] text-slate-500 dark:text-slate-400">Stays in this browser session only.</p>
                    </div>
                    <div class="rounded-lg border border-slate-200 dark:border-slate-700 p-3">
                        <p class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Use Uploaded GeoJSON (Account)</p>
                        <select id="geoJsonUploadedSelect" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
                            <option value="">Sign in to /account to manage uploads</option>
                        </select>
                        <p id="geoJsonUploadedHint" class="mt-1 text-[11px] text-slate-500 dark:text-slate-400">Upload files from /account.</p>
                    </div>
                </div>
                <div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-5">
                    <button id="geoJsonPickerCancelBtn" class="px-4 py-2 rounded-lg text-slate-600 font-medium hover:bg-slate-50 border border-slate-200 transition-all">Close</button>
                    <button id="geoJsonPickerApplyBtn" class="px-4 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-500">Use Selected Uploaded</button>
                </div>
            </div>
        </div>
    </div>

    <!-- Export Modal -->
    <div id="exportModal" class="modal fixed w-full h-full top-0 left-0 flex items-center justify-center opacity-0 pointer-events-none z-[2000] p-4 sm:p-6 overflow-y-auto">
        <div class="modal-overlay absolute w-full h-full bg-slate-900 opacity-50"></div>
        <div class="modal-container bg-white dark:bg-slate-900 w-full max-w-3xl mx-auto rounded-2xl shadow-2xl z-50 border border-slate-200 dark:border-slate-800 flex flex-col max-h-[90vh] my-8">
            <div class="modal-content py-4 sm:py-6 text-left px-4 sm:px-8 flex flex-col overflow-hidden">
                <div class="flex justify-between items-center pb-4 border-b border-slate-100 dark:border-slate-800 flex-none">
                    <h3 class="text-xl font-bold flex items-center gap-2 dark:text-white" id="modalTitle">
                        <i data-lucide="code" class="w-6 h-6 text-blue-500"></i> Export Code
                    </h3>
                    <button onclick="closeModal()" class="modal-close p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
                        <i data-lucide="x" class="w-6 h-6 text-slate-400"></i>
                    </button>
                </div>
                
                <div class="mt-6 flex-1 overflow-hidden flex flex-col min-h-0">
                    <div class="bg-slate-900 rounded-xl p-4 sm:p-6 relative group flex-1 overflow-hidden flex flex-col">
                        <pre class="overflow-auto custom-scrollbar flex-1"><code id="modalCode" class="text-sm font-mono leading-relaxed"></code></pre>
                        <button id="modalCopyBtn" class="absolute top-4 right-4 p-2 bg-slate-800 text-slate-300 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity hover:text-white border border-slate-700">
                            <i data-lucide="copy" class="w-4 h-4"></i>
                        </button>
                    </div>
                </div>

                <div class="flex flex-col-reverse sm:flex-row justify-end pt-6 mt-4 border-t border-slate-100 gap-3 flex-none">
                    <button onclick="closeModal()" class="px-6 py-2 rounded-lg text-slate-600 font-medium hover:bg-slate-50 border border-slate-200 transition-all">Close</button>
                    <button id="modalDownloadBtn" class="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-500 transition-all shadow-md">Download</button>
                </div>
            </div>
        </div>
    </div>

    <!-- Changelog Modal -->
    <div id="changelogModal" class="modal fixed w-full h-full top-0 left-0 flex items-center justify-center opacity-0 pointer-events-none z-[2100] p-4 sm:p-6 overflow-y-auto">
        <div class="modal-overlay-changelog absolute w-full h-full bg-slate-900 opacity-50"></div>
        <div class="modal-container bg-white dark:bg-slate-900 w-full max-w-2xl mx-auto rounded-2xl shadow-2xl z-50 border border-slate-200 dark:border-slate-800 flex flex-col max-h-[85vh] my-8">
            <div class="modal-content py-4 sm:py-6 text-left px-4 sm:px-8 flex flex-col overflow-hidden">
                <div class="flex justify-between items-center pb-4 border-b border-slate-100 dark:border-slate-800 flex-none">
                    <h3 class="text-xl font-bold flex items-center gap-2 dark:text-white">
                        <i data-lucide="history" class="w-6 h-6 text-blue-500"></i> Changelog
                    </h3>
                    <button onclick="closeChangelogModal()" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
                        <i data-lucide="x" class="w-6 h-6 text-slate-400"></i>
                    </button>
                </div>

                <div class="mt-6 space-y-6 overflow-auto custom-scrollbar pr-2">
                    <section>
                        <p class="text-xs font-bold uppercase tracking-wider text-cyan-500 mb-2">March 23, 2026 (Polish & Fixes)</p>
                        <ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Added <code>Export MapLibre</code> option with generated discrete and continuous MapLibre snippets.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Added/adjusted MapLibre 3D controls (bottom-left toggle, dynamic height slider, and smoother slider performance).</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Improved 3D URL restore: <code>map3d</code>, <code>map3dh</code>, <code>pitch</code>, and <code>bearing</code> now load more reliably with bird&apos;s-eye fallback.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Fixed map recoloring when changing step count by updating feature colors and restyling existing layers directly.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Updated Colors &amp; Stops rows to use full-row color backgrounds and row-wide click-to-open color picker.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Fixed color-picker drag behavior so picker stays open while colors update live.</span></li>
                        </ul>
                    </section>
                    <section>
                        <p class="text-xs font-bold uppercase tracking-wider text-emerald-500 mb-2">March 23, 2026 (Map Upgrade)</p>
                        <ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Added dual map engine support with a live <code>Leaflet / MapLibre</code> selector.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Added GeoJSON sample switching and custom GeoJSON upload in map preview.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Set <code>us-county.json</code> as the county sample and reduced file size by stripping non-essential feature properties.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Unified feature color assignment so Leaflet and MapLibre render matching colors.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Removed polygon borders on both engines and disabled Leaflet polygon simplification.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Added bookmarkable map view state in URL (<code>lat</code>, <code>lon</code>, <code>z</code>, <code>pitch</code>, <code>bearing</code>).</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Added MapLibre 3D controls in the bottom-left with dynamic height slider and URL persistence (<code>map3d</code>, <code>map3dh</code>).</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Improved 3D slider responsiveness on counties by batching height updates and throttling URL writes.</span></li>
                        </ul>
                    </section>
                    <section>
                        <p class="text-xs font-bold uppercase tracking-wider text-blue-500 mb-2">March 23, 2026</p>
                        <ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Fixed export dropdown layering so it appears above the map.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Added clean API endpoint support via <code>/api/</code> rewrite.</span></li>
                            <li class="flex items-start gap-2"><i data-lucide="check-circle-2" class="w-4 h-4 mt-0.5 text-green-500"></i><span>Updated API export links to use <code>/api/</code> instead of <code>ramp.php</code>.</span></li>
                        </ul>
                    </section>
                    <section>
                        <p class="text-xs font-bold uppercase tracking-wider text-purple-500 mb-2">Previous</p>
                        <ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
                            <li class="flex items-start gap-2"><i data-lucide="sparkles" class="w-4 h-4 mt-0.5 text-purple-500"></i><span>Introduced the modern RampGen 2.0 interface with live map preview and export tools.</span></li>
                        </ul>
                    </section>
                </div>

                <div class="flex justify-end pt-6 mt-4 border-t border-slate-100 gap-3 flex-none">
                    <button onclick="closeChangelogModal()" class="px-6 py-2 rounded-lg text-slate-600 font-medium hover:bg-slate-50 border border-slate-200 transition-all">Close</button>
                </div>
            </div>
        </div>
    </div>

    <div id="saveAsModal" class="modal fixed w-full h-full top-0 left-0 flex items-center justify-center opacity-0 pointer-events-none z-[2200] p-4 sm:p-6 overflow-y-auto">
        <div class="modal-overlay-save absolute w-full h-full bg-slate-900 opacity-50"></div>
        <div class="modal-container bg-white dark:bg-slate-900 w-full max-w-md mx-auto rounded-2xl shadow-2xl z-50 border border-slate-200 dark:border-slate-800 my-8">
            <div class="py-5 sm:py-6 px-4 sm:px-6">
                <div class="flex justify-between items-center pb-4 border-b border-slate-100 dark:border-slate-800">
                    <h3 class="text-lg font-bold dark:text-white flex items-center gap-2">
                        <i data-lucide="save" class="w-5 h-5 text-indigo-500"></i> Save Ramp As
                    </h3>
                    <button id="saveAsCloseBtn" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
                        <i data-lucide="x" class="w-5 h-5 text-slate-400"></i>
                    </button>
                </div>
                <div class="pt-4 space-y-3">
                    <input id="saveAsTitleInput" type="text" placeholder="Ramp title" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 dark:text-white rounded-lg outline-none">
                    <p class="text-xs text-slate-500 dark:text-slate-400">This creates a new saved ramp in the selected project.</p>
                </div>
                <div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-5">
                    <button id="saveAsCancelBtn" class="px-4 py-2 rounded-lg text-slate-600 font-medium hover:bg-slate-50 border border-slate-200 transition-all">Cancel</button>
                    <button id="saveAsConfirmBtn" class="px-4 py-2 rounded-lg bg-indigo-600 text-white font-medium hover:bg-indigo-500">Save</button>
                </div>
            </div>
        </div>
    </div>

    <div id="choroplethSettingsModal" class="modal fixed w-full h-full top-0 left-0 flex items-center justify-center opacity-0 pointer-events-none z-[2250] p-4 sm:p-6 overflow-y-auto">
        <div class="modal-overlay-choropleth absolute w-full h-full bg-slate-900 opacity-50"></div>
        <div class="modal-container bg-white dark:bg-slate-900 w-full max-w-md mx-auto rounded-2xl shadow-2xl z-50 border border-slate-200 dark:border-slate-800 my-8">
            <div class="py-5 sm:py-6 px-4 sm:px-6">
                <div class="flex justify-between items-center pb-4 border-b border-slate-100 dark:border-slate-800">
                    <h3 class="text-lg font-bold dark:text-white flex items-center gap-2">
                        <i data-lucide="settings-2" class="w-5 h-5 text-blue-500"></i> Choropleth Settings
                    </h3>
                    <button id="choroplethSettingsCloseBtn" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
                        <i data-lucide="x" class="w-5 h-5 text-slate-400"></i>
                    </button>
                </div>
                <div class="pt-4 space-y-4">
                    <div class="rounded-lg border border-slate-200 dark:border-slate-700 p-3 bg-slate-50 dark:bg-slate-800/60">
                        <div class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Selected Field</div>
                        <div id="choroplethStatsField" class="font-mono text-sm text-slate-700 dark:text-slate-200 break-all">Auto detect</div>
                        <div id="choroplethStatsRange" class="mt-2 text-xs text-slate-600 dark:text-slate-300">Min: - | Max: -</div>
                    </div>
                    <label class="flex items-center justify-between gap-3 p-3 rounded-lg border border-slate-200 dark:border-slate-700">
                        <span class="text-sm text-slate-700 dark:text-slate-200">Automatically use min/max on field change</span>
                        <input id="autoFieldMinMaxToggle" type="checkbox" class="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
                    </label>
                    <label class="flex items-center justify-between gap-3 p-3 rounded-lg border border-slate-200 dark:border-slate-700">
                        <span class="text-sm text-slate-700 dark:text-slate-200">Show value on click</span>
                        <input id="showFeatureValueToggle" type="checkbox" class="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
                    </label>
                </div>
                <div class="flex justify-end gap-2 pt-5">
                    <button id="choroplethSettingsCloseFooterBtn" class="px-4 py-2 rounded-lg text-slate-600 font-medium hover:bg-slate-50 border border-slate-200 transition-all">Close</button>
                </div>
            </div>
        </div>
    </div>

    <script>
        // --- Core Logic ---
        const colorPresets = {
            sequential: {
                'Blues': ['#eff3ff', '#bdd7e7', '#6baed6', '#3182bd', '#08519c'],
                'Greens': ['#edf8e9', '#bae4b3', '#74c476', '#31a354', '#006d2c'],
                'Oranges': ['#feedde', '#fdbe85', '#fd8d3c', '#e6550d', '#a63603'],
                'Purples': ['#f2f0f7', '#cbc9e2', '#9e9ac8', '#756bb1', '#54278f'],
                'Reds': ['#fee5d9', '#fcae91', '#fb6a4a', '#de2d26', '#a50f15'],
                'Greys': ['#f7f7f7', '#cccccc', '#969696', '#636363', '#252525'],
                'YlGnBu': ['#ffffd9', '#edf8b1', '#c7e9b4', '#7fcdbb', '#41b6c4', '#1d91c0', '#225ea8', '#253494', '#081d58'],
                'YlOrRd': ['#ffffcc', '#ffeda0', '#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c', '#bd0026', '#800026'],
                'BuPu': ['#f7fcfd', '#e0ecf4', '#bfd3e6', '#9ebcda', '#8c96c6', '#8c6bb1', '#88419d', '#810f7c', '#4d004b'],
                'Magma': ['#000004', '#3b0f70', '#8c2981', '#de4968', '#fe9f6d', '#fcfdbf'],
                'Viridis': ['#440154', '#3b528b', '#21918c', '#5ec962', '#fde725'],
                'Inferno': ['#000004', '#420a68', '#932667', '#dd513a', '#fca50a', '#fcffa4'],
                'Plasma': ['#0d0887', '#46039f', '#7201a8', '#9c179e', '#bd3786', '#d8576b', '#ed7953', '#fb9f3a', '#fdca26', '#f0f921'],
                'Cividis': ['#00224e', '#123570', '#3b496c', '#575d6d', '#707173', '#8a8678', '#a59c74', '#c3b369', '#e1cc55', '#fee838'],
                'Turbo': ['#30123b', '#4144a4', '#4777ef', '#3da9fc', '#23d3ce', '#26ec94', '#66fd55', '#a4fc3b', '#d2e935', '#f1ca3a', '#fe9b2d', '#f1601b', '#d23105', '#a41001', '#7a0403']
            },
            diverging: {
                'RdBu': ['#b2182b', '#ef8a62', '#fddbc7', '#f7f7f7', '#d1e5f0', '#67a9cf', '#2166ac'],
                'PiYG': ['#8e0152', '#c51b7d', '#de77ae', '#f1b6da', '#fde0ef', '#f7f7f7', '#e6f5d0', '#b8e186', '#7fbc41', '#4d9221', '#276419'],
                'PRGn': ['#40004b', '#762a83', '#9970ab', '#c2a5cf', '#e7d4e8', '#f7f7f7', '#d9f0d3', '#a6dba0', '#5aae61', '#1b7837', '#00441b'],
                'BrBG': ['#543005', '#8c510a', '#bf812d', '#dfc27d', '#f6e8c3', '#f7f7f7', '#c7eae5', '#80cdc1', '#35978f', '#01665e', '#003c30'],
                'RdYlGn': ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#d9ef8b', '#a6d96a', '#66bd63', '#1a9850', '#006837'],
                'RdYlBu': ['#a50026', '#d73027', '#f46d43', '#fdae61', '#fee090', '#ffffbf', '#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'],
                'Spectral': ['#9e0142', '#d53e4f', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#e6f598', '#abdda4', '#66c2a5', '#3288bd', '#5e4fa2'],
                'RdGy': ['#67001f', '#b2182b', '#d6604d', '#f4a582', '#fddbc7', '#ffffff', '#e0e0e0', '#bababa', '#878787', '#4d4d4d', '#1a1a1a']
            },
            qualitative: {
                'Set1': ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628', '#f781bf', '#999999'],
                'Set2': ['#66c2a5', '#fc8d62', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', '#e5c494', '#b3b3b3'],
                'Set3': ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd', '#ccebc5', '#ffed6f'],
                'Paired': ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#b15928'],
                'Pastel1': ['#fbb4ae', '#b3cde3', '#ccebc5', '#decbe4', '#fed9a6', '#ffffcc', '#e5d8bd', '#fddaec', '#f2f2f2'],
                'Dark2': ['#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#66a61e', '#e6ab02', '#a6761d', '#666666']
            },
            colorblind: {
                'Viridis 9': ['#440154', '#482878', '#3e4989', '#31688e', '#26828e', '#1f9e89', '#35b779', '#6ece58', '#fde725'],
                'Cividis 9': ['#00224e', '#273b71', '#3f4f77', '#58636f', '#71776c', '#8b8c6d', '#a7a26c', '#c5ba68', '#fee838'],
                'Magma 9': ['#000004', '#1b0c41', '#4f117b', '#812581', '#b5367a', '#e55063', '#fb8761', '#fec287', '#fcfdbf'],
                'Inferno 9': ['#000004', '#1f0c48', '#550f6d', '#88226a', '#b73759', '#e35933', '#f98c0a', '#fbcf3a', '#fcffa4'],
                'Okabe-Ito 8': ['#000000', '#e69f00', '#56b4e9', '#009e73', '#f0e442', '#0072b2', '#d55e00', '#cc79a7'],
                'Tol Bright 7': ['#4477aa', '#66ccee', '#228833', '#ccbb44', '#ee6677', '#aa3377', '#bbbbbb']
            }
        };

        const IS_DIRECT_RAMPGENERATOR_ACCESS = (() => {
            const host = String(window.location.hostname || '').toLowerCase();
            const isRampHost = host === 'rampgenerator.com' || host === 'www.rampgenerator.com';
            if (!isRampHost) return false;
            const path = String(window.location.pathname || '').toLowerCase();
            const isDirectPath = path === '/' || path === '/index.php';
            return isDirectPath;
        })();
        const INITIAL_MAP3D_HEIGHT_DEFAULT = IS_DIRECT_RAMPGENERATOR_ACCESS ? 100 : 1;

        const state = {
            stops: [
                { id: 1, color: '#ffffd9' },
                { id: 2, color: '#edf8b1' },
                { id: 3, color: '#c7e9b4' },
                { id: 4, color: '#7fcdbb' },
                { id: 5, color: '#41b6c4' },
                { id: 6, color: '#1d91c0' },
                { id: 7, color: '#225ea8' },
                { id: 8, color: '#253494' },
                { id: 9, color: '#081d58' }
            ],
            steps: 10,
            minValue: 0,
            maxValue: 100,
            isContinuous: false,
            decimals: 2,
            mode: 'rgb', // default mode
            vision: 'none',
            mapEngine: 'maplibre',
            geoJsonSample: 'us_counties',
            geoJsonShareId: '',
            map3d: true,
            map3dHeight: INITIAL_MAP3D_HEIGHT_DEFAULT,
            strokeColor: '#FFFFFF',
            strokeWidth: 0,
            strokeOpacity: 1,
            mapCameraCustom: true,
            mapLat: 35.693337,
            mapLon: -89.291772,
            mapZoom: 4.49,
            mapPitch: 60,
            mapBearing: -25,
            mapClickLat: null,
            mapClickLon: null,
            mapClickValue: null,
            mapClickLabel: '',
            geoJsonIsCustom: false,
            choroplethProperty: '__auto__',
            autoUseFieldMinMax: true,
            showFeatureValueOnClick: false,
            currentProjectId: null,
            currentRampId: null
        };

        const accountState = {
            authenticated: false,
            user: null,
            projects: [],
            ramps: [],
            geojsonUploads: []
        };

        // --- Map Setup ---
        let leafletMap, leafletGeoJsonLayer, leafletTileLayer;
        let mapLibreMap;
        let mapLibreCameraSyncReady = false;
        let map3dHeightRafId = null;
        let map3dUrlDebounceId = null;
        let mapFullscreenButtons = [];
        let geoJsonBusyCount = 0;
        let forceFitDataBounds = false;
        let statesData = null;
        let mapLibreValuePopup = null;
        const DEFAULT_MAP_VIEW = { lat: 37.8, lng: -96, zoom: 4 };
        const MAP_TILES = {
            light: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
            dark: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
        };
        const GEOJSON_SAMPLES = {
            us_states: 'us-states.geojson.php',
            us_counties: 'us-county.json'
        };
        const MAPLIBRE_SOURCE_ID = 'ramp-source';
        const MAPLIBRE_FILL_LAYER_ID = 'ramp-fill';
        const MAPLIBRE_LINE_LAYER_ID = 'ramp-outline';
        const MAPLIBRE_EXTRUSION_LAYER_ID = 'ramp-extrusion';
        const MAPLIBRE_BIRDSEYE = { pitch: 60, bearing: -25 };
        const MAP3D_HEIGHT_MIN = 0;
        const MAP3D_HEIGHT_MAX = 1000;
        const MAP3D_HEIGHT_DEFAULT = INITIAL_MAP3D_HEIGHT_DEFAULT;
        const CUSTOM_GEOJSON_MAX_BYTES = 120 * 1024 * 1024; // Hard cap to avoid tab crashes.
        const CUSTOM_GEOJSON_3D_DISABLE_BYTES = 35 * 1024 * 1024; // Disable 3D on larger datasets.
        const CHOROPLETH_AUTO_PROPERTY = '__auto__';
        const APP_API_ENDPOINT = 'app-api/';

        async function appApi(action, method = 'GET', payload = null, query = null) {
            const params = new URLSearchParams(query || {});
            params.set('action', action);
            const response = await fetch(`${APP_API_ENDPOINT}?${params.toString()}`, {
                method,
                headers: method === 'GET' ? {} : { 'Content-Type': 'application/json' },
                credentials: 'same-origin',
                body: method === 'GET' ? null : JSON.stringify(payload || {})
            });
            const data = await response.json().catch(() => ({}));
            if (!response.ok || data.ok === false) {
                throw new Error(data.error || `Request failed (${response.status})`);
            }
            return data;
        }

        function trackAnalyticsEvent(eventType, meta = {}) {
            appApi('analytics.track', 'POST', {
                event_type: eventType,
                meta
            }).catch(() => {});
        }

        let builderAnalyticsEnabled = false;
        let builderAnalyticsTimer = null;
        let builderAnalyticsState = null;
        let builderAnalyticsSignature = '';
        const builderAnalyticsReasons = new Set();

        function getBuilderAnalyticsState() {
            return {
                stops: state.stops.map((s) => s.color),
                steps: state.steps,
                min_value: state.minValue,
                max_value: state.maxValue,
                decimals: state.decimals,
                mode: state.mode,
                vision: state.vision,
                is_continuous: !!state.isContinuous,
                map_engine: state.mapEngine,
                map_3d: !!state.map3d,
                map_3d_height: Number(state.map3dHeight),
                stroke_color: state.strokeColor,
                stroke_width: Number(state.strokeWidth),
                stroke_opacity: Number(state.strokeOpacity),
                geojson_sample: state.geoJsonSample,
                has_geojson_upload: !!state.geoJsonShareId,
                choropleth_property: state.choroplethProperty || CHOROPLETH_AUTO_PROPERTY,
                auto_field_minmax: !!state.autoUseFieldMinMax,
                show_value_click: !!state.showFeatureValueOnClick
            };
        }

        function diffBuilderAnalyticsKeys(previousState, nextState) {
            if (!previousState) {
                return Object.keys(nextState);
            }
            const keys = Object.keys(nextState);
            const changed = [];
            keys.forEach((key) => {
                if (JSON.stringify(previousState[key]) !== JSON.stringify(nextState[key])) {
                    changed.push(key);
                }
            });
            return changed;
        }

        function flushBuilderAnalyticsTracking() {
            builderAnalyticsTimer = null;
            if (!builderAnalyticsEnabled) return;

            const nextState = getBuilderAnalyticsState();
            const nextSignature = JSON.stringify(nextState);
            if (nextSignature === builderAnalyticsSignature) {
                builderAnalyticsReasons.clear();
                return;
            }

            const changedKeys = diffBuilderAnalyticsKeys(builderAnalyticsState, nextState);
            const reasons = Array.from(builderAnalyticsReasons);
            builderAnalyticsReasons.clear();
            builderAnalyticsState = nextState;
            builderAnalyticsSignature = nextSignature;

            trackAnalyticsEvent('builder_config_change', {
                reasons,
                changed_keys: changedKeys,
                stops_count: nextState.stops.length,
                mode: nextState.mode,
                is_continuous: nextState.is_continuous,
                map_engine: nextState.map_engine,
                map_3d: nextState.map_3d,
                geojson_sample: nextState.geojson_sample,
                has_geojson_upload: nextState.has_geojson_upload,
                auto_field_minmax: nextState.auto_field_minmax
            });
        }

        function queueBuilderAnalyticsTracking(reason = 'builder_update') {
            if (!builderAnalyticsEnabled) return;
            builderAnalyticsReasons.add(reason);
            if (builderAnalyticsTimer !== null) {
                clearTimeout(builderAnalyticsTimer);
            }
            builderAnalyticsTimer = setTimeout(flushBuilderAnalyticsTracking, 1200);
        }

        function initializeBuilderAnalyticsBaseline() {
            builderAnalyticsState = getBuilderAnalyticsState();
            builderAnalyticsSignature = JSON.stringify(builderAnalyticsState);
            builderAnalyticsEnabled = true;
        }

        function serializeCurrentState() {
            return {
                stops: state.stops.map(s => ({ id: s.id, color: s.color })),
                steps: state.steps,
                minValue: state.minValue,
                maxValue: state.maxValue,
                isContinuous: state.isContinuous,
                decimals: state.decimals,
                mode: state.mode,
                vision: state.vision,
                mapEngine: state.mapEngine,
                geoJsonSample: state.geoJsonSample,
                geoJsonShareId: state.geoJsonShareId,
                map3d: state.map3d,
                map3dHeight: state.map3dHeight,
                strokeColor: state.strokeColor,
                strokeWidth: state.strokeWidth,
                strokeOpacity: state.strokeOpacity,
                mapCameraCustom: state.mapCameraCustom,
                mapLat: state.mapLat,
                mapLon: state.mapLon,
                mapZoom: state.mapZoom,
                mapPitch: state.mapPitch,
                mapBearing: state.mapBearing,
                mapClickLat: state.mapClickLat,
                mapClickLon: state.mapClickLon,
                mapClickValue: state.mapClickValue,
                mapClickLabel: state.mapClickLabel,
                choroplethProperty: state.choroplethProperty,
                autoUseFieldMinMax: state.autoUseFieldMinMax,
                showFeatureValueOnClick: state.showFeatureValueOnClick
            };
        }

        async function applySavedState(savedState) {
            if (!savedState || typeof savedState !== 'object') return;
            const restoredStops = Array.isArray(savedState.stops) ? savedState.stops : [];
            if (restoredStops.length >= 2) {
                state.stops = restoredStops.map((s, i) => ({
                    id: s && s.id ? s.id : (Date.now() + i),
                    color: (s && s.color) ? s.color : '#000000'
                }));
            }
            if (Number.isFinite(Number(savedState.steps))) state.steps = Number(savedState.steps);
            if (Number.isFinite(Number(savedState.minValue))) state.minValue = Number(savedState.minValue);
            if (Number.isFinite(Number(savedState.maxValue))) state.maxValue = Number(savedState.maxValue);
            if (typeof savedState.isContinuous === 'boolean') state.isContinuous = savedState.isContinuous;
            if (Number.isFinite(Number(savedState.decimals))) state.decimals = Number(savedState.decimals);
            if (typeof savedState.mode === 'string') state.mode = savedState.mode;
            if (typeof savedState.vision === 'string') state.vision = savedState.vision;
            if (typeof savedState.mapEngine === 'string') state.mapEngine = savedState.mapEngine;
            if (typeof savedState.geoJsonSample === 'string') state.geoJsonSample = savedState.geoJsonSample;
            if (typeof savedState.geoJsonShareId === 'string') state.geoJsonShareId = savedState.geoJsonShareId;
            if (typeof savedState.map3d === 'boolean') state.map3d = savedState.map3d;
            if (Number.isFinite(Number(savedState.map3dHeight))) state.map3dHeight = Number(savedState.map3dHeight);
            if (typeof savedState.strokeColor === 'string') state.strokeColor = savedState.strokeColor;
            if (Number.isFinite(Number(savedState.strokeWidth))) state.strokeWidth = Number(savedState.strokeWidth);
            if (Number.isFinite(Number(savedState.strokeOpacity))) state.strokeOpacity = Number(savedState.strokeOpacity);
            if (state.map3dHeight > MAP3D_HEIGHT_MAX && state.map3dHeight <= 1000000) {
                // Backward compatibility: older state stored this value in meters.
                state.map3dHeight = state.map3dHeight / 1000;
            }
            if (!Number.isFinite(state.map3dHeight)) state.map3dHeight = MAP3D_HEIGHT_DEFAULT;
            if (state.map3dHeight < MAP3D_HEIGHT_MIN) state.map3dHeight = MAP3D_HEIGHT_MIN;
            if (state.map3dHeight > MAP3D_HEIGHT_MAX) state.map3dHeight = MAP3D_HEIGHT_MAX;
            if (!/^#[0-9a-fA-F]{6}$/.test(state.strokeColor)) state.strokeColor = '#FFFFFF';
            if (!Number.isFinite(state.strokeWidth) || state.strokeWidth < 0) state.strokeWidth = 0;
            if (state.strokeWidth > 20) state.strokeWidth = 20;
            if (!Number.isFinite(state.strokeOpacity)) state.strokeOpacity = 1;
            if (state.strokeOpacity < 0) state.strokeOpacity = 0;
            if (state.strokeOpacity > 1) state.strokeOpacity = 1;
            if (typeof savedState.mapCameraCustom === 'boolean') state.mapCameraCustom = savedState.mapCameraCustom;
            if (Number.isFinite(Number(savedState.mapLat))) state.mapLat = Number(savedState.mapLat);
            if (Number.isFinite(Number(savedState.mapLon))) state.mapLon = Number(savedState.mapLon);
            if (Number.isFinite(Number(savedState.mapZoom))) state.mapZoom = Number(savedState.mapZoom);
            if (Number.isFinite(Number(savedState.mapPitch))) state.mapPitch = Number(savedState.mapPitch);
            if (Number.isFinite(Number(savedState.mapBearing))) state.mapBearing = Number(savedState.mapBearing);
            if (Number.isFinite(Number(savedState.mapClickLat))) state.mapClickLat = Number(savedState.mapClickLat);
            if (Number.isFinite(Number(savedState.mapClickLon))) state.mapClickLon = Number(savedState.mapClickLon);
            if (Number.isFinite(Number(savedState.mapClickValue))) state.mapClickValue = Number(savedState.mapClickValue);
            if (typeof savedState.mapClickLabel === 'string') state.mapClickLabel = savedState.mapClickLabel;
            if (typeof savedState.choroplethProperty === 'string') state.choroplethProperty = savedState.choroplethProperty;
            if (typeof savedState.autoUseFieldMinMax === 'boolean') state.autoUseFieldMinMax = savedState.autoUseFieldMinMax;
            if (typeof savedState.showFeatureValueOnClick === 'boolean') state.showFeatureValueOnClick = savedState.showFeatureValueOnClick;

            document.getElementById('stepsInput').value = state.steps;
            document.getElementById('minValueInput').value = state.minValue;
            document.getElementById('maxValueInput').value = state.maxValue;
            document.getElementById('decimalsInput').value = state.decimals;
            document.getElementById('continuousToggle').checked = state.isContinuous;
            document.getElementById('modeSelect').value = state.mode;
            document.getElementById('visionSelect').value = state.vision;
            document.getElementById('mapEngineSelect').value = state.mapEngine;
            document.getElementById('geoJsonSampleSelect').value = state.geoJsonSample;
            document.getElementById('choroplethPropertySelect').value = state.choroplethProperty;
            document.getElementById('autoFieldMinMaxToggle').checked = !!state.autoUseFieldMinMax;
            document.getElementById('showFeatureValueToggle').checked = !!state.showFeatureValueOnClick;
            document.getElementById('strokeColorInput').value = state.strokeColor;
            document.getElementById('strokeWidthInput').value = String(state.strokeWidth);
            document.getElementById('strokeWidthSlider').value = String(state.strokeWidth);
            document.getElementById('strokeOpacityInput').value = String(state.strokeOpacity);
            document.getElementById('strokeOpacityValue').textContent = Number(state.strokeOpacity).toFixed(2);

            renderStops();
            renderPresets();
            await initMap();
            updateUI();
        }

        function hasStoredMapView() {
            return Number.isFinite(state.mapLat) && Number.isFinite(state.mapLon) && Number.isFinite(state.mapZoom);
        }

        function saveMapView(lat, lon, zoom) {
            state.mapLat = Number(lat);
            state.mapLon = Number(lon);
            state.mapZoom = Number(zoom);
            updateURL();
        }

        function saveMapCamera(lat, lon, zoom, pitch, bearing) {
            state.mapLat = Number(lat);
            state.mapLon = Number(lon);
            state.mapZoom = Number(zoom);
            state.mapPitch = Number(pitch);
            state.mapBearing = Number(bearing);
            state.mapCameraCustom = true;
            updateURL();
        }

        function scheduleMap3dHeightRender() {
            if (map3dHeightRafId !== null) return;
            map3dHeightRafId = requestAnimationFrame(() => {
                map3dHeightRafId = null;
                if (state.mapEngine === 'maplibre' && state.map3d && mapLibreMap && mapLibreMap.getLayer(MAPLIBRE_EXTRUSION_LAYER_ID)) {
                    mapLibreMap.setPaintProperty(MAPLIBRE_EXTRUSION_LAYER_ID, 'fill-extrusion-height', mapLibreHeightExpression());
                }
            });
        }

        function scheduleUrlUpdate(delay = 120) {
            if (map3dUrlDebounceId !== null) {
                clearTimeout(map3dUrlDebounceId);
            }
            map3dUrlDebounceId = setTimeout(() => {
                map3dUrlDebounceId = null;
                updateURL();
            }, delay);
        }

        function update3dButtonState() {
            const controls = document.getElementById('map3dControls');
            const btn = document.getElementById('map3dToggle');
            const heightWrap = document.getElementById('map3dHeightWrap');
            const heightSlider = document.getElementById('map3dHeightSlider');
            const heightValue = document.getElementById('map3dHeightValue') || document.getElementById('map3dHeigh tValue');
            if (!btn || !controls || !heightWrap || !heightSlider || !heightValue) return;
            const show = state.mapEngine === 'maplibre';
            controls.classList.toggle('hidden', !show);
            const active = !!state.map3d;
            btn.textContent = active ? '2D' : '3D';
            const isDark = document.documentElement.classList.contains('dark');
            if (active) {
                btn.style.backgroundColor = isDark ? '#3b82f6' : '#2563eb';
                btn.style.color = '#ffffff';
                btn.style.borderColor = isDark ? '#3b82f6' : '#2563eb';
            } else {
                btn.style.backgroundColor = isDark ? '#0f172a' : '#ffffff';
                btn.style.color = isDark ? '#e2e8f0' : '#1e293b';
                btn.style.borderColor = isDark ? '#334155' : '#cbd5e1';
            }
            heightWrap.classList.toggle('hidden', !(show && active));
            heightSlider.value = String(state.map3dHeight);
            heightValue.textContent = `${Number(state.map3dHeight).toFixed(1)}km`;
            updateStrokeControlsVisibility();
        }

        function updateStrokeControlsVisibility() {
            const strokeControls = document.getElementById('strokeControlsWrap');
            if (!strokeControls) return;
            const hideForMapLibre3d = state.mapEngine === 'maplibre' && !!state.map3d;
            strokeControls.classList.toggle('hidden', hideForMapLibre3d);
        }

        function setGeoJsonNotice(message = '', isError = false) {
            const el = document.getElementById('geoJsonNotice');
            if (!message) {
                el.classList.add('hidden');
                el.textContent = '';
                return;
            }
            el.textContent = message;
            el.classList.remove('hidden');
            el.classList.toggle('bg-amber-50', !isError);
            el.classList.toggle('text-amber-700', !isError);
            el.classList.toggle('border-amber-200', !isError);
            el.classList.toggle('dark:bg-amber-950/40', !isError);
            el.classList.toggle('dark:text-amber-200', !isError);
            el.classList.toggle('dark:border-amber-900', !isError);
            el.classList.toggle('bg-red-50', isError);
            el.classList.toggle('text-red-700', isError);
            el.classList.toggle('border-red-200', isError);
            el.classList.toggle('dark:bg-red-950/40', isError);
            el.classList.toggle('dark:text-red-200', isError);
            el.classList.toggle('dark:border-red-900', isError);
        }

        function formatBytes(bytes) {
            if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
            const units = ['B', 'KB', 'MB', 'GB'];
            let value = bytes;
            let unitIndex = 0;
            while (value >= 1024 && unitIndex < units.length - 1) {
                value /= 1024;
                unitIndex += 1;
            }
            return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
        }

        function sanitizeGeoJsonShareId(raw) {
            const id = String(raw || '').trim();
            return /^[A-Za-z0-9_-]{8,64}$/.test(id) ? id : '';
        }

        function setGeoJsonLoading(message = '') {
            const el = document.getElementById('geoJsonMapLoading');
            const text = document.getElementById('geoJsonMapLoadingText');
            if (!el || !text) return;
            if (!message) {
                el.classList.add('hidden');
                text.textContent = '';
                return;
            }
            text.textContent = message;
            el.classList.remove('hidden');
        }

        function setGeoJsonControlsBusy(isBusy) {
            const sampleSelect = document.getElementById('geoJsonSampleSelect');
            const engineSelect = document.getElementById('mapEngineSelect');
            const propertySelect = document.getElementById('choroplethPropertySelect');
            const openBtn = document.getElementById('openGeoJsonPickerBtn');
            const localFileInput = document.getElementById('geoJsonLocalFileInput');
            const uploadedSelect = document.getElementById('geoJsonUploadedSelect');
            const applyUploadedBtn = document.getElementById('geoJsonPickerApplyBtn');
            if (sampleSelect) sampleSelect.disabled = !!isBusy;
            if (engineSelect) engineSelect.disabled = !!isBusy;
            if (propertySelect) propertySelect.disabled = !!isBusy || !state.geoJsonIsCustom;
            if (openBtn) openBtn.disabled = !!isBusy;
            if (localFileInput) localFileInput.disabled = !!isBusy;
            if (uploadedSelect) uploadedSelect.disabled = !!isBusy;
            if (applyUploadedBtn) applyUploadedBtn.disabled = !!isBusy;
            updateChoroplethSettingsButtonState();
        }

        function beginGeoJsonBusy(message = 'Loading GeoJSON...') {
            geoJsonBusyCount += 1;
            setGeoJsonLoading(message);
            setGeoJsonControlsBusy(true);
        }

        function endGeoJsonBusy() {
            geoJsonBusyCount = Math.max(0, geoJsonBusyCount - 1);
            if (geoJsonBusyCount !== 0) return;
            setGeoJsonLoading('');
            setGeoJsonControlsBusy(false);
        }

        async function runWithMapProcessing(message, work) {
            beginGeoJsonBusy(message);
            try {
                await new Promise((resolve) => requestAnimationFrame(resolve));
                return await work();
            } finally {
                endGeoJsonBusy();
            }
        }

        function normalizeGeoJsonData(rawData) {
            if (!rawData || typeof rawData !== 'object') {
                throw new Error('Invalid GeoJSON content.');
            }
            const featureCollection = rawData.type === 'FeatureCollection'
                ? rawData
                : (rawData.type === 'Feature' ? { type: 'FeatureCollection', features: [rawData] } : null);
            if (!featureCollection || !Array.isArray(featureCollection.features)) {
                throw new Error('GeoJSON must be a FeatureCollection or Feature.');
            }
            return {
                ...featureCollection,
                features: featureCollection.features
                    .filter(f => f && f.type === 'Feature' && f.geometry)
                    .map(f => ({ ...f, properties: { ...(f.properties || {}) } }))
            };
        }

        function detectNumericGeoJsonProperties(data) {
            if (!data || !Array.isArray(data.features)) return [];
            const stats = new Map();
            data.features.forEach((feature) => {
                const props = (feature && feature.properties && typeof feature.properties === 'object') ? feature.properties : {};
                Object.entries(props).forEach(([key, raw]) => {
                    const parsed = Number(raw);
                    if (!Number.isFinite(parsed)) return;
                    if (!stats.has(key)) stats.set(key, 0);
                    stats.set(key, stats.get(key) + 1);
                });
            });
            return [...stats.entries()]
                .sort((a, b) => {
                    if (b[1] !== a[1]) return b[1] - a[1];
                    return a[0].localeCompare(b[0]);
                })
                .map(([key]) => key);
        }

        function isNoDataValue(rawValue, numericValue) {
            if (!Number.isFinite(numericValue)) return true;
            const normalizedRaw = String(rawValue ?? '').trim().toLowerCase();
            const noDataText = new Set([
                '', 'null', 'none', 'nan', 'na', 'n/a', 'nodata', 'no data', 'missing'
            ]);
            if (noDataText.has(normalizedRaw)) return true;
            const noDataNumbers = new Set([-999, -9999, -99999, -999999, -32768, 32767, 9999, 99999, 999999]);
            if (noDataNumbers.has(numericValue)) return true;
            if (numericValue <= -1e20 || numericValue >= 1e20) return true;
            return false;
        }

        function updateChoroplethSettingsButtonState() {
            const button = document.getElementById('choroplethSettingsBtn');
            const select = document.getElementById('choroplethPropertySelect');
            if (!button || !select) return;
            const disabled = !state.geoJsonIsCustom || select.disabled;
            button.disabled = disabled;
            button.classList.toggle('opacity-50', disabled);
            button.classList.toggle('cursor-not-allowed', disabled);
        }

        function configureChoroplethPropertySelector(data, isCustomGeoJson) {
            const select = document.getElementById('choroplethPropertySelect');
            if (!select) return;

            const numericProperties = detectNumericGeoJsonProperties(data);
            const useSelector = isCustomGeoJson && numericProperties.length > 0;

            select.innerHTML = '';
            const autoOption = document.createElement('option');
            autoOption.value = CHOROPLETH_AUTO_PROPERTY;
            autoOption.textContent = 'Auto detect';
            select.appendChild(autoOption);

            if (useSelector) {
                numericProperties.forEach((propName) => {
                    const option = document.createElement('option');
                    option.value = propName;
                    option.textContent = propName;
                    select.appendChild(option);
                });
            }

            state.geoJsonIsCustom = !!isCustomGeoJson;
            if (!useSelector) {
                state.choroplethProperty = CHOROPLETH_AUTO_PROPERTY;
            } else if (state.choroplethProperty !== CHOROPLETH_AUTO_PROPERTY && !numericProperties.includes(state.choroplethProperty)) {
                state.choroplethProperty = CHOROPLETH_AUTO_PROPERTY;
            }
            select.disabled = !useSelector;
            select.value = state.choroplethProperty;
            updateChoroplethSettingsButtonState();
        }

        function getActiveFeatureValueStats() {
            if (!statesData || !Array.isArray(statesData.features) || !statesData.features.length) {
                return { min: NaN, max: NaN, count: 0 };
            }
            const selectedProperty = getActiveValuePropertyName();
            let min = Infinity;
            let max = -Infinity;
            let count = 0;
            statesData.features.forEach((feature) => {
                const props = feature && feature.properties && typeof feature.properties === 'object' ? feature.properties : {};
                let value;
                if (selectedProperty) {
                    const rawValue = props[selectedProperty];
                    const parsed = Number(rawValue);
                    if (isNoDataValue(rawValue, parsed)) return;
                    value = parsed;
                } else {
                    value = featureNumericValue(feature);
                }
                if (!Number.isFinite(value)) return;
                min = Math.min(min, value);
                max = Math.max(max, value);
                count += 1;
            });
            if (count === 0) return { min: NaN, max: NaN, count: 0 };
            return { min, max, count };
        }

        function applyActiveStatsToRampRange() {
            const stats = getActiveFeatureValueStats();
            if (!Number.isFinite(stats.min) || !Number.isFinite(stats.max)) return false;
            state.minValue = stats.min;
            state.maxValue = stats.max;
            const minInput = document.getElementById('minValueInput');
            const maxInput = document.getElementById('maxValueInput');
            if (minInput) minInput.value = state.minValue;
            if (maxInput) maxInput.value = state.maxValue;
            return true;
        }

        function renderChoroplethSettings() {
            const fieldEl = document.getElementById('choroplethStatsField');
            const rangeEl = document.getElementById('choroplethStatsRange');
            const autoToggle = document.getElementById('autoFieldMinMaxToggle');
            const toggle = document.getElementById('showFeatureValueToggle');
            if (!fieldEl || !rangeEl || !autoToggle || !toggle) return;

            const selectedProperty = getActiveValuePropertyName();
            fieldEl.textContent = selectedProperty || 'Auto detect';

            const stats = getActiveFeatureValueStats();
            if (Number.isFinite(stats.min) && Number.isFinite(stats.max)) {
                rangeEl.textContent = `Min: ${Number(stats.min).toLocaleString()} | Max: ${Number(stats.max).toLocaleString()}`;
            } else {
                rangeEl.textContent = 'Min: - | Max: -';
            }
            autoToggle.checked = !!state.autoUseFieldMinMax;
            toggle.checked = !!state.showFeatureValueOnClick;
        }

        function openChoroplethSettingsModal() {
            const modal = document.getElementById('choroplethSettingsModal');
            if (!modal) return;
            renderChoroplethSettings();
            modal.classList.remove('opacity-0', 'pointer-events-none');
            document.body.classList.add('modal-active');
            lucide.createIcons();
        }

        function closeChoroplethSettingsModal() {
            const modal = document.getElementById('choroplethSettingsModal');
            if (!modal) return;
            modal.classList.add('opacity-0', 'pointer-events-none');
            document.body.classList.remove('modal-active');
        }

        function getActiveValuePropertyName() {
            if (state.geoJsonIsCustom && state.choroplethProperty && state.choroplethProperty !== CHOROPLETH_AUTO_PROPERTY) {
                return state.choroplethProperty;
            }
            return null;
        }

        function featureNumericValue(feature) {
            if (!feature || typeof feature !== 'object') {
                return state.minValue;
            }
            const props = feature && feature.properties && typeof feature.properties === 'object' ? feature.properties : {};
            const selectedProperty = getActiveValuePropertyName();
            if (selectedProperty) {
                const selectedValue = Number(props[selectedProperty]);
                if (Number.isFinite(selectedValue)) return selectedValue;
            }
            const candidates = [props.FID, props.value, props.VALUE, props.val, props.density, props.POP, props.population];
            for (const candidate of candidates) {
                const parsed = Number(candidate);
                if (Number.isFinite(parsed)) return parsed;
            }
            const seedSource = String(
                props.GEOID10 ||
                props.GEOID ||
                props.COUNTYNS10 ||
                props.COUNTYFP10 ||
                feature.id ||
                props.NAME10 ||
                props.NAME ||
                props.name ||
                '50'
            );
            let hash = 0;
            for (let i = 0; i < seedSource.length; i++) {
                hash = ((hash << 5) - hash) + seedSource.charCodeAt(i);
                hash |= 0;
            }
            const span = Math.max(state.maxValue - state.minValue, 1);
            const normalized = Math.abs(hash) % (span + 1);
            return state.minValue + normalized;
        }

        function assignFeatureValues(data) {
            data.features.forEach((feature) => {
                if (!feature.properties || typeof feature.properties !== 'object') {
                    feature.properties = {};
                }
                const value = featureNumericValue(feature);
                feature.properties._rampValue = value;
            });
        }

        function refreshFeatureColors(data) {
            if (!data || !Array.isArray(data.features)) return;
            data.features.forEach((feature) => {
                const props = feature.properties || {};
                const value = featureNumericValue(feature);
                props._rampValue = value;
                feature.properties = props;
            });
        }

        function getGeoJsonBounds(data) {
            let minX = Infinity;
            let minY = Infinity;
            let maxX = -Infinity;
            let maxY = -Infinity;
            const scan = (coords) => {
                if (!Array.isArray(coords)) return;
                if (coords.length >= 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') {
                    minX = Math.min(minX, coords[0]);
                    minY = Math.min(minY, coords[1]);
                    maxX = Math.max(maxX, coords[0]);
                    maxY = Math.max(maxY, coords[1]);
                    return;
                }
                coords.forEach(scan);
            };
            data.features.forEach((f) => scan(f.geometry && f.geometry.coordinates));
            if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
                return null;
            }
            return [[minX, minY], [maxX, maxY]];
        }

        async function loadSelectedGeoJson() {
            try {
                const shareId = sanitizeGeoJsonShareId(state.geoJsonShareId);
                const isShared = shareId !== '';
                const sampleUrl = GEOJSON_SAMPLES[state.geoJsonSample] || GEOJSON_SAMPLES.us_states;
                let sourceUrl = sampleUrl;
                if (isShared) {
                    const metaResp = await appApi('geojson.public_meta', 'GET', null, { geojson_id: shareId });
                    const sharedMeta = metaResp && metaResp.geojson ? metaResp.geojson : null;
                    const sharedBytes = sharedMeta && Number.isFinite(Number(sharedMeta.bytes)) ? Number(sharedMeta.bytes) : null;
                    if (sharedBytes !== null && sharedBytes > CUSTOM_GEOJSON_MAX_BYTES) {
                        throw new Error(
                            `Shared GeoJSON is too large (${formatBytes(sharedBytes)}). Maximum supported in builder is ${formatBytes(CUSTOM_GEOJSON_MAX_BYTES)}.`
                        );
                    }
                    sourceUrl = sharedMeta && typeof sharedMeta.url === 'string' && sharedMeta.url.trim() !== ''
                        ? sharedMeta.url
                        : `shared-geojson/${encodeURIComponent(shareId)}.geojson`;
                }
                const response = await fetch(sourceUrl);
                if (!response.ok) {
                    throw new Error(`Failed to load ${sourceUrl}`);
                }
                const rawData = await response.json();
                const normalized = normalizeGeoJsonData(rawData);
                if (!isShared && state.geoJsonSample === 'us_states') {
                    normalized.features = normalized.features.filter(f => !['02', '15', '72'].includes(String(f.id)));
                }
                configureChoroplethPropertySelector(normalized, isShared);
                assignFeatureValues(normalized);
                statesData = normalized;
                if (isShared) {
                    setGeoJsonNotice(`Using shared GeoJSON (${shareId}).`);
                } else {
                    setGeoJsonNotice('');
                }
                updateGeoJsonSourceLabel();
            } catch (error) {
                console.error('Error loading map data:', error);
                statesData = { type: 'FeatureCollection', features: [] };
                configureChoroplethPropertySelector(statesData, false);
                state.geoJsonShareId = '';
                setGeoJsonNotice(error && error.message ? error.message : 'Could not load selected GeoJSON source.', true);
                updateGeoJsonSourceLabel();
            }
        }

        function destroyMapInstances() {
            if (leafletMap) {
                leafletMap.remove();
                leafletMap = null;
                leafletGeoJsonLayer = null;
                leafletTileLayer = null;
            }
            if (mapLibreMap) {
                mapLibreMap.remove();
                mapLibreMap = null;
            }
            if (mapLibreValuePopup) {
                mapLibreValuePopup.remove();
                mapLibreValuePopup = null;
            }
            mapLibreCameraSyncReady = false;
            mapFullscreenButtons = [];
            document.getElementById('map').innerHTML = '';
        }

        function getFullscreenElement() {
            return document.fullscreenElement || document.webkitFullscreenElement || null;
        }

        async function requestFullscreenElement(el) {
            if (!el) return;
            if (el.requestFullscreen) {
                await el.requestFullscreen();
                return;
            }
            if (el.webkitRequestFullscreen) {
                el.webkitRequestFullscreen();
            }
        }

        async function exitFullscreenElement() {
            if (document.exitFullscreen) {
                await document.exitFullscreen();
                return;
            }
            if (document.webkitExitFullscreen) {
                document.webkitExitFullscreen();
            }
        }

        function cleanupFullscreenButtons() {
            mapFullscreenButtons = mapFullscreenButtons.filter((button) => button && button.isConnected);
        }

        function setFullscreenButtonState() {
            cleanupFullscreenButtons();
            const isFullscreen = !!getFullscreenElement();
            mapFullscreenButtons.forEach((button) => {
                if (!button) return;
                button.textContent = isFullscreen ? '⤢' : '⛶';
                button.title = isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen';
                button.setAttribute('aria-label', isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen');
            });
        }

        function registerFullscreenButton(button) {
            if (!button) return;
            mapFullscreenButtons.push(button);
            setFullscreenButtonState();
        }

        async function toggleMapFullscreen() {
            const mapEl = document.getElementById('map');
            if (!mapEl) return;
            try {
                if (getFullscreenElement()) {
                    await exitFullscreenElement();
                } else {
                    await requestFullscreenElement(mapEl);
                }
            } catch (error) {
                console.warn('Fullscreen toggle failed:', error);
            }
            setFullscreenButtonState();
        }

        function addLeafletFullscreenControl() {
            if (!leafletMap || !window.L) return;
            const FullscreenControl = L.Control.extend({
                options: { position: 'topleft' },
                onAdd: function () {
                    const container = L.DomUtil.create('div', 'leaflet-bar ramp-fullscreen-control');
                    const button = L.DomUtil.create('a', 'ramp-fullscreen-button', container);
                    button.href = '#';
                    button.setAttribute('role', 'button');
                    button.setAttribute('aria-label', 'Enter fullscreen');
                    button.title = 'Enter fullscreen';
                    button.textContent = '⛶';
                    L.DomEvent.disableClickPropagation(container);
                    L.DomEvent.on(button, 'click', (event) => {
                        L.DomEvent.stop(event);
                        toggleMapFullscreen();
                    });
                    registerFullscreenButton(button);
                    return container;
                }
            });
            leafletMap.addControl(new FullscreenControl());
        }

        function createMapLibreFullscreenControl() {
            return {
                onAdd: function () {
                    this._container = document.createElement('div');
                    this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group ramp-fullscreen-control';
                    this._button = document.createElement('button');
                    this._button.type = 'button';
                    this._button.className = 'maplibregl-ctrl-icon ramp-fullscreen-button';
                    this._button.setAttribute('aria-label', 'Enter fullscreen');
                    this._button.title = 'Enter fullscreen';
                    this._button.textContent = '⛶';
                    this._button.addEventListener('click', () => {
                        toggleMapFullscreen();
                    });
                    this._container.appendChild(this._button);
                    registerFullscreenButton(this._button);
                    return this._container;
                },
                onRemove: function () {
                    if (this._button) {
                        this._button.remove();
                    }
                    if (this._container && this._container.parentNode) {
                        this._container.parentNode.removeChild(this._container);
                    }
                    this._container = null;
                    this._button = null;
                    cleanupFullscreenButtons();
                }
            };
        }

        function initLeafletMap() {
            if (leafletMap) return;
            const centerLat = hasStoredMapView() ? state.mapLat : DEFAULT_MAP_VIEW.lat;
            const centerLon = hasStoredMapView() ? state.mapLon : DEFAULT_MAP_VIEW.lng;
            const zoom = hasStoredMapView() ? state.mapZoom : DEFAULT_MAP_VIEW.zoom;
            leafletMap = L.map('map', { scrollWheelZoom: true }).setView([centerLat, centerLon], zoom);
            const isDark = document.documentElement.classList.contains('dark');
            leafletTileLayer = L.tileLayer(isDark ? MAP_TILES.dark : MAP_TILES.light, {
                attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
                subdomains: 'abcd',
                maxZoom: 20
            }).addTo(leafletMap);
            addLeafletFullscreenControl();
            leafletMap.on('moveend zoomend', () => {
                const center = leafletMap.getCenter();
                saveMapView(center.lat, center.lng, leafletMap.getZoom());
            });
        }

        function initMapLibreMap() {
            if (mapLibreMap) return;
            const isDark = document.documentElement.classList.contains('dark');
            mapLibreCameraSyncReady = false;
            const target3dCamera = getTarget3dCamera();
            mapLibreMap = new maplibregl.Map({
                container: 'map',
                style: {
                    version: 8,
                    sources: {
                        carto: {
                            type: 'raster',
                            tiles: [
                                (isDark ? MAP_TILES.dark : MAP_TILES.light).replace('{s}', 'a'),
                                (isDark ? MAP_TILES.dark : MAP_TILES.light).replace('{s}', 'b'),
                                (isDark ? MAP_TILES.dark : MAP_TILES.light).replace('{s}', 'c'),
                                (isDark ? MAP_TILES.dark : MAP_TILES.light).replace('{s}', 'd')
                            ],
                            attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
                            tileSize: 256
                        }
                    },
                    layers: [{ id: 'carto-base', type: 'raster', source: 'carto' }]
                },
                center: hasStoredMapView() ? [state.mapLon, state.mapLat] : [DEFAULT_MAP_VIEW.lng, DEFAULT_MAP_VIEW.lat],
                zoom: hasStoredMapView() ? state.mapZoom : DEFAULT_MAP_VIEW.zoom,
                pitch: target3dCamera.pitch,
                bearing: target3dCamera.bearing,
                scrollZoom: true,
                dragRotate: true,
                pitchWithRotate: true,
                touchZoomRotate: true
            });
            mapLibreMap.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
            mapLibreMap.addControl(createMapLibreFullscreenControl(), 'top-left');
            mapLibreMap.on('moveend', () => {
                if (!mapLibreCameraSyncReady) return;
                const center = mapLibreMap.getCenter();
                saveMapCamera(
                    center.lat,
                    center.lng,
                    mapLibreMap.getZoom(),
                    mapLibreMap.getPitch(),
                    mapLibreMap.getBearing()
                );
            });
        }

        function mapLibreFillExpression() {
            const valueExpr = ['to-number', ['coalesce', ['get', '_rampValue'], state.minValue]];
            if (state.isContinuous) {
                const colors = state.stops.map((s) => s.color);
                if (!colors.length) return '#999999';
                const stops = [];
                const denom = Math.max(colors.length - 1, 1);
                colors.forEach((color, i) => {
                    const t = i / denom;
                    const v = state.minValue + ((state.maxValue - state.minValue) * t);
                    stops.push(Number(v.toFixed(6)), color);
                });
                return ['interpolate', ['linear'], valueExpr, ...stops];
            }
            const ramp = generateRamp();
            if (!ramp.length) return '#999999';
            const expr = ['step', valueExpr, ramp[0].color];
            for (let i = 1; i < ramp.length; i++) {
                expr.push(Number(ramp[i].value), ramp[i].color);
            }
            return expr;
        }

        function mapLibreHeightExpression() {
            const range = Math.max(state.maxValue - state.minValue, 1);
            const maxHeightKm = Math.min(MAP3D_HEIGHT_MAX, Math.max(MAP3D_HEIGHT_MIN, Number(state.map3dHeight) || 0));
            const maxHeight = maxHeightKm * 1000;
            const normalizedValue = [
                'min',
                [
                    '/',
                    ['max', ['-', ['to-number', ['coalesce', ['get', '_rampValue'], state.minValue]], state.minValue], 0],
                    range
                ],
                1
            ];
            const minVisibleHeight = ['min', ['*', maxHeight, 0.02], 250];
            const easedHeight = ['*', ['^', normalizedValue, 0.35], maxHeight];
            return [
                'case',
                ['<=', normalizedValue, 0],
                0,
                ['max', easedHeight, minVisibleHeight]
            ];
        }

        function hasMeaningfulCustomCamera() {
            return state.mapCameraCustom &&
                Number.isFinite(state.mapPitch) &&
                Number.isFinite(state.mapBearing) &&
                (Math.abs(state.mapPitch) > 0.01 || Math.abs(state.mapBearing) > 0.01);
        }

        function getTarget3dCamera() {
            if (!state.map3d) {
                return { pitch: 0, bearing: 0 };
            }
            if (hasMeaningfulCustomCamera()) {
                return { pitch: state.mapPitch, bearing: state.mapBearing };
            }
            return { pitch: MAPLIBRE_BIRDSEYE.pitch, bearing: MAPLIBRE_BIRDSEYE.bearing };
        }

        function applyMapLibre3dState(animate = true) {
            if (!mapLibreMap || !mapLibreMap.isStyleLoaded()) return;
            const target3dCamera = getTarget3dCamera();
            const targetPitch = target3dCamera.pitch;
            const targetBearing = target3dCamera.bearing;
            if (mapLibreMap.getLayer(MAPLIBRE_EXTRUSION_LAYER_ID)) {
                mapLibreMap.setLayoutProperty(MAPLIBRE_EXTRUSION_LAYER_ID, 'visibility', state.map3d ? 'visible' : 'none');
                mapLibreMap.setPaintProperty(MAPLIBRE_EXTRUSION_LAYER_ID, 'fill-extrusion-color', mapLibreFillExpression());
                mapLibreMap.setPaintProperty(MAPLIBRE_EXTRUSION_LAYER_ID, 'fill-extrusion-height', mapLibreHeightExpression());
            }
            if (mapLibreMap.getLayer(MAPLIBRE_FILL_LAYER_ID)) {
                mapLibreMap.setPaintProperty(MAPLIBRE_FILL_LAYER_ID, 'fill-opacity', state.map3d ? 0.25 : 0.8);
                mapLibreMap.setPaintProperty(MAPLIBRE_FILL_LAYER_ID, 'fill-outline-color', getMapLibreFillOutlineColor());
                mapLibreMap.setPaintProperty(MAPLIBRE_FILL_LAYER_ID, 'fill-antialias', getMapLibreFillAntialias());
            }
            if (mapLibreMap.getLayer(MAPLIBRE_LINE_LAYER_ID)) {
                mapLibreMap.setLayoutProperty(MAPLIBRE_LINE_LAYER_ID, 'visibility', state.map3d ? 'none' : 'visible');
            }
            if (animate) {
                mapLibreMap.easeTo({
                    pitch: targetPitch,
                    bearing: targetBearing,
                    duration: 450
                });
            } else {
                mapLibreMap.jumpTo({
                    pitch: targetPitch,
                    bearing: targetBearing
                });
            }
            state.mapPitch = targetPitch;
            state.mapBearing = targetBearing;
            update3dButtonState();
            updateURL();
        }

        function getFeatureClickContent(feature) {
            if (!feature || typeof feature !== 'object') return '';
            const props = feature && feature.properties ? feature.properties : {};
            const value = Number.isFinite(Number(props._rampValue)) ? Number(props._rampValue) : featureNumericValue(feature);
            if (!Number.isFinite(value)) return '';
            const selectedProperty = getActiveValuePropertyName();
            const label = selectedProperty || 'value';
            return `<div class="value-tooltip-content"><strong>${label}</strong>: <span>${value.toLocaleString()}</span></div>`;
        }

        function getStoredClickContent() {
            if (!Number.isFinite(state.mapClickValue)) return '';
            const label = state.mapClickLabel && state.mapClickLabel.trim() !== '' ? state.mapClickLabel : (getActiveValuePropertyName() || 'value');
            return `<div class="value-tooltip-content"><strong>${label}</strong>: <span>${Number(state.mapClickValue).toLocaleString()}</span></div>`;
        }

        function getValueTooltipThemeClass() {
            return document.documentElement.classList.contains('dark') ? 'value-tooltip-dark' : 'value-tooltip-light';
        }

        function applyImportantStyles(element, styles) {
            if (!element) return;
            Object.entries(styles).forEach(([property, value]) => {
                element.style.setProperty(property, value, 'important');
            });
        }

        function styleMapLibreValuePopup(popup) {
            if (!popup || typeof popup.getElement !== 'function') return;
            const root = popup.getElement();
            if (!root) return;
            const isDark = document.documentElement.classList.contains('dark');
            const content = root.querySelector('.maplibregl-popup-content');
            const closeBtn = root.querySelector('.maplibregl-popup-close-button');
            const textNodes = root.querySelectorAll('.value-tooltip-content, .value-tooltip-content *, strong, span');
            const bg = isDark ? 'rgba(15, 23, 42, 0.98)' : '#ffffff';
            const fg = isDark ? '#f8fafc' : '#0f172a';
            const border = isDark ? '#334155' : '#cbd5e1';

            applyImportantStyles(root, {
                opacity: '1',
                filter: 'none',
                'mix-blend-mode': 'normal'
            });
            if (content) {
                applyImportantStyles(content, {
                    background: bg,
                    color: fg,
                    border: `1px solid ${border}`,
                    'border-radius': '8px',
                    padding: '10px 30px 10px 10px',
                    opacity: '1',
                    filter: 'none',
                    'box-shadow': '0 10px 25px rgba(2, 6, 23, 0.25)'
                });
            }
            textNodes.forEach((node) => {
                applyImportantStyles(node, {
                    color: fg,
                    opacity: '1',
                    filter: 'none',
                    'text-shadow': 'none'
                });
            });
            if (closeBtn) {
                closeBtn.textContent = '×';
                applyImportantStyles(closeBtn, {
                    color: fg,
                    opacity: '1',
                    right: '6px',
                    top: '4px',
                    width: '20px',
                    height: '20px',
                    'font-size': '18px',
                    'font-weight': '700',
                    'line-height': '18px',
                    'text-shadow': 'none',
                    border: '0',
                    outline: 'none',
                    'box-shadow': 'none',
                    background: 'transparent',
                    appearance: 'none',
                    '-webkit-appearance': 'none'
                });
            }
        }

        function onLeafletFeatureClick(e) {
            if (!state.showFeatureValueOnClick) return;
            const content = getFeatureClickContent(e.target && e.target.feature);
            if (!content) return;
            e.target.bindPopup(content, { className: `value-tooltip-popup ${getValueTooltipThemeClass()}` }).openPopup(e.latlng);
        }

        function mapLibreFeatureClickHandler(e) {
            if (!state.showFeatureValueOnClick) return;
            const feature = e && e.features && e.features[0] ? e.features[0] : null;
            const content = getFeatureClickContent(feature);
            if (!content || !mapLibreMap) return;
            if (mapLibreValuePopup) {
                mapLibreValuePopup.remove();
            }
            mapLibreValuePopup = new maplibregl.Popup({
                closeButton: true,
                closeOnClick: true,
                className: `value-tooltip-popup ${getValueTooltipThemeClass()}`
            })
                .setLngLat(e.lngLat)
                .setHTML(content)
                .addTo(mapLibreMap);
            styleMapLibreValuePopup(mapLibreValuePopup);
            requestAnimationFrame(() => styleMapLibreValuePopup(mapLibreValuePopup));
            state.mapClickLat = Number(e.lngLat.lat);
            state.mapClickLon = Number(e.lngLat.lng);
            const selectedProperty = getActiveValuePropertyName();
            const clickedFeature = feature && feature.properties ? feature : null;
            const clickedValue = clickedFeature && Number.isFinite(Number(clickedFeature.properties._rampValue))
                ? Number(clickedFeature.properties._rampValue)
                : NaN;
            state.mapClickValue = Number.isFinite(clickedValue) ? clickedValue : null;
            state.mapClickLabel = selectedProperty || 'value';
            updateURL();
        }

        function restoreMapLibreClickPopupFromState() {
            if (!mapLibreMap || !state.showFeatureValueOnClick) return;
            if (!Number.isFinite(state.mapClickLat) || !Number.isFinite(state.mapClickLon)) return;
            if (!mapLibreMap.getLayer(MAPLIBRE_FILL_LAYER_ID)) return;
            const point = mapLibreMap.project([state.mapClickLon, state.mapClickLat]);
            const features = mapLibreMap.queryRenderedFeatures(point, { layers: [MAPLIBRE_FILL_LAYER_ID] });
            const feature = features && features[0] ? features[0] : null;
            const content = getFeatureClickContent(feature) || getStoredClickContent();
            if (!content) return;
            if (mapLibreValuePopup) {
                mapLibreValuePopup.remove();
            }
            mapLibreValuePopup = new maplibregl.Popup({
                closeButton: true,
                closeOnClick: true,
                className: `value-tooltip-popup ${getValueTooltipThemeClass()}`
            })
                .setLngLat([state.mapClickLon, state.mapClickLat])
                .setHTML(content)
                .addTo(mapLibreMap);
            styleMapLibreValuePopup(mapLibreValuePopup);
            requestAnimationFrame(() => styleMapLibreValuePopup(mapLibreValuePopup));
        }

        function renderLeafletData() {
            if (!leafletMap || !statesData) return;
            if (leafletGeoJsonLayer) {
                leafletMap.removeLayer(leafletGeoJsonLayer);
            }
            leafletGeoJsonLayer = L.geoJson(statesData, {
                style: style,
                smoothFactor: 0,
                onEachFeature: (_feature, layer) => {
                    layer.on('click', onLeafletFeatureClick);
                }
            }).addTo(leafletMap);
            const bounds = leafletGeoJsonLayer.getBounds();
            if ((forceFitDataBounds || !hasStoredMapView()) && bounds && bounds.isValid()) {
                leafletMap.fitBounds(bounds, { padding: [20, 20] });
            }
            forceFitDataBounds = false;
        }

        function waitForMapLibreRender(timeoutMs = 10000) {
            return new Promise((resolve) => {
                if (!mapLibreMap) {
                    resolve();
                    return;
                }
                if (mapLibreMap.isStyleLoaded() && !mapLibreMap.isMoving()) {
                    requestAnimationFrame(() => resolve());
                    return;
                }
                let done = false;
                const finish = () => {
                    if (done) return;
                    done = true;
                    clearTimeout(timer);
                    mapLibreMap.off('idle', onIdle);
                    mapLibreMap.off('render', onRender);
                    resolve();
                };
                const onIdle = () => finish();
                const onRender = () => {
                    requestAnimationFrame(() => requestAnimationFrame(finish));
                };
                const timer = setTimeout(finish, timeoutMs);
                mapLibreMap.on('idle', onIdle);
                mapLibreMap.on('render', onRender);
            });
        }

        function renderMapLibreData() {
            return new Promise((resolve) => {
                if (!mapLibreMap || !statesData) {
                    resolve();
                    return;
                }
            const applyData = async () => {
                mapLibreMap.off('click', MAPLIBRE_FILL_LAYER_ID, mapLibreFeatureClickHandler);
                if (mapLibreMap.getLayer(MAPLIBRE_EXTRUSION_LAYER_ID)) mapLibreMap.removeLayer(MAPLIBRE_EXTRUSION_LAYER_ID);
                if (mapLibreMap.getLayer(MAPLIBRE_LINE_LAYER_ID)) mapLibreMap.removeLayer(MAPLIBRE_LINE_LAYER_ID);
                if (mapLibreMap.getLayer(MAPLIBRE_FILL_LAYER_ID)) mapLibreMap.removeLayer(MAPLIBRE_FILL_LAYER_ID);
                if (mapLibreMap.getSource(MAPLIBRE_SOURCE_ID)) mapLibreMap.removeSource(MAPLIBRE_SOURCE_ID);

                mapLibreMap.addSource(MAPLIBRE_SOURCE_ID, { type: 'geojson', data: statesData });
                mapLibreMap.addLayer({
                    id: MAPLIBRE_FILL_LAYER_ID,
                    type: 'fill',
                    source: MAPLIBRE_SOURCE_ID,
                    paint: {
                        'fill-color': mapLibreFillExpression(),
                        'fill-opacity': 0.8,
                        'fill-outline-color': getMapLibreFillOutlineColor(),
                        'fill-antialias': getMapLibreFillAntialias()
                    }
                });
                mapLibreMap.on('click', MAPLIBRE_FILL_LAYER_ID, mapLibreFeatureClickHandler);
                mapLibreMap.addLayer({
                    id: MAPLIBRE_EXTRUSION_LAYER_ID,
                    type: 'fill-extrusion',
                    source: MAPLIBRE_SOURCE_ID,
                    layout: {
                        visibility: state.map3d ? 'visible' : 'none'
                    },
                    paint: {
                        'fill-extrusion-color': mapLibreFillExpression(),
                        'fill-extrusion-height': mapLibreHeightExpression(),
                        'fill-extrusion-base': 0,
                        'fill-extrusion-opacity': 0.9
                    }
                });
                mapLibreMap.addLayer({
                    id: MAPLIBRE_LINE_LAYER_ID,
                    type: 'line',
                    source: MAPLIBRE_SOURCE_ID,
                    layout: {
                        visibility: state.map3d ? 'none' : 'visible'
                    },
                    paint: {
                        'line-color': getStrokeColorRgba(),
                        'line-width': getStrokeWidth(),
                        'line-opacity': getStrokeOpacity()
                    }
                });
                const bounds = getGeoJsonBounds(statesData);
                if ((forceFitDataBounds || !hasStoredMapView()) && bounds) {
                    mapLibreMap.fitBounds(bounds, { padding: 20, animate: false });
                }
                forceFitDataBounds = false;
                applyMapLibre3dState(false);
                mapLibreCameraSyncReady = true;
                restoreMapLibreClickPopupFromState();
                await waitForMapLibreRender(30000);
                resolve();
            };

            if (mapLibreMap.isStyleLoaded()) {
                applyData();
            } else {
                mapLibreMap.once('load', applyData);
            }
            });
        }

        async function initMap() {
            destroyMapInstances();
            if (!statesData) {
                await loadSelectedGeoJson();
            }
            if (state.mapEngine === 'maplibre') {
                initMapLibreMap();
                await renderMapLibreData();
            } else {
                initLeafletMap();
                renderLeafletData();
                await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
            }
            updateLegendOverlay();
            update3dButtonState();
        }

        function getColor(v) {
            const { steps, minValue, maxValue, stops, isContinuous, mode } = state;
            
            if (isContinuous) {
                const colors = stops.map(s => s.color);
                const scale = chroma.scale(colors).mode(mode).domain([minValue, maxValue]);
                const result = scale(v);
                return (result.hex ? result.hex() : result.toString()).toUpperCase();
            } else {
                const ramp = generateRamp();
                // Find the first step where threshold is >= v
                for (let i = 0; i < ramp.length; i++) {
                    const threshold = parseFloat(ramp[i].value);
                    if (v <= threshold) return ramp[i].color;
                }
                return ramp[ramp.length - 1].color;
            }
        }

        function style(feature) {
            const props = feature && feature.properties ? feature.properties : {};
            const val = Number.isFinite(Number(props._rampValue))
                ? Number(props._rampValue)
                : featureNumericValue(feature);
            return {
                fillColor: getColor(val),
                weight: getStrokeWidth(),
                opacity: getStrokeOpacity(),
                color: normalizeStrokeColor(state.strokeColor),
                fillOpacity: 0.8
            };
        }

        function updateLegendContent(div) {
            const ramp = generateRamp();
            const { minValue, maxValue, isContinuous, stops } = state;
            
            let labels = [''];
            
            // Show the legend content depending on the mode
            if (isContinuous) {
                const colors = stops.map(s => s.color);
                const gradient = `linear-gradient(to bottom, ${[...colors].reverse().join(', ')})`;
                labels.push(
                    '<div class="flex gap-3">' +
                    '<div style="background:' + gradient + '; width: 20px; height: 120px; border-radius: 4px;"></div>' +
                    '<div class="flex flex-col justify-between text-[10px] py-1">' +
                    '<span>' + maxValue + '</span>' +
                    '<span>' + ((maxValue + minValue) / 2).toFixed(2) + '</span>' +
                    '<span>' + minValue + '</span>' +
                    '</div>' +
                    '</div>'
                );
            } else {
                // Reverse ramp for legend display (highest at top)
                const displayRamp = [...ramp].reverse();
                
                displayRamp.forEach((step, i) => {
                    labels.push(
                        '<div class="flex items-center">' +
                        '<i style="background:' + step.color + '; width: 15px; height: 15px; display: inline-block;"></i> ' +
                        '<span class="ml-2 text-[10px]">' + step.value + '</span>' +
                        '</div>'
                    );
                });
            }

            div.innerHTML = labels.join('');
        }

        function updateLegendOverlay() {
            const legendEl = document.getElementById('mapLegend');
            if (!legendEl) return;
            updateLegendContent(legendEl);
        }

        function updateMap(dataRefresh = false) {
            if (!statesData) return;
            if (dataRefresh) {
                refreshFeatureColors(statesData);
            }
            if (state.mapEngine === 'maplibre') {
                if (mapLibreMap && mapLibreMap.isStyleLoaded() && mapLibreMap.getSource(MAPLIBRE_SOURCE_ID)) {
                    if (dataRefresh) {
                        const source = mapLibreMap.getSource(MAPLIBRE_SOURCE_ID);
                        if (source && source.setData) {
                            source.setData(statesData);
                        }
                    }
                    if (mapLibreMap.getLayer(MAPLIBRE_FILL_LAYER_ID)) {
                        mapLibreMap.setPaintProperty(MAPLIBRE_FILL_LAYER_ID, 'fill-color', mapLibreFillExpression());
                        mapLibreMap.setPaintProperty(MAPLIBRE_FILL_LAYER_ID, 'fill-outline-color', getMapLibreFillOutlineColor());
                        mapLibreMap.setPaintProperty(MAPLIBRE_FILL_LAYER_ID, 'fill-antialias', getMapLibreFillAntialias());
                    }
                    if (mapLibreMap.getLayer(MAPLIBRE_LINE_LAYER_ID)) {
                        mapLibreMap.setPaintProperty(MAPLIBRE_LINE_LAYER_ID, 'line-color', getStrokeColorRgba());
                        mapLibreMap.setPaintProperty(MAPLIBRE_LINE_LAYER_ID, 'line-width', getStrokeWidth());
                        mapLibreMap.setPaintProperty(MAPLIBRE_LINE_LAYER_ID, 'line-opacity', getStrokeOpacity());
                        mapLibreMap.setLayoutProperty(MAPLIBRE_LINE_LAYER_ID, 'visibility', state.map3d ? 'none' : 'visible');
                    }
                    if (mapLibreMap.getLayer(MAPLIBRE_EXTRUSION_LAYER_ID)) {
                        mapLibreMap.setPaintProperty(MAPLIBRE_EXTRUSION_LAYER_ID, 'fill-extrusion-color', mapLibreFillExpression());
                        mapLibreMap.setPaintProperty(MAPLIBRE_EXTRUSION_LAYER_ID, 'fill-extrusion-height', mapLibreHeightExpression());
                    }
                } else {
                    renderMapLibreData();
                }
            } else {
                if (leafletGeoJsonLayer) {
                    leafletGeoJsonLayer.setStyle(style);
                    applyLeafletStrokeStyles();
                } else {
                    renderLeafletData();
                }
            }
            updateLegendOverlay();
        }

        async function updateMapAndWaitForRender() {
            updateMap(true);
            if (state.mapEngine === 'maplibre') {
                await waitForMapLibreRender(12000);
            } else {
                await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
            }
        }

        function hexToRgb(hex) {
            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
            return result ? {
                r: parseInt(result[1], 16),
                g: parseInt(result[2], 16),
                b: parseInt(result[3], 16)
            } : null;
        }

        function rgbToHex(r, g, b) {
            return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1).toUpperCase();
        }

        function normalizeStrokeColor(value) {
            const raw = String(value || '').trim();
            if (raw === '') return '#FFFFFF';

            if (/^#[0-9a-fA-F]{3}$/.test(raw)) {
                const r = raw[1];
                const g = raw[2];
                const b = raw[3];
                return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
            }

            if (/^#[0-9a-fA-F]{6}$/.test(raw)) {
                return raw.toUpperCase();
            }

            if (/^#[0-9a-fA-F]{8}$/.test(raw)) {
                return raw.slice(0, 7).toUpperCase();
            }

            const rgbMatch = raw.match(/^rgba?\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})(?:\s*,\s*([0-9]*\.?[0-9]+))?\s*\)$/i);
            if (rgbMatch) {
                const r = Math.max(0, Math.min(255, Number(rgbMatch[1]) || 0));
                const g = Math.max(0, Math.min(255, Number(rgbMatch[2]) || 0));
                const b = Math.max(0, Math.min(255, Number(rgbMatch[3]) || 0));
                return rgbToHex(r, g, b).toUpperCase();
            }

            return '#FFFFFF';
        }

        function parseStrokeColorAndAlpha(value) {
            const raw = String(value || '').trim();
            if (raw === '') return { color: '#FFFFFF', alpha: null };

            if (/^#[0-9a-fA-F]{4}$/.test(raw)) {
                const r = raw[1];
                const g = raw[2];
                const b = raw[3];
                const a = raw[4];
                return {
                    color: `#${r}${r}${g}${g}${b}${b}`.toUpperCase(),
                    alpha: Math.max(0, Math.min(1, parseInt(`${a}${a}`, 16) / 255))
                };
            }

            if (/^#[0-9a-fA-F]{8}$/.test(raw)) {
                return {
                    color: raw.slice(0, 7).toUpperCase(),
                    alpha: Math.max(0, Math.min(1, parseInt(raw.slice(7, 9), 16) / 255))
                };
            }

            const rgbaMatch = raw.match(/^rgba?\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})(?:\s*,\s*([0-9]*\.?[0-9]+))?\s*\)$/i);
            if (rgbaMatch) {
                const r = Math.max(0, Math.min(255, Number(rgbaMatch[1]) || 0));
                const g = Math.max(0, Math.min(255, Number(rgbaMatch[2]) || 0));
                const b = Math.max(0, Math.min(255, Number(rgbaMatch[3]) || 0));
                const alpha = rgbaMatch[4] === undefined ? null : Math.max(0, Math.min(1, Number(rgbaMatch[4]) || 0));
                return {
                    color: rgbToHex(r, g, b).toUpperCase(),
                    alpha
                };
            }

            return { color: normalizeStrokeColor(raw), alpha: null };
        }

        function getStrokeWidth() {
            const width = Number(state.strokeWidth);
            if (!Number.isFinite(width)) return 0;
            return Math.max(0, Math.min(20, width));
        }

        function getStrokeOpacity() {
            const opacity = Number(state.strokeOpacity);
            if (!Number.isFinite(opacity)) return 1;
            return Math.max(0, Math.min(1, opacity));
        }

        function getStrokeColorRgba() {
            if (getStrokeWidth() <= 0 || getStrokeOpacity() <= 0) {
                return 'rgba(0,0,0,0)';
            }
            return normalizeStrokeColor(state.strokeColor);
        }

        function getMapLibreFillOutlineColor() {
            if (!state.map3d || getStrokeWidth() <= 0 || getStrokeOpacity() <= 0) {
                return 'rgba(0,0,0,0)';
            }
            const rgb = hexToRgb(normalizeStrokeColor(state.strokeColor)) || { r: 255, g: 255, b: 255 };
            return `rgba(${rgb.r},${rgb.g},${rgb.b},${getStrokeOpacity().toFixed(3)})`;
        }

        function getMapLibreFillAntialias() {
            return state.map3d && getStrokeWidth() > 0 && getStrokeOpacity() > 0;
        }

        function applyLeafletStrokeStyles() {
            if (!leafletGeoJsonLayer || typeof leafletGeoJsonLayer.eachLayer !== 'function') return;
            const strokeWidth = getStrokeWidth();
            const strokeColor = normalizeStrokeColor(state.strokeColor);
            const strokeOpacity = getStrokeOpacity();
            leafletGeoJsonLayer.eachLayer((layer) => {
                if (!layer || typeof layer.setStyle !== 'function') return;
                layer.setStyle({
                    weight: strokeWidth,
                    color: strokeColor,
                    opacity: strokeOpacity
                });
            });
        }

        function interpolate(color1, color2, factor) {
            const rgb1 = hexToRgb(color1);
            const rgb2 = hexToRgb(color2);
            const r = rgb1.r + factor * (rgb2.r - rgb1.r);
            const g = rgb1.g + factor * (rgb2.g - rgb1.g);
            const b = rgb1.b + factor * (rgb2.b - rgb1.b);
            return rgbToHex(r, g, b);
        }

        function generateRamp() {
            const { stops, steps, minValue, maxValue, mode, vision, decimals } = state;
            if (stops.length < 2) return [];

            let colors = stops.map(s => s.color);
            let scale = chroma.scale(colors).mode(mode).domain([0, 1]);
            const ramp = [];

            for (let i = 0; i < steps; i++) {
                const relativePos = i / (steps - 1);
                let colorObj = scale(relativePos);
                
                // Apply vision simulation if needed
                if (vision !== 'none') {
                    // Simple simulation using chroma or CSS filters?
                    // chroma doesn't have native vision simulation, but we can do some transforms
                    // For a true advanced experience, we can use a small matrix or just inform the user
                    // Let's implement basic ones or use a filter approach on the UI
                }

                const color = colorObj.hex().toUpperCase();
                const value = minValue + (relativePos * (maxValue - minValue));
                ramp.push({ color, value: value.toFixed(decimals) });
            }
            return ramp;
        }

        // --- UI Rendering ---
        function renderStops() {
            const container = document.getElementById('stopsContainer');
            container.innerHTML = '';
            
            state.stops.forEach((stop, index) => {
                const isLight = chroma(stop.color).luminance() > 0.55;
                const textColor = isLight ? '#0f172a' : '#f8fafc';
                const chipBg = isLight ? 'rgba(15,23,42,0.12)' : 'rgba(248,250,252,0.22)';
                const div = document.createElement('div');
                div.className = 'stop-item group flex items-center gap-3 p-2 rounded-xl border transition-all';
                div.dataset.stopIndex = String(index);
                div.style.backgroundColor = stop.color;
                div.style.color = textColor;
                div.style.borderColor = isLight ? 'rgba(15,23,42,0.2)' : 'rgba(248,250,252,0.2)';
                div.innerHTML = `
                    <div class="stop-index-chip flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold" style="background:${chipBg}; color:${textColor};">
                        ${index + 1}
                    </div>
                    <div class="flex-1 min-w-0 flex items-center">
                        <span class="stop-hex-label font-mono text-sm select-none" style="color:${textColor};">${stop.color.toUpperCase()}</span>
                        <input type="text" value="${stop.color}" data-coloris class="color-picker-input absolute opacity-0 w-0 h-0 pointer-events-none" data-id="${index}">
                    </div>
                    <button class="remove-stop opacity-0 group-hover:opacity-100 p-2 transition-all" data-id="${index}" title="Remove stop" style="color:${textColor};">
                        <i data-lucide="trash-2" class="w-4 h-4"></i>
                    </button>
                `;
                container.appendChild(div);
            });
            lucide.createIcons();
            initColoris();
        }

        function updateStopRowVisual(index) {
            const stop = state.stops[index];
            if (!stop) return;
            const row = document.querySelector(`.stop-item[data-stop-index="${index}"]`);
            if (!row) return;
            const isLight = chroma(stop.color).luminance() > 0.55;
            const textColor = isLight ? '#0f172a' : '#f8fafc';
            const chipBg = isLight ? 'rgba(15,23,42,0.12)' : 'rgba(248,250,252,0.22)';

            row.style.backgroundColor = stop.color;
            row.style.color = textColor;
            row.style.borderColor = isLight ? 'rgba(15,23,42,0.2)' : 'rgba(248,250,252,0.2)';

            const chip = row.querySelector('.stop-index-chip');
            if (chip) {
                chip.style.background = chipBg;
                chip.style.color = textColor;
            }
            const label = row.querySelector('.stop-hex-label');
            if (label) {
                label.textContent = stop.color.toUpperCase();
                label.style.color = textColor;
            }
            const removeBtn = row.querySelector('.remove-stop');
            if (removeBtn) {
                removeBtn.style.color = textColor;
            }
        }

        function initColoris() {
            const isDark = document.documentElement.classList.contains('dark');
            Coloris({ 
                el: '.color-picker-input',
                theme: 'pill',
                themeMode: isDark ? 'dark' : 'light',
                format: 'hex',
                margin: 0,
                swatches: [
                    '#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51', '#d62828',
                    '#023e8a', '#0077b6', '#0096c7', '#00b4d8', '#48cae4'
                ]
            });
            Coloris({
                el: '.stroke-color-picker-input',
                theme: 'pill',
                themeMode: isDark ? 'dark' : 'light',
                format: 'hex',
                alpha: false,
                margin: 0,
                swatches: [
                    '#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51', '#d62828',
                    '#023e8a', '#0077b6', '#0096c7', '#00b4d8', '#48cae4'
                ]
            });
        }

        function updateStrokeColorTrigger() {
            const trigger = document.getElementById('strokeColorTrigger');
            const hex = document.getElementById('strokeColorHex');
            const input = document.getElementById('strokeColorInput');
            if (!trigger || !hex || !input) return;
            const color = normalizeStrokeColor(state.strokeColor);
            input.value = color;
            const alphaLabel = getStrokeOpacity() < 1 ? ` (${Math.round(getStrokeOpacity() * 100)}%)` : '';
            hex.textContent = `${color}${alphaLabel}`;
            trigger.style.backgroundColor = color;
            const rgb = hexToRgb(color) || { r: 255, g: 255, b: 255 };
            const luminance = ((0.2126 * rgb.r) + (0.7152 * rgb.g) + (0.0722 * rgb.b)) / 255;
            const textColor = luminance > 0.58 ? '#0f172a' : '#f8fafc';
            trigger.style.borderColor = luminance > 0.58 ? 'rgba(15,23,42,0.25)' : 'rgba(248,250,252,0.25)';
            hex.style.color = textColor;
        }

        function renderPresets() {
            const renderPresetList = (presets, containerId) => {
                const container = document.getElementById(containerId);
                container.innerHTML = '';
                Object.entries(presets).forEach(([name, colors]) => {
                    const btn = document.createElement('button');
                    btn.className = 'group relative h-6 w-full rounded-md overflow-hidden flex border border-slate-200 dark:border-slate-700 hover:border-blue-400 dark:hover:border-blue-500 transition-all';
                    btn.title = name;
                    btn.onclick = () => {
                        state.stops = colors.map((c, i) => ({ id: i + 1, color: c }));
                        trackAnalyticsEvent('preset_select', {
                            preset_name: name,
                            preset_group: containerId,
                            color_count: colors.length
                        });
                        queueBuilderAnalyticsTracking('preset_select');
                        renderStops();
                        updateUI();
                    };
                    
                    if (state.isContinuous) {
                        btn.style.background = `linear-gradient(90deg, ${colors.join(', ')})`;
                    } else {
                        colors.forEach(c => {
                            const span = document.createElement('span');
                            span.className = 'flex-1 h-full';
                            span.style.backgroundColor = c;
                            btn.appendChild(span);
                        });
                    }
                    container.appendChild(btn);
                });
            };

            renderPresetList(colorPresets.sequential, 'sequentialPresets');
            renderPresetList(colorPresets.diverging, 'divergingPresets');
            renderPresetList(colorPresets.qualitative, 'qualitativePresets');
            renderPresetList(colorPresets.colorblind, 'colorblindPresets');
        }

        function updateUI() {
            const ramp = generateRamp();
            updateStrokeControlsVisibility();
            
            // Apply vision simulation class to all relevant containers
            const preview = document.getElementById('rampPreview');
            const mapEl = document.getElementById('map');
            const tbody = document.getElementById('rampTableBody');
            
            // Vision classes to manage
            const visionClasses = ['vision-protanopia', 'vision-deuteranopia', 'vision-tritanopia', 'vision-achromatopsia'];
            const currentVisionClass = state.vision !== 'none' ? 'vision-' + state.vision : null;

            // Update preview
            preview.classList.remove(...visionClasses);
            if (currentVisionClass) preview.classList.add(currentVisionClass);

            // Update table body
            tbody.classList.remove(...visionClasses);
            if (currentVisionClass) tbody.classList.add(currentVisionClass);

            // Update Map: apply vision filter only to rendered map layers, never popup containers.
            const mapVisionTargets = [
                ...mapEl.querySelectorAll('.leaflet-tile-pane, .leaflet-overlay-pane, .maplibregl-canvas'),
                ...mapEl.querySelectorAll('.leaflet-pane, .maplibregl-canvas-container')
            ];
            mapVisionTargets.forEach((el) => el.classList.remove(...visionClasses));
            const scopedTargets = mapEl.querySelectorAll('.leaflet-tile-pane, .leaflet-overlay-pane, .maplibregl-canvas');
            scopedTargets.forEach((el) => {
                if (currentVisionClass) el.classList.add(currentVisionClass);
            });

            // Update Legend
            const legendDiv = document.getElementById('mapLegend');
            if (legendDiv) {
                legendDiv.classList.remove(...visionClasses);
                if (currentVisionClass) legendDiv.classList.add(currentVisionClass);
            }

            // Ensure stops and presets are always original colors (remove vision classes if they exist)
            document.getElementById('stopsContainer').classList.remove(...visionClasses);
            [
                document.getElementById('sequentialPresets'),
                document.getElementById('divergingPresets'),
                document.getElementById('qualitativePresets'),
                document.getElementById('colorblindPresets')
            ].forEach(container => {
                if (container) container.classList.remove(...visionClasses);
            });

            const strokeColorInput = document.getElementById('strokeColorInput');
            const strokeWidthInput = document.getElementById('strokeWidthInput');
            const strokeWidthSlider = document.getElementById('strokeWidthSlider');
            const strokeOpacityInput = document.getElementById('strokeOpacityInput');
            const strokeOpacityValue = document.getElementById('strokeOpacityValue');
            if (strokeColorInput) strokeColorInput.value = normalizeStrokeColor(state.strokeColor);
            if (strokeWidthInput) strokeWidthInput.value = String(getStrokeWidth());
            if (strokeWidthSlider) strokeWidthSlider.value = String(getStrokeWidth());
            if (strokeOpacityInput) strokeOpacityInput.value = String(getStrokeOpacity());
            if (strokeOpacityValue) strokeOpacityValue.textContent = getStrokeOpacity().toFixed(2);
            updateStrokeColorTrigger();

            // Preview Render
            preview.innerHTML = '';
            if (state.isContinuous) {
                preview.style.background = `linear-gradient(90deg, ${state.stops.map(s => s.color).join(', ')})`;
            } else {
                preview.style.background = 'none';
                ramp.forEach(step => {
                    const bar = document.createElement('div');
                    bar.className = 'flex-1 h-full';
                    bar.style.backgroundColor = step.color;
                    preview.appendChild(bar);
                });
            }

            // Table
            tbody.innerHTML = '';
            
            // If continuous, we'll apply a gradient to the tbody itself
            if (state.isContinuous) {
                const gradientColors = state.stops.map(s => s.color).join(', ');
                tbody.style.background = `linear-gradient(180deg, ${gradientColors})`;
            } else {
                tbody.style.background = 'none';
            }

            ramp.forEach((step, i) => {
                const row = document.createElement('tr');
                row.className = 'transition-colors font-medium';
                
                if (state.isContinuous) {
                    row.style.backgroundColor = 'transparent';
                } else {
                    row.style.backgroundColor = step.color;
                }
                
                // Add a text shadow for readability
                const textShadow = '0 1px 2px rgba(0,0,0,0.5), 0 0 1px rgba(0,0,0,0.8)';
                row.style.textShadow = textShadow;
                row.style.color = '#fff'; // Default to white text with shadow

                row.innerHTML = `
                    <td class="px-4 py-3 font-mono opacity-80 border-none">${i + 1}</td>
                    <td class="px-4 py-3 flex items-center gap-3 border-none">
                        <div class="w-4 h-4 rounded shadow-sm border border-white/30" style="background-color: ${step.color}"></div>
                        <span class="font-mono">${step.color}</span>
                    </td>
                    <td class="px-4 py-3 text-right font-mono border-none">${step.value}</td>
                `;
                tbody.appendChild(row);
            });
            
            updateURL();
            updateMap();
        }

        function updateURL() {
            const params = new URLSearchParams();
            params.set('colors', state.stops.map(s => s.color.replace('#', '')).join(','));
            params.set('steps', state.steps);
            params.set('min', state.minValue);
            params.set('max', state.maxValue);
            params.set('decimals', state.decimals);
            params.set('mode', state.mode);
            params.set('map_engine', state.mapEngine);
            params.set('geojson_sample', state.geoJsonSample);
            if (state.geoJsonShareId) params.set('geojson_share', state.geoJsonShareId);
            if (state.choroplethProperty && state.choroplethProperty !== CHOROPLETH_AUTO_PROPERTY) {
                params.set('choropleth_prop', state.choroplethProperty);
            }
            params.set('auto_field_minmax', state.autoUseFieldMinMax ? '1' : '0');
            if (state.showFeatureValueOnClick) params.set('show_value_click', '1');
            if (Number.isFinite(state.mapClickLat) && Number.isFinite(state.mapClickLon)) {
                params.set('click_lat', state.mapClickLat.toFixed(6));
                params.set('click_lon', state.mapClickLon.toFixed(6));
                if (Number.isFinite(state.mapClickValue)) params.set('click_val', String(state.mapClickValue));
                if (state.mapClickLabel) params.set('click_lbl', state.mapClickLabel);
            }
            if (state.currentRampId) params.set('rid', state.currentRampId);
            params.set('map3d', state.map3d ? '1' : '0');
            if (Number.isFinite(state.map3dHeight) && state.map3dHeight !== MAP3D_HEIGHT_DEFAULT) params.set('map3dh', String(Math.round(state.map3dHeight)));
            if (state.strokeColor !== '#FFFFFF') params.set('stroke_color', state.strokeColor.replace('#', ''));
            if (Number.isFinite(state.strokeWidth) && state.strokeWidth > 0) params.set('stroke_width', String(state.strokeWidth));
            if (Number.isFinite(state.strokeOpacity) && state.strokeOpacity < 1) params.set('stroke_opacity', String(state.strokeOpacity));
            if (Number.isFinite(state.mapLat)) params.set('lat', state.mapLat.toFixed(6));
            if (Number.isFinite(state.mapLon)) params.set('lon', state.mapLon.toFixed(6));
            if (Number.isFinite(state.mapZoom)) params.set('z', state.mapZoom.toFixed(2));
            if (state.mapEngine === 'maplibre' && Number.isFinite(state.mapPitch) && (state.map3d || state.mapCameraCustom)) params.set('pitch', state.mapPitch.toFixed(2));
            if (state.mapEngine === 'maplibre' && Number.isFinite(state.mapBearing) && (state.map3d || state.mapCameraCustom)) params.set('bearing', state.mapBearing.toFixed(2));
            if (state.vision !== 'none') params.set('vision', state.vision);
            if (state.isContinuous) params.set('cont', 1);
            window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
            queueBuilderAnalyticsTracking('state_sync');
        }

        function loadFromURL() {
            const params = new URLSearchParams(window.location.search);
            if (params.has('colors')) {
                state.stops = params.get('colors').split(',').map((c, i) => ({ id: i + 1, color: '#' + c }));
            }
            if (params.has('steps')) state.steps = parseInt(params.get('steps'));
            if (params.has('min')) state.minValue = parseFloat(params.get('min'));
            if (params.has('max')) state.maxValue = parseFloat(params.get('max'));
            if (params.has('decimals')) state.decimals = parseInt(params.get('decimals'));
            if (params.has('mode')) state.mode = params.get('mode');
            if (params.has('vision')) state.vision = params.get('vision');
            if (params.has('cont')) state.isContinuous = params.get('cont') === '1';
            if (params.has('map_engine')) state.mapEngine = params.get('map_engine');
            if (params.has('geojson_sample')) state.geoJsonSample = params.get('geojson_sample');
            if (params.has('geojson_share')) state.geoJsonShareId = sanitizeGeoJsonShareId(params.get('geojson_share'));
            if (params.has('choropleth_prop')) state.choroplethProperty = params.get('choropleth_prop');
            if (params.has('auto_field_minmax')) state.autoUseFieldMinMax = params.get('auto_field_minmax') === '1';
            if (params.has('show_value_click')) state.showFeatureValueOnClick = params.get('show_value_click') === '1';
            if (params.has('click_lat')) state.mapClickLat = parseFloat(params.get('click_lat'));
            if (params.has('click_lon')) state.mapClickLon = parseFloat(params.get('click_lon'));
            if (params.has('click_val')) state.mapClickValue = parseFloat(params.get('click_val'));
            if (params.has('click_lbl')) state.mapClickLabel = String(params.get('click_lbl') || '');
            if (params.has('rid')) state.currentRampId = params.get('rid');
            if (params.has('map3d')) state.map3d = params.get('map3d') === '1';
            if (params.has('map3dh')) state.map3dHeight = parseFloat(params.get('map3dh'));
            if (params.has('stroke_color')) {
                const rawStrokeColor = String(params.get('stroke_color') || '');
                state.strokeColor = normalizeStrokeColor(rawStrokeColor.startsWith('#') ? rawStrokeColor : ('#' + rawStrokeColor));
            }
            if (params.has('stroke_width')) state.strokeWidth = parseFloat(params.get('stroke_width'));
            if (params.has('stroke_opacity')) state.strokeOpacity = parseFloat(params.get('stroke_opacity'));
            if (state.map3dHeight > MAP3D_HEIGHT_MAX && state.map3dHeight <= 1000000) {
                // Backward compatibility: older URL used meters.
                state.map3dHeight = state.map3dHeight / 1000;
            }
            const hasMapEngine = params.has('map_engine');
            if (state.map3d && !hasMapEngine) state.mapEngine = 'maplibre';
            if (params.has('lat')) state.mapLat = parseFloat(params.get('lat'));
            if (params.has('lon')) state.mapLon = parseFloat(params.get('lon'));
            if (params.has('z')) state.mapZoom = parseFloat(params.get('z'));
            const hasPitch = params.has('pitch');
            const hasBearing = params.has('bearing');
            const parsedPitch = hasPitch ? parseFloat(params.get('pitch')) : NaN;
            const parsedBearing = hasBearing ? parseFloat(params.get('bearing')) : NaN;
            state.mapPitch = Number.isFinite(parsedPitch) ? parsedPitch : undefined;
            state.mapBearing = Number.isFinite(parsedBearing) ? parsedBearing : undefined;
            state.mapCameraCustom = (hasPitch || hasBearing) &&
                Number.isFinite(state.mapPitch) &&
                Number.isFinite(state.mapBearing) &&
                (Math.abs(state.mapPitch) > 0.01 || Math.abs(state.mapBearing) > 0.01);
            if (!Number.isFinite(state.map3dHeight)) state.map3dHeight = MAP3D_HEIGHT_DEFAULT;
            if (state.map3dHeight < MAP3D_HEIGHT_MIN) state.map3dHeight = MAP3D_HEIGHT_MIN;
            if (state.map3dHeight > MAP3D_HEIGHT_MAX) state.map3dHeight = MAP3D_HEIGHT_MAX;
            state.strokeColor = normalizeStrokeColor(state.strokeColor);
            if (!Number.isFinite(state.strokeWidth) || state.strokeWidth < 0) state.strokeWidth = 0;
            if (state.strokeWidth > 20) state.strokeWidth = 20;
            if (!Number.isFinite(state.strokeOpacity)) state.strokeOpacity = 1;
            if (state.strokeOpacity < 0) state.strokeOpacity = 0;
            if (state.strokeOpacity > 1) state.strokeOpacity = 1;
            if (!Number.isFinite(state.mapClickLat)) state.mapClickLat = null;
            if (!Number.isFinite(state.mapClickLon)) state.mapClickLon = null;
            if (!Number.isFinite(state.mapClickValue)) state.mapClickValue = null;
            if (typeof state.mapClickLabel !== 'string') state.mapClickLabel = '';
            if (!Number.isFinite(state.mapPitch)) state.mapPitch = state.map3d ? MAPLIBRE_BIRDSEYE.pitch : 0;
            if (!Number.isFinite(state.mapBearing)) state.mapBearing = state.map3d ? MAPLIBRE_BIRDSEYE.bearing : 0;
            if (state.map3d && !hasMeaningfulCustomCamera()) {
                state.mapPitch = MAPLIBRE_BIRDSEYE.pitch;
                state.mapBearing = MAPLIBRE_BIRDSEYE.bearing;
                state.mapCameraCustom = false;
            }
            
            document.getElementById('stepsInput').value = state.steps;
            document.getElementById('minValueInput').value = state.minValue;
            document.getElementById('maxValueInput').value = state.maxValue;
            document.getElementById('decimalsInput').value = state.decimals;
            document.getElementById('continuousToggle').checked = state.isContinuous;
            document.getElementById('modeSelect').value = state.mode;
            document.getElementById('visionSelect').value = state.vision;
            document.getElementById('mapEngineSelect').value = state.mapEngine;
            document.getElementById('geoJsonSampleSelect').value = state.geoJsonSample;
            document.getElementById('choroplethPropertySelect').value = state.choroplethProperty;
            document.getElementById('autoFieldMinMaxToggle').checked = !!state.autoUseFieldMinMax;
            document.getElementById('showFeatureValueToggle').checked = !!state.showFeatureValueOnClick;
            document.getElementById('strokeColorInput').value = state.strokeColor;
            document.getElementById('strokeWidthInput').value = String(state.strokeWidth);
            document.getElementById('strokeWidthSlider').value = String(state.strokeWidth);
            document.getElementById('strokeOpacityInput').value = String(state.strokeOpacity);
            document.getElementById('strokeOpacityValue').textContent = Number(state.strokeOpacity).toFixed(2);
        }

        function setAccountStatus(message, isError = false) {
            const el = document.getElementById('accountStatus');
            if (!el) return;
            el.textContent = message;
            el.classList.toggle('text-red-500', isError);
            el.classList.toggle('dark:text-red-400', isError);
            el.classList.toggle('text-slate-500', !isError);
            el.classList.toggle('dark:text-slate-400', !isError);
        }

        function updateGeoJsonSourceLabel() {
            const label = document.getElementById('geoJsonSourceLabel');
            if (!label) return;
            if (state.geoJsonShareId) {
                label.textContent = `Using uploaded GeoJSON (${state.geoJsonShareId}).`;
                return;
            }
            if (state.geoJsonIsCustom) {
                label.textContent = 'Using local custom GeoJSON (session only).';
                return;
            }
            label.textContent = 'Using built-in sample.';
        }

        function openGeoJsonPickerModal() {
            const modal = document.getElementById('geoJsonPickerModal');
            if (!modal) return;
            const uploadedHint = document.getElementById('geoJsonUploadedHint');
            if (uploadedHint) {
                uploadedHint.textContent = accountState.authenticated
                    ? 'Pick a file uploaded from /account.'
                    : 'Sign in to use uploaded GeoJSON.';
            }
            modal.classList.remove('opacity-0', 'pointer-events-none');
            document.body.classList.add('modal-active');
            lucide.createIcons();
        }

        function closeGeoJsonPickerModal() {
            const modal = document.getElementById('geoJsonPickerModal');
            if (!modal) return;
            modal.classList.add('opacity-0', 'pointer-events-none');
            document.body.classList.remove('modal-active');
        }

        function renderUploadedGeoJsonOptions() {
            const select = document.getElementById('geoJsonUploadedSelect');
            if (!select) return;
            select.innerHTML = '';
            if (!accountState.authenticated) {
                const opt = document.createElement('option');
                opt.value = '';
                opt.textContent = 'Sign in on /account to use uploaded files';
                select.appendChild(opt);
                select.disabled = true;
                return;
            }
            const uploads = Array.isArray(accountState.geojsonUploads) ? accountState.geojsonUploads : [];
            if (!uploads.length) {
                const opt = document.createElement('option');
                opt.value = '';
                opt.textContent = 'No uploaded GeoJSON files';
                select.appendChild(opt);
                select.disabled = true;
                return;
            }
            uploads.forEach((item) => {
                const opt = document.createElement('option');
                opt.value = item.geojson_id;
                opt.textContent = `${item.name} (${formatBytes(item.bytes)})`;
                select.appendChild(opt);
            });
            if (state.geoJsonShareId) {
                select.value = state.geoJsonShareId;
            }
            select.disabled = false;
        }

        async function refreshUploadedGeoJsonOptions() {
            if (!accountState.authenticated) {
                accountState.geojsonUploads = [];
                renderUploadedGeoJsonOptions();
                return;
            }
            try {
                const data = await appApi('geojson.list', 'GET');
                accountState.geojsonUploads = Array.isArray(data.geojsons) ? data.geojsons : [];
            } catch (error) {
                accountState.geojsonUploads = [];
                setAccountStatus(error.message || 'Could not load uploaded GeoJSON files.', true);
            }
            renderUploadedGeoJsonOptions();
        }

        async function applyLocalGeoJsonFile(file) {
            if (!file) return;
            if (file.size > CUSTOM_GEOJSON_MAX_BYTES) {
                setGeoJsonNotice(
                    `Custom GeoJSON is too large (${formatBytes(file.size)}). Limit is ${formatBytes(CUSTOM_GEOJSON_MAX_BYTES)} to prevent browser crashes.`,
                    true
                );
                return;
            }
            beginGeoJsonBusy(`Loading local GeoJSON: ${file.name}...`);
            try {
                state.geoJsonShareId = '';
                await new Promise((resolve) => requestAnimationFrame(resolve));
                await new Promise((resolve) => requestAnimationFrame(resolve));
                const text = await file.text();
                setGeoJsonLoading('Parsing GeoJSON...');
                await new Promise((resolve) => requestAnimationFrame(resolve));
                const parsed = JSON.parse(text);
                const normalized = normalizeGeoJsonData(parsed);
                configureChoroplethPropertySelector(normalized, true);
                setGeoJsonLoading('Assigning values and styling features...');
                assignFeatureValues(normalized);
                statesData = normalized;
                forceFitDataBounds = true;
                if (file.size > CUSTOM_GEOJSON_3D_DISABLE_BYTES && state.map3d) {
                    state.map3d = false;
                    update3dButtonState();
                    setGeoJsonNotice(
                        `Using local GeoJSON: ${file.name}. 3D was disabled for stability on large files (${formatBytes(file.size)}).`
                    );
                } else {
                    setGeoJsonNotice(`Using local GeoJSON: ${file.name}.`);
                }
                setGeoJsonLoading('Rendering map preview...');
                updateUI();
                await initMap();
                updateURL();
                updateGeoJsonSourceLabel();
            } catch (error) {
                console.error('Invalid local GeoJSON:', error);
                setGeoJsonNotice('Could not parse local GeoJSON file.', true);
            } finally {
                endGeoJsonBusy();
            }
        }

        async function applyUploadedGeoJsonSelection() {
            const select = document.getElementById('geoJsonUploadedSelect');
            if (!select) return;
            const selectedId = sanitizeGeoJsonShareId(select.value || '');
            if (!selectedId) {
                setGeoJsonNotice('Select an uploaded GeoJSON first.', true);
                return;
            }
            state.geoJsonShareId = selectedId;
            await runWithMapProcessing('Loading uploaded GeoJSON...', async () => {
                await loadSelectedGeoJson();
                updateUI();
                await initMap();
                updateURL();
                updateGeoJsonSourceLabel();
            });
        }

        function renderQuickProjectOptions() {
            const select = document.getElementById('quickProjectSelect');
            if (!select) return;
            select.innerHTML = '';
            const allOpt = document.createElement('option');
            allOpt.value = '';
            allOpt.textContent = 'All Projects';
            select.appendChild(allOpt);
            accountState.projects.forEach((project) => {
                const opt = document.createElement('option');
                opt.value = String(project.id);
                opt.textContent = project.name;
                select.appendChild(opt);
            });
            select.value = state.currentProjectId ? String(state.currentProjectId) : '';
        }

        function renderQuickRampOptions() {
            const select = document.getElementById('quickSavedRampSelect');
            if (!select) return;
            select.innerHTML = '';
            if (!accountState.ramps.length) {
                const opt = document.createElement('option');
                opt.value = '';
                opt.textContent = 'No saved ramps';
                select.appendChild(opt);
                return;
            }
            accountState.ramps.forEach((ramp) => {
                const opt = document.createElement('option');
                opt.value = ramp.ramp_uid;
                opt.textContent = ramp.title;
                select.appendChild(opt);
            });
            if (state.currentRampId) {
                select.value = state.currentRampId;
            }
        }

        function renderQuickAccountState() {
            const authActions = document.getElementById('quickAuthActions');
            const savedActions = document.getElementById('quickSavedActions');
            if (!authActions || !savedActions) return;
            authActions.classList.toggle('hidden', accountState.authenticated);
            savedActions.classList.toggle('hidden', !accountState.authenticated);
            if (accountState.authenticated) {
                setAccountStatus(`Signed in as ${accountState.user.display_name}.`);
            } else {
                setAccountStatus('Sign in on the account page to save or load ramps.');
            }
            renderUploadedGeoJsonOptions();
            renderQuickProjectOptions();
            renderQuickRampOptions();
        }

        async function refreshProjectsAndRamps() {
            if (!accountState.authenticated) {
                accountState.projects = [];
                accountState.ramps = [];
                accountState.geojsonUploads = [];
                renderQuickAccountState();
                return;
            }
            const projectsResp = await appApi('projects.list', 'GET');
            accountState.projects = projectsResp.projects || [];
            const query = {};
            if (state.currentProjectId) query.project_id = state.currentProjectId;
            const rampsResp = await appApi('ramps.list', 'GET', null, query);
            accountState.ramps = rampsResp.ramps || [];
            await refreshUploadedGeoJsonOptions();
            renderQuickAccountState();
        }

        async function refreshSessionAndAccountData() {
            try {
                const data = await appApi('session', 'GET');
                accountState.authenticated = !!data.authenticated;
                accountState.user = data.user || null;
                if (!accountState.authenticated) {
                    state.currentProjectId = null;
                    state.currentRampId = null;
                    accountState.projects = [];
                    accountState.ramps = [];
                    accountState.geojsonUploads = [];
                    renderQuickAccountState();
                    return;
                }
                await refreshProjectsAndRamps();
                if (state.currentRampId) {
                    await loadRampById(state.currentRampId, false);
                }
            } catch (error) {
                setAccountStatus(error.message || 'Could not load account details.', true);
            }
        }

        async function loadRampById(rampId, announce = true) {
            if (!rampId) return;
            const data = await appApi('ramps.get', 'GET', null, { ramp_id: rampId });
            const ramp = data.ramp || null;
            if (!ramp || !ramp.state) {
                throw new Error('Saved ramp is missing data.');
            }
            state.currentRampId = ramp.ramp_id;
            state.currentProjectId = ramp.project_id ? Number(ramp.project_id) : null;
            await applySavedState(ramp.state);
            renderQuickProjectOptions();
            renderQuickRampOptions();
            if (announce) {
                setAccountStatus(`Loaded "${ramp.title}".`);
            }
        }

        // --- Modal Logic ---
        function openExportModal(type) {
            trackAnalyticsEvent('export_option_click', {
                option: type,
                map_engine: state.mapEngine,
                continuous: !!state.isContinuous
            });

            const modal = document.getElementById('exportModal');
            const title = document.getElementById('modalTitle');
            const codeElem = document.getElementById('modalCode');
            const ramp = generateRamp();
            const exportValueProperty = getActiveValuePropertyName() || 'value';
            const exportValuePropertyLiteral = JSON.stringify(exportValueProperty);
            const exportStrokeColor = normalizeStrokeColor(state.strokeColor);
            const exportStrokeWidth = Number(getStrokeWidth().toFixed(2));
            const exportStrokeOpacity = Number(getStrokeOpacity().toFixed(2));
            let content = '';
            let language = 'javascript';

            if (type === 'json') {
                title.innerHTML = '<i data-lucide="file-json" class="w-6 h-6 text-blue-500"></i> Export JSON (Full)';
                const jsonRamp = {
                    mode: state.mode,
                    isContinuous: state.isContinuous,
                    stops: state.stops.map(s => s.color),
                    ramp: ramp.map(step => ({
                        value: parseFloat(step.value),
                        color: step.color
                    }))
                };
                content = JSON.stringify(jsonRamp, null, 3);
                language = 'json';
            } else if (type === 'json-colors') {
                title.innerHTML = '<i data-lucide="list" class="w-6 h-6 text-green-500"></i> Export JSON Colors';
                content = JSON.stringify(ramp.map(r => r.color), null, 2);
                language = 'json';
            } else if (type === 'css') {
                title.innerHTML = '<i data-lucide="palette" class="w-6 h-6 text-purple-500"></i> Export CSS Gradient';
                const cssColors = ramp.map(r => r.color).join(', ');
                content = `background: linear-gradient(90deg, ${cssColors});`;
                language = 'css';
            } else if (type === 'javascript') {
                title.innerHTML = '<i data-lucide="code" class="w-6 h-6 text-blue-500"></i> Export JavaScript';
                
                if (state.isContinuous) {
                    const stopsStr = state.stops.map(s => `'${s.color}'`).join(', ');
                    content = `// Blending mode: ${state.mode} (Continuous)
const colors = [${stopsStr}];
const minValue = ${state.minValue};
const maxValue = ${state.maxValue};

/**
 * Returns an interpolated color between two hex values.
 */
function interpolate(c1, c2, f) {
  const r1 = parseInt(c1.substring(1, 3), 16);
  const g1 = parseInt(c1.substring(3, 5), 16);
  const b1 = parseInt(c1.substring(5, 7), 16);
  const r2 = parseInt(c2.substring(1, 3), 16);
  const g2 = parseInt(c2.substring(3, 5), 16);
  const b2 = parseInt(c2.substring(5, 7), 16);
  const r = Math.round(r1 + (r2 - r1) * f).toString(16).padStart(2, '0');
  const g = Math.round(g1 + (g2 - g1) * f).toString(16).padStart(2, '0');
  const b = Math.round(b1 + (b2 - b1) * f).toString(16).padStart(2, '0');
  return '#' + r + g + b;
}

/**
 * Returns the interpolated color for a given value.
 * @param {number} v - The value to color.
 * @returns {string} Hex color string.
 */
function getColor(v) {
  if (v <= minValue) return colors[0];
  if (v >= maxValue) return colors[colors.length - 1];
  
  const pos = (v - minValue) / (maxValue - minValue);
  const segment = pos * (colors.length - 1);
  const index = Math.floor(segment);
  const factor = segment - index;
  
  return interpolate(colors[index], colors[index + 1], factor);
}`;
                } else {
                    // Build the function string manually for discrete steps
                    const rampReversed = [...ramp].reverse();
                    content = `// Blending mode: ${state.mode}\n` +
                        'function getColor(v) {\n  ' + 
                        rampReversed.map((step, i) => {
                            const val = parseFloat(step.value);
                            if (i === rampReversed.length - 1) return `return '${step.color}';`;
                            return `if (v >= ${val.toFixed(state.decimals)}) return '${step.color}';`;
                        }).join('\n  ') + 
                        '\n}';
                }
                language = 'javascript';
            } else if (type === 'leaflet') {
                title.innerHTML = '<i data-lucide="map" class="w-6 h-6 text-green-500"></i> Export Leaflet';
                
                const rampReversed = [...ramp].reverse();
                const colorsStr = ramp.map(s => `'${s.color}'`).join(', ');
                const valuesStr = ramp.map(s => s.value).join(', ');
                
                if (state.isContinuous) {
                    const stopsStr = state.stops.map(s => `'${s.color}'`).join(', ');
                    content = `/* 
 * Leaflet Choropleth Styling & Legend (Continuous)
 * -----------------------------------------------
 */

// 1. Color scale configuration
const colors = [${stopsStr}];
const minValue = ${state.minValue};
const maxValue = ${state.maxValue};

// Helper for linear interpolation
function interpolate(c1, c2, f) {
  const hex = x => x.toString(16).padStart(2, '0');
  const r = i => parseInt(i.substring(1, 3), 16);
  const g = i => parseInt(i.substring(3, 5), 16);
  const b = i => parseInt(i.substring(5, 7), 16);
  return '#' + 
    hex(Math.round(r(c1) + (r(c2) - r(c1)) * f)) +
    hex(Math.round(g(c1) + (g(c2) - g(c1)) * f)) +
    hex(Math.round(b(c1) + (b(c2) - b(c1)) * f));
}

function getColor(v) {
    if (v <= minValue) return colors[0];
    if (v >= maxValue) return colors[colors.length - 1];
    const pos = (v - minValue) / (maxValue - minValue);
    const s = pos * (colors.length - 1);
    const i = Math.floor(s);
    return interpolate(colors[i], colors[i + 1], s - i);
}

// 2. Feature styling function
const valueProperty = ${exportValuePropertyLiteral};
const borderColor = '${exportStrokeColor}';
const borderWidth = ${exportStrokeWidth};
const borderOpacity = ${exportStrokeOpacity};
function style(feature) {
    return {
        fillColor: getColor(Number(feature.properties[valueProperty])),
        weight: borderWidth,
        opacity: borderOpacity,
        color: borderColor,
        fillOpacity: 0.8
    };
}

// 3. Adding the legend to your map
const legend = L.control({position: 'bottomright'});

legend.onAdd = function (map) {
    const div = L.DomUtil.create('div', 'info legend');
    const gradient = 'linear-gradient(to bottom, ' + colors.join(', ') + ')';
    
    div.style.background = 'white';
    div.style.padding = '10px';
    div.style.borderRadius = '5px';
    div.style.boxShadow = '0 0 15px rgba(0,0,0,0.2)';
    
    div.innerHTML = \`
        <div style="margin-bottom: 5px; font-weight: bold; font-size: 12px;">Range</div>
        <div style="display: flex; flex-direction: column; height: 150px;">
            <div style="flex: 1; display: flex;">
                <div style="width: 20px; height: 100%; background: \${gradient}; margin-right: 10px; border: 1px solid #ccc;"></div>
                <div style="display: flex; flex-direction: column; justify-content: space-between; font-size: 11px; color: #666;">
                    <span>\${maxValue}</span>
                    <span>\${((maxValue + minValue) / 2).toFixed(state.decimals)}</span>
                    <span>\${minValue}</span>
                </div>
            </div>
        </div>
    \`;

    return div;
};

legend.addTo(map);`;
                } else {
                    content = `/* 
 * Leaflet Choropleth Styling & Legend
 * ----------------------------------
 */

// 1. Color scale function
function getColor(d) {
    return ${rampReversed.map((step, i) => {
        const val = parseFloat(step.value);
        if (i === rampReversed.length - 1) return `'${step.color}';`;
        return `d >= ${val.toFixed(state.decimals)} ? '${step.color}' :`;
    }).join('\n           ')}
}

// 2. Feature styling function
const valueProperty = ${exportValuePropertyLiteral};
const borderColor = '${exportStrokeColor}';
const borderWidth = ${exportStrokeWidth};
const borderOpacity = ${exportStrokeOpacity};
function style(feature) {
    return {
        fillColor: getColor(Number(feature.properties[valueProperty])),
        weight: borderWidth,
        opacity: borderOpacity,
        color: borderColor,
        fillOpacity: 0.8
    };
}

// 3. Adding the legend to your map
const legend = L.control({position: 'bottomright'});

legend.onAdd = function (map) {
    const div = L.DomUtil.create('div', 'info legend');
    const grades = [${valuesStr}];
    const colors = [${colorsStr}];
    let labels = [];

    // Loop through our density intervals and generate a label with a colored square for each interval
    for (let i = 0; i < grades.length; i++) {
        div.innerHTML +=
            '<i style="background:' + colors[i] + '; width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7;"></i> ' +
            grades[i] + (grades[i + 1] ? '&ndash;' + grades[i + 1] + '<br>' : '+');
    }

    return div;
};

legend.addTo(map);

/* CSS required for the legend:
.legend {
    line-height: 18px;
    color: #555;
    background: white;
    padding: 10px;
    border-radius: 5px;
    box-shadow: 0 0 15px rgba(0,0,0,0.2);
}
.legend i {
    width: 18px;
    height: 18px;
    float: left;
    margin-right: 8px;
    opacity: 0.7;
}
*/`;
                }
                language = 'javascript';
                } else if (type === 'api') {
                    title.innerHTML = '<i data-lucide="server" class="w-6 h-6 text-blue-500"></i> API Endpoint URL';
                    const baseUrl = new URL('api/', window.location.href).toString();
                    if (state.currentRampId) {
                        content = `${baseUrl}?api_key=YOUR_API_KEY&ramp_id=${encodeURIComponent(state.currentRampId)}`;
                    } else {
                        const params = new URLSearchParams();
                        params.set('min_color', state.stops[0].color);
                        params.set('max_color', state.stops[state.stops.length - 1].color);
                        params.set('min_value', state.minValue);
                        params.set('max_value', state.maxValue);
                        params.set('steps', state.steps);
                        params.set('decimals', state.decimals);
                        content = `${baseUrl}?${params.toString()}`;
                    }
                    language = 'text';
            } else if (type === 'maplibre') {
                title.innerHTML = '<i data-lucide="box" class="w-6 h-6 text-cyan-500"></i> Export MapLibre';
                const rampReversed = [...ramp].reverse();
                const colorsStr = ramp.map(s => `'${s.color}'`).join(', ');
                const valuesStr = ramp.map(s => Number(s.value).toFixed(state.decimals)).join(', ');
                const stopsStr = state.stops.map(s => `'${s.color}'`).join(', ');
                const contourStr = state.stops.map((s, i) => {
                    const t = i / Math.max(state.stops.length - 1, 1);
                    const v = state.minValue + (state.maxValue - state.minValue) * t;
                    return `${Number(v.toFixed(state.decimals))}, '${s.color}'`;
                }).join(',\n                ');

                if (state.isContinuous) {
                    content = `/* 
 * MapLibre Choropleth Styling (Continuous)
 * ---------------------------------------
 * Replace 'YOUR_GEOJSON' as needed.
 */

const minValue = ${state.minValue};
const maxValue = ${state.maxValue};
const colors = [${stopsStr}];
const valueProperty = ${exportValuePropertyLiteral};
const borderColor = '${exportStrokeColor}';
const borderWidth = ${exportStrokeWidth};
const borderOpacity = ${exportStrokeOpacity};

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [${Number(state.mapLon || -96).toFixed(6)}, ${Number(state.mapLat || 37.8).toFixed(6)}],
  zoom: ${Number(state.mapZoom || 4).toFixed(2)}
});

map.on('load', () => {
  map.addSource('regions', { type: 'geojson', data: YOUR_GEOJSON });

  map.addLayer({
    id: 'regions-fill',
    type: 'fill',
    source: 'regions',
    paint: {
      'fill-color': [
        'interpolate', ['linear'],
        ['to-number', ['coalesce', ['get', valueProperty], 0]],
                ${contourStr}
      ],
      'fill-opacity': 0.8
    }
  });
  
  map.addLayer({
    id: 'regions-outline',
    type: 'line',
    source: 'regions',
    paint: {
      'line-color': borderColor,
      'line-width': borderWidth,
      'line-opacity': borderOpacity
    }
  });
});

// Optional: simple DOM legend
const legendValues = [${valuesStr}];
const legendColors = [${colorsStr}];`;
                } else {
                    content = `/* 
 * MapLibre Choropleth Styling (Discrete)
 * -------------------------------------
 * Replace 'YOUR_GEOJSON' as needed.
 */
const valueProperty = ${exportValuePropertyLiteral};
const borderColor = '${exportStrokeColor}';
const borderWidth = ${exportStrokeWidth};
const borderOpacity = ${exportStrokeOpacity};

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [${Number(state.mapLon || -96).toFixed(6)}, ${Number(state.mapLat || 37.8).toFixed(6)}],
  zoom: ${Number(state.mapZoom || 4).toFixed(2)}
});

map.on('load', () => {
  map.addSource('regions', { type: 'geojson', data: YOUR_GEOJSON });

  map.addLayer({
    id: 'regions-fill',
    type: 'fill',
    source: 'regions',
    paint: {
      'fill-color': [
        'step',
        ['to-number', ['coalesce', ['get', valueProperty], 0]],
        '${ramp[0].color}',
${ramp.slice(1).map(step => `        ${Number(step.value).toFixed(state.decimals)}, '${step.color}'`).join(',\n')}
      ],
      'fill-opacity': 0.8
    }
  });
  
  map.addLayer({
    id: 'regions-outline',
    type: 'line',
    source: 'regions',
    paint: {
      'line-color': borderColor,
      'line-width': borderWidth,
      'line-opacity': borderOpacity
    }
  });
});

// Equivalent JS color helper:
function getColor(v) {
    return ${rampReversed.map((step, i) => {
        const val = parseFloat(step.value);
        if (i === rampReversed.length - 1) return `'${step.color}';`;
        return `v >= ${val.toFixed(state.decimals)} ? '${step.color}' :`;
    }).join('\n           ')}
}`;
                }
                language = 'javascript';
            } else if (type === 'image-url') {
                title.innerHTML = '<i data-lucide="image-plus" class="w-6 h-6 text-pink-500"></i> Export Image URL';
                const imageBase = new URL('ramp-image/', window.location.href).toString();
                const params = new URLSearchParams();
                const exportColors = (state.isContinuous ? state.stops.map(s => s.color) : ramp.map(s => s.color))
                    .map(c => c.replace('#', ''))
                    .join(',');
                params.set('colors', exportColors);
                params.set('continuous', state.isContinuous ? '1' : '0');
                params.set('w', '1200');
                params.set('h', '160');
                content = `${imageBase}?${params.toString()}`;
                language = 'text';
            }

            codeElem.className = `language-${language} text-sm font-mono leading-relaxed`;
            codeElem.textContent = content;
            
            // Re-highlight
            hljs.highlightBlock(codeElem);
            
            modal.classList.remove('opacity-0', 'pointer-events-none');
            document.body.classList.add('modal-active');
            lucide.createIcons();

            // Set up modal buttons
            document.getElementById('modalCopyBtn').onclick = () => {
                navigator.clipboard.writeText(content);
                const btn = document.getElementById('modalCopyBtn');
                const original = btn.innerHTML;
                btn.innerHTML = '<i data-lucide="check" class="w-4 h-4 text-green-500"></i>';
                lucide.createIcons();
                setTimeout(() => {
                    btn.innerHTML = original;
                    lucide.createIcons();
                }, 2000);
            };

            const openOnly = (type === 'api' || type === 'image-url');
            document.getElementById('modalDownloadBtn').innerHTML = openOnly ? 'Open in Browser' : 'Download';
            document.getElementById('modalDownloadBtn').onclick = () => {
                if (openOnly) {
                    window.open(content, '_blank');
                    return;
                }
                const blob = new Blob([content], { type: type === 'json' || type === 'json-colors' ? 'application/json' : 'application/javascript' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                const extension = type === 'json' || type === 'json-colors' ? 'json' : (type === 'css' ? 'css' : 'js');
                a.download = `ramp.${extension}`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            };
        }

        function closeModal() {
            const modal = document.getElementById('exportModal');
            modal.classList.add('opacity-0', 'pointer-events-none');
            document.body.classList.remove('modal-active');
        }

        function openChangelogModal() {
            const modal = document.getElementById('changelogModal');
            modal.classList.remove('opacity-0', 'pointer-events-none');
            document.body.classList.add('modal-active');
            lucide.createIcons();
        }

        function closeChangelogModal() {
            const modal = document.getElementById('changelogModal');
            modal.classList.add('opacity-0', 'pointer-events-none');
            document.body.classList.remove('modal-active');
        }

        function openSaveAsModal() {
            const modal = document.getElementById('saveAsModal');
            const input = document.getElementById('saveAsTitleInput');
            input.value = '';
            modal.classList.remove('opacity-0', 'pointer-events-none');
            document.body.classList.add('modal-active');
            lucide.createIcons();
            setTimeout(() => input.focus(), 10);
        }

        function closeSaveAsModal() {
            const modal = document.getElementById('saveAsModal');
            modal.classList.add('opacity-0', 'pointer-events-none');
            document.body.classList.remove('modal-active');
        }

        // Close modal on escape key
        document.addEventListener('keydown', (e) => {
            if (e.key !== 'Escape') return;
            closeModal();
            closeChangelogModal();
            closeSaveAsModal();
            closeChoroplethSettingsModal();
            closeGeoJsonPickerModal();
        });
        const onFullscreenChange = () => {
            setFullscreenButtonState();
        };
        document.addEventListener('fullscreenchange', onFullscreenChange);
        document.addEventListener('webkitfullscreenchange', onFullscreenChange);

        // Close modal on overlay click
        document.querySelector('.modal-overlay').addEventListener('click', closeModal);
        document.querySelector('.modal-overlay-changelog').addEventListener('click', closeChangelogModal);
        document.querySelector('.modal-overlay-save').addEventListener('click', closeSaveAsModal);
        document.querySelector('.modal-overlay-choropleth').addEventListener('click', closeChoroplethSettingsModal);
        document.querySelector('.modal-overlay-geojson-picker').addEventListener('click', closeGeoJsonPickerModal);
        document.getElementById('saveAsCloseBtn').addEventListener('click', closeSaveAsModal);
        document.getElementById('saveAsCancelBtn').addEventListener('click', closeSaveAsModal);
        document.getElementById('choroplethSettingsCloseBtn').addEventListener('click', closeChoroplethSettingsModal);
        document.getElementById('choroplethSettingsCloseFooterBtn').addEventListener('click', closeChoroplethSettingsModal);
        document.getElementById('geoJsonPickerCloseBtn').addEventListener('click', closeGeoJsonPickerModal);
        document.getElementById('geoJsonPickerCancelBtn').addEventListener('click', closeGeoJsonPickerModal);
        document.getElementById('choroplethSettingsBtn').addEventListener('click', () => {
            if (document.getElementById('choroplethSettingsBtn').disabled) return;
            openChoroplethSettingsModal();
        });
        document.getElementById('autoFieldMinMaxToggle').addEventListener('change', async (e) => {
            state.autoUseFieldMinMax = !!e.target.checked;
            if (state.autoUseFieldMinMax && applyActiveStatsToRampRange()) {
                await runWithMapProcessing('Applying min/max range...', async () => {
                    await updateMapAndWaitForRender();
                });
            } else {
                updateURL();
            }
            renderChoroplethSettings();
        });
        document.getElementById('showFeatureValueToggle').addEventListener('change', (e) => {
            state.showFeatureValueOnClick = !!e.target.checked;
            if (!state.showFeatureValueOnClick) {
                if (leafletMap) leafletMap.closePopup();
                if (mapLibreValuePopup) {
                    mapLibreValuePopup.remove();
                    mapLibreValuePopup = null;
                }
                state.mapClickLat = null;
                state.mapClickLon = null;
                state.mapClickValue = null;
                state.mapClickLabel = '';
            }
            updateURL();
        });

        // --- Event Listeners ---
        document.getElementById('addStopBtn').addEventListener('click', () => {
            const lastColor = state.stops.length > 0 ? state.stops[state.stops.length - 1].color : '#000000';
            state.stops.push({ id: Date.now(), color: lastColor });
            renderStops();
            updateUI();
        });

        document.getElementById('reverseBtn').addEventListener('click', () => {
            state.stops.reverse();
            renderStops();
            updateUI();
        });

        document.getElementById('strokeColorTrigger').addEventListener('click', () => {
            const input = document.getElementById('strokeColorInput');
            if (!input) return;
            input.focus();
            input.click();
        });

        document.getElementById('stopsContainer').addEventListener('click', (e) => {
            const removeBtn = e.target.closest('.remove-stop');
            if (removeBtn && state.stops.length > 2) {
                const index = parseInt(removeBtn.dataset.id);
                state.stops.splice(index, 1);
                renderStops();
                updateUI();
                return;
            }

            const row = e.target.closest('.stop-item');
            if (row) {
                const input = row.querySelector('.color-picker-input');
                if (input) {
                    input.focus();
                    input.click();
                }
                return;
            }

            const trigger = e.target.closest('.color-trigger');
            if (trigger) {
                const input = trigger.parentElement.querySelector('input');
                input.click();
            }
        });

        document.addEventListener('input', (e) => {
            if (e.target.id === 'continuousToggle') {
                state.isContinuous = e.target.checked;
                renderPresets();
                updateUI();
            }
            if (e.target.id === 'modeSelect') {
                state.mode = e.target.value;
                updateUI();
            }
            if (e.target.id === 'visionSelect') {
                state.vision = e.target.value;
                updateUI();
            }
            if (e.target.classList.contains('color-picker-input')) {
                const index = parseInt(e.target.dataset.id);
                const stop = state.stops[index];
                if (stop) {
                    stop.color = e.target.value;
                    updateStopRowVisual(index);
                    updateUI();
                }
            }
            if (e.target.id === 'stepsInput') {
                state.steps = parseInt(e.target.value) || 2;
                if (state.steps > 100) state.steps = 100;
                updateUI();
            }
            if (e.target.id === 'minValueInput') {
                state.minValue = parseFloat(e.target.value) || 0;
                updateUI();
            }
            if (e.target.id === 'maxValueInput') {
                state.maxValue = parseFloat(e.target.value) || 100;
                updateUI();
            }
            if (e.target.id === 'decimalsInput') {
                state.decimals = parseInt(e.target.value) || 0;
                if (state.decimals < 0) state.decimals = 0;
                if (state.decimals > 10) state.decimals = 10;
                updateUI();
            }
            if (e.target.id === 'strokeColorInput') {
                const parsed = parseStrokeColorAndAlpha(e.target.value);
                state.strokeColor = parsed.color;
                if (parsed.alpha !== null) {
                    state.strokeOpacity = parsed.alpha;
                }
                updateUI();
            }
            if (e.target.id === 'strokeWidthInput' || e.target.id === 'strokeWidthSlider') {
                state.strokeWidth = parseFloat(e.target.value);
                if (!Number.isFinite(state.strokeWidth) || state.strokeWidth < 0) state.strokeWidth = 0;
                if (state.strokeWidth > 20) state.strokeWidth = 20;
                updateUI();
            }
            if (e.target.id === 'strokeOpacityInput') {
                state.strokeOpacity = parseFloat(e.target.value);
                if (!Number.isFinite(state.strokeOpacity)) state.strokeOpacity = 1;
                if (state.strokeOpacity < 0) state.strokeOpacity = 0;
                if (state.strokeOpacity > 1) state.strokeOpacity = 1;
                const opacityValue = document.getElementById('strokeOpacityValue');
                if (opacityValue) opacityValue.textContent = state.strokeOpacity.toFixed(2);
                updateUI();
            }
        });

        // Listen for Coloris changes specifically
        document.addEventListener('coloris:pick', event => {
            const input = event.detail.el;
            if (input.classList.contains('color-picker-input')) {
                const index = parseInt(input.dataset.id);
                const stop = state.stops[index];
                if (stop) {
                    stop.color = event.detail.color;
                    updateStopRowVisual(index);
                    updateUI();
                }
                return;
            }
            if (input.id === 'strokeColorInput' || input.classList.contains('stroke-color-picker-input')) {
                const parsed = parseStrokeColorAndAlpha(event.detail.color);
                state.strokeColor = parsed.color;
                if (parsed.alpha !== null) {
                    state.strokeOpacity = parsed.alpha;
                }
                updateUI();
            }
        });

        document.querySelectorAll('.copy-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const targetId = btn.dataset.target;
                const text = document.getElementById(targetId).textContent;
                navigator.clipboard.writeText(text);
                const originalText = btn.innerHTML;
                btn.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i> Copied!';
                lucide.createIcons();
                setTimeout(() => {
                    btn.innerHTML = originalText;
                    lucide.createIcons();
                }, 2000);
            });
        });

        document.getElementById('shareBtn').addEventListener('click', () => {
            navigator.clipboard.writeText(window.location.href);
            alert('URL copied to clipboard! You can share this ramp.');
        });

        document.getElementById('changelogBtn').addEventListener('click', () => {
            openChangelogModal();
        });

        document.getElementById('exportSvgBtn').addEventListener('click', (e) => {
            e.preventDefault();
            trackAnalyticsEvent('export_option_click', {
                option: 'svg',
                map_engine: state.mapEngine,
                continuous: !!state.isContinuous
            });
            const ramp = generateRamp();
            const width = 500;
            const height = 50;
            const stepWidth = width / ramp.length;
            
            let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
            ramp.forEach((step, i) => {
                svg += `<rect x="${i * stepWidth}" y="0" width="${stepWidth + 0.5}" height="${height}" fill="${step.color}" />`;
            });
            svg += `</svg>`;
            
            const blob = new Blob([svg], { type: 'image/svg+xml' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'ramp.svg';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });

        document.getElementById('mapEngineSelect').addEventListener('change', async (e) => {
            state.mapEngine = e.target.value === 'maplibre' ? 'maplibre' : 'leaflet';
            if (state.mapEngine === 'leaflet') {
                state.map3d = false;
            }
            await runWithMapProcessing('Switching map engine...', async () => {
                await initMap();
                updateUI();
            });
        });

        document.getElementById('map3dToggle').addEventListener('click', () => {
            state.map3d = !state.map3d;
            if (state.mapEngine === 'maplibre') {
                applyMapLibre3dState(true);
            } else {
                update3dButtonState();
                updateURL();
            }
        });

        document.getElementById('map3dHeightSlider').addEventListener('input', (e) => {
            const value = parseFloat(e.target.value);
            if (!Number.isFinite(value)) return;
            state.map3dHeight = Math.min(MAP3D_HEIGHT_MAX, Math.max(MAP3D_HEIGHT_MIN, value));
            update3dButtonState();
            scheduleMap3dHeightRender();
            scheduleUrlUpdate(120);
        });

        document.getElementById('map3dHeightSlider').addEventListener('change', () => {
            if (map3dUrlDebounceId !== null) {
                clearTimeout(map3dUrlDebounceId);
                map3dUrlDebounceId = null;
            }
            updateURL();
        });

        document.getElementById('geoJsonSampleSelect').addEventListener('change', async (e) => {
            state.geoJsonSample = e.target.value;
            state.geoJsonShareId = '';
            await runWithMapProcessing('Loading GeoJSON sample...', async () => {
                await loadSelectedGeoJson();
                updateUI();
                await initMap();
                updateGeoJsonSourceLabel();
            });
        });

        document.getElementById('choroplethPropertySelect').addEventListener('change', async () => {
            state.choroplethProperty = document.getElementById('choroplethPropertySelect').value || CHOROPLETH_AUTO_PROPERTY;
            if (state.autoUseFieldMinMax) {
                applyActiveStatsToRampRange();
            }
            await runWithMapProcessing('Recoloring map...', async () => {
                await updateMapAndWaitForRender();
                renderChoroplethSettings();
                updateURL();
            });
        });

        document.getElementById('openGeoJsonPickerBtn').addEventListener('click', async () => {
            await refreshUploadedGeoJsonOptions();
            openGeoJsonPickerModal();
        });

        document.getElementById('geoJsonLocalFileInput').addEventListener('change', async (e) => {
            const [file] = e.target.files || [];
            if (!file) {
                return;
            }
            closeGeoJsonPickerModal();
            await applyLocalGeoJsonFile(file);
            e.target.value = '';
        });

        document.getElementById('geoJsonPickerApplyBtn').addEventListener('click', async () => {
            closeGeoJsonPickerModal();
            await applyUploadedGeoJsonSelection();
        });

        document.getElementById('themeToggle').addEventListener('click', async () => {
            const isDark = document.documentElement.classList.toggle('dark');
            localStorage.setItem('darkMode', isDark);
            
            if (state.mapEngine === 'leaflet' && leafletTileLayer && leafletMap) {
                leafletTileLayer.setUrl(isDark ? MAP_TILES.dark : MAP_TILES.light);
            } else if (state.mapEngine === 'maplibre') {
                await runWithMapProcessing('Updating map style...', async () => {
                    await initMap();
                });
            }

            initColoris();
            lucide.createIcons();
            if (state.mapEngine === 'maplibre' && state.showFeatureValueOnClick) {
                restoreMapLibreClickPopupFromState();
            }
            updateUI(); // Refresh UI components like table rows
        });

        document.getElementById('quickProjectSelect').addEventListener('change', async (e) => {
            const value = e.target.value;
            state.currentProjectId = value ? Number(value) : null;
            try {
                await refreshProjectsAndRamps();
            } catch (error) {
                setAccountStatus(error.message, true);
            }
        });

        document.getElementById('quickLoadBtn').addEventListener('click', async () => {
            try {
                const rampId = document.getElementById('quickSavedRampSelect').value;
                if (!rampId) {
                    setAccountStatus('Select a saved ramp first.', true);
                    return;
                }
                await loadRampById(rampId, true);
            } catch (error) {
                setAccountStatus(error.message, true);
            }
        });

        document.getElementById('quickSaveBtn').addEventListener('click', async () => {
            if (!accountState.authenticated) {
                setAccountStatus('Sign in first on the account page.', true);
                return;
            }
            openSaveAsModal();
        });

        document.getElementById('quickUpdateBtn').addEventListener('click', async () => {
            try {
                if (!accountState.authenticated) {
                    setAccountStatus('Sign in first on the account page.', true);
                    return;
                }
                const selectedRampId = document.getElementById('quickSavedRampSelect').value || state.currentRampId;
                if (!selectedRampId) {
                    setAccountStatus('Load or select a saved ramp first.', true);
                    return;
                }
                const selectedRamp = (accountState.ramps || []).find((r) => r.ramp_uid === selectedRampId) || null;
                const projectValue = document.getElementById('quickProjectSelect').value;
                const payload = {
                    project_id: projectValue ? Number(projectValue) : (selectedRamp && selectedRamp.project_id ? Number(selectedRamp.project_id) : null),
                    title: selectedRamp && selectedRamp.title ? selectedRamp.title : 'Updated Ramp',
                    ramp_id: selectedRampId,
                    state: serializeCurrentState()
                };
                const data = await appApi('ramps.save', 'POST', payload);
                state.currentRampId = data.ramp_id;
                await refreshProjectsAndRamps();
                updateURL();
                setAccountStatus('Saved changes to selected ramp.');
            } catch (error) {
                setAccountStatus(error.message, true);
            }
        });

        document.getElementById('saveAsConfirmBtn').addEventListener('click', async () => {
            try {
                const title = document.getElementById('saveAsTitleInput').value.trim();
                if (!title) {
                    setAccountStatus('Title is required.', true);
                    return;
                }
                const projectValue = document.getElementById('quickProjectSelect').value;
                const payload = {
                    project_id: projectValue ? Number(projectValue) : null,
                    title,
                    ramp_id: null,
                    state: serializeCurrentState()
                };
                const data = await appApi('ramps.save', 'POST', payload);
                state.currentRampId = data.ramp_id;
                await refreshProjectsAndRamps();
                updateURL();
                closeSaveAsModal();
                setAccountStatus(`Ramp saved (ID: ${state.currentRampId}).`);
            } catch (error) {
                setAccountStatus(error.message, true);
            }
        });

        // Init
        async function bootstrapApp() {
            loadFromURL();
            renderUploadedGeoJsonOptions();
            updateGeoJsonSourceLabel();
            renderStops();
            renderPresets();
            await runWithMapProcessing('Loading map...', async () => {
                await initMap();
                updateUI();
            });
            refreshSessionAndAccountData();
            lucide.createIcons();
            initializeBuilderAnalyticsBaseline();
        }

        bootstrapApp().catch((error) => {
            console.error('Bootstrap failed:', error);
            setGeoJsonNotice('Could not initialize map.', true);
            endGeoJsonBusy();
        });
    </script>
</body>
</html>
