GeoGuessr Map-Making Helper

enhance your mapping experience on map making app

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         GeoGuessr Map-Making Helper
// @name:zh-CN   GeoGuessr Map-Making 助手
// @namespace    https://greasyfork.org/users/1179204
// @version      1.1.3
// @description  enhance your mapping experience on map making app
// @description:zh-CN  enhance your mapping experience on map making app
// @icon         https://map-making.app/favicon.ico
// @author       KaKa
// @license      MIT
// @match        *://map-making.app/maps/*
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @require      https://unpkg.com/[email protected]/dist.min.js
// @require      https://unpkg.com/[email protected]/dist/h3-js.umd.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/index.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/suncalc.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/geotz.min.js
// @require      https://update.greasyfork.org/scripts/572089/1788027/OSM%20Map%20Features%20Data.js
// ==/UserScript==
(function () {

    // ======================================================================================================================================
    // =============================================================== Config ===============================================================
    // ======================================================================================================================================

    const CONSTANS={
        MONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],

        TAGS: ['Year',
               'Month',
               'Day',
               'Time',
               'Type',
               'Generation',
               'Country',
               'Subdivision',
               'Update Type',
               'No BadCam',
               'Update',
               'Fix',
               'Reset Heading',
               'Driving Direction',
               'Road',
               'Elevation',
               'Sun',
               'Weather',],

        TOOLTIPS: {
            'Year': 'Year of pano in format yyyy',
            'Month': 'Month of pano in format yy-mm',
            'Day': 'Specific date of pano in format yyyy-mm-dd',
            'Time': 'Exact time of pano with optional time range description, e.g., 09:35:21 marked as Morning',
            'Country': 'Country of pano (Google data)',
            'Subdivision': 'Primary administrative subdivision of pano location',
            'Road': 'Road name of pano location',
            'Generation': 'Camera generation of pano, categorized as Gen1, Gen2orGen3, Gen3, Gen4, BadCam',
            'Elevation': 'Elevation of street view location (Google data)',
            'Brightness': 'Average brightness of pano',
            'Type': 'Type of pano, categorized as Official, Unofficial, Trekker/Tripod',
            'Driving Direction': 'Absolute driving direction of streetview vehicle',
            'Reset Heading': 'Reset heading to default driving direction',
            'Update Type': 'Determine whether the location is newroad or or gen1update or ariupdate or gen2/3update or gen4update',
            'Fix': 'Fix broken locs by updating to latest coverage or searching for specific coverage based on saved date from map-making',
            'Update': 'Update pano to latest coverage or based on saved date from map-making, effective only for locs with panoID',
            'Detect': "Detect pano that is about to be removed and mark it as 'Dangerous' ",
            'Sun': 'Detect whether it is sunset or sunrise coverage(effective only for defalut coverage)',
            'Pan to Sun': 'Make pano heading to sun(moon),effective only for defalut coverage',
            'No BadCam': 'Change BadCam to higher quality if available',
            'Weather': 'Weather type recorded by the closest weather station closest, with an accuracy of 10mins(effective only for defalut coverage)',
        },

        WEATHER_CODE_MAP: {
            0: 'Clear sky',
            1: 'Mainly clear',
            2: 'Partly cloudy',
            3: 'Mostly cloudy',
            4: 'Overcast',
            61: 'Slight Rain',
            63: 'Moderate Rain',
            65: 'Heavy Rain',
            51: 'Light Drizzle',
            53: 'Moderate Drizzle',
            55: 'Dense Drizzle',
            77: 'Snow',
            85: 'Slight Snow',
            86: 'Heavy Snow',
        },

        DIRECTION_RANGE: [
            { name: 'North', range: [355, 5] },
            { name: 'Northeast', range: [5, 85] },
            { name: 'East', range: [85, 95] },
            { name: 'Southeast', range: [95, 175] },
            { name: 'South', range: [175, 185] },
            { name: 'Southwest', range: [185, 265] },
            { name: 'West', range: [265, 275] },
            { name: 'Northwest', range: [275, 355] }
        ],

        GEN2_COUNTRIES: [
            'AU', 'BR', 'CA', 'CL', 'JP', 'GB', 'IE', 'NZ', 'MX', 'RU', 'US', 'IT', 'DK',
            'PL', 'CZ', 'CH', 'SE', 'FI', 'BE', 'LU', 'NL', 'ZA', 'SG', 'TW', 'HK', 'MO',
            'AD', 'IM', 'JE', 'FR', 'DE', 'ES', 'PT', 'SJ', 'RO', 'SM', 'MC', 'GR',
        ],

        DEFAULT_SHORTCUTS: {
            togglePanel: { key: 'Tab', requireShift: false },
            switchLoc: { key: 'Q', requireShift: false },
            rewindLoc: { key: 'E', requireShift: false },
            deleteLoc: { key: 'C', requireShift: false },
            exitLoc: { key: 'X', requireShift: false },
            closeAndSaveLoc: { key: 'V', requireShift: false },
            copyLoc: { key: 'G', requireShift: false },
            hideElement: { key: 'H', requireShift: false },
            deSelectAll: { key: 'Z', requireShift: false },
            mergeTags: { key: 'P', requireShift: false },
            tagDialog: { key: 'T', requireShift: false },
            miniMap: { key: 'M', requireShift: false },
            deleteTags: { key: 'B', requireShift: true },
            resetGulf: { key: 'R', requireShift: true },
            classicMap: { key: 'N', requireShift: true },
            GeoguessrModal: { key: 'G', requireShift: true },
            findLinkPanos: { key: 'K', requireShift: true },
            exportAsCsv: { key: 'C', requireShift: true },
            fullScreenMap: { key: 'F', requireShift: true },
            jumpForward: {key: 'W', requireShift: true},
            jumpBackward: {key: 'S', requireShift: true},
        },

        MESSAGES : {
            NETWORK_ERROR:'Network request error',
            NO_SELECTION: 'Please select at least one location',
            NO_FEATURE: 'Please select at least one feature',
            DEPENDENCY_MISSING: 'Required dependencies are missing',
            TAGGING_SUCCESS: 'Tagging completed successfully',
            TAGGING_FAILED: 'Tagging failed',
            DOWNLOAD_SUCCESS: 'Downloading completed successfully',
            DOWNLOAD_FAILED : 'Error Downloading',
        },

        SHORTCUTS_DESCRIPTION:[
            { id: 'togglePanel', label: 'Toggle Main Panel' },
            { id: 'switchLoc', label: 'Switch to  Next Location' },
            { id: 'rewindLoc', label: 'Rewind to Previous Location' },
            { id: 'deleteLoc', label: 'Delete Current Location' },
            { id: 'exitLoc', label: 'Exit Current Location' },
            { id: 'closeAndSaveLoc', label: 'Close & Save Current Location' },
            { id: 'copyLoc', label: 'Copy Location' },
            { id: 'hideElement', label: 'Toggle Hide Map Making UI' },
            { id: 'deSelectAll', label: 'Deselect All Selections' },
            { id: 'mergeTags', label: 'Merge Selected Locations Tags' },
            { id: 'tagDialog', label: 'Toggle Tag Overlay' },
            { id: 'miniMap', label: 'Toggle Mini Map' },
            { id: 'deleteTags', label: 'Delete Selected Tags' },
            { id: 'classicMap', label: 'Toggle Classic Google Map Style' },
            { id: 'findLinkPanos', label: 'Find Link Panoramas' },
            { id: 'exportAsCsv', label: 'Export Selected Locations as CSV' },
            { id: 'fullScreenMap', label: 'Toggle Fullscreen Map' },
            { id: 'resetGulf', label: 'Reset Mexico Gulf' },
            { id: 'GeoguessrModal', label: 'Toggle GeoGuessr Modal' },
            { id: 'jumpForward', label: 'Jump forward 100 metres'},
            { id: 'jumpBackward', label: 'Jump backward 100 metres'}
        ],

        SVG_SOURCE : {
            APP: `<svg  width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M2.06 7.61c-.25.95.31 1.92 1.26 2.18l4.3 1.15c.94.25 1.91-.31 2.17-1.26l1.15-4.3c.25-.94-.31-1.91-1.26-2.17l-4.3-1.15c-.94-.25-1.91.31-2.17 1.26l-1.15 4.3ZM12.98 7.87a2 2 0 0 0 1.75 2.95H20a2 2 0 0 0 1.76-2.95l-2.63-4.83a2 2 0 0 0-3.51 0l-2.63 4.83ZM5.86 13.27a.89.89 0 0 1 1.28 0l.75.77a.9.9 0 0 0 .54.26l1.06.12c.5.06.85.52.8 1.02l-.13 1.08c-.02.2.03.42.14.6l.56.92c.27.43.14 1-.28 1.26l-.9.58a.92.92 0 0 0-.37.48l-.36 1.02a.9.9 0 0 1-1.15.57l-1-.36a.89.89 0 0 0-.6 0l-1 .36a.9.9 0 0 1-1.15-.57l-.36-1.02a.92.92 0 0 0-.37-.48l-.9-.58a.93.93 0 0 1-.28-1.26l.56-.93c.11-.17.16-.38.14-.59l-.12-1.08c-.06-.5.3-.96.8-1.02l1.05-.12a.9.9 0 0 0 .54-.26l.75-.77ZM18.52 13.71a1.1 1.1 0 0 0-2.04 0l-.46 1.24c-.19.5-.57.88-1.07 1.07l-1.24.46a1.1 1.1 0 0 0 0 2.04l1.24.46c.5.19.88.57 1.07 1.07l.46 1.24c.35.95 1.7.95 2.04 0l.46-1.24c.19-.5.57-.88 1.07-1.07l1.24-.46a1.1 1.1 0 0 0 0-2.04l-1.24-.46a1.8 1.8 0 0 1-1.07-1.07l-.46-1.24Z" class=""></path></svg>`,
            BIN: `<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="8" y1="6" x2="8" y2="4"/><line x1="16" y1="6" x2="16" y2="4"/><rect x="5" y="6" width="14" height="14" rx="2" ry="2"/> <line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`,
            COPY: `<svg height="24" width="24" viewBox="0 0 24 24"><path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg>`,
            EDIT: `<svg height="21" width="21" viewBox="0 0 24 24"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"></path></svg>`,
            CROSS: `<svg height="24" width="24" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"></path></svg>`,
            LOADING: `<svg class="mm-spin" height="24" width="24" viewBox="0 0 24 24"><path d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z"></path></svg>`,
            SUCCESS: `<svg height="25" width="25" viewBox="0 0 24 24"><path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg>`,
            KEYBOARD:`<svg width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M4 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h16a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3H4Zm-.5 3a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm4 0a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM7 11.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM3.5 11a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM11 7.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm.5 3.5a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM15 7.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm.5 3.5a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM19 7.5c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm.5 3.5a.5.5 0 0 0-.5.5v1c0 .28.22.5.5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM7 15.5c0-.28.22-.5.5-.5h9c.28 0 .5.22.5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1Z" clip-rule="evenodd" class=""></path></svg>`,
            DOWNLOAD: `<svg width="24" height="24" viewBox="0 0 24 24"><path d="M21,14a1,1,0,0,0-1,1v4a1,1,0,0,1-1,1H5a1,1,0,0,1-1-1V15a1,1,0,0,0-2,0v4a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V15A1,1,0,0,0,21,14Zm-9.71,1.71a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l4-4a1,1,0,0,0-1.42-1.42L13,12.59V3a1,1,0,0,0-2,0v9.59l-2.29-2.3a1,1,0,1,0-1.42,1.42Z"></path></svg>`,
            SETTINGS: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M23 12C23 10.895 22.105 10 21 10H19.738C19.549 9.268 19.261 8.578 18.886 7.942L19.778 7.05C20.559 6.269 20.559 5.003 19.778 4.222C18.997 3.441 17.731 3.441 16.95 4.222L16.058 5.114C15.422 4.739 14.732 4.451 14 4.262V3C14 1.896 13.105 1 12 1C10.895 1 10 1.895 10 3V4.262C9.268 4.451 8.578 4.739 7.942 5.114L7.05 4.222C6.269 3.441 5.003 3.441 4.222 4.222C3.441 5.003 3.441 6.269 4.222 7.05L5.114 7.942C4.739 8.578 4.451 9.268 4.262 10H3C1.896 10 1 10.895 1 12C1 13.105 1.895 14 3 14H4.262C4.451 14.732 4.739 15.422 5.114 16.058L4.222 16.95C3.441 17.731 3.441 18.997 4.222 19.778C5.003 20.559 6.269 20.559 7.05 19.778L7.942 18.886C8.577 19.261 9.268 19.549 10 19.738V21C10 22.104 10.895 23 12 23C13.105 23 14 22.105 14 21V19.738C14.732 19.549 15.422 19.261 16.058 18.886L16.95 19.778C17.731 20.559 18.997 20.559 19.778 19.778C20.559 18.997 20.559 17.731 19.778 16.95L18.886 16.058C19.261 15.423 19.549 14.732 19.738 14H21C22.104 14 23 13.105 23 12Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
            EYE_OPEN : `<svg height="24" width="24" viewBox="0 0 24 24"><path d="M22 12s-3.636 7-10 7-10-7-10-7 3.636-7 10-7c2.878 0 5.198 1.432 6.876 3M9 12a3 3 0 1 0 3-3"/></svg>`,
            EYE_CLOSE : `<svg height="24" width="24" viewBox="0 0 24 24"><path d="M22 12s-.692 1.332-2 2.834M10 5.236A8.7 8.7 0 0 1 12 5c2.878 0 5.198 1.432 6.876 3M12 9a2.995 2.995 0 0 1 3 3M3 3l18 18m-9-6a3 3 0 0 1-2.959-2.5M4.147 9c-.308.345-.585.682-.828 1C2.453 11.128 2 12 2 12s3.636 7 10 7q.512 0 1-.058"stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
            IMPORT : `<svg width="20" height="20" viewBox="0 0 24 24"><path d="M21,14a1,1,0,0,0-1,1v4a1,1,0,0,1-1,1H5a1,1,0,0,1-1-1V15a1,1,0,0,0-2,0v4a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V15A1,1,0,0,0,21,14Zm-9.71,1.71a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l4-4a1,1,0,0,0-1.42-1.42L13,12.59V3a1,1,0,0,0-2,0v9.59l-2.29-2.3a1,1,0,1,0-1.42,1.42Z"></path></svg>`,
            SEARCH: `<svg height="20" width="20" viewBox="0 0 300 300"><path d="M273.587,214.965c49.11-49.111,49.109-129.021,0-178.132c-49.111-49.111-129.02-49.111-178.13,0 C53.793,78.497,47.483,140.462,76.51,188.85c0,0,2.085,3.498-0.731,6.312c-16.065,16.064-64.263,64.263-64.263,64.263 c-12.791,12.79-15.836,30.675-4.493,42.02l1.953,1.951c11.343,11.345,29.229,8.301,42.019-4.49c0,0,48.096-48.097,64.128-64.128 c2.951-2.951,6.448-0.866,6.448-0.866C169.958,262.938,231.923,256.629,273.587,214.965z M118.711,191.71 c-36.288-36.288-36.287-95.332,0.001-131.62c36.288-36.287,95.332-36.288,131.619,0c36.288,36.287,36.288,95.332,0,131.62 C214.043,227.996,155,227.996,118.711,191.71z"></path> <g> <path d="M126.75,118.424c-1.689,0-3.406-0.332-5.061-1.031c-6.611-2.798-9.704-10.426-6.906-17.038 c17.586-41.559,65.703-61.062,107.261-43.476c6.611,2.798,9.704,10.426,6.906,17.038c-2.799,6.612-10.425,9.703-17.039,6.906 c-28.354-11.998-61.186,1.309-73.183,29.663C136.629,115.445,131.815,118.424,126.75,118.424z"></path></svg>`,
            INFO: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>`,
            PLUS: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>`,
            BACK: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>`,
            RESET: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
            LOCK:`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="10" width="12" height="10" rx="2" ry="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg>`,
            UNDO:`<svg height="24" width="24" viewBox="0 0 24 24"><path d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z"></path></svg>`,
            Github:`<svg viewBox="0 0 24 24"><path d="M12 .5a12 12 0 0 0-3.79 23.39c.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.41-4.04-1.41-.55-1.39-1.34-1.76-1.34-1.76-1.1-.75.08-.74.08-.74 1.21.08 1.85 1.25 1.85 1.25 1.08 1.84 2.84 1.31 3.53 1 .11-.78.42-1.31.76-1.61-2.66-.31-5.46-1.33-5.46-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.55.12-3.22 0 0 1.01-.32 3.3 1.23a11.4 11.4 0 0 1 6 0c2.29-1.55 3.29-1.23 3.29-1.23.67 1.67.25 2.92.13 3.22.77.84 1.23 1.91 1.23 3.22 0 4.61-2.8 5.62-5.47 5.92.43.37.81 1.08.81 2.18v3.24c0 .32.21.69.82.57A12 12 0 0 0 12 .5z"/></svg>`,
            Discord:`<svg fill="currentColor" viewBox="0 0 24 24" xml:space="preserve"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M18.942 5.556a16.299 16.299 0 0 0-4.126-1.297c-.178.321-.385.754-.529 1.097a15.175 15.175 0 0 0-4.573 0 11.583 11.583 0 0 0-.535-1.097 16.274 16.274 0 0 0-4.129 1.3c-2.611 3.946-3.319 7.794-2.965 11.587a16.494 16.494 0 0 0 5.061 2.593 12.65 12.65 0 0 0 1.084-1.785 10.689 10.689 0 0 1-1.707-.831c.143-.106.283-.217.418-.331 3.291 1.539 6.866 1.539 10.118 0 .137.114.277.225.418.331-.541.326-1.114.606-1.71.832a12.52 12.52 0 0 0 1.084 1.785 16.46 16.46 0 0 0 5.064-2.595c.415-4.396-.709-8.209-2.973-11.589zM8.678 14.813c-.988 0-1.798-.922-1.798-2.045s.793-2.047 1.798-2.047 1.815.922 1.798 2.047c.001 1.123-.793 2.045-1.798 2.045zm6.644 0c-.988 0-1.798-.922-1.798-2.045s.793-2.047 1.798-2.047 1.815.922 1.798 2.047c0 1.123-.793 2.045-1.798 2.045z"></path></g></svg>`,
            GreasyFork:`<svg viewBox="0 0 96 96"><path fill="currentColor" stroke="#000" stroke-width="4" d="M 44,29  a6.36396,6.36396 0,0,1 0,9  l36,36  a3.25,3.25 0,0,1 -6.5,6.5  l-36,-36  a6.36396,6.36396 0,0,1 -9,0  l-19,-19  a1.76777,1.76777 0,0,1 0,-2.5  l13.0,-13  a1.76777,1.76777 0,0,1 2.5,0  z"/><path fill="currentColor" d="M 44,29  a6.36396,6.36396 0,0,1 0,9  l36,36  a3.25,3.25 0,0,1 -6.5,6.5  l-36,-36  a6.36396,6.36396 0,0,1 -9,0  l-19,-19  a1.76777,1.76777 0,0,1 2.5,-2.5  l14,14 4,-4 -14,-14  a1.76777,1.76777 0,0,1 2.5,-2.5  l14,14 4,-4 -14,-14  a1.76777,1.76777 0,0,1 2.5,-2.5  z"/></svg>`
        },

        RESIZERS:[
            { name: 'top', cursor: 'n-resize', style: 'top:0;left:0;right:0;height:3px;' },
            { name: 'bottom', cursor: 's-resize', style: 'bottom:0;left:0;right:0;height:3px;' },
            { name: 'left', cursor: 'w-resize', style: 'left:0;top:0;bottom:0;width:3px;' },
            { name: 'right', cursor: 'e-resize', style: 'right:0;top:0;bottom:0;width:3px;' },
            { name: 'top-left', cursor: 'nw-resize', style: 'top:0;left:0;width:6px;height:6px;' },
            { name: 'top-right', cursor: 'ne-resize', style: 'top:0;right:0;width:6px;height:6px;' },
            { name: 'bottom-left', cursor: 'sw-resize', style: 'bottom:0;left:0;width:6px;height:6px;' },
            { name: 'bottom-right', cursor: 'se-resize', style: 'bottom:0;right:0;width:6px;height:6px;' }
        ],

        TILE_SIZE_MAP:{
            4: [6656, 3328],
            3: [3328, 1664],
            2: [1664, 832],
            1: [832, 416]
        }
    };


    const DEFAULT_CONFIG = {
        ACCURACY: {
            DEFAULT: 60,
            EXACT_TIME: 60,
            WEATHER: 600,
            SUN: 300,
            DAY: 18000,
        },

        CHUNK_SIZE: {
            DEFAULT: 1200,
            DOWNLOAD: 20,
            TIME: 50,
        },

        SEARCH_RADIUS: {
            DEFAULT: 50,
            EXACT_TIME: 30,
            UPDATE: 15,
        },

        exportMode: 'save',

        SHORTCUTS_DEFAULT: false,

        DOWNLOAD:{
            type: 'Equirectangular',
            quality:5
        },

        LAYOUT:{
            mapWidth:document.querySelector('.map-embed').offsetWidth,
            mapHeight:document.querySelector('.map-embed').offsetHeight,
            previewWidth:document.querySelector('.map-overview').offsetWidth,
            previewHeight:document.querySelector('.map-overview').offsetHeight,
        },

        ELEVATION: {
            useRanges: true,
            ranges: [
                { min: -100, max: 0 },
                { min: 0, max: 100 },
                { min: 100, max: 200 },
                { min: 200, max: 400 },
                { min: 400, max: 700 },
                { min: 700, max: 1200 },
                { min: 1200, max: 2000 },
                { min: 2000, max: 3000 },
                { min: 3000, max: 9999 },
            ],
        },
    };


    let CONFIG = GM_getValue('mm_config') ? JSON.parse(GM_getValue('mm_config')) : DEFAULT_CONFIG;

    // ======================================================================================================================================
    // =============================================================== Cache ================================================================
    // ======================================================================================================================================

    class TTLCache {
        constructor(maxSize = 1000, ttl = 3600000) {
            this._cache = new Map();
            this._maxSize = maxSize;
            this._ttl = ttl;
        }

        _normalizeKey(key) {
            return typeof key === 'object' ? JSON.stringify(key) : String(key);
        }

        get(key) {
            const keyStr = this._normalizeKey(key);
            const entry = this._cache.get(keyStr);
            if (!entry) return undefined;
            if (Date.now() - entry.time > this._ttl) {
                this._cache.delete(keyStr);
                return undefined;
            }
            return entry.value;
        }

        set(key, value) {
            const keyStr = this._normalizeKey(key);
            if (this._cache.size >= this._maxSize) {
                const oldestKey = this._cache.keys().next().value;
                this._cache.delete(oldestKey);
            }
            this._cache.set(keyStr, { value, time: Date.now() });
        }

        has(key) {
            return this.get(key) !== undefined;
        }

        delete(key) {
            const keyStr = this._normalizeKey(key);
            return this._cache.delete(keyStr);
        }

        clear() {
            this._cache.clear();
        }

        values() {
            const now = Date.now();
            const result = [];
            for (const { value, time } of this._cache.values()) {
                if (now - time <= this._ttl) {
                    result.push(value);
                }
            }
            return result;
        }

        get size() {
            return this._cache.size;
        }
    }


    const CACHE = {
        metadataCache: new TTLCache(300, 600000),
        timezoneCache: new TTLCache(500, 1800000),
        imageCache : new TTLCache(300, 300000),
        tileCache : new TTLCache(800, 300000),
        inflightControllers : new TTLCache(800, 600000),
        osmFeaturesById: new TTLCache(800, 600000),
        observationsById: new TTLCache(800, 600000),
        initializedContainers: new WeakMap(),
        resizeHistory: [],
    };

    // ======================================================================================================================================
    // ============================================================== States ================================================================
    // ======================================================================================================================================

    const STATES = {
        taggingTaskCancelled: false,
        downloadTaskCancelled: false,
    }

    // ======================================================================================================================================
    // =============================================================== Utils ================================================================
    // ======================================================================================================================================

    const Logger = {
        _logs: [],
        MAX_LOGS: 100,

        _addLog(level, args) {
            const timestamp = new Date().toLocaleTimeString();
            const message = Array.from(args).map(a =>
                                                 typeof a === 'object' ? (a.message || JSON.stringify(a)) : String(a)
                                                ).join(' ');

            this._logs.push({ timestamp, level, message });
            if (this._logs.length > this.MAX_LOGS) {
                this._logs.shift();
            }
            // Notify any listeners
            if (this._onLog) this._onLog(this._logs);
        },

        set onLog(callback) {
            this._onLog = callback;
        },

        get logs() {
            return this._logs;
        },

        clear() {
            this._logs = [];
            if (this._onLog) this._onLog(this._logs);
        },

        info(...args) { this._addLog('info', args); },
        warn(...args) { this._addLog('warn', args); console.warn('[WARN]', ...args); },
        error(...args) { this._addLog('error', args); console.error('[ERROR]', ...args); },
        debug(...args) { this._addLog('debug', args); console.debug('[DEBUG]', ...args); }
    };

    function throttleAsync(asyncFn, delay) {
        let lastCall = 0;
        let timer = null;
        let isRunning = false;

        return async (...args) => {
            const now = Date.now();
            if (isRunning) return;

            if (now - lastCall >= delay) {
                lastCall = now;
                isRunning = true;
                try { await asyncFn.apply(this, args); }
                finally { isRunning = false; }
            } else {
                clearTimeout(timer);
                timer = setTimeout(async () => {
                    lastCall = Date.now();
                    isRunning = true;
                    try { await asyncFn.apply(this, args); }
                    finally { isRunning = false; }
                }, delay - (now - lastCall));
            }
        };
    }

    async function runWithConcurrency(tasks, limit = 5) {
        const results = [];
        let index = 0;

        async function worker() {
            while (index < tasks.length) {
                const currentIndex = index++;
                try {
                    results[currentIndex] = await tasks[currentIndex]();
                } catch (error) {
                    results[currentIndex] = null;
                }
            }
        }

        const workers = Array.from({ length: limit }, worker);
        await Promise.all(workers);

        return results.filter(r => r !== null);
    }

    function calculateDistance(lat1, lng1, lat2, lng2) {
        const R = 6371;
        const dLat = (lat2 - lat1) * Math.PI / 180;
        const dLng = (lng2 - lng1) * Math.PI / 180;
        const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng/2) * Math.sin(dLng/2);
        return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
    }

    function lngToTileX(lng, zoom) {
        return Math.floor((lng + 180) / 360 * Math.pow(2, zoom));
    }

    function latToTileY(lat, zoom) {
        const rad = lat * Math.PI / 180;
        return Math.floor((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2 * Math.pow(2, zoom));
    }

    function tileToBBox(x, y, zoom) {
        const n = Math.pow(2, zoom);
        const west = x / n * 360 - 180;
        const east = (x + 1) / n * 360 - 180;
        const north = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
        const south = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI;
        return { north, south, east, west };
    }

    function uniqueBy(array, keyFn) {
        const seen = new Set();

        return array.filter(item => {
            const key = keyFn(item);

            if (seen.has(key)) return false;

            seen.add(key);
            return true;
        });
    }

    function stripPanoId(pano,prefix) {
        return pano.replace(prefix, "");
    }

    function chunkArray(array, maxSize) {
        const result = [];
        for (let i = 0; i < array.length; i += maxSize) {
            result.push(array.slice(i, i + maxSize));
        }
        return result;
    }

    function applyStyles(element, styles) {
        for (const [prop, value] of Object.entries(styles)) {
            element.style.setProperty(prop, value, "important");
        }
    }

    const setVal = (id, val) => {
        const el = document.getElementById(id);
        if (el) {
            el.value = val;
            const event = new Event('input', {
                bubbles: true,
                cancelable: true
            });
            el.dispatchEvent(event);
        }
    };
    const getVal = (id) => parseInt(document.getElementById(id).value) || document.getElementById(id).value || 0;

    function extractDate(array) {
        let year, month;

        array.forEach(element => {
            const yearRegex1 = /^(\d{2})-(\d{2})$/;
            const yearRegex2 = /^(\d{4})-(\d{2})$/;
            const yearRegex3 = /^(\d{4})$/;
            const monthRegex1 = /^(\d{2})$/;
            const monthRegex2 = /^(January|February|March|April|May|June|July|August|September|October|November|December)$/i;

            if (!month && yearRegex1.test(element)) {
                const match = yearRegex1.exec(element);
                year = parseInt(match[1]) + 2000;
                month = parseInt(match[2]);
            }

            if (!month && yearRegex2.test(element)) {
                const match = yearRegex2.exec(element);
                year = parseInt(match[1]);
                month = parseInt(match[2]);
            }

            if (!year && yearRegex3.test(element)) {
                year = parseInt(element);
            }

            if (!month && monthRegex1.test(element)) {
                month = parseInt(element);
            }

            if (!month && monthRegex2.test(element)) {
                const months = {
                    "January": 1, "February": 2, "March": 3, "April": 4,
                    "May": 5, "June": 6, "July": 7, "August": 8,
                    "September": 9, "October": 10, "November": 11, "December": 12
                };
                month = months[element];
            }
        });
        return { year, month }
    }

    function dateToTimestamp(date) {
        const [year, month] = date.split('-');
        const startDate = Math.round(new Date(year, month - 1, 1).getTime() / 1000);
        const endDate = Math.round(new Date(year, month, 1).getTime() / 1000) - 1;
        return { startDate, endDate };
    }

    function getDirection(heading) {
        for (const direction of CONSTANS.DIRECTION_RANGE) {
            const [start, end] = direction.range;
            if (start <= end) {
                if (heading >= start && heading < end) {
                    return direction.name;
                }
            } else {
                if (heading >= start || heading < end) {
                    return direction.name;
                }
            }
        }
        return 'Unknown direction';
    }

    function findRange(elevation, ranges) {
        if (!elevation) return 'Elevation not found';

        for (let i = 0; i < ranges.length; i++) {
            const range = ranges[i];
            if (elevation >= range.min && elevation <= range.max) {
                return `${range.min}-${range.max}m`;
            }
        }

        return `${JSON.stringify(elevation)}m`;
    }

    function buildThumbnailUrl(panoId, heading, pitch) {
        const url = new URL("https://streetviewpixels-pa.googleapis.com/v1/thumbnail");

        url.search = new URLSearchParams({
            panoid: panoId,
            cb_client: "maps_sv.tactile.gps",
            yaw: heading,
            pitch: pitch,
            thumbfov: 120,
            width: 1024,
            height: 768
        }).toString();
        return url.toString();
    }

    function buildTileUrl(panoId, zoom, x, y) {
        const base = "https://streetviewpixels-pa.googleapis.com/v1/tile";
        const params = new URLSearchParams({
            cb_client: "apiv3",
            panoId,
            output: "tile",
            zoom,
            nbt: 0,
            fover: 2,
            x,
            y
        });
        return `${base}?${params.toString()}`;
    }

    function rotationMatrix(axis, angle) {
        const rad = angle * (Math.PI / 180);
        const c = Math.cos(rad);
        const s = Math.sin(rad);
        const t = 1 - c;
        const [x, y, z] = axis;

        return [
            [t*x*x + c, t*x*y - s*z, t*x*z + s*y],
            [t*x*y + s*z, t*y*y + c, t*y*z - s*x],
            [t*x*z - s*y, t*y*z + s*x, t*z*z + c]
        ];
    }

    function applyRotation(matrix, vector) {
        return [
            matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2],
            matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2],
            matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2]
        ];
    }

    function multiplyMatrices(A, B) {
        const result = Array(3).fill(null).map(() => Array(3).fill(0));
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                for (let k = 0; k < 3; k++) {
                    result[i][j] += A[i][k] * B[k][j];
                }
            }
        }
        return result;
    }

    async function loadImage(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'Anonymous';
            img.onload = () => resolve(img);
            img.onerror = (e) => reject(Logger.error(`Failed to load image from ${url}: ${e.message}`));
            img.src = url;
        });
    }
    // ======================================================================================================================================
    // =============================================================   Notification  ========================================================
    // ======================================================================================================================================

    class NotificationSystem {
        constructor() {
            this.container = null;
            this.toasts = new Map();
        }

        _ensureContainer() {
            if (!this.container) {
                this.container = document.createElement('div');
                this.container.className = 'mm-notification-container';
                document.body.appendChild(this.container);
            }
        }

        _showToast(message, type = 'info', duration = 3000) {
            this._ensureContainer();

            const id = Math.random().toString(36).substr(2, 9);
            const toast = document.createElement('div');
            toast.id = `toast-${id}`;
            toast.className = 'mm-notification-toast'

            const colors = {
                info: { bg: 'var(--mm-accent)', icon: CONSTANS.SVG_SOURCE.INFO },
                success: { bg: 'var(--mm-success)', icon: CONSTANS.SVG_SOURCE.SUCCESS},
                warning: { bg: 'var(--mm-warning)', icon: CONSTANS.SVG_SOURCE.EDIT },
                error: { bg: 'var(--mm-error)', icon: CONSTANS.SVG_SOURCE.CROSS }
            };

            const color = colors[type] || colors.info;

            toast.style.background = color.bg;

            toast.innerHTML = `${color.icon}${message}`;
            this.container.appendChild(toast);
            this.toasts.set(id, toast);

            if (duration > 0) {
                setTimeout(() => {
                    toast.style.animation = 'mm-slideOut 0.3s ease';
                    setTimeout(() => {
                        toast.remove();
                        this.toasts.delete(id);
                    }, 300);
                }, duration);
            }

            return id;
        }

        _clearToast(id) {
            if (!id) return;
            const toast = document.getElementById(`toast-${id}`);
            if (toast) {
                toast.style.animation = 'mm-slideOut 0.3s ease';
                setTimeout(() => {
                    toast.remove();
                    Notification.toasts.delete(id);
                }, 300);
            }
        }

        _showModal(title, messageHTML, buttons = null) {
            this._ensureContainer();

            const modal = document.createElement('div');
            modal.className = 'mm-modal-overlay';

            const content = document.createElement('div');
            content.className = 'mm-modal-content';

            const titleEl = document.createElement('h3');
            titleEl.textContent = title;
            titleEl.className = 'mm-modal-title';
            content.appendChild(titleEl);

            const msgEl = document.createElement('div');
            msgEl.className = 'mm-modal-message';
            msgEl.innerHTML = messageHTML;
            content.appendChild(msgEl);

            if (buttons) {
                const buttonsContainer = document.createElement('div');
                buttonsContainer.style.cssText = `display: flex; gap: 8px; justify-content: flex-end; margin-top: 15px;`;

                buttons.forEach(btn => {
                    const button = document.createElement('button');
                    button.textContent = btn.text;
                    button.style.cssText = `
                            padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;
                            background: ${btn.primary ? 'var(--mm-primary)' : 'var(--mm-bgSecondary)'};
                            color: ${btn.primary ? 'white' : 'var(--mm-text)'};
                            transition: background 0.2s ease;
                        `;
                    button.onmouseover = () => button.style.background = btn.primary ? 'var(--mm-primaryHover)' : 'var(--mm-border)';
                    button.onmouseout = () => button.style.background = btn.primary ? 'var(--mm-primary)' : 'var(--mm-bgSecondary)';
                    button.onclick = () => {
                        if (btn.callback) btn.callback();
                        modal.remove();
                    };
                    buttonsContainer.appendChild(button);
                });
                content.appendChild(buttonsContainer);
            }

            modal.appendChild(content);
            document.body.appendChild(modal);

            return { remove: () => modal.remove() };
        }

        info(message) {
            this._showToast(message, 'info');
        }

        success(message) {
            Logger.info(message);
            this._showToast(message, 'success');
        }

        warning(message) {
            Logger.warn(message);
            this._showToast(message, 'warning');
        }

        error(message) {
            Logger.error(message);
            this._showToast(message, 'error', 5000);
        }

        confirm(title, message, onConfirm) {
            return this._showModal(title, message, [
                { text: 'Cancel', primary: false, callback: null },
                { text: 'Confirm', primary: true, callback: onConfirm }
            ]);
        }

        alert(title, message) {
            Logger.warn(message);
            return this._showModal(title, message, [
                { text: 'OK', primary: true, callback: null }
            ]);
        }
    }

    const Notification = new NotificationSystem();

    // ======================================================================================================================================
    // ============================================================ Validators ==============================================================
    // ======================================================================================================================================

    const Validator = {
        checkEnvironment() {
            if (typeof editor === 'undefined') {
                Notification.error('Environment Missing, editor is undefined');
                return false
            }
            return true
        },

        checkDependencies() {
            const checks = [
                ['SunCalc', typeof SunCalc !== 'undefined'],
                ['GeoTZ', typeof GeoTZ !== 'undefined'],
                ['fflate', typeof fflate !=='undefined'],
                ['deck', typeof deck !=='undefined'],
            ];

            const failed = checks.filter(([, passed]) => !passed);

            if (failed.length) {
                Notification.error('Dependency Missing', failed.map(([name]) => name).join(', '));
                return false;
            }

            return true;
        }
    };

    // ======================================================================================================================================
    // ============================================================= Services ===============================================================
    // ======================================================================================================================================

    const Editor = {
        getSelections() {
            return editor.currentLocation ? [editor.currentLocation.updatedProps] : uniqueBy(editor.selections.flatMap(selection => selection.locations), location => location.id);
        },

        updateLocations(oldLocs, newLocs) {
            editor.removeLocations(oldLocs);
            editor.importLocations(newLocs);
        },

        getCurrentLocation() {
            return editor?.currentLocation?.updatedProps;
        },

        getSelectedPolygon() {
            try {
                return editor?.selections?.[0]?.props?.polygon?.geometry?.coordinates?.[0]?.[0];
            } catch (e) {
                return null;
            }
        },

        async singleImageSearch(location, radius){
            try{
                const metadata =await editor.getPanorama({location: location, radius: radius, source: 'google' });
                return metadata;
            }
            catch(e){
                return null;
            }
        },

        async getMetadata(panoId){
            try{
                const metadata =await editor.getPanorama({pano:panoId});
                return metadata;
            }
            catch(e){
                Logger.warning(`Invalid panoId: ${panoId}`);
                return null;
            }
        }
    };

    function parseMetadata(data) {
        let year = 'Year not found';
        let month = 'Month not found';
        let panoType = 'unofficial';
        let country = 'Country not found';
        let subdivision = 'Subdivision not found';
        let cameraType = 'ari';
        let isTrekker = false;
        let defaultHeading;
        let altitude;
        let roadname;

        if (data) {
            if (data.extra) {
                if (data.extra._levelId) isTrekker = true
                if (data.extra.countryCode) country = data.extra.countryCode
                if (data.extra.altitude) altitude = data.extra.altitude
                if (data.extra.cameraType) {
                    if (data.extra.cameraType == "thirdparty") {
                        cameraType = 'Badcam';
                    }
                    else if (data.extra.cameraType == "tripod" || (data.extra.cameraType == "2/3")) {
                        if (data.extra.cameraType == "tripod") isTrekker = true;
                        if (CONSTANS.GEN2_COUNTRIES.includes(country)) cameraType = 'Gen2/3';
                        else cameraType = 'Gen3';
                    }
                    else cameraType = `Gen${data.extra.cameraType}`;
                }
            }
            if (data.imageDate) {
                const matchYear = data.imageDate.match(/\d{4}/);
                if (matchYear) {
                    year = matchYear[0];
                }

                const matchMonth = data.imageDate.match(/-(\d{2})/);
                if (matchMonth) {
                    month = matchMonth[1];
                }
            }
            if (data.copyright && data.copyright.includes('Google')) {
                panoType = 'Official';
            }
            if (data.tiles && data.tiles.originHeading) {
                defaultHeading = data.tiles.originHeading
            }
            if (data.location && data.location.description) {
                let parts = data.location.description.split(',');
                if (parts.length > 1) {
                    subdivision = parts[parts.length - 1].trim();
                } else {
                    subdivision = data.location.description;
                }
            }
            if (data.links && data.links.length > 0) {
                roadname = data.links[0].description
                if (roadname == '' || !roadname) roadname = 'Road not found'
            }
        }

        return [year, month, panoType, country, subdivision, cameraType, altitude, defaultHeading, isTrekker, roadname, data.time, data.links]
    }

    function createPayload(mode, key, radius, range) {
        let payload;

        if (mode === 'GetMetadata') {
            const panoField = Array.isArray(key)
            ? key.map(id => [[2, id]])
            : [[[2, key]]];
            payload = [
                ["apiv3", null, null, null, "US", null, null, null, null, null, [[0]]],
                ["en", "US"],
                panoField,
                [[1, 2, 3, 4, 8, 6]]
            ];
        }
        else {
            if (range) {
                payload = [["apiv3"],
                           [[null, null, key.lat, key.lng], radius],
                           [[null, null, null, null, null, null, null, null, null, null, range], null, null, null, null, null, null, null, [2], null, [[[2, true, 2]]]],
                           [[2, 6]]]
            }
            else {
                payload = [["apiv3"],
                           [[null, null, key.lat, key.lng], radius],
                           [null, ["en", "US"], null, null, null, null, null, null, [2], null, [[[2, true, 2]]]],
                           [[1, 2, 3, 4, 8, 6]]];
            }
        }
        return JSON.stringify(payload);
    }

    async function getMetadata(mode, key, radius, range) {
        const cacheKey = JSON.stringify({ mode, key, range });
        const cached = CACHE.metadataCache.get(cacheKey);
        if (cached !== undefined) return cached;

        try {
            const url = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${mode}`;
            let payload = createPayload(mode, key, radius, range);

            const response = await fetch(url, {
                method: "POST",
                headers: {
                    "content-type": "application/json+protobuf",
                    "x-user-agent": "grpc-web-javascript/0.1"
                },
                body: payload,
                mode: "cors",
                credentials: "omit"
            });

            if (!response.ok) {
                Logger.error(`HTTP error! status: ${response.status}`);
                return null;
            } else {
                const data = await response.json();
                CACHE.metadataCache.set(cacheKey, data);
                return data;
            }
        } catch (error) {
            Logger.error(`Error fetching google panorama: ${error.message}`);
            return null;
        }
    }

    async function binarySearch(coord, start, end, accuracy) {

        let response;
        while ((end - start) > accuracy) {
            const mid = Math.round((start + end) / 2);
            response = await getMetadata("SingleImageSearch", coord, CONFIG.SEARCH_RADIUS.EXACT_TIME, [start, mid]);

            if (response && response.length > 1) {
                end = mid;
            } else {
                start = mid;
            }
        }

        const result = Math.round((start + end) / 2);
        return result;
    }

    function generatePerspective(canvas, FOV, THETA, PHI, outputWidth, outputHeight) {
        const perspectiveCanvas = new OffscreenCanvas(outputWidth, outputHeight);
        const perspectiveCtx = perspectiveCanvas.getContext('2d');

        const f = 0.5 * outputWidth / Math.tan((FOV / 2) * (Math.PI / 180));
        const cx = outputWidth / 2;
        const cy = outputHeight / 2;

        var inputWidth = canvas.width;
        var inputHeight = canvas.height;
        const inputCtx = canvas.getContext('2d');
        const inputImageData = inputCtx.getImageData(0, 0, inputWidth, inputHeight);

        const outputImageData = perspectiveCtx.createImageData(outputWidth, outputHeight);
        const outputData = outputImageData.data;

        const R1 = rotationMatrix([0, 1, 0], THETA);
        const rotatedXAxis = applyRotation(R1, [1, 0, 0]);
        const R2 = rotationMatrix(rotatedXAxis, PHI);
        const R = multiplyMatrices(R2, R1);

        for (let y = 0; y < outputHeight; y++) {
            for (let x = 0; x < outputWidth; x++) {
                const nx = (x - cx) / f;
                const ny = (y - cy) / f;
                const nz = 1;

                const [rx, ry, rz] = applyRotation(R, [nx, ny, nz]);

                const lon = Math.atan2(rx, rz);
                const lat = Math.asin(ry / Math.sqrt(rx * rx + ry * ry + rz * rz));

                const u = Math.floor(((lon / (2 * Math.PI)) + 0.5) * inputWidth);
                const v = Math.floor(((lat / Math.PI) + 0.5) * inputHeight);

                if (u >= 0 && u < inputWidth && v >= 0 && v < inputHeight) {
                    const srcOffset = (v * inputWidth + u) * 4;
                    const destOffset = (y * outputWidth + x) * 4;

                    outputData[destOffset] = inputImageData.data[srcOffset]; // Red
                    outputData[destOffset + 1] = inputImageData.data[srcOffset + 1]; // Green
                    outputData[destOffset + 2] = inputImageData.data[srcOffset + 2]; // Blue
                    outputData[destOffset + 3] = 255; // Alpha
                }
            }
        }

        perspectiveCtx.putImageData(outputImageData, 0, 0);
        return perspectiveCanvas;
    }

    async function getWeather(coordinate, timestamp) {
        let hours, weatherCodes
        const formatted_date = new Date(timestamp * 1000).toISOString().slice(0, 10);

        try {
            const url = `https://archive-api.open-meteo.com/v1/archive?latitude=${coordinate.lat}&longitude=${coordinate.lng}&start_date=${formatted_date}&end_date=${formatted_date}&hourly=weather_code`;
            const response = await fetch(url);
            const data = await response.json();
            hours = data.hourly.time;
            weatherCodes = data.hourly.weather_code;

            const targetHour = new Date(timestamp * 1000).getHours();
            let closestHourIndex = 0;
            let minDiff = Infinity;

            for (let i = 0; i < hours.length; i++) {
                const hour = new Date(hours[i]).getHours();
                const diff = Math.abs(hour - targetHour);
                if (diff < minDiff) {
                    minDiff = diff;
                    closestHourIndex = i;
                }
            }

            const weatherCode = weatherCodes[closestHourIndex];
            const weatherDescription = CONSTANS.WEATHER_CODE_MAP[weatherCode] || 'Unknown weather code';
            return weatherDescription;

        } catch (error) {
            Logger.error('Error fetching weather data:', error);
            return 'Weather not found';
        }
    }

    async function getLocal(coord, timestamp) {
        const cacheKey = JSON.stringify({ lat: Number(coord.lat.toFixed(3)), lng: Number(coord.lng.toFixed(3)) });
        let offset_hours = CACHE.timezoneCache.get(cacheKey);

        try {
            if (offset_hours === undefined) {
                const timezone = await GeoTZ.find(coord.lat, coord.lng)
                const offset = await GeoTZ.toOffset(timezone);
                offset_hours = offset ? parseInt(offset / 60) : 0;
                CACHE.timezoneCache.set(cacheKey, offset_hours);
            }

            const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;
            const offsetDiff = systemTimezoneOffset - offset_hours * 3600;
            const convertedTimestamp = Math.round(timestamp - offsetDiff);

            return convertedTimestamp;
        } catch (error) {
            Logger.error('Error fetching timezone data:', error);
            return null;
        }
    }

    function getSunEvents(date, lat, lng) {
        if (date && lat && lng) {
            const format_date = new Date(date);
            const times = SunCalc.getTimes(format_date, lat, lng);
            const sunsetTimestamp = Math.round(times.sunset.getTime() / 1000);
            const sunriseTimestamp = Math.round(times.sunrise.getTime() / 1000);
            const noonTimestamp = Math.round(times.solarNoon.getTime() / 1000);
            const result = {
                sunset: sunsetTimestamp,
                sunrise: sunriseTimestamp,
                noon: noonTimestamp,
            };
            return result;
        }

        return null;
    }

    // ======================================================================================================================================
    // ============================================================ Theme System ============================================================
    // ======================================================================================================================================

    class Theme {
        constructor() {
            this.defaultThemes = {
                default: {
                    name: 'Default Dark',
                    bg: '#1a1a1a',
                    bgSecondary: '#242424',
                    text: '#ffffff',
                    textDim: '#b0b0b0',
                    border: '#333333',
                    primary: '#74b816',
                    primaryHover: '#8bc34a',
                    accent: '#4a90e2',
                    success: '#4caf50',
                    error: '#f44336',
                    warning: '#ff9800',
                    fontSize: '14px',
                },
                light: {
                    name: 'Light Mode',
                    bg: '#ffffff',
                    bgSecondary: '#f0f0f0',
                    text: '#222222',
                    textDim: '#666666',
                    border: '#d0d0d0',
                    primary: '#5fa312',
                    primaryHover: '#4d8c0f',
                    accent: '#2563eb',
                    success: '#3d9e4b',
                    error: '#d32f2f',
                    warning: '#e68a00',
                    fontSize: '14px',
                },
                ocean: {
                    name: 'Ocean Blue',
                    bg: '#0f1a2e',
                    bgSecondary: '#162240',
                    text: '#e2e8f0',
                    textDim: '#94a3b8',
                    border: '#2a3a5c',
                    primary: '#3b82f6',
                    primaryHover: '#2563eb',
                    accent: '#06b6d4',
                    success: '#10b981',
                    error: '#ef4444',
                    warning: '#f97316',
                    fontSize: '14px',
                },
                forest: {
                    name: 'Forest Green',
                    bg: '#0f1a12',
                    bgSecondary: '#1a2a1e',
                    text: '#e0f0e0',
                    textDim: '#8ab88a',
                    border: '#2a4a2e',
                    primary: '#22c55e',
                    primaryHover: '#16a34a',
                    accent: '#84cc16',
                    success: '#10b981',
                    error: '#f87171',
                    warning: '#fbbf24',
                    fontSize: '14px',
                },
                sunset: {
                    name: 'Sunset',
                    bg: '#1e1008',
                    bgSecondary: '#2e1a10',
                    text: '#fef3c7',
                    textDim: '#d4a373',
                    border: '#4a2a1a',
                    primary: '#f97316',
                    primaryHover: '#ea580c',
                    accent: '#ec4899',
                    success: '#84cc16',
                    error: '#ef4444',
                    warning: '#eab308',
                    fontSize: '14px',
                },
                cyberpunk: {
                    name: 'Cyberpunk',
                    bg: '#0a0a1e',
                    bgSecondary: '#12122e',
                    text: '#e0ffe0',
                    textDim: '#88ddcc',
                    border: '#2a2a5a',
                    primary: '#ff00ff',
                    primaryHover: '#dd00dd',
                    accent: '#00ff88',
                    success: '#00ff88',
                    error: '#ff0055',
                    warning: '#ffff00',
                    fontSize: '14px',
                },
            };
            this.customThemes = {};
            this.currentTheme = GM_getValue('mm_current_theme', 'default');
            this._loadCustomThemes();
        }

        _loadCustomThemes() {
            try {
                const saved = GM_getValue('mm_custom_themes');
                if (saved) this.customThemes = JSON.parse(saved);
            } catch (e) { console.error('Failed to load custom themes:', e); }
        }

        _saveCustomThemes() {
            try { GM_setValue('mm_custom_themes', JSON.stringify(this.customThemes)); } catch (e) { console.error('Failed to save custom themes:', e); }
        }

        get themes() {
            return { ...this.defaultThemes, ...this.customThemes };
        }

        apply(themeName = 'default') {
            const allThemes = this.themes;
            if (!allThemes[themeName]) return;
            this.currentTheme = themeName;
            const themeData = allThemes[themeName];
            Object.entries(themeData).forEach(([key, value]) => {
                if (key !== 'name') {
                    document.documentElement.style.setProperty(`--mm-${key}`, value);
                }
            });
            // Apply font size to panel
            if (themeData.fontSize) {
                document.documentElement.style.setProperty('--mm-fontSize', themeData.fontSize);
            }
            GM_setValue('mm_current_theme', themeName);
        }

        get(themeName) {
            return this.themes[themeName];
        }

        getAll() {
            return this.themes;
        }

        saveCustomTheme(key, themeData) {
            this.customThemes[key] = themeData;
            this._saveCustomThemes();
        }

        deleteCustomTheme(key) {
            delete this.customThemes[key];
            this._saveCustomThemes();
            // Auto-fallback to default theme if the deleted theme was active
            if (this.currentTheme === key) {
                this.apply('default');
            }
        }
    }

    const theme = new Theme();

    // ======================================================================================================================================
    // ============================================================ Shortcut Manager ========================================================
    // ======================================================================================================================================

    class ShortcutManager {
        constructor() {
            this.KEYBOARD_SHORTCUTS = {};
            this.NUMBER_SHORTCUTS = {};
            this.currentIndex = 1;
            this.isShift = false;
            this.isCtrl = false;
            this.isHidden = false;
            this.isMapControlsHidden = false;
            this.isMiniMapHidden = false;
            this.isApplied = false;
            this.isDrawing = false;
            this.selectionBox = null;
            this._styleTag = null;
            this._mapStyleTag = null;
            this._currentNumberKey = null;
            this._miniMapEnterHandler = null;
            this._miniMapLeaveHandler = null;
            this._mapContainerParent = null;
            this._overlay = null;
            this._tagContainer = null;
            this._originalParent = null;
            this._originalNextSibling = null;
            this._isTagOverlayOpen = false;
            this._modal = null;
            this._mapInfo = null;
            this._attached = false;
            this.handlers = {
                togglePanel: () => {
                    mainPanel?.toggle();
                },
                jumpForward: () => {
                    document.querySelector('button[aria-label*="Jump forward"]')?.click();
                },
                jumpBackward: () => {
                    document.querySelector('button[aria-label*="Jump backward"]')?.click();
                },
                hideElement: () => {
                    this.toggleElementHidden();
                },
                deSelectAll: () => {
                    editor.resetSelections();
                },
                copyLoc: () => {
                    this.copyLoc();
                },
                miniMap: () => {
                    this.toggleMiniMap();
                },
                switchLoc: () => {
                    this.switchLoc();
                },
                rewindLoc: () => {
                    this.rewindLoc();
                },
                deleteLoc: () => {
                    this._resetMiniMap();
                    this.deleteLoc();
                },
                closeAndSaveLoc: () => {
                    this._resetMiniMap();
                    this.closeAndSaveLoc();
                },
                exitLoc: () => {
                    this.exitLoc();
                },
                mergeTags: () => {
                    this.mergeTags();
                },
                tagDialog: () => {
                    this.toggleTagOverlay();
                },
                exportAsCsv: () => {
                    this.exportAsCsv();
                },
                classicMap: () => {
                    this.classicMap();
                },
                resetGulf: () => {
                    this.resetGulf();
                },
                deleteTags: () => {
                    this.deleteTags();
                },
                findLinkPanos: () => {
                    this.findLinkPanos();
                },
                fullScreenMap: () => {
                    this.toggleFullScreenMap();
                },
                GeoguessrModal: () => {
                    this.toggleGeoguessrModal();
                    this.toggleElementHidden();
                },
            };

            this._loadShortcuts();
        }

        _getStorageKey() {
            const parts = location.pathname.split('/');
            const idx = parts.indexOf('maps');
            const mapId = idx >= 0 && idx + 1 < parts.length ? parts[idx + 1] : 'default';
            return `tagShortcuts_${mapId}`;
        }

        _loadShortcuts() {
            try {
                const saved = GM_getValue(this._getStorageKey());
                this.NUMBER_SHORTCUTS = saved ? JSON.parse(saved) : {};
            } catch { this.NUMBER_SHORTCUTS = {}; }
        }

        setShortcut(key, tagName) {
            for (const [k, v] of Object.entries(this.NUMBER_SHORTCUTS)) {
                if (parseInt(k) === key || v === tagName) {
                    delete this.NUMBER_SHORTCUTS[k];
                }
            }
            this.NUMBER_SHORTCUTS[key] = tagName;
            this.saveShortcuts();
            this.refreshBadges();
        }

        removeShortcut(key) {
            delete this.NUMBER_SHORTCUTS[key];
            this.saveShortcuts();
            this.refreshBadges();
        }

        getTagName(tagEl) {
            const selectors = ['.tag__text', '.tag__text-container', 'label[for]', 'span'];
            for (const sel of selectors) {
                const el = tagEl.querySelector(sel);
                if (el) {
                    const clone = el.cloneNode(true);
                    clone.querySelectorAll('.tag-shortcut-number, small').forEach(e => e.remove());
                    const name = clone.textContent.trim();
                    if (name) return name;
                }
            }
            return tagEl.textContent.trim().replace(/\s*\d+$/, '');
        }

        findTagElements(name) {
            if (!name) return [];
            return Array.from(document.querySelectorAll('.tag')).filter(tag => this.getTagName(tag) === name);
        }

        refreshBadges() {
            document.querySelectorAll('.tag').forEach(tag => {
                const tagName = this.getTagName(tag);
                const entry = Object.entries(this.NUMBER_SHORTCUTS).find(([, v]) => v === tagName);
                const num = entry ? entry[0] : null;

                if (num) {
                    tag.setAttribute('data-shortcut-bound', 'true');
                    this._applyBadge(tag, num);
                } else {
                    tag.removeAttribute('data-shortcut-bound');
                    this._removeBadge(tag);
                }
            });
        }

        _applyBadge(tagEl, number) {
            if (!tagEl || typeof tagEl.querySelector !== 'function') return;

            const selectors = ['.tag__text', '.tag__text-container', 'label[for]', 'span'];
            let container = null;
            for (const sel of selectors) {
                const el = tagEl.querySelector(sel);
                if (el) { container = el; break; }
            }
            if (!container) container = tagEl;

            const existing = container.querySelector('.tag-shortcut-number');
            if (existing) existing.remove();

            const badge = document.createElement('small');
            badge.className = 'tag-shortcut-number';
            badge.textContent = number;
            Object.assign(badge.style, {
                position: 'absolute', top: '-5px', right: '-5px',
                background: '#FF0000', color: 'white', borderRadius: '50%',
                width: '18px', height: '18px', display: 'flex',
                alignItems: 'center', justifyContent: 'center',
                fontSize: '0.7rem', fontWeight: 'bold', zIndex: '10',
                pointerEvents: 'none'
            });
            container.style.position = 'relative';
            container.appendChild(badge);
        }

        _removeBadge(tagEl) {
            const badge = tagEl.querySelector('.tag-shortcut-number');
            if (badge) badge.remove();
        }

        handleNumberKey(key) {
            const tagName = this.NUMBER_SHORTCUTS[key];
            if (tagName) {
                const tagEls = this.findTagElements(tagName);
                if (tagEls.length > 0) {
                    const tagEl = tagEls[0];
                    if (!editor || !editor.currentLocation) tagEl.click();
                    const deleteBtn = tagEl.querySelector('.tag__button--delete');
                    const addBtn = tagEl.querySelector('.tag__button--add');
                    const btn = deleteBtn || addBtn;
                    if (btn) btn.click();
                } else {
                    this._tagLocByName(tagName);
                }
            } else {
                if (!editor || !editor.currentLocation) {
                    const hasBtns = document.querySelectorAll('.tag__text');
                    if (hasBtns[key - 1]) hasBtns[key - 1].click();
                }
                const btns = document.querySelectorAll('.tag__button--add');
                if (btns[key - 1]) btns[key - 1].click();
            }
        }

        async _tagLocByName(tag) {
            if (!editor || !editor.currentLocation) return;
            const isReview = document.querySelector('.review-header');
            const prevBtn = document.querySelector('[data-qa="review-prev"]');
            const nextBtn = document.querySelector('[data-qa="review-next"]');
            const editLoc = Editor.getCurrentLocation();

            if (isReview) {
                editLoc.tags.push(tag);
                setTimeout(() => { if (nextBtn) nextBtn.click(); }, 100);
                setTimeout(() => { if (prevBtn) prevBtn.click(); }, 200);
            } else {
                await editor.closeAndDeleteLocation();
                editLoc.tags.push(tag);
                await editor.addAndOpenLocation(editLoc);
            }
        }

        getAllBindings() {
            return Object.entries(this.NUMBER_SHORTCUTS)
                .sort(([a], [b]) => parseInt(a) - parseInt(b))
                .map(([key, tag]) => ({ key: parseInt(key), tag }));
        }

        cloneShortcuts(shortcuts) {
            return Object.fromEntries(
                Object.entries(shortcuts).map(([action, config]) => [
                    action,
                    {
                        key: config.key,
                        requireShift: !!config.requireShift
                    }
                ])
            );
        }

        initShortcuts() {
            const defaults = this.cloneShortcuts(CONSTANS.DEFAULT_SHORTCUTS);

            try {
                const saved = GM_getValue('mm_keyboard_shortcuts');

                if (!saved) {
                    this.KEYBOARD_SHORTCUTS = defaults;
                    return;
                }

                const parsed = JSON.parse(saved);

                for (const [action, value] of Object.entries(parsed)) {
                    if (!(action in defaults)) continue;

                    // old format: { switchLoc: "Q" }
                    if (typeof value === 'string') {
                        defaults[action] = {
                            key: value.toUpperCase(),
                            requireShift:
                            CONSTANS.DEFAULT_SHORTCUTS[action]?.requireShift ?? false
                        };
                        continue;
                    }

                    // new format validation
                    if (
                        value &&
                        typeof value === 'object' &&
                        typeof value.key === 'string'
                    ) {
                        defaults[action] = {
                            key: value.key.toUpperCase(),
                            requireShift: !!value.requireShift
                        };
                    }
                }

                this.KEYBOARD_SHORTCUTS = defaults;
            } catch (e) {
                Notification.error(`Failed to load keyboard shortcuts: ${e.message}`);
                this.KEYBOARD_SHORTCUTS = defaults;
            }
        }

        saveShortcuts() {
            try {
                const normalized = {};

                for (const [action, value] of Object.entries(this.KEYBOARD_SHORTCUTS)) {
                    if (!value || typeof value !== 'object' || typeof value.key !== 'string') continue;

                    normalized[action] = {
                        key: value.key.toUpperCase(),
                        requireShift: !!value.requireShift
                    };
                }

                GM_setValue('mm_keyboard_shortcuts', JSON.stringify(normalized));
                GM_setValue(this._getStorageKey(), JSON.stringify(this.NUMBER_SHORTCUTS));

                mainPanel?._renderKeybinds();
            } catch (e) {
                Notification.error(`Failed to save keyboard shortcuts: ${e.message}`);
            }
        }

        resetShortcuts() {
            this.NUMBER_SHORTCUTS = {};
            this.KEYBOARD_SHORTCUTS = this.cloneShortcuts(
                CONSTANS.DEFAULT_SHORTCUTS
            );

            this.saveShortcuts();
            this.refreshBadges();
        }

        isShortcutMatched(action, keyEvent) {
            const shortcut = this.KEYBOARD_SHORTCUTS[action];
            if (!shortcut) return false;
            if (keyEvent.metaKey || keyEvent.ctrlKey || keyEvent.altKey) return false;

            const shortcutKey = typeof shortcut === 'string' ? shortcut : shortcut.key;
            const requireShift = typeof shortcut === 'string' ? false : (shortcut.requireShift ?? false);

            const k = keyEvent.key || keyEvent.code;
            const kLow = k.toLowerCase();

            const keyMatch = k === shortcutKey || kLow === shortcutKey.toLowerCase();
            const shiftMatch = keyEvent.shiftKey === requireShift;

            return keyMatch && shiftMatch;
        }

        _getSelections() {
            try {
                if (editor.selections && editor.selections.length > 0) {
                    return editor.selections.flatMap(s => s.locations);
                }

                return editor.getLocationsInBBox(editor.getLocationBounds());
            } catch (e) {
                return [];
            }
        }

        switchLoc() {
            const locs = this._getSelections();
            const isReview = document.querySelector('.review-header');
            if (isReview) {
                const nextBtn = document.querySelector('[data-qa="review-next"]');
                if (nextBtn) nextBtn.click();
            } else {
                if (!this.currentIndex) this.currentIndex = 1;
                else {
                    this.currentIndex++;
                    if (this.currentIndex > locs.length) this.currentIndex = 1;
                }
                editor.openLocation(locs[this.currentIndex - 1]);
                this._focusOnLoc(locs[this.currentIndex - 1]);
            }
        }

        rewindLoc() {
            const locs = this._getSelections();
            const isReview = document.querySelector('.review-header');
            if (isReview) {
                const prevBtn = document.querySelector('[data-qa="review-prev"]');
                if (prevBtn) prevBtn.click();
            } else {
                if (!this.currentIndex) this.currentIndex = 1;
                else {
                    this.currentIndex--;
                    if (this.currentIndex < 1) this.currentIndex = locs.length;
                }
                editor.openLocation(locs[this.currentIndex - 1]);
                this._focusOnLoc(locs[this.currentIndex - 1]);
            }
        }

        _focusOnLoc(loc) {
            if (map) map.panTo(loc.location);
        }

        deleteLoc() {
            this._resetMiniMap();
            const isReview = document.querySelector('.review-header');
            if (isReview) {
                const delBtn = document.querySelector('[data-qa="location-delete"]');
                if (delBtn) delBtn.click();
            } else {
                editor.closeAndDeleteLocation();
            }
        }

        copyLoc() {
            return editor.addLocation(Editor.getCurrentLocation());
        }

        closeAndSaveLoc() {
            this._resetMiniMap();
            const saveBtn = document.querySelector('[data-qa="location-save"]');
            if (saveBtn) saveBtn.click();
        }

        exitLoc() {
            const closeBtn = document.querySelector('[data-qa="location-close"]');
            if (closeBtn) closeBtn.click();
        }

        mergeTags() {
            const selections = this._getSelections();
            if (!selections.length) return;

            if (editor.selections && editor.selections.length > 0) {
                const locationsMap = selections.map(loc => loc.location);
                const allTags = selections.flatMap(loc => loc.tags || []);
                const mergedTags = Array.from(new Set(allTags));

                let mergedLocations = editor.getLocationsInBBox(editor.getLocationBounds())
                .filter(loc => locationsMap.includes(loc.location));

                mergedTags.forEach(tag => {
                    editor.addTag(mergedLocations, tag);
                    mergedLocations = editor.getLocationsInBBox(editor.getLocationBounds())
                        .filter(loc => locationsMap.includes(loc.location));
                });
            } else if (editor.currentLocation && editor.currentLocation.duplicates) {
                const allTags = editor.currentLocation.duplicates.flatMap(dup => dup.tags || []);
                const mergedTags = Array.from(new Set(allTags));
                const locationsMap = editor.currentLocation.duplicates.map(dup => dup.location);
                const others = editor.currentLocation.duplicates.slice(1);
                if (others.length > 0) editor.removeLocations(others);

                let pruned = editor.getLocationsInBBox(editor.getLocationBounds())
                .filter(loc => locationsMap.includes(loc.location));
                mergedTags.forEach(tag => {
                    editor.addTag(pruned, tag);
                    pruned = editor.getLocationsInBBox(editor.getLocationBounds())
                        .filter(loc => locationsMap.includes(loc.location));
                });
                editor.openLocation(pruned[0]);
            } else {
                document.querySelectorAll('.button').forEach(btn => {
                    if (btn.textContent.includes('Find duplicates')) btn.click();
                });
                const dups = editor.selections.filter(s => s?.key?.includes('dup'));
                dups.forEach(dup => {
                    const locMap = dup.locations.map(l => l.location);
                    const allTags = Array.from(new Set(dup.locations.flatMap(l => l.tags || [])));
                    editor.pruneDuplicates(dup);
                    let pruned = editor.getLocationsInBBox(editor.getLocationBounds())
                    .filter(loc => locMap.includes(loc.location));
                    allTags.forEach(tag => {
                        editor.addTag(pruned, tag);
                        pruned = editor.getLocationsInBBox(editor.getLocationBounds())
                            .filter(loc => locMap.includes(loc.location));
                    });
                });
            }
        }

        deleteTags() {
            let selections = editor.selections;
            while (selections && selections.length > 0) {
                const item = selections[0];
                const tag = JSON.parse(item.key);
                editor.deleteTag(tag.tagName, item.locations);
                selections = editor.selections;
            }
        }

        toggleElementHidden() {
            if (!this.isHidden) {
                this._styleTag = GM_addStyle(`
                    .embed-controls {display: none !important}
                    .SLHIdE-sv-links-control {display: none !important}
                    [alt="Google"] {display: none !important}
                    [class$="gmnoprint"], [class$="gm-style-cc"] {display: none !important}
                `);
                this.isHidden = true;
            } else {
                if (this._styleTag) this._styleTag.remove();
                this.isHidden = false;
            }
        }

        toggleMapControlsHidden() {
            if (!this.isMapControlsHidden) {
                this._mapStyleTag = GM_addStyle(`
                    .map-control.white {display: none !important}
                    .map-control--menu {display: none !important}
                    .sv-opacity-control {display: none !important}
                    .map-type-control {display: none !important}
                    .coordinate-control {display: none !important}
                    .search-control {display: none !important}
                    [alt="Google"] {display: none !important}
                    [class$="gmnoprint"], [class$="gm-style-cc"] {display: none !important}
                `);
                this.isMapControlsHidden = true;
            } else {
                if (this._mapStyleTag) this._mapStyleTag.remove();
                this._mapStyleTag = null;
                this.isMapControlsHidden = false;
            }
        }

        toggleMiniMap() {
            const mc = document.querySelector('.map-embed');
            if (mc) {
                mc.classList.toggle('hidden');
                this.isMiniMapHidden = !this.isMiniMapHidden;
            }
        }

        _resetMiniMap() {
            const mc = document.querySelector('.map-embed');
            if (mc && mc.classList.contains('minimap')) {
                if (this._mapContainerParent) this._mapContainerParent.appendChild(mc);
                mc.classList.remove('minimap', 'hidden');
            }
        }

        exportAsCsv() {
            const locs = this._getSelections();
            if (!locs.length) return;
            const csvContent = this._jsonToCSV(locs);
            const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
            const link = document.createElement('a');
            const url = URL.createObjectURL(blob);
            link.setAttribute('href', url);
            link.setAttribute('download', 'output.csv');
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

        _jsonToCSV(jsonData) {
            const maxTags = jsonData.reduce((max, item) => Math.max(max, (item.tags || []).length), 0);
            const tagHeaders = Array.from({ length: maxTags }, (_, i) => `tag${i + 1}`);
            const headers = ['lat', 'lng', 'panoId', 'heading', 'pitch', 'zoom', 'date', ...tagHeaders];
            const rows = jsonData.map(item => {
                const d = item.panoDate ? new Date(item.panoDate) : null;
                const dateStr = d && !isNaN(d.getTime())
                ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` : '';
                return [
                    item.location?.lat || '', item.location?.lng || '',
                    item.panoId || '', item.heading || '', item.pitch || '',
                    item.zoom || '', dateStr,
                    ...Array.from({ length: maxTags }, (_, i) => (item.tags || [])[i] || '')
                ];
            });
            return [headers, ...rows].map(r => r.join(',')).join('\n');
        }

        classicMap() {
            if (!google || !map) return;
            const tileUrl = 'https://mapsresources-pa.googleapis.com/v1/tiles?map_id=61449c20e7fc278b&version=15797339025669136861&pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m2!1e0!2sm!3m7!2sen!3sCN!5e1105!12m1!1e3!12m1!1e2!4e0!5m5!1e0!8m2!1e1!1e1!8i47083502!6m6!1e12!2i2!11e0!39b0!44e0!50e0';
            const layer = new google.maps.ImageMapType({
                getTileUrl: (coord, zoom) => tileUrl.replace('{z}', zoom).replace('{x}', coord.x).replace('{y}', coord.y),
                tileSize: new google.maps.Size(256, 256),
                name: 'google_labels_reset',
                maxZoom: 20,
            });
            map.mapTypes.stack.layers[0] = layer;
            map.setMapTypeId('stack');
        }

        resetGulf() {
            if (!google || !map) return;
            let tileUrl = 'https://maps.googleapis.com/maps/vt?pb=%211m5%211m4%211i{z}%212i{x}%213i{y}%214i256%212m2%211e0%212sm%213m17%212sen%213sMX%215e18%2112m4%211e68%212m2%211sset%212sRoadmap%2112m3%211e37%212m1%211ssmartmaps%2112m4%211e26%212m2%211sstyles%212ss.e%3Ag%7Cp.v%3Aoff%2Cs.t%3A1%7Cs.e%3Ag.s%7Cp.v%3Aon%2Cs.e%3Al%7Cp.v%3Aon%215m1%215f1.350000023841858';
            if (JSON.parse(localStorage.getItem('mapBoldCountryBorders') || 'false'))
                tileUrl = tileUrl.replace('212ss.e%3Ag%7Cp.v%3Aoff', '212ss.t%3A17%7Cs.e%3Ag.s%7Cp.w%3A2%7Cp.c%3A%23000000%2Cs.e%3Ag%7Cp.v%3Aoff');
            const layer = new google.maps.ImageMapType({
                getTileUrl: (coord, zoom) => tileUrl.replace('{z}', zoom).replace('{x}', coord.x).replace('{y}', coord.y),
                tileSize: new google.maps.Size(256, 256),
                name: 'google_labels_reset',
                maxZoom: 20,
            });
            map.mapTypes.stack.layers[2] = layer;
            map.setMapTypeId('stack');
        }

        async findLinkPanos() {
            if (!editor || !editor.currentLocation) return;
            const startLoc = Editor.getCurrentLocation();
            let prevHeading = startLoc.heading;
            const service = new google.maps.StreetViewService();
            const delay = ms => new Promise(r => setTimeout(r, ms));
            let metadata = await service.getPanorama({ pano: startLoc.panoId });
            while (metadata.data.links.length === 2) {
                const nextLoc = metadata.data.links.find(loc => Math.abs(loc.heading - prevHeading) <= 90);
                if (!nextLoc) break;
                await delay(100);
                metadata = await service.getPanorama({ pano: nextLoc.pano });
                editor.addLocation({
                    location: { lat: metadata.data.location.latLng.lat(), lng: metadata.data.location.latLng.lng() },
                    panoId: metadata.data.location.pano,
                    heading: nextLoc.heading,
                    pitch: 0, zoom: 0, tags: [], flags: 1
                });
                prevHeading = nextLoc.heading;
            }
        }

        toggleFullScreenMap() {
            const mc = document.querySelector('.map-embed');
            const fs = document.fullscreenElement;
            if (fs && fs.classList.contains('map-embed')) document.exitFullscreen();
            else if (!fs && mc) mc.requestFullscreen();
        }

        _handleFullscreenChange() {
            const fs = document.fullscreenElement;
            const mc = document.querySelector('.map-embed');
            if (!this._mapContainerParent && mc) this._mapContainerParent = mc.parentNode;

            if (fs && fs.classList.contains('location-preview__panorama')) {
                this.toggleMapControlsHidden();
                fs.appendChild(mc);
                mc.classList.add('minimap');
                if (this.isMiniMapHidden) mc.classList.add('hidden');

                if (editor && editor.currentLocation) {
                    map.setCenter(editor.currentLocation.location.location);
                }

                let hoverTimeout = null;
                let isHovering = false;

                this._miniMapEnterHandler = () => {
                    isHovering = true;
                    if (hoverTimeout) { clearTimeout(hoverTimeout); hoverTimeout = null; }
                    mc.classList.add('active');
                };
                this._miniMapLeaveHandler = () => {
                    isHovering = false;
                    if (hoverTimeout) clearTimeout(hoverTimeout);
                    hoverTimeout = setTimeout(() => {
                        if (!isHovering) mc.classList.remove('active');
                        hoverTimeout = null;
                    }, 800);
                };
                mc.addEventListener('mouseenter', this._miniMapEnterHandler);
                mc.addEventListener('mouseleave', this._miniMapLeaveHandler);
            } else {
                if (this._miniMapEnterHandler && mc) mc.removeEventListener('mouseenter', this._miniMapEnterHandler);
                if (this._miniMapLeaveHandler && mc) mc.removeEventListener('mouseleave', this._miniMapLeaveHandler);
                this._miniMapEnterHandler = null;
                this._miniMapLeaveHandler = null;
                if (this.isMapControlsHidden) this.toggleMapControlsHidden();
                this._resetMiniMap();
            }
        }

        toggleTagOverlay() {
            if (this._isTagOverlayOpen) this._closeTagOverlay();
            else this._showTagOverlay();
        }

        _showTagOverlay() {
            const fsParent = document.fullscreenElement;
            if (!fsParent || this._isTagOverlayOpen) return;
            const tc = document.querySelector('.location-preview__tags');
            if (!tc) return;

            const overlay = document.createElement('div');
            overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;justify-content:center;align-items:center;';
            fsParent.appendChild(overlay);

            const container = document.createElement('div');
            container.style.cssText = 'position:relative;width:90%;max-width:600px;max-height:80vh;background:#1a1a1a;border-radius:12px;padding:20px;overflow:auto;z-index:2147483647;box-shadow:0 10px 50px rgba(0,0,0,0.7);border:1px solid #444;';

            const closeBtn = document.createElement('button');
            closeBtn.textContent = '×';
            closeBtn.style.cssText = 'position:absolute;top:15px;right:15px;background:none;border:none;font-size:24px;cursor:pointer;color:white;z-index:10;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:50%;';
            closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255,255,255,0.1)'; });
            closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'none'; });
            closeBtn.addEventListener('click', () => this._closeTagOverlay());
            container.appendChild(closeBtn);

            this._originalParent = tc.parentNode;
            this._originalNextSibling = tc.nextSibling;
            container.appendChild(tc);
            overlay.appendChild(container);
            tc.classList.add('tag-overlay-content');

            const escHandler = (e) => { if (e.key === 'Escape') this._closeTagOverlay(); };
            overlay._escHandler = escHandler;
            document.addEventListener('keydown', escHandler, true);
            overlay.addEventListener('click', (e) => { if (e.target === overlay) this._closeTagOverlay(); });

            this._overlay = overlay;
            this._tagContainer = tc;
            this._isTagOverlayOpen = true;
        }

        _closeTagOverlay() {
            if (!this._isTagOverlayOpen || !this._overlay) return;
            if (this._overlay._escHandler) document.removeEventListener('keydown', this._overlay._escHandler, true);
            if (this._tagContainer && this._originalParent) {
                this._tagContainer.classList.remove('tag-overlay-content');
                if (this._originalNextSibling) this._originalParent.insertBefore(this._tagContainer, this._originalNextSibling);
                else this._originalParent.appendChild(this._tagContainer);
            }
            this._overlay.remove();
            this._overlay = null;
            this._tagContainer = null;
            this._isTagOverlayOpen = false;
        }

        toggleGeoguessrModal() {
            const container = document.querySelector('[class*="mapsImagerySceneScene__root"]');
            if (!container) return;

            if (this._modal && this._mapInfo) {
                container.removeChild(this._modal);
                container.removeChild(this._mapInfo);
                this._modal = null;
                this._mapInfo = null;
                return;
            }

            if (!google) return;
            const tileUrl = 'https://mapsresources-pa.googleapis.com/v1/tiles?map_id=61449c20e7fc278b&version=15797339025669136861&pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m3!1e0!2sm!3i744503673!3m12!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sRoadmap!12m3!1e37!2m1!1ssmartmaps!4e0!5m2!1e3!5f2!23i47083502!23i56565656!26m2!1e2!1e3';

            this._modal = document.createElement('div');
            Object.assign(this._modal.style, { position: 'absolute', right: '2rem', bottom: '1rem', zIndex: '9999', display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '0.8rem', opacity: '0.4' });

            const mapDiv = document.createElement('div');
            mapDiv.id = 'mini-map';
            Object.assign(mapDiv.style, { width: '17rem', height: '10.8rem', borderRadius: '0.25rem', overflow: 'hidden', boxShadow: '0 2px 6px rgba(0,0,0,0.3)', backgroundColor: '#e0e0e0' });

            const miniMap = new google.maps.Map(mapDiv, {
                center: { lat: 0, lng: 0 }, zoom: 1, disableDefaultUI: true,
                gestureHandling: 'none', clickableIcons: false, mapTypeId: 'roadmap'
            });
            const ct = new google.maps.ImageMapType({
                getTileUrl: (c, z) => tileUrl.replace('{z}', z).replace('{x}', c.x).replace('{y}', c.y),
                tileSize: new google.maps.Size(256, 256), name: 'geoguessr_map', maxZoom: 20,
            });
            miniMap.mapTypes.set('custom', ct);
            miniMap.setMapTypeId('custom');

            const guessBtn = document.createElement('button');
            guessBtn.textContent = 'PLACE YOUR PIN ON THE MAP';
            Object.assign(guessBtn.style, {
                borderRadius: '3.75rem', border: 'none', fontSize: '11px', width: '17rem', height: '2.22rem',
                padding: '0 1.5rem', fontWeight: '1200', fontStyle: 'italic', backgroundColor: 'black', color: '#fff', cursor: 'pointer'
            });

            this._modal.appendChild(mapDiv);
            this._modal.appendChild(guessBtn);

            this._mapInfo = document.createElement('div');
            this._mapInfo.style.cssText = 'position:absolute;top:1rem;right:0;z-index:9999;';

            const wrapper = document.createElement('div');
            Object.assign(wrapper.style, {
                position: 'relative', zIndex: '0', borderRadius: '0.25rem',
                boxShadow: 'inset 0 1px 0 hsla(0,0%,100%,.15), inset 0 -1px 0 rgba(0,0,0,0.25)',
                filter: 'drop-shadow(0 1rem 1rem rgba(0,0,0,0.4))', padding: '0.8rem',
                display: 'flex', flexDirection: 'column', gap: '0.5rem', color: '#fff', fontFamily: 'sans-serif',
            });

            const bgStart = document.createElement('div');
            Object.assign(bgStart.style, {
                position: 'absolute', top: '0', bottom: '0', left: '-1rem', width: '100%',
                background: 'linear-gradient(180deg, rgba(161,155,217,0.6) 0%, rgba(161,155,217,0) 50%), #4b3f72',
                transform: 'skewX(15deg)', zIndex: '-1'
            });
            const bgEnd = document.createElement('div');
            Object.assign(bgEnd.style, {
                position: 'absolute', top: '0', bottom: '0', right: '-1rem', width: '100%',
                background: 'linear-gradient(180deg, rgba(161,155,217,0.6) 0%, rgba(161,155,217,0) 50%), #4b3f72',
                zIndex: '-1', overflow: 'hidden'
            });

            const infoRow = document.createElement('div');
            infoRow.style.cssText = 'display:flex;gap:0.5rem;margin-top:0.5rem;z-index:0;';
            const sec = (l, v) => {
                const s = document.createElement('div');
                s.style.marginRight = '0.5rem';
                const lb = document.createElement('div');
                lb.textContent = l;
                Object.assign(lb.style, { fontStyle: 'italic', fontWeight: '1200', fontSize: '10px', textTransform: 'uppercase', whiteSpace: 'nowrap', color: '#cbbcf0' });
                const vl = document.createElement('div');
                vl.textContent = v;
                Object.assign(vl.style, { fontStyle: 'italic', fontWeight: '700', fontSize: '18px', whiteSpace: 'nowrap', color: '#fff' });
                s.appendChild(lb); s.appendChild(vl); return s;
            };
            infoRow.appendChild(sec('Map', 'A Community World'));
            infoRow.appendChild(sec('Round', '1 / 5'));
            infoRow.appendChild(sec('Score', '0'));
            wrapper.appendChild(bgStart);
            wrapper.appendChild(infoRow);
            wrapper.appendChild(bgEnd);
            this._mapInfo.appendChild(wrapper);
            container.appendChild(this._mapInfo);
            container.appendChild(this._modal);
        }

        attach() {
            if (this._attached) return;
            this._attached = true;

            this.initShortcuts();

            const observer = new MutationObserver(() => {
                if (this._refreshPending) return;
                this._refreshPending = true;
                setTimeout(() => {
                    this._refreshPending = false;
                    this.refreshBadges();
                }, 200);
            });
            observer.observe(document.body, { childList: true, subtree: true });

            document.addEventListener('fullscreenchange', () => this._handleFullscreenChange());

            document.addEventListener('keydown', (e) => {
                if (e.target.tagName === 'INPUT' || e.target.isContentEditable || (!this.isApplied && e.key.toLowerCase() != this.KEYBOARD_SHORTCUTS.togglePanel.key.toLowerCase())) return;

                if (e.shiftKey) this.isShift = true;
                if (e.ctrlKey || e.metaKey) this.isCtrl = true;

                if (e.key >= '1' && e.key <= '9') {
                    this._currentNumberKey = parseInt(e.key);
                    if (!e.altKey && !e.metaKey && !e.shiftKey) e.stopImmediatePropagation();
                    return;
                }

                if (e.key === 'Delete' || e.key === 'Enter') {
                    const mc = document.querySelector('.map-embed');
                    if (mc && mc.classList.contains('minimap')) this._resetMiniMap();
                }

                const locs = this._getSelections();
                const k = e.key || e.code;
                const kLow = k.toLowerCase();

                if (this.isShortcutMatched('togglePanel', e)) {
                    e.stopImmediatePropagation();
                    if (mainPanel) mainPanel.toggle();
                    return;
                }
                for (const [action, handler] of Object.entries(this.handlers)) {
                    if (!this.isShortcutMatched(action, e)) continue;
                    e.stopImmediatePropagation();
                    handler();
                    return;
                }

            }, true);

            document.addEventListener('keyup', (e) => {
                this.isCtrl = e.ctrlKey || e.metaKey;
                this.isShift = e.shiftKey;
                if (e.target.tagName === 'INPUT' || e.target.isContentEditable || !this.isApplied) return;

                if (e.key >= '1' && e.key <= '9') {
                    this.handleNumberKey(parseInt(e.key));
                    this._currentNumberKey = null;
                }
            }, true);

            document.addEventListener('mousedown', (e) => {
                if (e.button === 0 && this.isShift) {
                    this.isDrawing = true;
                    this._startX = e.clientX;
                    this._startY = e.clientY;
                    document.body.style.userSelect = 'none';
                    this.selectionBox = document.createElement('div');
                    this.selectionBox.style.cssText = 'position:absolute;border:2px solid rgba(0,128,255,0.7);background:rgba(0,128,255,0.2);z-index:999999;';
                    document.body.appendChild(this.selectionBox);
                }
                const tagEl = e.target.closest('.tag');
                if (!tagEl || !this.isApplied) return;
                if (e.button !== 0) return;

                const tagName = this.getTagName(tagEl);
                if (!tagName) return;

                const isCtrl = e.ctrlKey || e.metaKey;
                if (isCtrl) {
                    // Ctrl+Click = unbind
                    const matched = Object.entries(this.NUMBER_SHORTCUTS).find(([, v]) => v === tagName);
                    if (matched) {
                        this.removeShortcut(parseInt(matched[0]));
                        Notification.info(`Shortcut ${matched[0]} unbound from "${tagName}"`);
                    }
                } else if (this._currentNumberKey) {
                    // Number+Click = bind
                    this.setShortcut(this._currentNumberKey, tagName);
                    Notification.info(`Shortcut ${this._currentNumberKey} bound to "${tagName}"`);
                }
            }, true);

            document.addEventListener('mousemove', (e) => {
                if (this.isDrawing && this.selectionBox) {
                    const x = Math.min(this._startX, e.clientX);
                    const y = Math.min(this._startY, e.clientY);
                    this.selectionBox.style.left = x + 'px';
                    this.selectionBox.style.top = y + 'px';
                    this.selectionBox.style.width = Math.abs(e.clientX - this._startX) + 'px';
                    this.selectionBox.style.height = Math.abs(e.clientY - this._startY) + 'px';
                }
            }, true);

            document.addEventListener('mouseup', (e) => {
                if (!this.isDrawing || !this.selectionBox) return;
                this.isDrawing = false;
                const rect = this.selectionBox.getBoundingClientRect();
                document.body.removeChild(this.selectionBox);
                this.selectionBox = null;
                document.body.style.userSelect = 'text';

                document.querySelectorAll('ul.tag-list li.tag.has-button').forEach(child => {
                    const cr = child.getBoundingClientRect();
                    if (cr.top >= rect.top && cr.left >= rect.left && cr.bottom <= rect.bottom && cr.right <= rect.right) {
                        child.click();
                    }
                });
            }, true);
        }

        get isApplied() { return this._isApplied; }
        set isApplied(v) {
            this._isApplied = v;
            Notification.info(v ? 'Shortcuts enabled' : 'Shortcuts disabled');
        }
    }

    const shortcutManager = new ShortcutManager();

    // ======================================================================================================================================
    // =========================================================== Layout Manager ===========================================================
    // ======================================================================================================================================

    class LayoutManager {

        constructor() {
            this.resizing = false;
            this.resizeEventsBound = false;
            this.observer = null;
            this.current = null;
            this.containers = {
                map: null,
                minimap: null,
                preview: null
            };
            this.undoBtnEl = null;
            this.resetBtnEl = null;
            this.Default_Preview_Height = null;
            this.MIN_WIDTH = 50;
        }

        saveConfig() {
            GM_setValue('mm_config', JSON.stringify(CONFIG));
        }

        setPageGap(reset){
            const pageContainer = document.querySelector('.page-map-editor');
            pageContainer.style.setProperty("grid-gap", reset ? ".5rem 1rem" : ".2rem .4rem" , "important");
            pageContainer.style.setProperty("margin", reset ? ".5rem 1rem" : ".2rem .4rem" , "important");
        }

        initControls() {
            if (!this.undoBtnEl) {
                this.undoBtnEl = createMainBtn('Undo last resize', CONSTANS.SVG_SOURCE.UNDO);
                this.undoBtnEl.onclick = () => this.undo();
            }
            this.undoBtnEl.classList.toggle('disabled', !CACHE.resizeHistory || CACHE.resizeHistory.length === 0);

            if (!this.resetBtnEl) {
                this.resetBtnEl = createMainBtn('Reset layout', CONSTANS.SVG_SOURCE.RESET);
                this.resetBtnEl.onclick = () => this.reset();
            }

            this.bindConfigInputs();
        }

        pushHistory() {
            if (!CACHE.resizeHistory) CACHE.resizeHistory = [];

            CACHE.resizeHistory.push({
                map: {
                    width: this.containers.map?.offsetWidth,
                    height: this.containers.map?.offsetHeight
                },
                minimap: {
                    width: this.containers.minimap?.offsetWidth,
                    height: this.containers.minimap?.offsetHeight
                },
                preview: {
                    width: this.containers.preview?.offsetWidth,
                    height: this.containers.preview?.offsetHeight
                }
            });

            if (CACHE.resizeHistory.length > 30) {
                CACHE.resizeHistory.shift();
            }

            if (this.undoBtnEl) {
                this.undoBtnEl.classList.remove('disabled');
            }
        }

        initObserver() {
            if (this.observer) return;

            this.observer = new MutationObserver(() => {
                if (!unsafeWindow.map) return;

                const rawMap = document.querySelector('.map-embed');
                const isMinimap = rawMap && rawMap.classList.contains('minimap');

                const mapContainer = isMinimap ? null : rawMap;
                const minimapContainer = isMinimap ? rawMap : null;

                const previewContainer = document.querySelector('.map-overview') ||
                      document.querySelector('.location-preview__panorama') ||
                      document.querySelector('.duplicates');

                if (mapContainer && !CACHE.initializedContainers.has(mapContainer)) {
                    this.registerContainer('map', mapContainer);
                }
                if (minimapContainer && !CACHE.initializedContainers.has(minimapContainer)) {
                    this.registerContainer('minimap', minimapContainer);
                }
                if (previewContainer && (!CACHE.initializedContainers.has(previewContainer) || this.containers.preview !== previewContainer)) {
                    if(previewContainer.classList.contains('location-preview__panorama') && !this.Default_Preview_Height) this.Default_Preview_Height = previewContainer.offsetHeight;
                    this.registerContainer('preview', previewContainer);
                }

                if ((mapContainer || minimapContainer || previewContainer) && (!this.resizeEventsBound)) {
                    this.initControls();
                    this.bindResizeEvents();
                    document.addEventListener('fullscreenchange', () => {
                        const currentRawMap = document.querySelector('.map-embed');
                        if (!currentRawMap) return;

                        const isCurrentlyMinimap = currentRawMap.classList.contains('minimap');

                        if (isCurrentlyMinimap) {
                            this.registerContainer('minimap', currentRawMap);
                        } else {
                            this.registerContainer('map', currentRawMap);
                            const currentMinimap = document.querySelector('.map-embed.minimap');
                            if (currentMinimap) {
                                this.registerContainer('minimap', currentMinimap);
                            }
                        }
                    });
                }
            });

            this.observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        registerContainer(type, container) {
            CACHE.initializedContainers.set(container, {});
            this.containers[type] = container;
            this.applyStoredSize(type, container);
            this.makeResizable(type, container);
        }

        applyStoredSize(type, container, reset) {
            this.setPageGap(reset);

            if (type === 'map') {
                applyStyles(container, {
                    width: `${CONFIG.LAYOUT.mapWidth}px`,
                    height: `${CONFIG.LAYOUT.mapHeight}px`
                });
            } else if (type === 'minimap') {
                const savedMini = GM_getValue('miniMapContainerSize', { width: 300, height: 200 });
                applyStyles(container, {
                    width: `${savedMini.width}px`,
                    height: `${savedMini.height}px`
                });
            } else if (type === 'preview') {
                if(container.classList.contains('location-preview__panorama')){
                    if(this.Default_Preview_Height){
                        applyStyles(container, {
                            width: `${CONFIG.LAYOUT.previewWidth}px`,
                            height: `${reset ? this.Default_Preview_Height : CONFIG.LAYOUT.previewHeight}px`
                        });
                    }
                }
                else{
                    applyStyles(container, {
                        width: `${CONFIG.LAYOUT.previewWidth}px`,
                        height: `${CONFIG.LAYOUT.previewHeight}px`
                    });
                }
            }
        }

        resizeLinked(activeType, currentWidth) {
            const map = this.containers.map;
            const preview = this.containers.preview;

            if (!map || !preview || activeType === 'minimap') return { width: currentWidth };
            const pageContainer = document.querySelector('.page-map-editor')
            const totalWidth = pageContainer.offsetWidth;

            let targetWidth = currentWidth;
            let linkedWidth = totalWidth - targetWidth;
            if (activeType === 'map') {
                preview.style.setProperty('width', `${linkedWidth}px`, 'important');
                CONFIG.LAYOUT.previewWidth = linkedWidth;

            } else if (activeType === 'preview') {
                map.style.setProperty('width', `${linkedWidth}px`, 'important');
                CONFIG.LAYOUT.mapWidth = linkedWidth;
            }

            return { width: targetWidth };
        }

        setContainerSize(type, width, height, {
            save = true,
            updateInput = true,
            linked = true,
            reset = false
        } = {}) {
            const container = this.containers[type];
            if (!container) return;

            let finalWidth = width;

            if (linked) {
                const result = this.resizeLinked(type, width);
                finalWidth = result.width;
            }

            if (type === 'map') {
                CONFIG.LAYOUT.mapWidth = finalWidth;
                CONFIG.LAYOUT.mapHeight = height;
                if (save) this.saveConfig();
                if (updateInput) this.updateInputs();
            } else if (type === 'preview') {
                CONFIG.LAYOUT.previewWidth = finalWidth;
                CONFIG.LAYOUT.previewHeight = height;
                if (save) this.saveConfig();
                if (updateInput) this.updateInputs();
            } else if (type === 'minimap') {
                GM_setValue('miniMapContainerSize', { width: finalWidth, height });
            }

            this.applyStoredSize(type, container, reset);
        }

        makeResizable(type, container) {
            const data = CACHE.initializedContainers.get(container) || {};
            if (data.resizable) return;

            data.resizable = true;
            CACHE.initializedContainers.set(container, data);

            if (getComputedStyle(container).position === 'static') {
                container.style.position = 'relative';
            }

            for (const config of CONSTANS.RESIZERS) {
                const resizer = document.createElement('div');
                resizer.className = `mm-container-resizer mm-resizer-${config.name}`;
                resizer.style.cssText = `${config.style} cursor:${config.cursor};`;

                resizer.addEventListener('mousedown', (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    this.pushHistory();
                    this.resizing = true;
                    this.current = {
                        type,
                        container,
                        resizer: config.name,
                        startX: e.clientX,
                        startY: e.clientY,
                        startWidth: container.offsetWidth,
                        startHeight: container.offsetHeight
                    };

                    applyStyles(document.body, { cursor: config.cursor, userSelect: 'none' });
                });

                container.appendChild(resizer);
            }
        }

        bindResizeEvents() {
            this.resizeEventsBound = true;
            document.addEventListener('mousemove', this.handleResizeMove.bind(this));
            document.addEventListener('mouseup', this.handleResizeEnd.bind(this));
        }

        handleResizeMove(e) {
            if (!this.resizing || !this.current) return;
            const { type, resizer, startX, startY, startWidth, startHeight } = this.current;

            const dx = e.clientX - startX;
            const dy = e.clientY - startY;

            let width = startWidth;
            let height = startHeight;

            if (resizer.includes('right')) width += dx;
            else if (resizer.includes('left')) width -= dx;

            if (resizer.includes('bottom')) height += dy;
            else if (resizer.includes('top')) height -= dy;

            this.setContainerSize(type, width, height, {
                save: false,
                updateInput: true,
                linked: true
            });
        }

        handleResizeEnd() {
            if (!this.resizing) return;

            this.resizing = false;
            if (this.current) {
                const type = this.current.type;
                if (type === 'minimap') {
                    const mini = this.containers.minimap;
                    if (mini) GM_setValue('miniMapContainerSize', { width: mini.offsetWidth, height: mini.offsetHeight });
                } else {
                    this.saveConfig();
                }
            }
            this.current = null;

            applyStyles(document.body, { cursor: '', userSelect: '' });
        }

        bindConfigInputs() {
            const mapW = document.getElementById('cfg-layout-map-width');
            const mapH = document.getElementById('cfg-layout-map-height');
            const previewW = document.getElementById('cfg-layout-preview-width');
            const previewH = document.getElementById('cfg-layout-preview-height');

            if (mapW) mapW.addEventListener('input', (e) => this.updateFromConfig('map', 'width', Number(e.target.value)));
            if (mapH) mapH.addEventListener('input', (e) => this.updateFromConfig('map', 'height', Number(e.target.value)));
            if (previewW) previewW.addEventListener('input', (e) => this.updateFromConfig('preview', 'width', Number(e.target.value)));
            if (previewH) previewH.addEventListener('input', (e) => this.updateFromConfig('preview', 'height', Number(e.target.value)));
        }

        updateFromConfig(type, dimension, value) {
            const container = this.containers[type];
            if (!container) return;

            let width = container.offsetWidth;
            let height = container.offsetHeight;

            if (dimension === 'width') {
                width = value;
            } else {
                height = value;
            }

            this.setContainerSize(type, width, height, {
                save: true,
                updateInput: true,
                linked: true
            });
        }

        updateInputs() {
            const set = (id, value) => {
                const el = document.getElementById(id);
                if (el && value !== undefined) el.value = value;
            };

            set('cfg-layout-map-width', CONFIG.LAYOUT.mapWidth);
            set('cfg-layout-map-height', CONFIG.LAYOUT.mapHeight);
            set('cfg-layout-preview-width', CONFIG.LAYOUT.previewWidth);
            set('cfg-layout-preview-height', CONFIG.LAYOUT.previewHeight);
        }

        undo() {
            if (!CACHE.resizeHistory || CACHE.resizeHistory.length === 0) return;

            const snapshot = CACHE.resizeHistory.pop();

            if (snapshot.map && snapshot.map.width && snapshot.map.height) {
                this.setContainerSize('map', snapshot.map.width, snapshot.map.height, { save: true, updateInput: true, linked: false });
            }

            if (snapshot.minimap && snapshot.minimap.width && snapshot.minimap.height) {
                this.setContainerSize('minimap', snapshot.minimap.width, snapshot.minimap.height, { save: true, updateInput: false, linked: false });
            }

            if (snapshot.preview && snapshot.preview.width && snapshot.preview.height) {
                this.setContainerSize('preview', snapshot.preview.width, snapshot.preview.height, { save: true, updateInput: true, linked: false });
            }

            if (CACHE.resizeHistory.length === 0 && this.undoBtnEl) {
                this.undoBtnEl.classList.add('disabled');
            }
        }

        reset() {
            this.pushHistory();

            this.setContainerSize('map', DEFAULT_CONFIG.LAYOUT.mapWidth, DEFAULT_CONFIG.LAYOUT.mapHeight, { linked: false });
            this.setContainerSize('preview', DEFAULT_CONFIG.LAYOUT.previewWidth, DEFAULT_CONFIG.LAYOUT.previewHeight, { linked: false, reset: true });
            const rawMap = document.querySelector('.map-embed');
            if (rawMap.classList.contains('minimap')) {
                this.setContainerSize('minimap', 300, 200, { linked: false });
            }
        }
    }

    const layoutManager = new LayoutManager();

    // ======================================================================================================================================
    // ============================================================ Tag Processor ===========================================================
    // ======================================================================================================================================

    class TagProcessor {
        constructor() {
            this.accuracy = CONFIG.ACCURACY || DEFAULT_CONFIG.ACCURACY;
            this.taggedLocs = [];
            this.failedCount = 0;
            this.currentChunk = 0;
            this.totalChunks = 0;
        }

        setAccuracy(tags) {
            if (tags.includes('time')) this.accuracy = CONFIG.ACCURACY.EXACT_TIME;
            else if (tags.includes('sun') || tags.includes('pan to sun')) this.accuracy = CONFIG.ACCURACY.SUN;
            else if (tags.includes('day')) this.accuracy = CONFIG.ACCURACY.DAY;
            else if (tags.includes('weather')) this.accuracy = CONFIG.ACCURACY.WEATHER;
            else this.accuracy = CONFIG.ACCURACY.DEFAULT;
        }

        async processCoord(coord, tags, metadata) {
            if (STATES.taggingTaskCancelled) return;
            let panoYear, panoMonth
            if (tags.includes('fix') || tags.includes('update')) {
                if (coord.panoDate) {
                    panoYear = parseInt(coord.panoDate.toISOString().substring(0, 4));
                    panoMonth = parseInt(coord.panoDate.toISOString().substring(5, 7));
                } else if (metadata && metadata.imageDate) {
                    panoYear = parseInt(metadata.imageDate.substring(0, 4));
                    panoMonth = parseInt(metadata.imageDate.substring(5, 7));
                } else {
                    const dateInfo = extractDate(coord.tags);
                    panoYear = parseInt(dateInfo.year);
                    panoMonth = parseInt(dateInfo.month);
                }
            }

            if(metadata){
                await this._processMetadataBasedTags(coord, tags, metadata, panoYear, panoMonth);
            }
            else {
                if (!tags.includes('fix') && !tags.includes('update')) {
                    this.failedCount++;
                    coord.tags.push('Panorama not found');
                } else {
                    await this._handleFix(coord, tags, panoYear, panoMonth);
                }
            }

            if (coord.tags) {
                coord.tags = Array.from(new Set(coord.tags));
            }
            this.taggedLocs.push(coord);
        }

        async _processMetadataBasedTags(coord, tags, metadata, panoYear, panoMonth) {
            let [yearTag, monthTag, typeTag, countryTag, subdivisionTag, genTag, altitudeTag, driDirTag, trekkerTag, roadTag, history, links] = parseMetadata(metadata);

            if (monthTag) monthTag = metadata.imageDate.slice(2);
            else monthTag = 'Month not found';

            if (tags.includes('year')) coord.tags.push(yearTag);
            if (tags.includes('month')) coord.tags.push(monthTag);
            if (tags.includes('country')) coord.tags.push(countryTag);
            if (tags.includes('type')) coord.tags.push(typeTag);
            if (tags.includes('generation')) coord.tags.push(genTag);

            if (tags.includes('day') || tags.includes('time') || tags.includes('sun') || tags.includes('weather') || tags.includes('pan to sun')) {
                await this._processTimeDependentTags(coord, tags, metadata, panoYear, panoMonth);
            }

            if (tags.includes('subdivision') && typeTag === 'Official') coord.tags.push(subdivisionTag);
            if (tags.includes('road') && typeTag === 'Official' && roadTag) coord.tags.push(roadTag);
            if (tags.includes('driving direction')) coord.tags.push(getDirection(driDirTag));
            if (tags.includes('type') && trekkerTag && typeTag === 'Official') coord.tags.push('Trekker/Tripod');

            if (tags.includes('elevation')) {
                if (altitudeTag) {
                    altitudeTag = Math.round(altitudeTag * 100) / 100;
                    if (CONFIG.ELEVATION.useRanges) {
                        altitudeTag = findRange(altitudeTag, CONFIG.ELEVATION.ranges);
                    } else {
                        altitudeTag = altitudeTag.toString() + 'm';
                    }
                    coord.tags.push(altitudeTag);
                }
            }

            if (tags.includes('reset heading') && metadata.tiles) {
                coord.heading = metadata.tiles.originHeading;
            }

            if (tags.includes('no badcam') && genTag === 'Badcam') {
                await this._handleNoBadcam(coord, history);
            }

            if (tags.includes('update')) {
                await this._handleUpdate(coord, panoYear, panoMonth);
            }
        }

        async _processTimeDependentTags(coord, tags, metadata, panoYear, panoMonth) {
            const dateRange = dateToTimestamp(metadata.imageDate);
            const initialSearch = await getMetadata('SingleImageSearch', { lat: coord.location.lat, lng: coord.location.lng }, CONFIG.SEARCH_RADIUS.EXACT_TIME, [dateRange.startDate, dateRange.endDate]);

            let exactTime = null;
            if (initialSearch && initialSearch.length === 3) {
                exactTime = await binarySearch({ lat: coord.location.lat, lng: coord.location.lng }, dateRange.startDate, dateRange.endDate, this.accuracy);
            }

            if (!exactTime) {
                if (tags.includes('day')) coord.tags.push('Day not found');
                if (tags.includes('time')) coord.tags.push('Time not found');
                return;
            }

            // Process day tag
            if (tags.includes('day')) {
                const currentDate = new Date();
                const currentOffset = -(currentDate.getTimezoneOffset()) * 60;
                const dayOffset = currentOffset - Math.round((coord.location.lng / 15) * 3600);
                const LocalDay = new Date(Math.round(exactTime - dayOffset) * 1000);
                coord.tags.push(LocalDay.toISOString().split('T')[0]);
            }

            // Process time tag
            if (tags.includes('time')) {
                const localTime = await getLocal(coord.location, exactTime);
                if (!localTime) coord.tags.push('Time not found');

                else {
                    const timeObject = new Date(localTime * 1000);
                    const timeStr = `${timeObject.getHours().toString().padStart(2, '0')}:${timeObject.getMinutes().toString().padStart(2, '0')}:${timeObject.getSeconds().toString().padStart(2, '0')}`;
                    coord.tags.push(timeStr);

                    const hour = timeObject.getHours();
                    if (hour < 11) coord.tags.push('Morning');
                    else if (hour >= 11 && hour < 13) coord.tags.push('Noon');
                    else if (hour >= 13 && hour < 17) coord.tags.push('Afternoon');
                    else if (hour >= 17 && hour < 19) coord.tags.push('Dusk');
                    else coord.tags.push('Night');
                }
            }

            // Process sun tags
            if (tags.includes('sun')) {
                const utcDate = new Date(exactTime * 1000);
                const sunData = getSunEvents(utcDate.toISOString(), coord.location.lat, coord.location.lng);
                if (sunData) {
                    if (exactTime >= (sunData.sunset - 30 * 60) && exactTime <= (sunData.sunset + 30 * 60)) {
                        coord.tags.push('Sunset');
                    } else if (exactTime >= (sunData.sunset - 90 * 60) && exactTime <= (sunData.sunset + 90 * 60)) {
                        coord.tags.push('Sunset(check)');
                    } else if (exactTime >= (sunData.sunrise - 30 * 60) && exactTime <= (sunData.sunrise + 30 * 60)) {
                        coord.tags.push('Sunrise');
                    } else if (exactTime >= (sunData.sunrise - 90 * 60) && exactTime <= (sunData.sunrise + 90 * 60)) {
                        coord.tags.push('Sunrise(check)');
                    } else if (exactTime >= (sunData.noon - 30 * 60) && exactTime <= (sunData.noon + 30 * 60)) {
                        coord.tags.push('Noon');
                    }
                }
            }

            // Process pan to sun
            if (tags.includes('pan to sun')) {
                const date = new Date(exactTime * 1000);
                const position = SunCalc.getPosition(date, coord.location.lat, coord.location.lng);

                const altitude = position.altitude;
                const azimuth = position.azimuth;

                const altitudeDegrees = altitude * (180 / Math.PI);
                const azimuthDegrees = azimuth * (180 / Math.PI);

                if (azimuthDegrees && altitudeDegrees) {
                    if (altitudeDegrees < 0) {
                        const moonPosition = SunCalc.getMoonPosition(date, coord.location.lat, coord.location.lng);
                        const moon_altitude = moonPosition.altitude;
                        const moon_azimuth = moonPosition.azimuth;
                        const moon_altitudeDegrees = moon_altitude * (180 / Math.PI);
                        const moon_azimuthDegrees = moon_azimuth * (180 / Math.PI);
                        coord.heading = moon_azimuthDegrees + 180;
                        coord.pitch = moon_altitudeDegrees;
                        coord.zoom = 2;
                        coord.tags.push('pan to moon');
                    } else {
                        coord.heading = azimuthDegrees + 180;
                        coord.pitch = altitudeDegrees;
                        coord.tags.push('pan to sun');
                    }
                }
            }

            // Process weather tag
            if (tags.includes('weather')) {
                const weatherTag = await getWeather(coord.location, exactTime);
                if (weatherTag) coord.tags.push(weatherTag);
            }
        }

        async _handleNoBadcam(coord, history) {
            if (!history) return;
            try {
                const panoIds = history.map(entry => entry.pano);
                const metas = await getMetadata('GetMetadata', panoIds);
                const originPanoId = coord.panoId;

                for (const meta of metas[1]) {
                    const betterPanoId = meta[1][1];
                    const worldHeight = meta[2][2][0];
                    const year = meta[6][7][0];

                    if (worldHeight === '8192' || (worldHeight === '6656' && parseInt(year) < 2020)) {
                        coord.panoId = betterPanoId;
                        coord.tags.push('Updated');
                        break;
                    }
                }

                if (coord.panoId === originPanoId) coord.tags.push('Failed to update');
            } catch (error) {
                Logger.error('Error handling no badcam:', error);
                coord.tags.push('Failed to update');
            }
        }

        async _handleUpdate(coord, panoYear, panoMonth) {
            try {
                const result = await Editor.singleImageSearch(coord.location, CONFIG.SEARCH_RADIUS.UPDATE);
                if(!result) {
                    coord.tags.push('Failed to update');
                    this.failedCount++;
                    return;
                }
                const updatedPanoId = result.location.pano;
                const updatedYear = parseInt(result.imageDate.substring(0, 4));
                const updatedMonth = parseInt(result.imageDate.substring(5, 7));

                if (coord.panoId) {
                    if (updatedPanoId && updatedPanoId !== coord.panoId) {
                        if (panoYear !== updatedYear || panoMonth !== updatedMonth) {
                            coord.panoId = updatedPanoId;
                            coord.tags.push('Updated');
                        } else {
                            coord.panoId = updatedPanoId;
                            coord.tags.push('Copyright changed');
                        }
                    }
                } else {
                    if (panoYear && panoMonth && updatedYear && updatedMonth) {
                        if (panoYear !== updatedYear || panoMonth !== updatedMonth) {
                            coord.panoId = updatedPanoId;
                            coord.tags.push('Updated');
                        }
                    } else {
                        coord.panoId = result.location.pano;
                        coord.tags.push('PanoId is added');
                    }
                }
            } catch (error) {
                Logger.error('Error updating location:', error);
                coord.tags.push('Failed to update');
                this.failedCount++;
            }
        }

        async _handleFix(coord, tags, panoYear, panoMonth) {
            try {
                const result = await Editor.singleImageSearch(coord.location, CONFIG.SEARCH_RADIUS.UPDATE);
                if (!result){
                    this.failedCount++;
                    coord.tags.push('Failed to fix');
                    return;
                }
                for (const entry of result.time) {
                    const year = entry.date.getFullYear();
                    const month = result.date.getMonth() + 1;
                    if (year === panoYear && month === panoMonth) {
                        const fixedPano = await Editor.getMetadata(entry.pano)
                        coord.panoId = entry.pano
                        coord.location.lat = fixedPano.location.latLng.lat()
                        coord.location.lng = fixedPano.location.latLng.lng()
                        coord.tags.push('Fixed');
                        return;
                    }
                }

                coord.panoId = result.location.pano
                coord.location.lat = result.location.latLng.lat()
                coord.location.lng = result.location.latLng.lng()
                coord.tags.push('Fallback to default coverage');
            } catch (error) {
                Logger.error('Error in fix location:', error);
                coord.tags.push('Failed to fix');
                this.failedCount++;
            }
        }

        async processChunk(chunk, tags, onProgress) {
            const promises = chunk.map(async coord => {
                if (STATES.taggingTaskCancelled) return;

                let metadata;
                this.setAccuracy(tags);
                if (!tags.includes('detect')) {
                    metadata = coord.panoId ? await Editor.getMetadata(coord.panoId) : await Editor.singleImageSearch( coord.location, CONFIG.SEARCH_RADIUS.DEFAULT);
                }

                await this.processCoord(coord, tags, metadata);
                onProgress?.();
            });

            await Promise.all(promises);
        }

        async processAll(selections, tags, onProgress) {
            this.taggedLocs = [];
            const chunks = chunkArray(selections, tags.includes('time') ? CONFIG.CHUNK_SIZE.TIME : CONFIG.CHUNK_SIZE.DEFAULT);
            this.totalChunks = chunks.length;
            this.failedCount = 0;
            let processedCount = 0;

            try {
                for (let i = 0; i < chunks.length; i++) {
                    if (STATES.taggingTaskCancelled) break;

                    this.currentChunk = i + 1;

                    await this.processChunk(chunks[i], tags, () => {
                        processedCount++;
                        onProgress?.({
                            current: this.currentChunk,
                            total: chunks.length,
                            processed: processedCount,
                            failed: this.failedCount,
                            total_items: selections.length
                        });
                    });
                }

                return this.taggedLocs;
            } catch (error) {
                Logger.error('Error processing tagging:', error);
                throw error;
            }
        }
    }

    // ======================================================================================================================================
    // ============================================================= Downloader =============================================================
    // ======================================================================================================================================

    class TarBall {
        constructor() {
            this.elements = [];
        }

        _createHeader(filename, size) {
            const buf = new ArrayBuffer(512);
            const view = new Uint8Array(buf);
            const nameBytes = new TextEncoder().encode(filename);

            for (let i = 0; i < Math.min(nameBytes.length, 99); i++) view[i] = nameBytes[i];
            const mode = "0000644 \0";
            for (let i = 0; i < mode.length; i++) view[100 + i] = mode.charCodeAt(i);

            const ugid = "0000000 \0";
            for (let i = 0; i < ugid.length; i++) { view[108 + i] = ugid.charCodeAt(i); view[116 + i] = ugid.charCodeAt(i); }

            const sizeStr = size.toString(8).padStart(11, '0') + ' ';
            for (let i = 0; i < sizeStr.length; i++) view[124 + i] = sizeStr.charCodeAt(i);

            const mtimeStr = Math.floor(Date.now() / 1000).toString(8).padStart(11, '0') + ' ';
            for (let i = 0; i < mtimeStr.length; i++) view[136 + i] = mtimeStr.charCodeAt(i);

            view[156] = '0'.charCodeAt(0);

            const magic = "ustar\x00";
            for (let i = 0; i < magic.length; i++) view[257 + i] = magic.charCodeAt(i);

            for (let i = 0; i < 8; i++) view[148 + i] = ' '.charCodeAt(0);
            let chksum = 0;
            for (let i = 0; i < 512; i++) chksum += view[i];
            const chksumStr = chksum.toString(8).padStart(6, '0') + '\0 ';
            for (let i = 0; i < chksumStr.length; i++) view[148 + i] = chksumStr.charCodeAt(i);

            return view;
        }


        addFile(filename, blob) {
            const header = this._createHeader(filename, blob.size);
            this.elements.push(header);
            this.elements.push(blob);

            const paddingSize = (512 - (blob.size % 512)) % 512;
            if (paddingSize > 0) {
                this.elements.push(new Uint8Array(paddingSize));
            }
        }

        getBlob() {
            this.elements.push(new Uint8Array(1024));
            return new Blob(this.elements, { type: 'application/x-tar' });
        }
    }

    class Downloader {
        constructor() {
            this.CHUNK_SIZE = CONFIG.CHUNK_SIZE.DOWNLOAD || DEFAULT_CONFIG.CHUNK_SIZE.DOWNLOAD;
            this.failedCount = 0;
            this.currentChunk = 0;
            this.totalChunks = 0;
            this.initObserver();
        }

        async downloadPanorama(panoId, fileName, size, zoom, heading, centerHeading, pitch, mode) {
            return new Promise(async (resolve) => {
                try {
                    let tilesPerRow, tilesPerColumn, zoomLevels;
                    const [tileWidth, tileHeight] = [512, 512];
                    const zoomTiles = [2, 4, 8, 16, 32];
                    const sizeMap = CONSTANS.TILE_SIZE_MAP;

                    const template = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoom}&nbt=0&fover=2`;
                    tilesPerRow = Math.min(Math.ceil(size.width / tileWidth), zoomTiles[zoom - 1]);
                    tilesPerColumn = Math.min(Math.ceil(size.height / tileHeight), zoomTiles[zoom - 1] / 2);

                    const canvas = new OffscreenCanvas(
                        tilesPerRow * tileWidth,
                        tilesPerColumn * tileHeight
                    );

                    const ctx = canvas.getContext('2d');
                    canvas.width = tilesPerRow * tileWidth;
                    canvas.height = tilesPerColumn * tileHeight;

                    if (size.width === 13312) {
                        if (sizeMap[zoom]) {
                            [canvas.width, canvas.height] = sizeMap[zoom];
                        }
                    }

                    const loadTile = (x, y) => {
                        return new Promise(async (resolveTile) => {
                            let tile;
                            const tileUrl = `${template}&x=${x}&y=${y}`;
                            try {
                                tile = await loadImage(tileUrl);
                                ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
                                resolveTile();
                            } catch (error) {
                                //Logger.error(`Error loading tile at ${x},${y}: ${error.message}`);
                                resolveTile();
                            }
                        });
                    };

                    let tilePromises = [];
                    for (let y = 0; y < tilesPerColumn; y++) {
                        for (let x = 0; x < tilesPerRow; x++) {
                            tilePromises.push(loadTile(x, y));
                        }
                    }

                    await Promise.all(tilePromises);

                    if (mode === 'Perspective') {
                        var targetTheta;
                        if (heading || heading == 0) targetTheta = heading - centerHeading;
                        else targetTheta = 0;
                        const perspectiveCanvas = generatePerspective(canvas, 125, targetTheta, pitch, 1920, 1080);

                        const blob = await perspectiveCanvas.convertToBlob({
                            type: 'image/png'
                        });
                        perspectiveCanvas.width = 0;
                        perspectiveCanvas.height = 0;
                        resolve({ blob, fileName: fileName + '.png' });

                    } else {
                        const blob = await canvas.convertToBlob({
                            type: 'image/jpeg',
                            quality: 0.95
                        });
                        canvas.width = 0;
                        canvas.height = 0;
                        resolve({blob, fileName: fileName + '.jpg' });
                    }
                } catch (error) {
                    Logger.error(`${CONSTANS.MESSAGES.DOWNLOAD_FAILED} (${fileName}): ${error.message}`);
                    this.failedCount++;
                    resolve(null);
                }
            });
        }

        async downloadThumbnail(panoId, fileName, heading, pitch) {
            const url = buildThumbnailUrl(panoId, heading, pitch);
            try {
                const response = await fetch(url);
                if (!response.ok){
                    this.failedCount++;
                    Logger.error(`[${CONSTANS.MESSAGES.NETWORK_ERROR}] ${response.status}`);
                }

                const blob = await response.blob();
                return { blob, fileName: fileName + '_thumb.jpg' };
            } catch (error) {
                this.failedCount++;
                Logger.error(`[${CONSTANS.MESSAGES.DOWNLOAD_FAILED} Thumb] ${error.message}`);
            }
        }

        async downloadChunk(chunk, onProgress, tarInstance) {
            const promises = chunk.map(async coord => {
                try {
                    let result = null;
                    const metadata = coord.panoId ? await Editor.getMetadata(coord.panoId) : await Editor.singleImageSearch(coord.location, CONFIG.SEARCH_RADIUS.DEFAULT);
                    if(!metadata || !metadata?.tiles){
                        Logger.warn('Panorama not found!')
                        this.failedCount++;
                        return;
                    }

                    const panoId = metadata?.location?.pano || coord.panoId;
                    const fileName = `${metadata?.imageDate}_${metadata?.location?.description}_${panoId}`;
                    const heading = coord.heading || metadata?.tiles?.centerHeading || 0
                    const pitch = coord.pitch || 0;

                    if (CONFIG.DOWNLOAD.type === 'Thumbnail') result = await this.downloadThumbnail(panoId, fileName, heading, pitch);
                    else {
                        result = await this.downloadPanorama(
                            metadata?.location?.pano || coord.panoId,
                            fileName,
                            metadata.tiles?.worldSize,
                            CONFIG.DOWNLOAD.quality || DEFAULT_CONFIG.DOWNLOAD.quality,
                            heading,
                            metadata?.tiles?.centerHeading,
                            pitch,
                            CONFIG.DOWNLOAD.type || DEFAULT_CONFIG.DOWNLOAD.type
                        );
                    }

                    if (result && result.blob) {
                        tarInstance.addFile(result.fileName, result.blob);
                    }
                    else return;
                } catch (error) {
                    this.failedCount++;
                    Logger.error(`[${CONSTANS.MESSAGES.DOWNLOAD_FAILED}] ${error.message}`);
                } finally {
                    onProgress?.();
                }
            });

            await Promise.all(promises);
        }

        async downloadAll(selections, onProgress) {
            const tar = new TarBall();
            const chunks = chunkArray(selections, this.CHUNK_SIZE);
            this.totalChunks = chunks.length;
            this.failedCount = 0;
            let processedCount = 0;

            try {
                for (let i = 0; i < chunks.length; i++) {
                    if (STATES.downloadTaskCancelled) break;

                    this.currentChunk = i + 1;
                    await this.downloadChunk(chunks[i], () => {
                        processedCount++;
                        onProgress?.({
                            current: this.currentChunk,
                            total: chunks.length,
                            processed: processedCount,
                            failed: this.failedCount,
                            total_items: selections.length
                        });
                    }, tar);
                }

                if (STATES.downloadTaskCancelled) return null;

                Notification.info(`[${CONSTANS.MESSAGES.DOWNLOAD_SUCCESS}] Packaging image files..`);
                return tar.getBlob();
            } catch (error) {
                return null;
            }
        }

        createControl({ iconSVG, label }) {
            const wrapper = document.createElement("div");
            wrapper.className = "embed-controls__control";
            wrapper.setAttribute("data-position", "left-top");

            const inner = document.createElement("div");
            inner.className = "map-control map-control--button";

            const button = document.createElement("button");
            button.setAttribute("role", "tooltip");
            button.setAttribute("aria-label", label);
            button.setAttribute("data-microtip-position", "left");
            button.innerHTML = iconSVG;

            inner.appendChild(button);
            wrapper.appendChild(inner);
            return { wrapper, button };
        }

        appendControl(container, wrapper) {
            const controls = [
                ...container.querySelectorAll(
                    '[data-position="right-top"]'
                )
            ];

            const last = controls.at(-1);
            if (!last) {
                wrapper.style.inset = "0px auto auto 0px";
            } else {
                const top =
                      parseFloat(getComputedStyle(last).inset.split(" ")[0]) +
                      last.offsetHeight +
                      5;

                wrapper.style.inset = `${top}px 0px auto auto`;
            }

            container.appendChild(wrapper);
        }

        initObserver() {
            let viewerObserver = new MutationObserver((mutations) => {
                const container = document.querySelector(".location-preview__embed .embed-controls");
                if (container && unsafeWindow.streetView) {
                    if(!document.getElementById('mm-download-wrapper')){
                        const downloadControl = this.createControl(
                            {
                                iconSVG: CONSTANS.SVG_SOURCE.DOWNLOAD,
                                label: "Download Panorama Image",
                            })
                        downloadControl.wrapper.id = 'mm-download-wrapper'

                        const btn = downloadControl.button;

                        btn.onclick = async () => {
                            try {
                                btn.innerHTML = CONSTANS.SVG_SOURCE.LOADING;
                                btn.disabled = true;

                                const pano = Editor.getCurrentLocation();
                                const heading = pano.heading || 0;
                                const pitch = pano.pitch || 0;
                                const metadata = await Editor.getMetadata(pano.panoId);
                                if(!metadata || !metadata?.tiles){
                                    Logger.warn('Panorama not found!')
                                }

                                const fileName = `${metadata?.imageDate}_${metadata?.location?.description}_${pano.panoId}`;

                                let result = null;
                                const downloadType = CONFIG.DOWNLOAD.type || DEFAULT_CONFIG.DOWNLOAD.type;

                                if (downloadType === 'Thumbnail') {
                                    result = await this.downloadThumbnail(pano.panoId, fileName, heading, pitch);
                                } else {
                                    result = await this.downloadPanorama(
                                        pano.panoId,
                                        fileName,
                                        metadata.tiles?.worldSize,
                                        CONFIG.DOWNLOAD.quality || DEFAULT_CONFIG.DOWNLOAD.quality || 5,
                                        heading,
                                        metadata?.tiles?.centerHeading,
                                        pitch,
                                        downloadType
                                    );
                                }

                                const downloadUrl = URL.createObjectURL(result.blob);
                                const a = document.createElement('a');
                                a.href = downloadUrl;
                                a.download = result.fileName;
                                document.body.appendChild(a);
                                a.click();

                                document.body.removeChild(a);
                                URL.revokeObjectURL(downloadUrl);

                                btn.innerHTML = CONSTANS.SVG_SOURCE.SUCCESS;
                                Notification.info(`[${CONSTANS.MESSAGES.DOWNLOAD_SUCCESS}] Packaging image files..`);

                                setTimeout(() => {
                                    btn.innerHTML = CONSTANS.SVG_SOURCE.DOWNLOAD;
                                    btn.disabled = false;
                                }, 1500);

                            } catch (error) {
                                btn.disabled = false;
                                btn.innerHTML = CONSTANS.SVG_SOURCE.DOWNLOAD;
                                Notification.error(`[${CONSTANS.MESSAGES.DOWNLOAD_FAILED}] ${error.message}`);
                            }
                        };

                        this.appendControl(container, downloadControl.wrapper)
                    }
                }
            });

            const targetNode = document.querySelector(".page-map-editor");
            if (targetNode) {
                viewerObserver.observe(document.body, { childList: true, subtree: true });
            }
        }
    }

    // ======================================================================================================================================
    // ============================================================= Map Feature ============================================================
    // ======================================================================================================================================

    class MapFeatureManager {
        constructor() {
            this.overlay = null;
            this.currentTaxonId = null;
            this.currentOSMTag = null;
            this.observationsArray = [];
            this.osmFeaturesArray = [];

            this.layerVisible = true;
            this.currentZoom = null;
            this.mapBoundsListener = null;
            this.lastRequestedBounds = null;
            this.currentLoadRequest = null;
            this.viewportRequestId = 0;
            this.MIN_TILE_Z = 1;
            this.MAX_TILE_Z = 10;
            this.TILE_ZOOM_OFFSET = 2;
            this.TILE_TTL_MS = 3 * 60 * 1000;
            this.MAX_TILES = 300;
            this.MAX_CONCURRENCY = 6;
            this.RENDER_BUFFER_RATIO = 0.25;
            this.MAX_RENDER_POINTS = 50000;

            this.INAT_LAYER_ID = "inat-layer";
            this.OSM_LAYER_ID = "osm-layer";

            this.currentDataSource = localStorage.getItem('dataSource') || 'inaturalist';
            this.queryOrigin = null;
            this.queryCenter = null;
        }

        initControls() {
            const search = this.createControl({
                iconSVG:CONSTANS.SVG_SOURCE.SEARCH,
                label: "Search species & OSM features"
            });
            search.button.onclick = () => this.openSearchModal();

            const toggle = this.createControl({
                iconSVG: CONSTANS.SVG_SOURCE.EYE_OPEN,
                label: "Toggle layer visibility"
            });

            const importControl = this.createControl({
                iconSVG: CONSTANS.SVG_SOURCE.IMPORT,
                label: "Import locations to map making app",
            });
            importControl.button.onclick = () => this.importPointsToMapMaking();

            const container = document.querySelector(".embed-controls");
            this.appendControl(container, search.wrapper);
            this.appendControl(container, toggle.wrapper);
            this.appendControl(container, importControl.wrapper);

            toggle.button.onclick = () => {
                this.layerVisible = !this.layerVisible;
                if (this.layerVisible) {
                    toggle.button.innerHTML = CONSTANS.SVG_SOURCE.EYE_OPEN;
                    this.currentDataSource === 'inaturalist' ? this.loadDataForViewport() : this.renderDeckLayer(this.osmFeaturesArray, 'osm');
                } else {
                    toggle.button.innerHTML = CONSTANS.SVG_SOURCE.EYE_CLOSE;
                    if(this.overlay) this.overlay.setProps({ layers: [] });
                }
            };

            this.setupMapListeners();
        }

        createControl({ iconSVG, label }) {
            const wrapper = document.createElement("div");
            wrapper.className = "embed-controls__control";
            wrapper.setAttribute("data-position", "left-top");

            const inner = document.createElement("div");
            inner.className = "map-control white map-control--button";

            const button = document.createElement("button");
            button.setAttribute("role", "tooltip");
            button.setAttribute("aria-label", label);
            button.setAttribute("data-microtip-position", "right");
            button.innerHTML = iconSVG;

            inner.appendChild(button);
            wrapper.appendChild(inner);
            return { wrapper, button };
        }

        appendControl(container, wrapper) {
            if(!container) return;
            const controls = [...container.querySelectorAll('[data-position="left-top"]')];
            const last = controls.at(-1);
            if (!last) {
                wrapper.style.inset = "0px auto auto 0px";
            } else {
                const top = parseFloat(getComputedStyle(last).inset.split(" ")[0] || 0) + last.offsetHeight + 5;
                wrapper.style.inset = `${top}px auto auto 0px`;
            }
            container.appendChild(wrapper);
        }

        openSearchModal() {
            const htmlContent = `
            <div style="display: flex; flex-direction: column; gap: 10px;">
                <label style="cursor:pointer;"><input type="radio" name="dataSourceSelect" value="inaturalist" ${this.currentDataSource === 'inaturalist' ? 'checked' : ''}> 🌿 iNaturalist (Species)</label>
                <label style="cursor:pointer;"><input type="radio" name="dataSourceSelect" value="osm" ${this.currentDataSource === 'osm' ? 'checked' : ''}> 🗺️ OpenStreetMap (Features)</label>
            </div>
        `;

            Notification.confirm("Select data source", htmlContent, () => {
                const selected = document.querySelector('input[name="dataSourceSelect"]:checked').value;
                if (!selected) return;

                this.currentDataSource = selected;
                localStorage.setItem('dataSource', this.currentDataSource);

                if (selected === 'inaturalist') {
                    this.openSpeciesSelector();
                } else {
                    this.openOSMFeatureSelector();
                }
            });
        }

        openSpeciesSelector() {
            const inputHTML = `<input id="species-search-input" type="text" placeholder="Example: Panthera tigris" style="width: 100%; padding: 8px; border: 1px solid var(--mm-border); border-radius: 4px;">`;

            Notification.confirm("Enter species name", inputHTML, async () => {
                const keyword = document.getElementById("species-search-input")?.value.trim();
                if (!keyword) return;

                const loadingId = Notification._showToast("Loading species...", "info", 0);

                try {
                    const res = await fetch(`https://api.inaturalist.org/v1/taxa?q=${encodeURIComponent(keyword)}&per_page=100`);
                    const data = await res.json();

                    Notification._clearToast(loadingId);

                    if (!data.results || data.results.length === 0) {
                        Notification.warning("No results found. Try another name.");
                        return;
                    }

                    const listHTML = data.results.map(item => `
                    <div class="inat-item" data-id="${item.id}" style="cursor:pointer; display:flex; gap:10px; padding:5px; border-bottom:1px solid #ccc;">
                        <img class="inat-thumb" src="${item.default_photo?.square_url || ''}" style="width:40px;height:40px;" />
                        <div>
                            <div class="inat-sci" style="font-weight:bold;">${item.name || ""}</div>
                            <div style="font-size:12px;">[${item.rank || "—"}] - Count: ${item.observations_count}</div>
                        </div>
                    </div>
                `).join("");

                    const modal = Notification._showModal("Select a species", `<div class="inat-list" style="max-height:300px; overflow-y:auto;">${listHTML}</div>`, [
                        { text: 'Cancel', primary: false, callback: null }
                    ]);

                    setTimeout(() => {
                        document.querySelectorAll(".inat-item").forEach(el => {
                            el.addEventListener("click", () => {
                                this.currentTaxonId = el.dataset.id;
                                this.observationsArray = [];
                                this.loadDataForViewport();
                                modal.remove();
                            });
                        });
                    }, 0);

                } catch (error) {
                    document.getElementById(`toast-${loadingId}`)?.remove();
                    Notification.error(`[Failed to search species] ${error.messgae}`);
                }
            });
        }

        openOSMFeatureSelector() {
            const contentHTML = `
            <input id="osm-search-input" style="width: 100%; padding: 8px; font-size:15px;" placeholder="Type keyword (e.g. school, tree, bench)">
            <div id="osm-results" style="max-height:300px; overflow:auto; margin-top:10px;"></div>`;

            const modal = Notification._showModal("Search OSM features", contentHTML, [
                { text: 'Close', primary: false, callback: null }
            ]);

            setTimeout(() => {
                const input = document.getElementById("osm-search-input");
                const resultsContainer = document.getElementById("osm-results");

                const renderResults = (results) => {
                    if (!results || results.length === 0) {
                        resultsContainer.innerHTML = `<div style="padding:10px;color:#888;">No results</div>`;
                        return;
                    }

                    resultsContainer.innerHTML = results.map(item => `
                    <div class="osm-item" data-key="${item.key}" data-value="${item.value}" title="${item.description || ''}" style="cursor:pointer; padding:5px; border-bottom:1px solid #ccc; display:flex; gap:10px;">
                        ${item.thumbnail ? `<img class="osm-thumb" src="${item.thumbnail}" style="width:40px;height:40px;">` : ''}
                        <div class="osm-kv">
                            <span class="osm-key" style="font-weight:bold;">${item.key}</span>
                            <span class="osm-eq">=</span>
                            <span class="osm-value" style="color:#555;">${item.value}</span>
                        </div>
                    </div>
                `).join("");

                    resultsContainer.querySelectorAll(".osm-item").forEach(el => {
                        el.addEventListener("click", () => {
                            this.currentOSMTag = { key: el.dataset.key, value: el.dataset.value };
                            CACHE.osmFeaturesById.clear();
                            this.osmFeaturesArray = [];
                            modal.remove();
                            this.loadPOIForViewport();
                        });
                    });
                };

                const throttledSearch = throttleAsync(async () => {
                    const keyword = input.value.trim();
                    if (!keyword) {
                        renderResults([]);
                        return;
                    }
                    const results = await this.searchOSMTags(keyword);
                    renderResults(results);
                }, 200);

                input.addEventListener("input", throttledSearch);
            }, 0);
        }

        importPointsToMapMaking() {
            const pointsToImport = this.currentDataSource === 'inaturalist' ? this.observationsArray : this.osmFeaturesArray;

            if (!pointsToImport || pointsToImport.length === 0) {
                Notification.warning("Data not found");
                return;
            }

            const mmd = pointsToImport.map(item => {
                let tags = [String(item.name)];
                if (item.distance !== undefined) {
                    tags.push(this.getDistanceLabel(item.distance));
                }
                return {
                    lat: item.lat, lng: item.lng,
                    heading: 0, pitch: 0, zoom: 0.1,
                    panoId: null, countryCode: null, stateCode: null,
                    extra: { tags: tags }
                };
            });

            try {
                editor.importFromString(JSON.stringify(mmd));
                Notification.success(`${mmd.length} locations have been imported`);
            } catch (err) {
                Notification.error(`[Failed to import] ${err.message}`);
            }
        }

        setupMapListeners() {
            const debouncedLoad = throttleAsync(() => this.loadDataForViewport(), 300);
            map.addListener('zoom_changed', debouncedLoad);
            map.addListener('bounds_changed', debouncedLoad);
            map.addListener('dragend', debouncedLoad);
        }

        ensureOverlay() {
            if (!this.overlay && deck.GoogleMapsOverlay) {
                this.overlay = new deck.GoogleMapsOverlay({ layers: [] });
                this.overlay.setMap(map);
            }
        }

        getTileKey(z, x, y, source = 'inat') {
            return `${source}_${z}/${x}/${y}`;
        }

        touchTile(key) {
            const entry = CACHE.tileCache.get(key);
            if (!entry) return;
            entry.lastUsed = Date.now();
            CACHE.tileCache.delete(key);
            CACHE.tileCache.set(key, entry);
            if (CACHE.size > this.MAX_TILES) {
                const oldestKey = CACHE.keys().next().value;
                CACHE.delete(oldestKey);
            }
        }

        buildQuery(tag, bbox) {
            const bboxStr = `${bbox.south},${bbox.west},${bbox.north},${bbox.east}`;

            const elements = tag.element || ["node", "way", "relation"];

            const parts = elements.map(type => {
                return `${type}["${tag.key}"="${tag.value}"](${bboxStr});`;
            }).join("\n  ");

            return `[out:json][timeout:180];
                    (
                      ${parts}
                    );
                    out center;`;
        }

        buildAroundQuery(tag, center, radiusKm) {
            const radiusMeters = radiusKm * 1000;
            const elements = tag.element || ["node", "way", "relation"];

            const parts = elements.map(type => {
                return `${type}["${tag.key}"="${tag.value}"](around:${radiusMeters},${center.lat},${center.lng});`;
            }).join("\n  ");

            return `[out:json][timeout:180];
                    (
                      ${parts}
                    );
                    out center;`;
        }

        buildPolygonQuery(tag, polygonCoords) {
            const coordString = polygonCoords.map(coord => `${coord[1]} ${coord[0]}`).join(" ");
            const elements = tag.element || ["node", "way", "relation"];

            const parts = elements.map(type => {
                return `${type}["${tag.key}"="${tag.value}"](poly:"${coordString}");`;
            }).join("\n  ");

            return `[out:json][timeout:180];
                    (
                      ${parts}
                    );
                    out center;`;
        }

        async fetchINatTile({ taxonId, bbox, signal, per_page = 200, page = 1 }) {
            const url =
                  `https://api.inaturalist.org/v1/observations?` +
                  `taxon_id=${taxonId}` +
                  `&nelat=${bbox.north}` +
                  `&nelng=${bbox.east}` +
                  `&swlat=${bbox.south}` +
                  `&swlng=${bbox.west}` +
                  `&per_page=${per_page}&page=${page}`;

            const res = await fetch(url, { signal });
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            const data = await res.json();
            return data.results.map(d => ({
                id: d.id,
                lat: d.geojson?.coordinates?.[1],
                lng: d.geojson?.coordinates?.[0],
                name: d.species_guess || "Unknown",
                photo: d.observation_photos?.[0]?.photo?.url?.replace("square", "medium"),
                observed_at: d.time_observed_at || d.observed_on || null
            })).filter(d => d.lat && d.lng);
        }

        async fetchOSMPOI({ bbox, query = null }) {
            const queryStr = query || this.buildQuery(this.currentOSMTag, bbox);

            const url = 'https://overpass-api.de/api/interpreter';

            const res = await fetch(url, {
                method: 'POST',
                body: queryStr
            });

            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            const data = await res.json();

            const features = [];

            if (data.elements) {
                for (const element of data.elements) {
                    let lat, lng, name;

                    if (element.type === 'node') {
                        lat = element.lat;
                        lng = element.lon;
                    } else if (element.center) {
                        lat = element.center.lat;
                        lng = element.center.lon;
                    } else if (element.bounds) {
                        lat = (element.bounds.minlat + element.bounds.maxlat) / 2;
                        lng = (element.bounds.minlon + element.bounds.maxlon) / 2;
                    }

                    if (!lat || !lng) continue;

                    name = element.tags?.highway || element.tags?.['name:en'] || element.tags?.name;

                    features.push({
                        id: `osm_${element.type}_${element.id}`,
                        lat,
                        lng,
                        name,
                        tags: element.tags || {}
                    });
                }
            }

            return features;
        }

        async getINatImage(name) {
            if (!name) return null;

            if (CACHE.imageCache.has(name)) return CACHE.imageCache.get(name);

            try {
                const res = await fetch(`https://api.inaturalist.org/v1/taxa?q=${encodeURIComponent(name)}&per_page=1`);
                const data = await res.json();
                const url = data.results?.[0]?.default_photo?.medium_url || null;

                CACHE.imageCache.set(name, url);
                return url;
            } catch {
                return null;
            }
        }

        renderDeckLayer(points, source) {
            if (!this.layerVisible) return;
            this.ensureOverlay();

            if (!points || points.length === 0) return;

            const fillColor = source === 'inaturalist' ? [255, 120, 0, 160] : [112, 72, 235, 160];
            const layerId = source === 'inaturalist' ? this.INAT_LAYER_ID : this.OSM_LAYER_ID;

            const newLayer = new deck.ScatterplotLayer({
                id: layerId,
                data: points,
                getPosition: d => [d.lng, d.lat],
                getRadius: source === 'inaturalist' ? 5 : 4,
                radiusUnits: 'pixels',
                getFillColor: fillColor,
                pickable: true,
                onHover: ({ object, x, y }) => {
                    if (object) this.showTooltip(object, x, y);
                    else this.hideTooltip();
                }
            });
            const otherLayers = (this.overlay.props.layers || []).filter(l => l.id !== layerId);
            this.overlay.setProps({ layers: [...otherLayers, newLayer] });
        }

        getPointsForRender(bounds) {
            const ne = bounds.getNorthEast();
            const sw = bounds.getSouthWest();

            const latMin = sw.lat();
            const latMax = ne.lat();
            const lngMin = sw.lng();
            const lngMax = ne.lng();

            const latPad = (latMax - latMin) * this.RENDER_BUFFER_RATIO;
            const lngPad = (lngMax - lngMin) * this.RENDER_BUFFER_RATIO;

            const minLat = latMin - latPad;
            const maxLat = latMax + latPad;
            const minLng = lngMin - lngPad;
            const maxLng = lngMax + lngPad;

            const filtered = this.observationsArray.filter(d => d.lat >= minLat && d.lat <= maxLat && d.lng >= minLng && d.lng <= maxLng);

            if (filtered.length > this.MAX_RENDER_POINTS) {
                const step = Math.ceil(filtered.length / this.MAX_RENDER_POINTS);
                const sampled = [];
                for (let i = 0; i < filtered.length; i += step) sampled.push(filtered[i]);
                return sampled;
            }

            return filtered;
        }

        async loadDataForViewport() {
            if (!this.currentTaxonId || !this.layerVisible) return;

            const bounds = map.getBounds();
            if (!bounds) return;
            if (this.currentLoadRequest) {
                this.currentLoadRequest.abort();
            }

            const controller = new AbortController();
            this.currentLoadRequest = controller;
            const requestId = ++this.viewportRequestId;

            const mapZoom = map.getZoom();
            const tileZoom = this.computeTileZoom(mapZoom);

            const tiles = this.getTilesForViewport(bounds, tileZoom);

            const now = Date.now();
            const tasks = [];

            for (const { x, y, z } of tiles) {
                const key = this.getTileKey(z, x, y);

                const cached = CACHE.tileCache.get(key);
                if (cached && cached.expiresAt > now) {
                    this.touchTile(key);
                    this.mergeINatTileData(cached.data);
                    continue;
                }

                if (CACHE.inflightControllers.has(key)) continue;

                tasks.push(async () => {
                    if (requestId !== this.viewportRequestId) return [];

                    const controller = new AbortController();
                    CACHE.inflightControllers.set(key, controller);

                    try {
                        const bbox = tileToBBox(x, y, z);
                        const data = await this.fetchINatTile({
                            taxonId: this.currentTaxonId,
                            bbox,
                            signal: controller.signal,
                            per_page: 200,
                            page: 1
                        });

                        if (requestId !== this.viewportRequestId) return [];

                        CACHE.tileCache.set(key, {
                            data,
                            expiresAt: Date.now() + this.TILE_TTL_MS,
                            lastUsed: Date.now()
                        });
                        this.touchTile(key);

                        this.mergeINatTileData(data);

                        return data;
                    } catch (e) {
                        if (e.name === 'AbortError') {
                            return [];
                        }
                        console.error('Tile fetch error', key, e);
                        return [];
                    } finally {
                        CACHE.inflightControllers.delete(key);
                    }
                });
            }

            if (tasks.length > 0) {
                try {
                    await runWithConcurrency(tasks, this.MAX_CONCURRENCY);
                } catch (e) {
                    console.error('Concurrency runner error', e);
                }
            }

            if (requestId !== this.viewportRequestId) return;

            const pointsToRender = this.getPointsForRender(bounds);

            this.renderDeckLayer(pointsToRender, 'inaturalist');
        }


        async loadPOIForViewport() {
            if (!this.currentOSMTag) return;

            const currentLocation = Editor.getCurrentLocation().location;
            const selectedPolygon = Editor.getSelectedPolygon();
            const bounds = map.getBounds();

            let query = null;
            let features = [];

            if (currentLocation && currentLocation.lat !== undefined && currentLocation.lng !== undefined) {

                const radiusInput = await new Promise((resolve) => {
                    const inputId = `radius-input-${Math.random().toString(36).substr(2, 5)}`;
                    const modalInstance = Notification._showModal(
                        "Enter search radius",
                        `
                    <div style="margin: 10px 0;">
                        <input type="number" id="${inputId}" value="50" min="1" max="200" step="1"
                               style="width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid var(--mm-border); border-radius: 4px; background: var(--mm-bgPrimary); color: var(--mm-text);" />
                        <div style="font-size: 12px; color: var(--mm-textSecondary); margin-top: 5px;">Radius in kilometers</div>
                    </div>
                    `,
                        [
                            {
                                text: 'Cancel',
                                primary: false,
                                callback: () => resolve(null)
                            },
                            {
                                text: 'OK',
                                primary: true,
                                callback: () => {
                                    const inputEl = document.getElementById(inputId);
                                    resolve(inputEl ? inputEl.value : "50");
                                }
                            }
                        ]
                    );

                    setTimeout(() => {
                        const inputEl = document.getElementById(inputId);
                        if (inputEl) inputEl.focus();
                    }, 50);
                });

                if (radiusInput === null) return;

                const radius = parseFloat(radiusInput);
                if (isNaN(radius) || radius <= 0) {
                    Notification.error("Invalid radius entered ❌");
                    return;
                }

                if (radius > 100) {
                    Notification.alert(
                        "⚠️ Large search area",
                        "Query range exceeds 100km. This may fail or return incomplete results."
                    );
                }

                let searchToastId = null;
                try {
                    this.queryOrigin = 'location';
                    this.queryCenter = currentLocation;
                    query = this.buildAroundQuery(this.currentOSMTag, currentLocation, radius);

                    searchToastId = Notification._showToast("Searching… Querying Overpass API around location…", 'info', 0);

                    features = await this.fetchOSMPOI({ bbox: null, query });

                    for (const feature of features) {
                        feature.distance = calculateDistance(
                            currentLocation.lat,
                            currentLocation.lng,
                            feature.lat,
                            feature.lng
                        );
                    }

                    this.mergeOSMTileData(features);
                    this.renderDeckLayer(this.osmFeaturesArray, 'osm');

                    Notification._clearToast(searchToastId);
                    Notification.success(features.length > 0 ? `${features.length} POIs loaded ✔️` : "No POIs found!");

                    return;
                } catch (err) {
                    Notification._clearToast(searchToastId);
                    Notification.error("Failed to load POIs: Overpass API request failed ❌");
                    return;
                }
            }

            if (selectedPolygon && selectedPolygon.length > 0) {
                let searchToastId = null;
                try {
                    this.queryOrigin = 'polygon';
                    query = this.buildPolygonQuery(this.currentOSMTag, selectedPolygon);

                    searchToastId = Notification._showToast("Searching… Querying Overpass API within polygon…", 'info', 0);

                    features = await this.fetchOSMPOI({ bbox: null, query });

                    this.mergeOSMTileData(features);
                    this.renderDeckLayer(this.osmFeaturesArray, 'osm');

                    Notification._clearToast(searchToastId);
                    Notification.success(features.length > 0 ? `${features.length} POIs loaded ✔️` : "No POIs found!");

                    return;
                } catch (err) {
                    Notification._clearToast(searchToastId);
                    Notification.error("Failed to load POIs: Overpass API request failed ❌");
                    return;
                }
            }

            const bbox = {
                south: bounds.getSouthWest().lat(),
                west: bounds.getSouthWest().lng(),
                north: bounds.getNorthEast().lat(),
                east: bounds.getNorthEast().lng()
            };

            const latDiff = bbox.north - bbox.south;
            const lngDiff = bbox.east - bbox.west;
            const latDistKm = latDiff * 111;
            const lngDistKm = lngDiff * 111 * Math.cos((bbox.north + bbox.south) / 2 * Math.PI / 180);

            if (latDistKm > 100 || lngDistKm > 100) {
                Notification.alert(
                    "⚠️ Map bounds too large",
                    `Current viewport (${Math.round(latDistKm)}km × ${Math.round(lngDistKm)}km) may exceed Overpass API limits. Consider zooming in or selecting a specific location.`
                );
            }

            let searchToastId = null;
            try {
                this.queryOrigin = 'bbox';
                this.queryCenter = {
                    lat: (bbox.north + bbox.south) / 2,
                    lng: (bbox.east + bbox.west) / 2
                };

                searchToastId = Notification._showToast("Searching… Querying Overpass API for current viewport…", 'info', 0);

                features = await fetchOSMPOI({ bbox });

                for (const feature of features) {
                    feature.distance = calculateDistance(
                        this.queryCenter.lat,
                        this.queryCenter.lng,
                        feature.lat,
                        feature.lng
                    );
                }

                this.mergeOSMTileData(features);
                this.renderDeckLayer(this.osmFeaturesArray, 'osm');

                Notification._clearToast(searchToastId);
                Notification.success(features.length > 0 ? `${features.length} POIs loaded ✔️` : "No POIs found!");

            } catch (err) {
                Notification._clearToast(searchToastId);
                Notification.error(`[OSM POI load failed] ${err.message}`);
            }
        }

        createTooltip() {
            this.tooltip = document.createElement("div");
            this.tooltip.style.position = "fixed";
            this.tooltip.style.pointerEvents = "none";
            this.tooltip.style.background = "white";
            this.tooltip.style.padding = "8px 12px";
            this.tooltip.style.borderRadius = "6px";
            this.tooltip.style.boxShadow = "0 2px 12px rgba(0,0,0,0.15)";
            this.tooltip.style.fontSize = "12px";
            this.tooltip.style.zIndex = 99999;
            this.tooltip.style.maxWidth = "200px";
            this.tooltip.style.wordWrap = "break-word";
            document.body.appendChild(this.tooltip);
        }

        showTooltip(obj, x, y) {
            if (!this.tooltip) this.createTooltip();

            this.tooltip.style.left = x + 15 + "px";
            this.tooltip.style.top = y + 10 + "px";

            let html = `<div style="font-weight: 600; color:#74AC00">${obj.name}</div>`;

            if (obj.observed_at) {
                const time = new Date(obj.observed_at).toLocaleString();
                html += `<div style="font-weight: 600; color:#74AC00">${time}</div>` ;
            }

            if (obj.distance !== undefined) {
                html += `<div style="font-weight: 600; color:#7048eb">${obj.distance.toFixed(1)} km</div>`;
            }

            if (obj.photo) {
                html += `<img src="${obj.photo}" width="100%" height="100%" style="margin-top: 4px; border-radius: 4px;"/>`;
            }

            if (obj.tags && Object.keys(obj.tags).length > 0) {
                html += `<div style="font-size: 11px; color: #666; margin-top: 4px;">`;
                for (const [k, v] of Object.entries(obj.tags).slice(0, 3)) {
                    html += `<div>${k}: ${v}</div>`;
                }
                html += `</div>`;
            }

            this.tooltip.innerHTML = html;
            this.tooltip.style.display = "block";
        }

        hideTooltip() {
            if (this.tooltip) this.tooltip.style.display = "none";
        }

        getDistanceLabel(distanceKm) {
            if (distanceKm <= 5) return "<5km";
            if (distanceKm <= 10) return "5-10km";
            if (distanceKm <= 50) return "10-50km";
            if (distanceKm <= 100) return "50-100km";
            return ">100km";
        }

        computeTileZoom(mapZoom) {
            return Math.max(this.MIN_TILE_Z, Math.min(this.MAX_TILE_Z, Math.floor(mapZoom) - this.TILE_ZOOM_OFFSET));
        }

        async searchOSMTags(keyword) {
            const q = keyword.trim().toLowerCase();
            if (!q || !window.OSM_MAP_FEATURES) return [];

            if (q.includes("=")) {
                const [k, v] = q.split("=").map(s => s.trim());
                return window.OSM_MAP_FEATURES
                    .filter(item => item.key === k && item.value === v)
                    .map(item => ({ ...item, score: 999, thumbnail: item.image_url }));
            }

            return window.OSM_MAP_FEATURES.map(item => {
                const key = item.key || "";
                const value = item.value || "";
                const text = `${key} ${value} ${item.description || ""}`.toLowerCase();
                let score = 0;

                if (key === q || value === q) score += 100;
                if (key.includes(q) || value.includes(q)) score += 50;
                if (text.includes(q)) score += 10;

                return { ...item, score, thumbnail: item.image_url };
            }).filter(item => item.score > 0).sort((a, b) => b.score - a.score).slice(0, 50);
        }

        getTilesForViewport(bounds, tileZoom) {
            const ne = bounds.getNorthEast();
            const sw = bounds.getSouthWest();

            const xMin = lngToTileX(sw.lng(), tileZoom);
            const xMax = lngToTileX(ne.lng(), tileZoom);
            const yMin = latToTileY(ne.lat(), tileZoom);
            const yMax = latToTileY(sw.lat(), tileZoom);

            const tiles = [];
            for (let x = xMin; x <= xMax; x++) {
                for (let y = yMin; y <= yMax; y++) {
                    tiles.push({ x, y, z: tileZoom });
                }
            }
            return tiles;
        }

        mergeINatTileData(newPoints) {
            let added = 0;
            for (const p of newPoints) {
                if (!CACHE.observationsById.has(p.id)) {
                    CACHE.observationsById.set(p.id, p);
                    added++;
                } else {
                    CACHE.observationsById.set(p.id, Object.assign(CACHE.observationsById.get(p.id), p));
                }
            }
            if (added > 0) {
                this.observationsArray = Array.from(CACHE.observationsById.values());
            }
            return added;
        }

        mergeOSMTileData(newFeatures) {
            let added = 0;
            for (const p of newFeatures) {
                if (!CACHE.osmFeaturesById.has(p.id)) {
                    CACHE.osmFeaturesById.set(p.id, p);
                    added++;
                } else {
                    CACHE.osmFeaturesById.set(p.id, Object.assign(CACHE.osmFeaturesById.get(p.id), p));
                }
            }
            if (added > 0) {
                this.osmFeaturesArray = Array.from(CACHE.osmFeaturesById.values());
            }
            return added;
        }

    }

    const mapFeatureManager = new MapFeatureManager();

    // ======================================================================================================================================
    // ============================================================== Main Panel ============================================================
    // ======================================================================================================================================

    class MainPanel {
        constructor() {
            this.isOpen = false;
            this.processor = new TagProcessor();
            this.downloader = new Downloader();
            this.selectedFeatures = [];
            this.panelElement = null;
            this._taggingProgressFrame = null;
            this._downloadProgressFrame = null;
            this._pendingTaggingProgress = null;
            this._pendingDownloadProgress = null;
            this.isDragging = false;
            this.isResizing = false;
            this._themeEditorOpen = false;
            this.dragOffset = { x: 0, y: 0 };
            this.resizeStart = { x: 0, y: 0, w: 0, h: 0 };
        }

        create() {
            const panelHTML = `
                <div id="mm-main-panel" class="hidden">
                    <div id="mm-panel-header">
                        <div class="mm-panel-header-left">
                            <img src="https://map-making.app/favicon.ico" class="mm-header-icon">
                            <span class="mm-header-title">Map-Making Helper</span>
                        </div>
                        <div class="mm-panel-header-right">
                            <button class="mm-header-btn" id="mm-close-panel" title="Close panel">${CONSTANS.SVG_SOURCE.CROSS}</button>
                        </div>
                    </div>
                    <div id="mm-panel-body">
                        <div id="mm-panel-sidebar">
                            <nav id="mm-sidebar-nav">
                                <div class="mm-nav-item mm-nav-active" data-tab="workspace">
                                    <span class="mm-nav-label">Auto Tag</span>
                                </div>
                                <div class="mm-nav-item" data-tab="download">
                                    <span class="mm-nav-label">Download</span>
                                </div>
                                <div class="mm-nav-item" data-tab="config">
                                    <span class="mm-nav-label">Config</span>
                                </div>
                                <div class="mm-nav-item" data-tab="keybinds">
                                    <span class="mm-nav-label">Keybinds</span>
                                </div>
                                <div class="mm-nav-item" data-tab="theme">
                                    <span class="mm-nav-label">Theme</span>
                                </div>
                                <div class="mm-nav-item" data-tab="logs">
                                    <span class="mm-nav-label">Logs</span>
                                </div>
                                <div class="mm-nav-item" data-tab="about">
                                    <span class="mm-nav-label">About</span>
                                </div>
                            </nav>
                        </div>
                        <div id="mm-panel-main">
                            <div class="mm-tab-panel mm-tab-active" id="mm-tab-workspace">
                                <div class="mm-tab-scroll">
                                    <div class="mm-workspace-status">
                                        <div class="mm-status-card">
                                            <span class="mm-status-label">Selections</span>
                                            <span class="mm-status-value mm-selection-count" id="mm-selection-count">0</span>
                                        </div>
                                        <div class="mm-status-card">
                                            <span class="mm-status-label">Features</span>
                                            <span class="mm-status-value" id="mm-feature-count">0</span>
                                        </div>

                                        <div class="mm-status-card mm-progress-card" id="mm-workspace-progress">
                                            <span class="mm-status-sub" id="mm-progress-detail"> 0 / 0</span>
                                            <div class="mm-progress-ring" id="mm-progress-ring">
                                                <div class="mm-progress-ring-inner">
                                                    <span id="mm-tag-percent">0%</span>
                                                </div>
                                            </div>
                                        </div>
                                        <div class="mm-status-card">
                                            <span class="mm-status-label">Failed</span>
                                            <span class="mm-status-value mm-status-error" id="mm-failed-count"> 0</span>
                                        </div>
                                    </div>

                                    <div id="mm-workspace-actions">
                                        <button class="mm-act-btn" id="mm-select-all">Select All</button>
                                        <button class="mm-act-btn" id="mm-deselect-all">Deselect</button>
                                        <button class="mm-act-btn" id="mm-invert-select">Invert</button>
                                    </div>
                                    <div id="mm-features-grid"></div>
                                </div>
                                <div class="mm-footer-bar">
                                    <button class="mm-footer-btn mm-footer-start" id="mm-start-task">Start Tagging</button>
                                    <button class="mm-footer-btn mm-footer-cancel hidden" id="mm-cancel-task">Cancel Task</button>
                                </div>
                            </div>

                            <!-- Tab: Download Workspace-->
                            <div class="mm-tab-panel" id="mm-tab-download">
                                <div class="mm-tab-scroll">
                                    <div class="mm-workspace-status">
                                        <div class="mm-status-card">
                                            <span class="mm-status-label">Selections</span>
                                            <span id="mm-download-selection-count" class="mm-status-value mm-selection-count">0</span>
                                        </div>
                                        <div class="mm-status-card">
                                            <span class="mm-status-label">Type</span>
                                            <span id="mm-download-type" class="mm-status-value">${CONFIG.DOWNLOAD.type}</span>
                                        </div>
                                        <div class="mm-status-card">
                                            <span class="mm-status-label">Quality</span>
                                            <span id="mm-download-quality" class="mm-status-value">Zoom ${CONFIG.DOWNLOAD.quality}</span>
                                        </div>
                                        <div class="mm-status-card">
                                            <span class="mm-status-label">Failed</span>
                                            <span id="mm-download-failed" class="mm-status-value mm-status-error">0</span>
                                        </div>
                                    </div>
                                    <div class="mm-download-progress-wrapper">
                                        <div id="mm-liquid-progress" class="mm-liquid-progress">
                                            <div class="wave-layer wave-back"></div>
                                            <div class="wave-layer wave-front"></div>

                                            <div class="mm-liquid-center">
                                                <span id="mm-download-percent">0%</span>
                                            </div>
                                        </div>
                                        <div id="mm-download-progress-detail" class="mm-liquid-detail">0 / 0</div>
                                    </div>
                                    <div class="mm-footer-bar" id="mm-download-footer">
                                        <button class="mm-footer-btn mm-footer-start" id="mm-start-download">Start Downloading</button>
                                        <button class="mm-footer-btn mm-footer-cancel hidden" id="mm-cancel-download">Cancel Downloading</button>
                                    </div>
                                </div>
                            </div>

                            <!-- Tab: Config Settings -->
                            <div class="mm-tab-panel" id="mm-tab-config">
                                <div class="mm-tab-scroll">
                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Layout</div>
                                        <div class="mm-layout-grid">
                                            <span></span>
                                            <span class="mm-layout-column-title">Width</span>
                                            <span class="mm-layout-column-title">Height</span>
                                            <span class="mm-config-label">Map</span>
                                            <input type="number" class="mm-config-input" id="cfg-layout-map-width" value="${CONFIG.LAYOUT.mapWidth}">
                                            <input type="number" class="mm-config-input" id="cfg-layout-map-height" value="${CONFIG.LAYOUT.mapHeight}">
                                            <span class="mm-config-label">Preview</span>
                                            <input type="number" class="mm-config-input" id="cfg-layout-preview-width" value="${CONFIG.LAYOUT.previewWidth}">
                                            <input type="number" class="mm-config-input" id="cfg-layout-preview-height" value="${CONFIG.LAYOUT.previewHeight}">
                                        </div>
                                    </div>
                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Layout</div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Enable shortcuts as default</span>
                                            <label class="mm-switch">
                                                <input type="checkbox" id="cfg-shortcuts-default"${(CONFIG.SHORTCUTS_DEFAULT || DEFAULT_CONFIG.SHORTCUTS_DEFAULT) ? 'checked' : ''}>
                                                <span class="mm-switch-track">
                                                    <span class="mm-switch-knob"></span>
                                                </span>
                                            </label>
                                        </div>
                                    </div>
                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Batch Task Locations Count</div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Default</span>
                                            <input type="number" class="mm-config-input" id="cfg-chunk-default" value="${CONFIG.CHUNK_SIZE.DEFAULT}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Exact Time Tagging</span>
                                            <input type="number" class="mm-config-input" id="cfg-chunk-time" value="${CONFIG.CHUNK_SIZE.TIME}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Download Panoramas</span>
                                            <input type="number" class="mm-config-input" id="cfg-chunk-download" value="${CONFIG.CHUNK_SIZE.DOWNLOAD}">
                                        </div>
                                    </div>
                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Export Mode</div>
                                        <div class="mm-config-row">
                                            <label class="mm-radio-label">
                                                <input type="radio" name="export-mode" value="save" id="cfg-export-save">
                                                <span  class="mm-config-label">Save to Map</span>
                                            </label>
                                        </div>
                                        <div class="mm-config-row">
                                            <label class="mm-radio-label">
                                                <input type="radio" name="export-mode" value="clipboard" id="cfg-export-clipboard">
                                                <span  class="mm-config-label">Copy JSON</span>
                                            </label>
                                        </div>
                                    </div>
                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Google Street View Search Radius</div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Default</span>
                                            <input type="number" class="mm-config-input" id="cfg-radius-default" value="${CONFIG.SEARCH_RADIUS.DEFAULT}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Fetch Exact Time</span>
                                            <input type="number" class="mm-config-input" id="cfg-radius-time" value="${CONFIG.SEARCH_RADIUS.EXACT_TIME}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Update & Fix Locations</span>
                                            <input type="number" class="mm-config-input" id="cfg-radius-update" value="${CONFIG.SEARCH_RADIUS.UPDATE}">
                                        </div>
                                    </div>

                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Download Settings</div>
                                        <div class="mm-config-row">
                                            <label for="cfg-download-type" class="mm-config-label">
                                                Image Type
                                            </label>
                                            <select id="cfg-download-type" class="mm-config-select" value=${CONFIG.DOWNLOAD.type}>
                                                <option value="Equirectangular">Equirectangular</option>
                                                <option value="Perspective">Perspective</option>
                                                <option value="Thumbnail">Thumbnail</option>
                                            </select>
                                        </div>
                                        <div class="mm-config-row">
                                            <label for="cfg-download-zoom" class="mm-config-label">
                                                Image Quality
                                            </label>
                                            <select id="cfg-download-zoom" class="mm-config-select">
                                                <option value="1" ${CONFIG.DOWNLOAD.quality === 1 ? 'selected' : ''}>Zoom 1</option>
                                                <option value="2" ${CONFIG.DOWNLOAD.quality === 2 ? 'selected' : ''}>Zoom 2</option>
                                                <option value="3" ${CONFIG.DOWNLOAD.quality === 3 ? 'selected' : ''}>Zoom 3</option>
                                                <option value="4" ${CONFIG.DOWNLOAD.quality === 4 ? 'selected' : ''}>Zoom 4</option>
                                                <option value="5" ${CONFIG.DOWNLOAD.quality === 5 ? 'selected' : ''}>Zoom 5</option>
                                            </select>
                                        </div>
                                    </div>
                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Google Street View Exact Time Accuracy (seconds)</div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Default</span>
                                            <input type="number" class="mm-config-input" id="cfg-accuracy-default" value="${CONFIG.ACCURACY.DEFAULT}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Exact Time Tagging</span>
                                            <input type="number" class="mm-config-input" id="cfg-accuracy-time" value="${CONFIG.ACCURACY.EXACT_TIME}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Weather Tagging</span>
                                            <input type="number" class="mm-config-input" id="cfg-accuracy-weather" value="${CONFIG.ACCURACY.WEATHER}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Sun Tagging</span>
                                            <input type="number" class="mm-config-input" id="cfg-accuracy-sun" value="${CONFIG.ACCURACY.SUN}">
                                        </div>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Day Tagging</span>
                                            <input type="number" class="mm-config-input" id="cfg-accuracy-day" value="${CONFIG.ACCURACY.DAY}">
                                        </div>
                                    </div>
                                    <div class="mm-config-section">
                                        <div class="mm-config-title">Elevation Tag Mode</div>
                                        <p class="mm-config-desc">Choose how elevation values are displayed</p>
                                        <div class="mm-config-row">
                                            <span class="mm-config-label">Use Ranges</span>
                                            <label class="mm-switch">
                                                <input type="checkbox" id="cfg-elevation-mode"${CONFIG.ELEVATION.useRanges ? 'checked' : ''}>
                                                <span class="mm-switch-track">
                                                    <span class="mm-switch-knob"></span>
                                                </span>
                                            </label>
                                        </div>
                                        <div id="cfg-elevation-ranges-area">
                                            <div style="display:flex;justify-content:space-between;align-items:center;margin:8px 0 4px;">
                                                <span style="font-size:13px;color:var(--mm-textDim);font-weight:500;">Elevation Ranges (min-max)</span>
                                                <button class="mm-act-btn mm-act-sm" id="cfg-elevation-add-range" style="font-size:12px;padding:2px 8px;">+ Add</button>
                                            </div>
                                            <div id="cfg-elevation-ranges-list">
                                                ${CONFIG.ELEVATION.ranges.map((r, i) => `
                                                    <div class="cfg-elevation-range-row" data-index="${i}" style="display:flex;gap:6px;align-items:center;margin-bottom:4px;">
                                                        <input type="number" class="mm-config-input" value="${r.min}" style="width:70px;font-size:13px;" data-range-min="${i}">
                                                        <span style="color:var(--mm-textDim);font-size:13px;">to</span>
                                                        <input type="number" class="mm-config-input" value="${r.max}" style="width:70px;font-size:13px;" data-range-max="${i}">
                                                        <button class="mm-act-btn mm-act-sm cfg-elevation-del-range" data-index="${i}" style="color:var(--mm-error);font-size:12px;padding:2px 6px;border-color:var(--mm-error);">×</button>
                                                    </div>
                                                `).join('')}
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <div class="mm-footer-bar">
                                    <button class="mm-footer-btn mm-footer-save" id="cfg-save-btn">Save Config</button>
                                    <button class="mm-footer-btn mm-footer-reset" id="cfg-reset-btn">Reset</button>
                                </div>
                            </div>
                            <!-- Tab: Keybind Settings -->
                            <div class="mm-tab-panel" id="mm-tab-keybinds">
                                <div class="mm-tab-scroll">
                                <div class="mm-config-section">
                                    <div class="mm-section-title">Keybinds</div>
                                    <div id="mm-keybind-list" style="display:flex;flex-direction:column;gap:6px;"></div>
                                </div>
                                </div>
                                <div class="mm-footer-bar">
                                    <button class="mm-footer-btn mm-footer-save" id="hk-save-btn">Save Keybinds</button>
                                    <button class="mm-footer-btn mm-footer-reset" id="hk-reset-btn">Reset All</button>
                                </div>
                            </div>
                            <!-- Tab: Theme Manager -->
                            <div class="mm-tab-panel" id="mm-tab-theme">
                                <div class="mm-tab-scroll">
                                <div id="mm-theme-main-view">
                                    <div class="mm-theme-header-actions">
                                        <span class="mm-section-title">Themes</span>
                                        <button class="mm-act-btn mm-act-primary" id="mm-create-theme-btn" style="padding:4px 10px;font-size:14px;">+ New</button>
                                    </div>
                                    <div id="mm-theme-list"></div>
                                </div>
                                <div id="mm-theme-editor-view" style="display:none;">
                                    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
                                        <button class="mm-act-btn" id="mm-theme-back-btn" style="padding:6px 12px;">← Back</button>
                                        <h3 id="mm-theme-editor-title" style="margin:0;font-size:18px;font-weight:600;color:var(--mm-text);flex:1;text-align:center;">Edit Theme</h3>
                                        <button class="mm-act-btn mm-act-primary" id="mm-theme-save-btn" style="padding:6px 12px;">Save</button>
                                    </div>
                                    <div id="mm-theme-editor-content" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;max-height:480px;overflow-y:auto;padding-right:4px;"></div>
                                </div>
                                </div>
                            </div>
                            <!-- Tab: Logs -->
                            <div class="mm-tab-panel" id="mm-tab-logs">
                                <div class="mm-tab-scroll">
                                <div class="mm-theme-header-actions">
                                    <span class="mm-section-title">Logs</span>
                                    <button class="mm-act-btn" id="mm-clear-logs" style="padding:4px 10px;font-size:14px;">Clear</button>
                                </div>
                                <div id="mm-logs-container"></div>
                                </div>
                            </div>
                            <!-- Tab: About -->
                            <div class="mm-tab-panel" id="mm-tab-about">
                                <div class="mm-tab-scroll">
                                    <div class="mm-about-card">
                                        <img src="https://map-making.app/favicon.ico" class="mm-about-logo">

                                        <div class="mm-about-title">GeoGuessr Map-Making Helper</div>

                                        <div class="mm-about-version">Version 1.0</div>

                                        <div class="mm-about-desc">Advanced toolkit for map-making.app</div>

                                        <div class="mm-about-desc">Auto Tagging • StreetView Batch Download • Shortcuts</div>

                                        <div class="mm-about-divider"></div>

                                        <div class="mm-about-info">
                                            <div class="mm-about-row">
                                                <span>Author</span>
                                                <span>KaKa</span>
                                            </div>
                                            <div class="mm-about-row">
                                                <span>License</span>
                                                <span>MIT</span>
                                            </div>
                                            <div class="mm-about-row">
                                                <span>Platform</span>
                                                <span>Tampermonkey</span>
                                            </div>

                                        </div>
                                        <div class="mm-about-divider"></div>
                                        <div class="mm-about-links">
                                            <a href="https://github.com/Saka1zum1" target="_blank" class="mm-about-link">
                                                ${CONSTANS.SVG_SOURCE.Github}
                                                GitHub
                                            </a>

                                            <a href="https://greasyfork.org/users/1179204" target="_blank" class="mm-about-link">${CONSTANS.SVG_SOURCE.GreasyFork}GreasyFork</a>

                                            <a href="https://discord.com/channels/867130388777926687/867130389222916158" target="_blank" class="mm-about-link">${CONSTANS.SVG_SOURCE.Discord}Discord</a>
                                        </div>
                                        <div class="mm-about-footer">
                                            Made for the GeoGuessr Mapping Community
                                        </div>
                                    </div>

                                </div>
                            </div>
                        </div>
                    </div>
                    <div id="mm-resize-handle"></div>
                </div>
            `;

            document.body.insertAdjacentHTML('beforeend', panelHTML);
            this.panelElement = document.getElementById('mm-main-panel');
            this._initPanelPosition();
            this._attachEventListeners();
            this._renderFeatures();
            this._renderThemes();
            this._renderKeybinds();
            this._renderLogs();
            this._startSelectionsPolling();
            theme.apply(theme.currentTheme);
        }

        _initPanelPosition() {
            const saved = GM_getValue('mm_panel_rect');
            if (saved) {
                try {
                    const rect = JSON.parse(saved);
                    this.panelElement.style.left = rect.left + 'px';
                    this.panelElement.style.top = rect.top + 'px';
                    this.panelElement.style.width = rect.width + 'px';
                    this.panelElement.style.height = rect.height + 'px';
                    return;
                } catch (e) { }
            }
            // Center by default
            const w = 700, h = 630;
            this.panelElement.style.left = Math.max(10, (innerWidth - w) / 2) + 'px';
            this.panelElement.style.top = Math.max(10, (innerHeight - h) / 2) + 'px';
            this.panelElement.style.width = w + 'px';
            this.panelElement.style.height = h + 'px';
        }

        _savePanelRect() {
            const rect = this.panelElement.getBoundingClientRect();
            GM_setValue('mm_panel_rect', JSON.stringify({
                left: rect.left, top: rect.top,
                width: rect.width, height: rect.height
            }));
        }

        _startSelectionsPolling() {
            setInterval(() => {
                if (!this.isOpen) return;
                try {
                    const sels = Editor.getSelections();
                    const count = sels ? sels.length : 0;
                    const els = document.querySelectorAll('.mm-selection-count');
                    for (const el of els) {
                        el.textContent = count;
                    }
                } catch (e) { }
            }, 500);
        }

        _attachEventListeners() {
            const header = document.getElementById('mm-panel-header');
            const closeBtn = document.getElementById('mm-close-panel');
            const startTaggingBtn = document.getElementById('mm-start-task');
            const cancelTaggingBtn = document.getElementById('mm-cancel-task');
            const startDownloadBtn = document.getElementById('mm-start-download');
            const cancelDownloadBtn = document.getElementById('mm-cancel-download');
            const selectAllBtn = document.getElementById('mm-select-all');
            const deselectBtn = document.getElementById('mm-deselect-all');
            const invertBtn = document.getElementById('mm-invert-select');
            const handle = document.getElementById('mm-resize-handle');

            closeBtn.addEventListener('click', () => this.close());

            // Sidebar navigation
            document.querySelectorAll('.mm-nav-item').forEach(item => {
                item.addEventListener('click', () => {
                    document.querySelectorAll('.mm-nav-item').forEach(n => n.classList.remove('mm-nav-active'));
                    item.classList.add('mm-nav-active');
                    document.querySelectorAll('.mm-tab-panel').forEach(p => p.classList.remove('mm-tab-active'));
                    const tab = document.getElementById(`mm-tab-${item.dataset.tab}`);
                    if (tab) tab.classList.add('mm-tab-active');
                    // Reset theme editor view when navigating back to theme tab
                    if (item.dataset.tab === 'theme') {
                        const mainView = document.getElementById('mm-theme-main-view');
                        const editorView = document.getElementById('mm-theme-editor-view');
                        if (mainView && editorView) {
                            mainView.style.display = 'block';
                            editorView.style.display = 'none';
                        }
                    }
                    // Refresh sub-views when navigating
                    if (item.dataset.tab === 'config') {
                        this._initElevationUI();
                    }
                    if (item.dataset.tab === 'keybinds') {
                        this._renderKeybinds();
                    }
                    if (item.dataset.tab === 'logs') {
                        this._renderLogs();
                    }
                });
            });

            // Draggable via header
            header.addEventListener('mousedown', (e) => {
                if (e.target.closest('.mm-panel-header-right') || e.target.closest('#mm-resize-handle')) return;
                this.isDragging = true;
                const rect = this.panelElement.getBoundingClientRect();
                this.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
            });
            document.addEventListener('mousemove', (e) => {
                if (this.isDragging) {
                    this.panelElement.style.left = (e.clientX - this.dragOffset.x) + 'px';
                    this.panelElement.style.top = (e.clientY - this.dragOffset.y) + 'px';
                }
                if (this.isResizing) {
                    const dw = e.clientX - this.resizeStart.x;
                    const dh = e.clientY - this.resizeStart.y;
                    const newW = Math.max(500, this.resizeStart.w + dw);
                    const newH = Math.max(360, this.resizeStart.h + dh);
                    this.panelElement.style.width = newW + 'px';
                    this.panelElement.style.height = newH + 'px';
                }
            });
            document.addEventListener('mouseup', () => {
                if (this.isDragging || this.isResizing) {
                    this.isDragging = false;
                    this.isResizing = false;
                    this._savePanelRect();
                }
            });

            // Resize handle
            handle.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                this.isResizing = true;
                const rect = this.panelElement.getBoundingClientRect();
                this.resizeStart = { x: e.clientX, y: e.clientY, w: rect.width, h: rect.height };
            });

            // Feature selection
            selectAllBtn.addEventListener('click', () => this._selectAllFeatures());
            deselectBtn.addEventListener('click', () => this._deselectAllFeatures());
            invertBtn.addEventListener('click', () => this._invertFeatures());

            startTaggingBtn.addEventListener('click', () => this._handleStartTask());
            cancelTaggingBtn.addEventListener('click', () => this._handleCancelTask());

            startDownloadBtn.addEventListener('click', () => this._handleStartDownload());
            cancelDownloadBtn.addEventListener('click', () => this._handleCancelDownload());

            // Config save & reset
            const cfgSave = document.getElementById('cfg-save-btn');
            if (cfgSave) cfgSave.addEventListener('click', () => this._saveConfig());
            const cfgReset = document.getElementById('cfg-reset-btn');
            if (cfgReset) cfgReset.addEventListener('click', () => this._resetConfig());

            // Elevation mode toggle - reads from localStorage on init, writes immediately on change
            this._initElevationUI();

            // Add elevation range
            const addRangeBtn = document.getElementById('cfg-elevation-add-range');
            if (addRangeBtn) {
                addRangeBtn.addEventListener('click', () => {
                    const list = document.getElementById('cfg-elevation-ranges-list');
                    if (!list) return;
                    const idx = list.children.length;
                    const row = document.createElement('div');
                    row.className = 'cfg-elevation-range-row';
                    row.dataset.index = idx;
                    row.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:4px;';
                    row.innerHTML = `
                        <input type="number" class="mm-config-input" value="0" style="width:70px;font-size:11px;" data-range-min="${idx}">
                        <span style="color:var(--mm-textDim);font-size:11px;">to</span>
                        <input type="number" class="mm-config-input" value="100" style="width:70px;font-size:11px;" data-range-max="${idx}">
                        <button class="mm-act-btn mm-act-sm cfg-elevation-del-range" data-index="${idx}" style="color:var(--mm-error);font-size:12px;padding:2px 6px;border-color:var(--mm-error);">×</button>
                    `;
                    row.querySelector('.cfg-elevation-del-range').addEventListener('click', (e) => {
                        row.remove();
                    });
                    list.appendChild(row);
                });
            }

            // Delete elevation range (delegation for existing rows)
            document.addEventListener('click', (e) => {
                if (e.target.classList.contains('cfg-elevation-del-range')) {
                    const row = e.target.closest('.cfg-elevation-range-row');
                    if (row) row.remove();
                }
            });

            // Keybinds save & reset
            const hkSave = document.getElementById('hk-save-btn');
            if (hkSave) {
                hkSave.addEventListener('click', () => {
                    // Validate and detect conflicts
                    const conflicts = [];
                    const used = new Map();

                    document.querySelectorAll('.mm-keybind-input').forEach(input => {
                        const action = input.dataset.action;
                        const val = input.value.trim().toUpperCase();
                        const shiftToggle = input.closest('.mm-keybind-row').querySelector('.mm-keybind-shift-toggle');
                        const requireShift = shiftToggle ? shiftToggle.checked : false;

                        if (val) {
                            const key = `${val}|${requireShift}`;
                            if (used.has(key)) {
                                conflicts.push(`"${val}" (Shift: ${requireShift}) is used by both "${action}" and "${used.get(key)}"`);
                            } else {
                                used.set(key, action);
                                shortcutManager.KEYBOARD_SHORTCUTS[action].key = val;
                                shortcutManager.KEYBOARD_SHORTCUTS[action].requireShift = requireShift;
                            }
                        }
                    });

                    if (conflicts.length > 0) {
                        Notification.warning('Conflict detected:\n' + conflicts.join('\n'));
                        return;
                    }

                    shortcutManager.saveShortcuts();
                    Notification.info('Keybinds saved');
                });
            }
            const hkReset = document.getElementById('hk-reset-btn');
            if (hkReset) {
                hkReset.addEventListener('click', () => {
                    Notification.confirm('Reset Keybinds', 'This will clear all key bindings. Continue?', () => {
                        shortcutManager.resetShortcuts();
                        this._renderKeybinds();
                        Notification.info('All keybinds reset');
                    });
                });
            }

            // Create theme
            const createThemeBtn = document.getElementById('mm-create-theme-btn');
            if (createThemeBtn) createThemeBtn.addEventListener('click', () => this._promptCreateTheme());

            // Clear logs with confirmation dialog
            const clearLogs = document.getElementById('mm-clear-logs');
            if (clearLogs){
                clearLogs.addEventListener('click', () => {
                    if (Logger.logs.length === 0) return;
                    const dialog = document.getElementById('mm-confirm-dialog');
                    if (!dialog) {
                        Notification.confirm('Clear All Logs', 'This action cannot be undone. Continue?', () => {
                            Logger.clear();
                            this._renderLogs();
                            Notification.info('All logs cleared');
                        });
                    }
                });
            }

            // Live log updates
            Logger.onLog = () => {
                if (this.isOpen) {
                    this._renderLogs();
                }
            };
        }

        _renderFeatures() {
            const grid = document.getElementById('mm-features-grid');
            grid.innerHTML = '';

            this._featureStates = {};

            CONSTANS.TAGS.forEach(tag => {
                const card = document.createElement('div');
                card.className = 'mm-feature-card';
                card.title = CONSTANS.TOOLTIPS[tag] || '';
                card.dataset.tag = tag.toLowerCase();
                card.innerHTML = `<span class="mm-feature-name">${tag}</span>`;
                card.addEventListener('click', () => {
                    card.classList.toggle('mm-feature-selected');
                    this._updateFeatureCount();
                });
                grid.appendChild(card);
            });

            this._updateFeatureCount();
        }

        _updateFeatureCount() {
            const selected = document.querySelectorAll('.mm-feature-card.mm-feature-selected').length;
            const el = document.getElementById('mm-feature-count');
            if (el) el.textContent = selected;
        }

        _renderKeybinds() {
            const list = document.getElementById('mm-keybind-list');
            if (!list) return;

            const bindings = shortcutManager.getAllBindings();
            let html = '<div class="mm-config-title" style="margin-bottom:8px;">Quick Tag (Num key)</div>';
            html += '<div class="mm-config-desc" style="margin-bottom:8px;">Click a tag while holding a number key to bind. Ctrl+Click to unbind.</div>';

            if (bindings.length === 0) {
                html += '<div class="mm-no-bindings">No shortcuts assigned.</div>';
            } else {
                bindings.forEach(({ key: num, tag }) => {
                    html += `<div class="mm-keybind-row"><span class="mm-keybind-badge">${num}</span><span class="mm-keybind-tag-name">${tag}</span><button class="mm-keybind-unbind" data-num="${num}" title="Unbind">${CONSTANS.SVG_SOURCE.CROSS}</button></div>`;
                });
            }

            const getConflictInfo = (key, requireShift, excludeAction) => {
                for (const [action, shortcut] of Object.entries(shortcutManager.KEYBOARD_SHORTCUTS)) {
                    if (action === excludeAction) continue;
                    const shortcutKey = shortcut.key;
                    const shortcutRequireShift = shortcut.requireShift;
                    if (shortcutKey.toLowerCase() === key.toLowerCase() && shortcutRequireShift === requireShift) {
                        return action;
                    }
                }
                return null;
            };

            html += '<div class="mm-config-title" style="margin:16px 0 8px;">Action Shortcuts</div>';
            CONSTANS.SHORTCUTS_DESCRIPTION.forEach(({ id, label }) => {
                const shortcut = shortcutManager.KEYBOARD_SHORTCUTS[id];
                const key = shortcut.key || '';
                const requireShift = shortcut.requireShift || false;
                html += `<div class="mm-keybind-row" style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
                    <span class="mm-keybind-tag-name" style="flex:1;min-width:140px;">${label}</span>
                    <label style="display:flex;align-items:center;gap:4px;cursor:pointer;">
                        <input type="checkbox" class="mm-keybind-shift-toggle" data-action="${id}" ${requireShift ? 'checked' : ''} style="cursor:pointer;width:16px;height:16px;">
                        <span style="font-size:14px;user-select:none;">Shift</span>
                    </label>
                    <input type="text" class="mm-config-input mm-keybind-input" data-action="${id}" value="${key}" maxlength="10" style="width:70px;font-family:monospace;text-transform:uppercase;text-align:center;">
                </div>`;
            });

            list.innerHTML = html;

            list.querySelectorAll('.mm-keybind-unbind').forEach(btn => {
                btn.addEventListener('click', () => {
                    const num = parseInt(btn.dataset.num);
                    shortcutManager.removeShortcut(num);
                    this._renderKeybinds();
                    Notification.info(`Shortcut ${num} unbound`);
                });
            });

            // Handle shift toggle
            list.querySelectorAll('.mm-keybind-shift-toggle').forEach(toggle => {
                toggle.addEventListener('change', (e) => {
                    const action = e.target.dataset.action;
                    shortcutManager.KEYBOARD_SHORTCUTS[action].requireShift = e.target.checked;
                });
            });

            // Handle keydown on action shortcut inputs to capture key presses
            list.querySelectorAll('.mm-keybind-input').forEach(input => {
                input.addEventListener('keydown', (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    const action = input.dataset.action;

                    if (e.key === 'Backspace' || e.key === 'Delete') {
                        input.value = '';
                        return;
                    }

                    let key = null;

                    if (/^Key[A-Z]$/.test(e.code)) key = e.code.slice(3);

                    if (!key) {
                        Notification.warning('Only single A-Z keys are allowed.');
                        return;
                    }

                    input.value = key;

                    const shiftToggle = input.closest('.mm-keybind-row').querySelector('.mm-keybind-shift-toggle');

                    const requireShift = shiftToggle.checked;

                    const conflictAction = getConflictInfo(key, requireShift, action);

                    if (conflictAction) {
                        const conflictDef = CONSTANS.SHORTCUTS_DESCRIPTION.find(d => d.id === conflictAction);

                        Notification.warning(`Key conflict: "${key}" (Shift: ${requireShift}) is already bound to "${conflictDef?.label ?? conflictAction}"`);
                    }
                });
            });
        }

        _selectAllFeatures() {
            document.querySelectorAll('.mm-feature-card').forEach(c => c.classList.add('mm-feature-selected'));
            this._updateFeatureCount();
        }

        _deselectAllFeatures() {
            document.querySelectorAll('.mm-feature-card').forEach(c => c.classList.remove('mm-feature-selected'));
            this._updateFeatureCount();
        }

        _invertFeatures() {
            document.querySelectorAll('.mm-feature-card').forEach(c => c.classList.toggle('mm-feature-selected'));
            this._updateFeatureCount();
        }

        _initDownloadProgress(selections){
            document.getElementById('mm-download-zip-btn')?.remove();
            document.getElementById("mm-download-percent").textContent = "0%";
            document.getElementById("mm-download-progress-detail").textContent = `0 / ${selections.length}`;
            document.getElementById("mm-download-failed").textContent = "0";
            document.getElementById("mm-liquid-progress").style.setProperty("--progress","0%");
        }

        _initTaggingdProgress(selections){
            document.getElementById('mm-tag-percent').textContent = '0%';
            document.getElementById("mm-failed-count").textContent = "0";
            document.getElementById('mm-progress-detail').textContent = `0 / ${selections.length}`;
            document.getElementById('mm-progress-ring').style.setProperty('--progress', '0%');
        }

        _toggleTaggingBtns(){
            document.getElementById('mm-start-task').classList.toggle('hidden');
            document.getElementById('mm-cancel-task').classList.toggle('hidden');
        }

        _toggleDownloadBtns(){
            document.getElementById('mm-start-download').classList.toggle('hidden');
            document.getElementById('mm-cancel-download').classList.toggle('hidden');
        }

        _toggleMainBtnSvg(state){
            const mainBtn = document.querySelector(".mm-main-btn");
            mainBtn.innerHTML = state ? CONSTANS.SVG_SOURCE.LOADING : CONSTANS.SVG_SOURCE.APP ;
            mainBtn.classList.toggle('active');
        }

        async _handleStartTask() {
            this.selectedFeatures = Array.from(document.querySelectorAll('.mm-feature-card.mm-feature-selected')).map(c => c.dataset.tag);

            if (!this.selectedFeatures || this.selectedFeatures.length === 0) {
                Notification.warning(CONSTANS.MESSAGES.NO_FEATURE);
                return;
            }

            const selections = Editor.getSelections();
            if (!selections || selections.length === 0) {
                Notification.warning(CONSTANS.MESSAGES.NO_SELECTION);
                return;
            }

            STATES.taggingTaskCancelled = false;
            this._toggleTaggingBtns();
            this._initTaggingdProgress(selections);
            this._toggleMainBtnSvg(true);

            try {
                const taggedLocs = await this.processor.processAll(
                    selections,
                    this.selectedFeatures,
                    (progress) => this._updateTaggingProgress(progress)
                );

                if (!STATES.taggingTaskCancelled) {
                    this._handleTaskComplete(taggedLocs);
                } else {
                    Notification.warning('Task Cancelled');
                }
            } catch (error) {
                Notification.error(`${CONSTANS.MESSAGES.TAGGING_FAILED}: ${error.message}`);
            }
        }

        _handleCancelTask() {
            STATES.taggingTaskCancelled = true;
            this._toggleTaggingBtns();
            this._toggleMainBtnSvg(false);
            Notification.info('Cancelling task...');
        }

        _updateTaggingProgress(progress) {
            this._pendingTaggingProgress = progress;

            if (this._taggingProgressFrame) return;

            this._taggingProgressFrame = requestAnimationFrame(() => {
                this._taggingProgressFrame = null;

                const p = this._pendingTaggingProgress;
                if (!p) return;

                const percentage = p.total_items > 0 ? Math.round((p.processed / p.total_items) * 100) : 0;

                document.getElementById('mm-progress-ring').style.setProperty('--progress',`${percentage * 3.6}deg`);
                document.getElementById('mm-tag-percent').textContent = `${percentage}%`;
                document.getElementById('mm-failed-count').textContent = progress.failed;
                document.getElementById('mm-progress-detail').textContent = `${p.processed}/${p.total_items}`;
            });
        }

        _handleTaskComplete(taggedLocs) {
            const exportMode = GM_getValue('mm_export_mode', 'save');;
            const oldLocs = Editor.getSelections();
            if (exportMode === 'save') {
                if (editor.currentLocation) {
                    editor.closeAndDeleteLocation();
                    editor.addAndOpenLocation(taggedLocs[0]);
                }
                else Editor.updateLocations(oldLocs, taggedLocs);
                Notification.success(`${CONSTANS.MESSAGES.TAGGING_SUCCESS} - (${taggedLocs.length} locations)`);
            } else {
                GM_setClipboard(JSON.stringify(taggedLocs, null, 2));
                Notification.success(`Copied to clipboard (${taggedLocs.length} locations)`);
            }
            this._toggleTaggingBtns();
            this._toggleMainBtnSvg(false);
        }

        async _handleStartDownload(){
            const selections = Editor.getSelections();
            if (!selections || selections.length === 0) {
                Notification.warning(CONSTANS.MESSAGES.NO_SELECTION);
                return;
            }

            STATES.downloadTaskCancelled = false;
            this._toggleDownloadBtns();
            this._initDownloadProgress(selections);
            this._toggleMainBtnSvg(true);

            try {
                const zipBlob = await this.downloader.downloadAll(
                    selections,
                    (progress) => this._updateDownloadProgress(progress)
                );

                if (!STATES.downloadTaskCancelled) {
                    this._handleDownloadComplete(zipBlob);
                } else {
                    Notification.warning('Download Cancelled');
                }
            } catch (error) {
                Notification.error(`[${CONSTANS.MESSAGES.DOWNLOAD_FAILED}] ${error.message}`);
            }
        }

        _handleCancelDownload() {
            STATES.downloadTaskCancelled = true;
            this._toggleDownloadBtns();
            this._toggleMainBtnSvg(false);
            Notification.info('Cancelling download...');
        }

        _updateDownloadProgress(progress) {
            this._pendingDownloadProgress = progress;

            if (this._downloadProgressFrame) return;

            this._downloadProgressFrame = requestAnimationFrame(() => {
                this._downloadProgressFrame = null;

                const p = this._pendingDownloadProgress;
                if (!p) return;

                const percentage = p.total_items > 0 ? Math.round((p.processed / p.total_items) * 100) : 0;
                document.getElementById("mm-liquid-progress").style.setProperty("--progress",`${percentage}%`);

                document.getElementById("mm-download-percent").textContent =`${percentage}%`;

                document.getElementById("mm-download-progress-detail").textContent =`${progress.processed} / ${progress.total_items}`;

                document.getElementById("mm-download-failed").textContent =progress.failed;
            })
        }

        _handleDownloadComplete(blob) {
            this._toggleDownloadBtns();
            this._toggleMainBtnSvg(false);

            const footer = document.getElementById('mm-download-footer');
            const oldBtn = document.getElementById('mm-download-zip-btn');
            if (oldBtn) oldBtn.remove();

            const url = window.URL.createObjectURL(blob);
            const zipBtn = document.createElement('a');
            zipBtn.id = 'mm-download-zip-btn';
            zipBtn.textContent = 'Save to local'
            zipBtn.className ='mm-footer-btn';
            zipBtn.href = url;
            zipBtn.download = `panorama_package_${Date.now()}.tar`;
            zipBtn.onclick = ()=>{
                zipBtn.remove();
            }
            footer.appendChild(zipBtn);
        }

        _renderThemes() {
            const list = document.getElementById('mm-theme-list');
            if (!list) return;
            list.innerHTML = '';

            Object.entries(theme.getAll()).forEach(([key, themeData]) => {
                const isCustom = !!theme.customThemes[key];
                const container = document.createElement('div');
                container.className = 'mm-theme-item';

                const info = document.createElement('div');
                info.className = 'mm-theme-item-info';

                const preview = document.createElement('div');
                preview.className = 'mm-theme-preview';
                preview.style.background = `linear-gradient(135deg, ${themeData.primary}, ${themeData.accent})`;

                const nameSpan = document.createElement('span');
                nameSpan.textContent = themeData.name;
                if (key === theme.currentTheme) {
                    nameSpan.style.color = 'var(--mm-primary)';
                    nameSpan.style.fontWeight = '600';
                }

                info.appendChild(preview);
                info.appendChild(nameSpan);

                const actions = document.createElement('div');
                actions.className = 'mm-theme-item-actions';

                const applyBtn = document.createElement('button');
                applyBtn.className = 'mm-act-btn mm-act-sm mm-apply';
                applyBtn.innerHTML = CONSTANS.SVG_SOURCE.SUCCESS;
                applyBtn.addEventListener('click', () => {
                    theme.apply(key);
                    this._renderThemes();
                });
                actions.appendChild(applyBtn);

                if (isCustom) {
                    // Custom theme: Edit button
                    const editBtn = document.createElement('button');
                    editBtn.className = 'mm-act-btn mm-act-sm mm-edit';
                    editBtn.innerHTML = CONSTANS.SVG_SOURCE.EDIT;
                    editBtn.addEventListener('click', () => {
                        this._openThemeEditor(key, { ...themeData });
                    });
                    actions.appendChild(editBtn);

                    // Custom theme: Delete button
                    const delBtn = document.createElement('button');
                    delBtn.className = 'mm-act-btn mm-act-sm mm-delete';
                    delBtn.innerHTML = CONSTANS.SVG_SOURCE.BIN;
                    delBtn.addEventListener('click', () => {
                        Notification.confirm('Delete Theme',
                                             `Are you sure you want to delete the theme "${themeData.name}"? This action cannot be undone.`,
                                             () => {
                            theme.deleteCustomTheme(key);
                            this._renderThemes();
                        });
                        Notification.info(`Theme "${themeData.name}" deleted`);
                    });
                    actions.appendChild(delBtn);
                } else {
                    // Preset theme: Duplicate button
                    const dupBtn = document.createElement('button');
                    dupBtn.className = 'mm-act-btn mm-act-sm mm-copy';
                    dupBtn.innerHTML = CONSTANS.SVG_SOURCE.COPY;
                    dupBtn.addEventListener('click', () => {
                        const newKey = 'custom_' + Date.now();
                        theme.saveCustomTheme(newKey, { ...themeData, name: themeData.name + ' (Copy)' });
                        this._renderThemes();
                        Notification.info(`Theme duplicated: "${themeData.name} (Copy)"`);
                    });
                    actions.appendChild(dupBtn);
                }

                container.appendChild(info);
                container.appendChild(actions);
                list.appendChild(container);
            });
        }

        _initElevationUI() {
            const modeCheckbox = document.getElementById('cfg-elevation-mode');
            const modeToggle = document.getElementById('cfg-elevation-toggle');
            const knob = document.getElementById('cfg-elevation-knob');
            const rangesArea = document.getElementById('cfg-elevation-ranges-area');
            if (!modeCheckbox || !modeToggle) return;

            modeCheckbox.checked = CONFIG.ELEVATION.useRanges;
            modeToggle.style.background = CONFIG.ELEVATION.useRanges ? 'var(--mm-primary)' : 'var(--mm-border)';
            if (knob) knob.style.left = CONFIG.ELEVATION.useRanges ? '20px' : '2px';
            if (rangesArea) rangesArea.style.display = CONFIG.ELEVATION.useRanges ? 'block' : 'none';

            if (CONFIG.ELEVATION.ranges && CONFIG.ELEVATION.ranges.length > 0) {

                const list = document.getElementById('cfg-elevation-ranges-list');
                if (list) {
                    list.innerHTML = CONFIG.ELEVATION.ranges.map((r, i) => `
                        <div class="cfg-elevation-range-row" data-index="${i}" style="display:flex;gap:6px;align-items:center;margin-bottom:4px;">
                            <input type="number" class="mm-config-input" value="${r.min}" style="width:70px;font-size:13px;" data-range-min="${i}">
                            <span style="color:var(--mm-textDim);font-size:13px;">to</span>
                            <input type="number" class="mm-config-input" value="${r.max}" style="width:70px;font-size:13px;" data-range-max="${i}">
                            <button class="mm-act-btn mm-act-sm cfg-elevation-del-range" data-index="${i}" style="color:var(--mm-error);font-size:10px;padding:2px 6px;border-color:var(--mm-error);">×</button>
                        </div>
                    `).join('');
                }
            }

            // Only bind change listener once
            if (!this._elevationInitialized) {
                this._elevationInitialized = true;
                modeCheckbox.addEventListener('change', () => {
                    const enabled = modeCheckbox.checked;
                    modeToggle.style.background = enabled ? 'var(--mm-primary)' : 'var(--mm-border)';
                    if (knob) knob.style.left = enabled ? '20px' : '2px';
                    if (rangesArea) rangesArea.style.display = enabled ? 'block' : 'none';
                    CONFIG.ELEVATION.useRanges = enabled;
                });
            }

            // Also refresh ranges list display on re-navigation
            if (rangesArea) {
                rangesArea.style.display = CONFIG.ELEVATION.useRanges ? 'block' : 'none';
            }
        }

        _getCurrentElevationRanges() {
            const rows = document.querySelectorAll('.cfg-elevation-range-row');
            const ranges = [];
            rows.forEach(row => {
                const minInput = row.querySelector('input[data-range-min]');
                const maxInput = row.querySelector('input[data-range-max]');
                if (minInput && maxInput) {
                    const min = parseFloat(minInput.value);
                    const max = parseFloat(maxInput.value);
                    if (!isNaN(min) && !isNaN(max)) ranges.push({ min, max });
                }
            });
            return ranges.length > 0 ? ranges : CONFIG.ELEVATION.ranges;
        }

        _renderLogs() {
            const container = document.getElementById('mm-logs-container');
            if (!container) return;
            const logs = Logger.logs;
            if (logs.length === 0) {
                container.innerHTML = '<div class="mm-log-empty">No logs yet.</div>';
                return;
            }
            container.innerHTML = '';
            const levelColors = {
                info: 'var(--mm-accent)',
                warn: 'var(--mm-warning)',
                error: 'var(--mm-error)',
                debug: 'var(--mm-textDim)',
            };
            // Render most recent first
            logs.slice().reverse().forEach((log, displayIdx) => {
                const color = levelColors[log.level] || 'var(--mm-text)';
                const card = document.createElement('div');
                card.className = 'mm-log-card';
                card.style.borderLeft = `3px solid ${color}`;

                const msgSpan = document.createElement('span');
                msgSpan.className = 'mm-log-msg';
                msgSpan.innerHTML = `<span class="mm-log-timestamp">[${log.timestamp}]</span> ${log.message}`;

                const actionsDiv = document.createElement('div');
                actionsDiv.className = 'mm-log-actions';

                const copyBtn = document.createElement('button');
                copyBtn.className = 'mm-log-btn';
                copyBtn.innerHTML = CONSTANS.SVG_SOURCE.COPY;
                copyBtn.title = 'Copy this log';
                copyBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const text = `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`;
                    if (typeof GM_setClipboard === 'function') {
                        GM_setClipboard(text).then(() => Notification.info('Log copied to clipboard'));
                    } else {
                        navigator.clipboard.writeText(text).then(() => Notification.info('Log copied to clipboard'));
                    }
                });
                actionsDiv.appendChild(copyBtn);

                const delBtn = document.createElement('button');
                delBtn.className = 'mm-log-btn';
                delBtn.innerHTML = CONSTANS.SVG_SOURCE.BIN;
                delBtn.title = 'Delete this log';
                delBtn.style.color = 'var(--mm-error)';
                delBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const originalIdx = logs.length - 1 - displayIdx;
                    Logger._logs.splice(originalIdx, 1);
                    this._renderLogs();
                });
                actionsDiv.appendChild(delBtn);

                card.appendChild(msgSpan);
                card.appendChild(actionsDiv);
                container.appendChild(card);
            });
        }

        _saveConfig() {
            try {
                CONFIG.CHUNK_SIZE.DEFAULT = getVal('cfg-chunk-default');
                CONFIG.CHUNK_SIZE.TIME = getVal('cfg-chunk-time');
                CONFIG.CHUNK_SIZE.DOWNLOAD = getVal('cfg-chunk-download');

                CONFIG.DOWNLOAD.type = getVal('cfg-download-type');
                CONFIG.DOWNLOAD.quality = getVal('cfg-download-zoom');

                CONFIG.LAYOUT.mapWidth = getVal('cfg-layout-map-width');
                CONFIG.LAYOUT.mapHeight = getVal('cfg-layout-map-height');
                CONFIG.LAYOUT.previewWidth = getVal('cfg-layout-preview-width');
                CONFIG.LAYOUT.previewHeight = getVal('cfg-layout-preview-height');

                CONFIG.SEARCH_RADIUS.DEFAULT = getVal('cfg-radius-default');
                CONFIG.SEARCH_RADIUS.EXACT_TIME = getVal('cfg-radius-time');
                CONFIG.SEARCH_RADIUS.UPDATE = getVal('cfg-radius-update');

                CONFIG.ACCURACY.DEFAULT = getVal('cfg-accuracy-default');
                CONFIG.ACCURACY.EXACT_TIME = getVal('cfg-accuracy-time');
                CONFIG.ACCURACY.WEATHER = getVal('cfg-accuracy-weather');
                CONFIG.ACCURACY.DAY = getVal('cfg-accuracy-day');
                CONFIG.ACCURACY.SUN = getVal('cfg-accuracy-sun');

                const modeCheckbox = document.getElementById('cfg-elevation-mode');
                if (modeCheckbox) {
                    CONFIG.ELEVATION.useRanges = modeCheckbox.checked;
                    CONFIG.ELEVATION.ranges = this._getCurrentElevationRanges();
                }

                const shortcutsCheckbox = document.getElementById('cfg-shortcuts-default')
                if (shortcutsCheckbox) CONFIG.SHORTCUTS_DEFAULT = shortcutsCheckbox.checked;

                document.getElementsByName('export-mode').forEach(r => {
                    const value = document.querySelector('input[name="export-mode"]:checked').value;
                    CONFIG.exportMode = value
                });
                document.getElementById('mm-download-type').textContent = getVal('cfg-download-type');
                document.getElementById('mm-download-quality').textContent = `Zoom ${getVal('cfg-download-zoom')}`;

                GM_setValue('mm_config', JSON.stringify(CONFIG));

                Notification.info('Configuration saved');
            } catch (e) {
                Notification.error('Failed to save config: ' + e.message);
            }
        }

        _resetConfig() {
            Notification.confirm('Reset Config', 'Reset all settings to default values?', () => {
                CONFIG = DEFAULT_CONFIG;
                GM_setValue('mm_config', JSON.stringify(DEFAULT_CONFIG));
                setVal('cfg-chunk-default', 1200);
                setVal('cfg-chunk-time', 50);
                setVal('cfg-chunk-download', 20);
                setVal('cfg-download-zoom', '5');
                setVal('cfg-download-type', 'Equirectangular');
                setVal('cfg-radius-default', 50);
                setVal('cfg-radius-time', 30);
                setVal('cfg-radius-update', 15);
                setVal('cfg-accuracy-default', 60);
                setVal('cfg-accuracy-time', 60);
                setVal('cfg-accuracy-weather', 600);
                setVal('cfg-accuracy-day', 18000);
                setVal('cfg-accuracy-sun', 300);

                document.getElementById('cfg-export-save').checked = true;
                document.getElementById('cfg-export-clipboard').checked = false;
                document.getElementById('cfg-shortcuts-default').checked = false;

                setVal('cfg-layout-map-width', DEFAULT_CONFIG.LAYOUT.mapWidth);
                setVal('cfg-layout-map-height', DEFAULT_CONFIG.LAYOUT.mapHeight);
                setVal('cfg-layout-preview-width', DEFAULT_CONFIG.LAYOUT.previewWidth);
                setVal('cfg-layout-preview-height', DEFAULT_CONFIG.LAYOUT.previewHeight);

                document.getElementById('mm-download-type').textContent = getVal('cfg-download-type');
                document.getElementById('mm-download-quality').textContent = `Zoom ${getVal('cfg-download-zoom')}`;
                this._initElevationUI();

                Notification.info('Configuration reset to defaults');
            });
        }

        _openThemeEditor(themeKey, themeData) {
            const isCustom = !!theme.customThemes[themeKey];
            const fields = [
                { key: 'name', label: 'Theme Name', type: 'text' },
                { key: 'bg', label: 'Background', type: 'color' },
                { key: 'bgSecondary', label: 'Secondary BG', type: 'color' },
                { key: 'text', label: 'Text Color', type: 'color' },
                { key: 'textDim', label: 'Dim Text', type: 'color' },
                { key: 'border', label: 'Border', type: 'color' },
                { key: 'primary', label: 'Primary', type: 'color' },
                { key: 'primaryHover', label: 'Primary Hover', type: 'color' },
                { key: 'accent', label: 'Accent', type: 'color' },
                { key: 'success', label: 'Success', type: 'color' },
                { key: 'error', label: 'Error', type: 'color' },
                { key: 'warning', label: 'Warning', type: 'color' },
                { key: 'fontSize', label: 'Font Size', type: 'text' },
            ];

            // Hide main view, show editor view
            const mainView = document.getElementById('mm-theme-main-view');
            const editorView = document.getElementById('mm-theme-editor-view');
            const contentDiv = document.getElementById('mm-theme-editor-content');
            const titleEl = document.getElementById('mm-theme-editor-title');

            titleEl.textContent = `Edit Theme: ${themeData.name}`;
            contentDiv.innerHTML = '';

            // Create input fields
            fields.forEach(f => {
                const val = themeData[f.key] || '';
                const wrapper = document.createElement('div');
                wrapper.style.cssText = 'display:flex;flex-direction:column;gap:4px;padding:8px;background:var(--mm-bg);border-radius:6px;border:1px solid var(--mm-border);';

                const label = document.createElement('label');
                label.style.cssText = 'font-size:16px;color:var(--mm-textDim);font-weight:500;';
                label.textContent = f.label;
                wrapper.appendChild(label);

                if (f.type === 'color') {
                    const input = document.createElement('input');
                    input.type = 'color';
                    input.id = `te-${f.key}`;
                    input.value = val;
                    input.style.cssText = 'height:32px;border:1px solid var(--mm-border);border-radius:4px;background:var(--mm-bg);cursor:pointer;';
                    wrapper.appendChild(input);
                } else {
                    const input = document.createElement('input');
                    input.type = 'text';
                    input.id = `te-${f.key}`;
                    input.value = val;
                    input.style.cssText = 'padding:6px 8px;background:var(--mm-bg);color:var(--mm-text);border:1px solid var(--mm-border);border-radius:4px;font-size:14px;';
                    if (f.key === 'name') wrapper.style.gridColumn = '1 / -1';
                    wrapper.appendChild(input);
                }

                contentDiv.appendChild(wrapper);
            });

            mainView.style.display = 'none';
            editorView.style.display = 'block';

            // Back button
            document.getElementById('mm-theme-back-btn').onclick = () => {
                mainView.style.display = 'block';
                editorView.style.display = 'none';
            };

            // Save button
            document.getElementById('mm-theme-save-btn').onclick = () => {
                const newData = {};
                fields.forEach(f => {
                    const el = document.getElementById(`te-${f.key}`);
                    if (el) newData[f.key] = el.value;
                });

                if (!newData.name) {
                    Notification.warning('Theme name is required');
                    return;
                }

                if (isCustom) {
                    // Update existing custom theme
                    theme.customThemes[themeKey] = newData;
                    theme._saveCustomThemes();
                    // If this is the current theme, re-apply it
                    if (themeKey === theme.currentTheme) {
                        theme.apply(themeKey);
                    }
                } else {
                    // Cannot edit preset themes - should not reach here
                    Notification.error('Cannot edit preset themes');
                    return;
                }

                this._renderThemes();
                mainView.style.display = 'block';
                editorView.style.display = 'none';
                Notification.info(`Theme "${newData.name}" saved`);
            };
        }

        _promptCreateTheme() {
            const name = prompt('Enter a name for the new theme:');
            if (!name) return;
            const key = name.toLowerCase().replace(/[^a-z0-9]/g, '_');
            if (theme.getAll()[key]) {
                Notification.warning('A theme with this key already exists');
                return;
            }
            theme.saveCustomTheme(key, {
                name: name,
                bg: '#1a1a1a',
                bgSecondary: '#242424',
                text: '#ffffff',
                textDim: '#aaaaaa',
                border: '#333333',
                primary: '#74b816',
                primaryHover: '#8bc34a',
                accent: '#4a90e2',
                success: '#4caf50',
                error: '#f44336',
                warning: '#ff9800',
                fontSize: '14px',
            });
            this._renderThemes();
            Notification.info(`Theme "${name}" created`);
        }

        open() {
            const fs = document.fullscreenElement;
            if (fs) fs.appendChild(this.panelElement);
            else document.body.appendChild(this.panelElement)
            const selections = Editor.getSelections();
            const countEl = document.getElementById('mm-selection-count');
            if (countEl) countEl.textContent = selections ? selections.length : 0;
            this.panelElement.classList.toggle('hidden');
            this.isOpen = true;
            document.getElementById('cfg-export-save').checked = GM_getValue('mm_export_mode', 'save') === 'save';
            document.getElementById('cfg-export-clipboard').checked = GM_getValue('mm_export_mode', 'save') === 'clipboard';
            this._renderLogs();
        }

        close() {
            if (this.processor.taggedLocs.length > 0) {
                STATES.taggingTaskCancelled = true;
            }
            this.panelElement.classList.toggle('hidden');
            this.isOpen = false;
        }

        toggle() {
            if (this.isOpen) {
                this.close();
            } else {
                this.open();
            }
        }
    }

    let mainPanel = new MainPanel();

    // ======================================================================================================================================
    // ================================================================ UI ==================================================================
    // ======================================================================================================================================

    function createMainBtn(label, svg){
        const container = document.querySelector('header')
        const button = document.createElement("button");
        button.className = "icon-button mm-main-btn";
        button.type = "button";
        button.setAttribute("role", "tooltip");
        button.setAttribute("aria-label", label);
        button.setAttribute("aria-expanded", "false");
        button.setAttribute("data-microtip-position", "bottom");
        button.innerHTML = svg;
        container.appendChild(button);
        return button;
    }

    function initUI() {
        const panelBtn = createMainBtn('Toggle Main Panel', CONSTANS.SVG_SOURCE.APP)
        panelBtn.onclick = toggleMainPanel;

        const scBtn = createMainBtn('Enable Shortcuts',CONSTANS.SVG_SOURCE.KEYBOARD);
        scBtn.onclick = () => {
            shortcutManager.isApplied = !shortcutManager.isApplied;
            scBtn.classList.toggle('active')
            scBtn.setAttribute("aria-label", shortcutManager.isApplied ? 'Disable Shortcuts' : 'Enable Shortcuts');
        };
        if(CONFIG.SHORTCUTS_DEFAULT || DEFAULT_CONFIG.SHORTCUTS_DEFAULT) scBtn.click();
    }

    GM_addStyle(`
        :root {
            --mm-bg: #1a1a1a;
            --mm-bgSecondary: #242424;
            --mm-text: #ffffff;
            --mm-textDim: #b0b0b0;
            --mm-border: #333333;
            --mm-primary: #74b816;
            --mm-primaryHover: #8bc34a;
            --mm-accent: #4a90e2;
            --mm-success: #4caf50;
            --mm-error: #f44336;
            --mm-warning: #ff9800;
            --mm-fontSize: 14px;
            --mm-sidebar-w: 120px;
            --mm-header-h: 44px;
            --progress: 5%;
        }

        @keyframes mm-slideIn {
            from { opacity: 0; transform: translateX(20px); }
            to { opacity: 1; transform: translateX(0); }
        }
        @keyframes mm-slideOut {
            from { opacity: 1; transform: translateX(0); }
            to { opacity: 0; transform: translateX(20px); }
        }
        @keyframes mm-fadeIn {
            from { opacity: 0; transform: scale(0.95); }
            to { opacity: 1; transform: scale(1); }
        }

        .mm-main-btn.active {
          color: var(--mm-primaryHover);
        }
        .mm-main-btn.disabled {
          color: #7e8282;
        }

        /* === Panel Container - Lunar Inspired === */
        #mm-main-panel {
            position: fixed;
            z-index: 10001;
            border: 1px solid var(--mm-border);
            border-radius: 10px;
            box-shadow: 0 8px 40px rgba(0,0,0,0.5);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
            font-size: var(--mm-fontSize);
            color: var(--mm-text);
            user-select: none;
            overflow: hidden;
            background: var(--mm-bg);
            display: flex;
            flex-direction: column;
            min-width: 500px;
            min-height: 360px;
            animation: mm-fadeIn 0.2s ease;
        }
        #mm-main-panel.hidden { display: none; }

        /* === Header === */
        #mm-panel-header {
            height: var(--mm-header-h);
            padding: 0 12px;
            border-bottom: 1px solid var(--mm-border);
            display: flex;
            align-items: center;
            justify-content: space-between;
            cursor: move;
            background: var(--mm-bgSecondary);
            flex-shrink: 0;
        }
        .mm-panel-header-left {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .mm-header-icon {
            width: 22px; height: 22px;
            border-radius: 4px;
        }
        .mm-header-title {
            font-size: 20px;
            font-weight: 600;
            color: var(--mm-text);
        }
        .mm-panel-header-right {
            display: flex;
            gap: 4px;
        }
        .mm-header-btn {
            background: transparent;
            border: none;
            color: var(--mm-textDim);
            cursor: pointer;
            font-size: 20px;
            padding: 0 8px 8px 8px;
            border-radius: 4px;
            transition: background 0.15s, color 0.15s;
            line-height: 2rem;
            text-align: center;
        }
        .mm-header-btn:hover {
            background: #b02525;
            color: var(--mm-text);
        }

        /* === Body: sidebar + main === */
        #mm-panel-body {
            display: flex;
            flex: 1;
            overflow: hidden;
        }

        /* === Sidebar === */
        #mm-panel-sidebar {
            width: var(--mm-sidebar-w);
            background: var(--mm-bgSecondary);
            border-right: 1px solid var(--mm-border);
            flex-shrink: 0;
            overflow-y: auto;
            padding: 8px 0;
        }
        .mm-nav-item {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 10px 14px;
            cursor: pointer;
            font-size: 15px;
            color: var(--mm-textDim);
            transition: background 0.15s, color 0.15s;
            border-left: 3px solid transparent;
        }
        .mm-nav-item:hover {
            background: var(--mm-bg);
            color: var(--mm-text);
        }
        .mm-nav-item.mm-nav-active {
            color: var(--mm-primary);
            background: var(--mm-bg);
            border-left-color: var(--mm-primary);
            font-weight: 600;
        }
        .mm-nav-label {
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        /* === Main Content Area === */
        #mm-panel-main {
            flex: 1;
            padding: 12px;
            display: flex;
            flex-direction: column;
            gap: 0;
            overflow: hidden;
            min-height: 0;
        }
        .mm-tab-panel {
            display: none;
            flex-direction: column;
            gap: 0;
            flex: 1;
            min-height: 0;
        }
        .mm-tab-panel.mm-tab-active {
            display: flex;
        }
        .mm-tab-scroll {
            flex: 1;
            overflow-y: auto;
            overflow-x: hidden;
            display: flex;
            flex-direction: column;
            gap: 10px;
            padding-right: 4px;
            min-height: 0;
        }

        /* === Workspace Status Bar === */
        .mm-workspace-status {
            display: grid;
            grid-template-columns:repeat(4, minmax(120px, 1fr));
            gap: 8px;
            margin-top: 12px;
        }
        .mm-status-card {
            position: relative;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 6px;
            padding: 8px 4px;
            border-radius: 12px;
            background: linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.015));
            border: 1px solid rgba(255,255,255,.08);
            backdrop-filter: blur(10px);
            overflow: hidden;
        }
        .mm-status-card::before {
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 2px;
            background:linear-gradient(90deg,transparent,var(--mm-primary),transparent);
            opacity: .8;
        }
        .mm-status-label {
            font-size: 11px;
            text-transform: uppercase;
            letter-spacing: 1px;
            color: var(--mm-textDim);
            font-weight: 600;
        }
        .mm-status-value {
            font-size: 32px;
            font-weight: 800;
            color: var(--mm-primary);
            line-height: 1;
        }
        .mm-status-sub {
            font-size: 12px;
            color: var(--mm-textDim);
            text-align: center;
            min-height: 14px;
            flex:1;
        }
        .mm-status-error {
            color: var(--mm-error);
        }
        .mm-progress-ring {
            --progress: 0deg;
            width: 60px;
            height: 60px;
            border-radius: 50%;
            position: relative;
            background: conic-gradient(from -90deg,var(--mm-primary) var(--progress),rgba(255,255,255,.08) 0);
            animation: mmRingPulse 2s ease infinite;
        }
        .mm-progress-ring::before {
            content: "";
            position: absolute;
            inset: -4px;
            border-radius: 50%;
            background:radial-gradient(circle,rgba(116,184,22,.22),transparent 75%);
            z-index: 0;
        }
        .mm-progress-ring-inner {
            position: absolute;
            inset: 7px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            background: var(--mm-bgSecondary);
            border: 1px solid rgba(255,255,255,.08);
            z-index: 1;
        }
        #mm-tag-percent {
            font-size: 13px;
            font-weight: 700;
            color: white;
        }
        @keyframes mmRingPulse {
            0% {
                transform: scale(1);
            }
            50% {
                transform: scale(1.04);
            }
            100% {
                transform: scale(1);
            }
        }
        @media (max-width: 700px) {
            .mm-workspace-status {
                grid-template-columns: repeat(2, 1fr);
            }
        }
        .mm-download-progress-wrapper {
            margin-top: 10px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 16px;
            padding: 12px 0 18px;
        }
        .mm-liquid-progress {
            position: relative;
            width:320px;
            height: 320px;
            border-radius: 50%;
            overflow: hidden;
            background: #161b22;
            border: 4px solid rgba(255, 255, 255, 0.1);
            box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5), 0 8px 24px rgba(0,0,0,0.3);
        }
        .mm-liquid-detail {
            font-size: 16px;
            font-weight: 500;
        }
        .wave-layer {
            position: absolute;
            left: -50%;
            top: -50%;
            width: 200%;
            height: 200%;
            border-radius: 43%;
            top: calc(107% - var(--progress));
            transition: top 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        }

        .wave-back {
            background: #2e78ff;
            opacity: 0.4;
            animation: rotateWave 10s linear infinite;
            z-index: 1;
        }

        .wave-front {
            background: linear-gradient(180deg, #58a6ff, #2e78ff);
            opacity: 0.9;
            animation: rotateWave 6s linear infinite;
            z-index: 2;
        }

        .mm-liquid-center {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 10;
        }

        #mm-download-percent {
            font-size: 28px;
            font-weight: 800;
            color: white;
            text-shadow: 0 2px 8px rgba(0,0,0,0.5);
            mix-blend-mode: difference;
        }

        @keyframes rotateWave {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }

        /* === Action Buttons === */
        #mm-workspace-actions {
            display: flex;
            gap: 8px;
        }
        .mm-act-btn {
            padding: 7px 14px;
            background: transparent;
            color: var(--mm-text);
            border: 1px solid var(--mm-border);
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: all 0.15s;
        }
        .mm-act-btn:hover {
            background: var(--mm-bgSecondary);
            border-color: var(--mm-textDim);
        }
        .mm-act-primary {
            background: var(--mm-primary);
            color: white;
            border-color: var(--mm-primary);
        }
        .mm-act-primary:hover {
            background: var(--mm-primaryHover);
            border-color: var(--mm-primaryHover);
        }
        .mm-act-sm {
            padding: 4px 10px;
            font-size: 13px;
        }
        .mm-apply:hover, .mm-edit:hover,
        .mm-copy:hover {
            color: var(--mm-primary);
        }
        .mm-delete:hover {
            color: var(--mm-error);
        }

        /* === Features Grid (Click Cards) === */
        #mm-features-grid {
            display: grid;
            grid-template-columns: 1fr 1fr 1fr;
            gap: 8px;
            max-height: 330px;
            overflow-y: auto;
            padding: 4px 2px;
        }
        .mm-feature-card {
            padding: 10px 8px;
            background: var(--mm-bgSecondary);
            border: 1.5px solid var(--mm-border);
            border-radius: 6px;
            cursor: pointer;
            text-align: center;
            font-size: 16px;
            font-weight: 500;
            transition: all 0.15s;
            user-select: none;
            color: var(--mm-text);
        }
        .mm-feature-card:hover {
            border-color: var(--mm-accent);
            background: rgba(74,144,226,0.12);
            color: var(--mm-accent);
        }
        .mm-feature-card.mm-feature-selected {
            background: var(--mm-primary);
            color: white;
            border-color: var(--mm-primary);
            box-shadow: 0 3px 12px rgba(116,184,22,0.4);
            font-weight: 600;
        }
        .mm-feature-name { pointer-events: none; }

        /* === Export Row === */
        .mm-export-row {
            display: flex;
            gap: 16px;
            align-items: center;
            padding: 4px 0;
        }
        .mm-radio-label {
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 13px;
            cursor: pointer;
            color: var(--mm-text);
        }
        .mm-radio-label input { accent-color: var(--mm-primary); }

        /* === Config Section === */
        .mm-section-title {
            font-size: 24px;
            font-weight: 600;
            margin-bottom: 10px;
            color: var(--mm-text);
        }
        .mm-config-section {
            background: var(--mm-bgSecondary);
            border-radius: 8px;
            padding: 12px 14px;
        }
        .mm-config-title {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 10px;
            color: var(--mm-text);
        }
        .mm-config-desc {
            font-size: 14px;
            color: var(--mm-textDim);
            margin: 0 0 10px 0;
        }
        .mm-config-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 6px 0;
        }
        .mm-config-label {
            font-size: 15px;
            color: var(--mm-textDim);
        }
        .mm-config-input {
            width: 80px;
            padding: 4px 8px;
            background: var(--mm-bg);
            color: var(--mm-text);
            border: 1px solid var(--mm-border);
            border-radius: 4px;
            font-size: 13px;
            text-align: right;
        }
        .mm-config-select {
            min-width: 180px;
            padding: 6px 10px;
            border: 1px solid var(--mm-border);
            border-radius: 8px;
            background: var(--mm-bg);
            color: var(--mm-text);
            font-size: 13px;
        }
        .mm-config-input:focus {
            outline: none;
            border-color: var(--mm-primary);
        }
        .mm-layout-grid {
            display: grid;
            grid-template-columns: 70px 1fr 1fr;
            gap: 6px 8px;
            align-items: center;
            padding: 6px 0;
            margin-bottom: 10px;
        }

        .mm-layout-column-title {
            text-align: center;
            font-size: 14px;
            color: var(--mm-textSecondary);
            font-weight: 600;
        }

        .mm-layout-grid .mm-config-label {
            width: auto;
        }

        .mm-layout-grid .mm-config-input {
            width: 100%;
            font-size: 16px;
            min-width: 0;
        }
        /* === Theme Manager === */
        .mm-theme-header-actions {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
        }
        .mm-theme-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px 12px;
            background: var(--mm-bgSecondary);
            border: 1px solid var(--mm-border);
            border-radius: 8px;
            margin-bottom: 8px;
            transition: all 0.15s;
        }
        .mm-theme-item:hover {
            background: rgba(116, 184, 22, 0.05);
            border-color: var(--mm-primary);
        }
        .mm-theme-item-info {
            display: flex;
            align-items: center;
            gap: 12px;
            flex: 1;
        }
        .mm-theme-preview {
            width: 32px;
            height: 32px;
            border-radius: 6px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            flex-shrink: 0;
            border: 1px solid var(--mm-border);
        }
        .mm-theme-item-info span {
            font-size: 15px;
            font-weight: 500;
            color: var(--mm-text);
        }
        .mm-theme-item-actions {
            display: flex;
            gap: 6px;
        }

        /* === About === */
        .mm-about-card{
            background:
                linear-gradient(
                    180deg,
                    rgba(255,255,255,.03),
                    rgba(255,255,255,.01)
                );
            border:1px solid rgba(255,255,255,.08);
            border-radius:16px;
            padding:24px;
            text-align:center;
            backdrop-filter:blur(12px);
        }
        .mm-about-logo{
            font-size:60px;
            margin-bottom:10px;
        }
        .mm-about-title{
            font-size:24px;
            font-weight:800;
            color:var(--mm-text);
        }
        .mm-about-version{
            display:inline-block;
            margin-top:8px;
            padding:4px 12px;
            border-radius:999px;
            background:rgba(var(--mm-primary-rgb),.15);
            color:var(--mm-primary);
            font-size:16px;
            font-weight:700;
        }
        .mm-about-desc{
            font-size:18px;
            color:var(--mm-textDim);
            margin-top:8px;
        }
        .mm-about-divider{
            height:1px;
            background:rgba(255,255,255,.08);
            margin:18px 0;
        }
        .mm-about-info{
            display:flex;
            flex-direction:column;
            gap:10px;
        }
        .mm-about-row{
            display:flex;
            justify-content:space-between;
            font-size:16px;
        }
        .mm-about-row span:first-child{
            color:var(--mm-textDim);
        }
        .mm-about-row span:last-child{
            font-weight:600;
        }
        .mm-about-links{
            display:flex;
            gap:10px;
            justify-content:center;
            flex-wrap:wrap;
        }
        .mm-about-link{
            display:flex;
            align-items:center;
            gap:8px;
            padding:10px 14px;
            border-radius:12px;
            text-decoration:none;
            color:var(--mm-text);
            background:rgba(255,255,255,.04);
            border:1px solid rgba(255,255,255,.08);
            transition:.2s;
        }
        .mm-about-link:hover{
            transform:translateY(-2px);
            background:rgba(255,255,255,.08);
        }
        .mm-about-link svg{
            width:18px;
            height:18px;
            fill:currentColor;
        }
        .mm-about-footer{
            margin-top:18px;
            font-size:14px;
            color:var(--mm-textDim);
        }

        /* === Logs === */
        .mm-log-empty {
            padding: 20px;
            text-align: center;
            color: var(--mm-textDim);
            font-size: 13px;
        }

        /* === Resize Handle === */
        #mm-resize-handle {
            position: absolute;
            right: 0;
            bottom: 0;
            width: 16px;
            height: 16px;
            cursor: nwse-resize;
            background: linear-gradient(135deg, transparent 50%, var(--mm-border) 50%);
            border-radius: 0 0 10px 0;
        }

        /* === Keybind Rows === */
        .mm-keybind-row {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 10px;
            background: var(--mm-bgSecondary);
            border: 1px solid var(--mm-border);
            border-radius: 6px;
            margin-bottom: 6px;
        }
        .mm-keybind-badge {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 28px;
            height: 28px;
            background: var(--mm-primary);
            color: white;
            border-radius: 50%;
            font-weight: 700;
            font-size: 14px;
            flex-shrink: 0;
        }
        .mm-keybind-tag-name {
            flex: 1;
            font-size: 16px;
            color: var(--mm-text);
            font-weight: 500;
        }
        .mm-keybind-unbind {
            background: transparent;
            border: 1px solid var(--mm-error);
            color: var(--mm-error);
            border-radius: 4px;
            cursor: pointer;
            padding: 2px 8px;
            font-size: 12px;
            transition: 0.15s;
            display: flex;
            align-items: center;
        }
        .mm-keybind-unbind:hover {
            background: var(--mm-error);
            color: white;
        }
        .mm-no-bindings {
            padding: 16px;
            text-align: center;
            color: var(--mm-textDim);
            font-size: 13px;
        }
        .mm-keybind-input {
            background: var(--mm-bg);
            color: var(--mm-text);
            border: 1px solid var(--mm-border);
            border-radius: 4px;
            padding: 4px 6px;
            font-size: 14px;
        }
        .mm-keybind-input:focus {
            outline: none;
            border-color: var(--mm-primary);
        }
        @keyframes mm-spin {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
        .mm-spin {
            animation: mm-spin 1s linear infinite;
        }

        /* === Log Cards === */
        .mm-log-card {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 10px 12px;
            background: var(--mm-bgSecondary);
            border: 1px solid var(--mm-border);
            border-radius: 6px;
            margin-bottom: 6px;
            font-size: var(--mm-fontSize);
        }
        .mm-log-msg {
            flex: 1;
            word-break: break-all;
            line-height: 1.4;
            color: var(--mm-text);
        }
        .mm-log-timestamp {
            color: var(--mm-textDim);
            font-size: 12px;
        }
        .mm-log-actions {
            display: flex;
            gap: 4px;
            flex-shrink: 0;
        }
        .mm-log-btn {
            background: transparent;
            border: none;
            color: #fff;
            cursor: pointer;
            padding: 2px 4px;
            border-radius: 3px;
            line-height: 1;
            opacity: 0.5;
            transition: opacity 0.15s;
            display: flex;
            align-items: center;
        }
        .mm-log-btn:hover {
            opacity: 1;
        }
        .mm-log-empty {
            padding: 20px;
            text-align: center;
            color: var(--mm-textDim);
            font-size: 13px;
        }

        /* === Footer Bar === */
        .mm-footer-bar {
            display: flex;
            gap: 8px;
            padding: 10px 0 0;
            border-top: 1px solid var(--mm-border);
            flex-shrink: 0;
            position: sticky;
            bottom: 0;
            background: var(--mm-bg);
        }
        .mm-footer-btn {
            flex: 1;
            padding: 10px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            background: var(--mm-primary);
            font-size: 20px;
            font-weight: 600;
            transition: background 0.2s;
            color: var(--mm-text);
            text-align: center;
        }
        .mm-footer-btn:hover {
            filter: brightness(1.1);
        }
        .mm-footer-btn.hidden {
            display: none;
        }
        .mm-footer-save {
            background: var(--mm-accent);
        }
        .mm-footer-reset {
            background: var(--mm-warning);
        }
        .mm-footer-start {
            background: var(--mm-primary);
        }
        .mm-footer-cancel {
            background: var(--mm-error);
        }
        .mm-container-resizer {
            position:absolute;
            background: rgba(100, 100, 255, 0.3);
            z-index: 9;
            opacity:0.2;
        }
        .mm-container-resizer:hover {
            position:absolute;
            background: rgba(100, 100, 255, 0.6);
            opacity: 0.9;
        }
        .mm-notification-container {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 10003;
            pointer-events: none;
        }
        .mm-notification-toast {
            color: white;
            padding: 12px 16px;
            border-radius: 4px;
            margin-bottom: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
            font-size: 14px;
            pointer-events: auto;
            animation: mm-slideIn 0.3s ease;
            max-width: 480px;
            word-wrap: break-word;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .mm-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 10002;
        }
        .mm-modal-content {
            background: var(--mm-bg);
            color: var(--mm-text);
            border: 1px solid var(--mm-border);
            border-radius: 8px;
            padding: 24px;
            max-width: 800px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }
        .mm-modal-title {
            margin: 0 0 12px 0;
            font-size: 18px;
            font-weight: 600;
        }
        .mm-modal-message {
            margin: 0 0 20px 0;
            font-size: 16px;
            color: var(--mm-textDim);
        }
        .mm-switch {
            position: relative;
            display: inline-block;
            width: 40px;
            height: 22px;
            margin-left: auto;
        }

        .mm-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .mm-switch-track {
            position: absolute;
            inset: 0;
            cursor: pointer;
            background: var(--mm-border);
            border-radius: 11px;
            transition: .25s;
        }

        .mm-switch-knob {
            position: absolute;
            width: 18px;
            height: 18px;
            left: 2px;
            bottom: 2px;
            background: white;
            border-radius: 50%;
            transition: .25s;
        }

        .mm-switch input:checked + .mm-switch-track {
            background: var(--mm-primary);
        }

        .mm-switch input:checked + .mm-switch-track .mm-switch-knob {
            transform: translateX(18px);
        }

        .inat-list {
            padding: 8xp 10px;
        }
        .inat-item, .osm-item {
            display: grid;
            grid-template-columns: 40px 1fr 90px 80px; /* OSM 可以调整为 50px 1fr */
            align-items: center;
            padding: 8px 10px;
            cursor: pointer;
            border-radius: 6px;
            transition: background 0.15s ease, border-left-color 0.15s ease;
            gap: 10px;
            border-left: 3px solid transparent;
        }

        .inat-item:hover, .osm-item:hover {
            background: var(--mm-bgSecondary);
            border-left-color: var(--mm-primary);
        }

        .inat-sci, .osm-key {
            font-size: var(--mm-fontSize);
            font-weight: 600;
            color: var(--mm-text);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .inat-rank, .osm-eq {
            font-size: 12px;
            color: var(--mm-textDim);
            white-space: nowrap;
        }

        .inat-count, .osm-value {
            text-align: right;
            font-size: 13px;
            font-weight: 600;
            color: var(--mm-primary);
            white-space: nowrap;
        }
        /* === Scrollbar Styles === */
        #mm-panel-main::-webkit-scrollbar,
        #mm-features-grid::-webkit-scrollbar,
        #mm-panel-sidebar::-webkit-scrollbar {
            width: 6px;
        }
        #mm-panel-main::-webkit-scrollbar-track,
        #mm-features-grid::-webkit-scrollbar-track,
        #mm-panel-sidebar::-webkit-scrollbar-track {
            background: transparent;
        }
        #mm-panel-main::-webkit-scrollbar-thumb,
        #mm-features-grid::-webkit-scrollbar-thumb,
        #mm-panel-sidebar::-webkit-scrollbar-thumb {
            background: var(--mm-border);
            border-radius: 3px;
        }
        #mm-panel-main::-webkit-scrollbar-thumb:hover,
        #mm-features-grid::-webkit-scrollbar-thumb:hover,
        #mm-panel-sidebar::-webkit-scrollbar-thumb:hover {
            background: var(--mm-textDim);
        }
        #mm-panel-main,
        #mm-features-grid,
        #mm-panel-sidebar {
            scrollbar-width: thin;
            scrollbar-color: var(--mm-border) transparent;
        }
        #mm-download-quality {
            margin-bottom: 8px;
            margin-top: 4px;
            font-size: 20px;
        }
        #mm-download-type {
            margin-top: 8px;
            margin-bottom: 10px;
            font-size: 13px;
        }
        .map-embed.minimap {
            position: absolute;
            z-index: 99999;
            width: 320px;
            height: 240px;
            right: 10px;
            bottom: 10px;
            opacity: 0.7;
            transform-origin: bottom right;
            transition: opacity 0.5s ease;
        }
        .map-embed.minimap.active {
            opacity: 1;
        }
        .minimap.hidden {
            display: none;
        }
    `);

    // ======================================================================================================================================
    // ========================================================== Event Handler =============================================================
    // ======================================================================================================================================

    function toggleMainPanel() {
        try {
            if (!Validator.checkEnvironment()) {
                return;
            }

            if (mainPanel) {
                mainPanel.toggle();
            }
        } catch (error) {
            Notification.error(error);
        }
    }

    // ======================================================================================================================================
    // ============================================================ Bootstrap ===============================================================
    // ======================================================================================================================================

    function bootstrap() {
        if (!Validator.checkDependencies()) {
            return;
        }

        const observer = new MutationObserver(() => {
            const header = document.querySelector(".icon-button");
            const mainBtn = document.querySelector(".mm-main-btn")

            if (header && !mainBtn && unsafeWindow.map) {
                shortcutManager.attach();
                initUI();
                layoutManager.initObserver();
                mapFeatureManager.initControls();
                mainPanel.create();
                observer.disconnect()
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    bootstrap();
})();