Nitan Airport Flair

Enhances webpages by identifying airport and multi-airport codes, styling them, and adding interactive tooltips/hyperlinks. Dismiss reverts instances.

Instalar este script¿?
Script recomendado por el autor

Puede que también te guste Nitan User Tags.

Instalar este script
// ==UserScript==
// @name         Nitan Airport Flair
// @namespace    http://tampermonkey.net/
// @version      0.5.7
// @description  Enhances webpages by identifying airport and multi-airport codes, styling them, and adding interactive tooltips/hyperlinks. Dismiss reverts instances.
// @author       s5kf
// @license      CC BY-NC-ND 4.0; https://creativecommons.org/licenses/by-nc-nd/4.0/
// @match        *://www.uscardforum.com/*
// @resource     airportJsonData https://raw.githubusercontent.com/s5kf/airport_flair/main/airports_filtered.json
// @resource     multiAirportJsonData https://raw.githubusercontent.com/s5kf/airport_flair/main/multi_airport_data.json
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @run-at       document-idle
// @supportURL   https://github.com/s5kf/airport_flair/issues
// ==/UserScript==

(function() {
    'use strict';

    let airportData = {}; // To store single airport data keyed by IATA code
    let multiAirportData = {}; // To store multi-airport area data
    let observer = null; // To store the MutationObserver instance

    // --- Constants (moved up for early initialization) ---
    const iataRegex = /\b([A-Za-z]{3})\b/g;
    const processedMark = 'airport-flair-processed';
    const flairTag = 'airport-flair-tag'; // Class for the flair span itself
    const potentialFlairClass = 'potential-airport-code'; // New class for potential flairs
    const multiAirportFlairClass = 'multi-airport-flair'; // New class for multi-airport flairs
    const COMMON_TLA_BLOCKLIST = new Set([
        "CEO", "CFO", "COO", "CTO", "CIO", // Common C-suite and department heads
        "USD", "EUR", "GBP", "JPY", "CNY", "INR", "BRL", "ARS", "MXN", "COP", "CLP", "PEN", "UYU", "PYG",// Currencies
        "BTC", "ETH", "XRP", "BCH", "LTC", "XMR", "XLM", "XEM", "XRP", "BCH", "LTC", "XMR", "XLM", "XEM", "XRP", "BCH", "LTC", "XMR", "XLM", "XEM", // Crypto
        "GDP", "GNP", "GPT", "ROI", "KPI", "ETA", "FAQ", "DIY", "AKA", // Common business & general acronyms
        "USB", "CPU", "GPU", "RAM", "SSD", "HDD", "OSX", "IOS", // Tech acronyms
        "LOL", "OMG", "BTW", "FYI", "IMO", "BRB", "BSO",// Internet slang
        "USA", "DOT", "DOJ", "DOL", "HHS", "FAA", "CAA", "FBI", "CIA", "CBP", "TSA","ICE", // Government and regulatory acronyms
        "ETA", "ETC", "INC", "LTD", "LLC", "DIY", "FAQ", "PDF", "XML", "DOC","CSV","TXT","ZIP","RAR","ISO","PPT",
        "API", "URL", "WWW", "CSS", "PHP", "SQL", "FTP","DNS", "VPN", "VPS", "TLS", "TTL",
        "ESG", "VIX", "AOC", "AND", "MVP", "OTA", "ITA", "AIR", "ADT", "JAL", "DSW",
        "CSP", "CSR", "CFU" ,"CIP", "CIU", "UAR", // User reported issues
        "ATM", "MCC", "SCO", "THE", "NOT", "GOT", "GOD", "BIZ","ECO","EQE","EQS","EQB",
        "OPT", "CPT", "PTO","SLH", "GAI", "SPG" // User reported issues
        // Add more as needed, ensure they are uppercase
    ]);

    // --- Load Data from @resource ---
    function initializeData() {
        let mainDataLoaded = false;
        let multiDataLoaded = false;

        // Load Main Airport Data
        try {
            console.log("[Airport Flair] Attempting to load main airport data from @resource...");
            const airportJsonDataString = GM_getResourceText("airportJsonData");
            if (!airportJsonDataString) {
                console.error("[Airport Flair] Failed to get main airport data. GM_getResourceText returned empty.");
            } else {
                const sanitizedJsonDataString = airportJsonDataString
                    .replace(/: Infinity,/g, ": null,")
                    .replace(/: Infinity}/g, ": null}")
                    .replace(/: NaN,/g, ": null,")
                    .replace(/: NaN}/g, ": null}");
                const data = JSON.parse(sanitizedJsonDataString);
                data.forEach(airport => {
                    if (airport.iata_code) {
                        airportData[airport.iata_code.toUpperCase()] = airport;
                    }
                });
                console.log("[Airport Flair] Main airport data loaded and processed:", Object.keys(airportData).length, "entries");
                mainDataLoaded = true;
            }
        } catch (e) {
            console.error("[Airport Flair] Error loading or parsing main airport data:", e);
            // Optional: log airportJsonDataString snippet if needed
        }

        // Load Multi-Airport Data
        try {
            console.log("[Airport Flair] Attempting to load multi-airport data from @resource...");
            const multiAirportJsonDataString = GM_getResourceText("multiAirportJsonData");
            if (!multiAirportJsonDataString) {
                console.error("[Airport Flair] Failed to get multi-airport data. GM_getResourceText returned empty.");
            } else {
                multiAirportData = JSON.parse(multiAirportJsonDataString); // Assuming this JSON is clean
                console.log("[Airport Flair] Multi-airport data loaded and processed:", Object.keys(multiAirportData).length, "entries");
                multiDataLoaded = true;
            }
        } catch (e) {
            console.error("[Airport Flair] Error loading or parsing multi-airport data:", e);
            // Optional: log multiAirportJsonDataString snippet if needed
        }

        return mainDataLoaded; // Script functionality primarily depends on main airport data for flags etc.
                               // Multi-airport data is supplementary.
    }

    // Initialize data and then process the page
    if (initializeData()) {
        processPage();
    } else {
        console.error("[Airport Flair] Script will not run effectively due to critical data loading issues (main airport data).");
    }

    // --- CSS Styles ---
    function getDynamicStyles() {
        const htmlElement = document.documentElement;
        const bodyElement = document.body;

        let isLikelyDarkMode; // Undetermined initially
        let detectionMethod = "initial"; // For debugging

        // --- Helper functions for color analysis (assuming they are defined correctly elsewhere or here) ---
        function isColorActuallyDark(colorString) {
            if (!colorString || colorString === 'transparent' || colorString === 'rgba(0, 0, 0, 0)') return false;
            if (colorString === 'rgb(0, 0, 0)' || colorString === '#000000') return true;
            try {
                const rgbMatch = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (rgbMatch) {
                    const r = parseInt(rgbMatch[1], 10);
                    const g = parseInt(rgbMatch[2], 10);
                    const b = parseInt(rgbMatch[3], 10);
                    if ((r + g + b) / 3 < 85 || (r < 110 && g < 110 && b < 110)) { // Adjusted threshold slightly
                        return true;
                    }
                }
            } catch (e) { /* ignore */ }
            return false;
        }

        function isVeryLightNonWhiteGrey(colorString) {
            if (!colorString || colorString === 'transparent' || colorString === 'rgba(0, 0, 0, 0)') return false;
            try {
                const rgbMatch = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (rgbMatch) {
                    const r = parseInt(rgbMatch[1], 10);
                    const g = parseInt(rgbMatch[2], 10);
                    const b = parseInt(rgbMatch[3], 10);
                    if (r > 225 && g > 225 && b > 225 && !(r === 255 && g === 255 && b === 255)) { // Adjusted threshold
                        if (Math.abs(r - g) < 25 && Math.abs(g - b) < 25 && Math.abs(r - b) < 25) {
                            return true;
                        }
                    }
                }
            } catch (e) { /* ignore */ }
            return false;
        }
        // --- End Helper Functions ---

        // 1. Check for specific Discourse theme classes on <html> (Highest Priority)
        const htmlClasses = htmlElement.classList;
        if (htmlClasses.contains('theme-dark') || htmlClasses.contains('discourse-dark-theme') || htmlClasses.contains('dark-scheme')) {
            isLikelyDarkMode = true;
            detectionMethod = "html_specific_class_dark";
        } else if (htmlClasses.contains('theme-light') || htmlClasses.contains('discourse-light-theme') || htmlClasses.contains('light-scheme')) {
            isLikelyDarkMode = false;
            detectionMethod = "html_specific_class_light";
        }

        // 2. If no specific HTML classes, try computed background colors (Body highest, then HTML)
        if (typeof isLikelyDarkMode === 'undefined') {
            const bodyBgColor = getComputedStyle(bodyElement).backgroundColor;
            const htmlBgColor = getComputedStyle(htmlElement).backgroundColor;
            const bodyIsTransparent = (!bodyBgColor || bodyBgColor === 'transparent' || bodyBgColor === 'rgba(0, 0, 0, 0)');
            const htmlIsTransparent = (!htmlBgColor || htmlBgColor === 'transparent' || htmlBgColor === 'rgba(0, 0, 0, 0)');

            if (!bodyIsTransparent) {
                isLikelyDarkMode = isColorActuallyDark(bodyBgColor);
                detectionMethod = "body_bg_color_check";
            } else if (!htmlIsTransparent) {
                isLikelyDarkMode = isColorActuallyDark(htmlBgColor);
                detectionMethod = "html_bg_color_check";
            }
        }

        // 3. If still undetermined, check generic classes on <html> or <body>
        if (typeof isLikelyDarkMode === 'undefined') {
            if (htmlElement.classList.contains('dark') || bodyElement.classList.contains('dark') ||
                htmlElement.classList.contains('dark-mode') || bodyElement.classList.contains('dark-mode')) {
                isLikelyDarkMode = true;
                detectionMethod = "generic_class_dark";
            }
            // If generic dark classes are not found, we assume light by omission at this stage if still undefined.
        }

        // 4. As a final fallback, use prefers-color-scheme if still undetermined.
        if (typeof isLikelyDarkMode === 'undefined') {
            const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
            isLikelyDarkMode = prefersDark;
            detectionMethod = "prefers_color_scheme";
        }

        // Ensure isLikelyDarkMode has a boolean value. If somehow it's still undefined, default to light.
        if (typeof isLikelyDarkMode === 'undefined') {
             console.warn("[Airport Flair] Dark mode detection inconclusive, defaulting to light mode. Final detection stage:", detectionMethod);
             isLikelyDarkMode = false; // Default to light
             detectionMethod += "_defaulted_light";
        }
        // console.log(`[Airport Flair] Theme detection: ${isLikelyDarkMode ? 'Dark' : 'Light'}. Method: ${detectionMethod}`);


        // --- Determine flair background color based on isLikelyDarkMode ---
        let flairBgColor;
        let flairTextColor; // New variable for text color

        // Re-get body/html background colors here for the specific "lights out" or "very light grey" checks
        const bodyBgColorFinal = getComputedStyle(bodyElement).backgroundColor;
        const htmlBgColorFinal = getComputedStyle(htmlElement).backgroundColor;

        if (isLikelyDarkMode) {
            // Check for "lights out" (pure black bg)
            if ((bodyBgColorFinal === 'rgb(0, 0, 0)' || bodyBgColorFinal === '#000000') ||
                (htmlBgColorFinal === 'rgb(0, 0, 0)' || htmlBgColorFinal === '#000000')) {
                flairBgColor = '#202327'; // Lights out
            } else {
                flairBgColor = '#273440'; // Dim mode (default dark)
            }
            flairTextColor = '#e6c07b'; // Standard dark mode text color
        } else { // Light mode
            // Default light mode flair background
            flairBgColor = '#eff3f4';
            flairTextColor = '#c18401'; // User requested light mode text color
            // Check if the background is a very light non-white grey.
            if (isVeryLightNonWhiteGrey(bodyBgColorFinal) || isVeryLightNonWhiteGrey(htmlBgColorFinal)) {
                flairBgColor = '#e0e0e0'; // Use a slightly more distinct grey for very light grey BGs
            }
        }

        return `
            .${flairTag} {
                display: inline-flex;
                align-items: center;
                vertical-align: baseline;
                font-family: 'Fira Code', 'Roboto Mono', Arial, sans-serif; /* UPDATED FONT */
                padding: 1px 4px; /* Reduced padding */
                color: ${flairTextColor}; /* DYNAMIC TEXT COLOR */
                background-color: ${flairBgColor};
                border-radius: 3px;
                text-decoration: none; /* For the anchor tag */
                margin: 0 1px; /* Small margin to prevent touching adjacent text */
                position: relative; /* For absolute positioning of the dismiss button */
            }
            .${flairTag} img.country-flag {
                width: 16px;
                height: 12px;
                margin-left: 4px; /* Switched from margin-right and slightly increased for balance */
                vertical-align: middle;
            }
            /* Unified base styles for dismiss button in both flair types */
            .${flairTag} .dismiss-flair,
            .${multiAirportFlairClass} .dismiss-flair {
                position: absolute;
                top: -6px;
                right: -6px;
                width: 16px;
                height: 16px;
                line-height: 16px;
                text-align: center;
                font-size: 14px;
                font-weight: bold;
                background-color: rgba(74, 74, 74, 0.6); /* Translucent background */
                color: #ffffff;
                border-radius: 50%;
                cursor: pointer;
                opacity: 0;
                pointer-events: none; /* Prevent interaction when hidden */
                z-index: 10;
                will-change: opacity, background-color; /* Hint for smoother transitions */
            }
            /* Removed redundant .${flairTag}:hover .dismiss-flair and .${flairTag} .dismiss-flair:hover rules
               as they are covered by the combined selectors below. */

            .${potentialFlairClass} {
                text-decoration: underline dotted rgba(128, 128, 128, 0.7);
                cursor: help;
                /* Ensure it doesn't pick up parent link styles if it's inside an <a> not yet processed */
                color: inherit;
            }
            .${potentialFlairClass}:hover {
                text-decoration-color: rgba(100, 100, 100, 1); /* Darker underline on hover */
                background-color: rgba(200, 200, 200, 0.1); /* Very subtle hover background */
            }
            /* No specific hover title CSS needed if using default browser title attribute */

            /* Styles for Multi-Airport Flairs */
            .${multiAirportFlairClass} {
                display: inline-flex;
                align-items: center;
                vertical-align: baseline;
                font-family: 'Fira Code', 'Roboto Mono', Arial, sans-serif; /* UPDATED FONT */
                padding: 1px 4px;
                /* color: #d19a66; // Default, will be overridden by theme specific below */
                /* background-color: #3a3d41; // Default, will be overridden by theme specific below */
                border-radius: 3px;
                text-decoration: none;
                margin: 0 1px;
                position: relative;
            }
            /* Dynamic background AND color for multi-airport flairs based on theme */
            /* Light mode for multi-airport */
            html:not(.dark) body:not(.dark) .${multiAirportFlairClass},
            body:not([style*="background-color: rgb(0, 0, 0)"]):not([style*="background-color: #000"]) .${multiAirportFlairClass} {
                background-color: ${isLikelyDarkMode ? '#273440' : '#e0e0e0'}; /* Lighter grey for light mode */
                color: ${isLikelyDarkMode ? '#e6c07b' : '#c18401'}; /* USER REQUESTED LIGHT MODE COLOR */
            }
            html.dark .${multiAirportFlairClass},
            body.dark .${multiAirportFlairClass},
            body[style*="background-color: rgb(0, 0, 0)"] .${multiAirportFlairClass},
            body[style*="background-color: #000"] .${multiAirportFlairClass} {
                 background-color: #273440; /* Dim mode dark */
                 color: #e6c07b; /* Revert to standard flair text color in dark mode */
            }
            /* Consider a specific lights-out color if needed, for now dim is fine */

            .${multiAirportFlairClass} img.country-flag {
                width: 16px;
                height: 12px;
                margin-left: 4px;
                vertical-align: middle;
            }
            /* No specific hover title CSS needed */

            /* Combining dismiss button hover selectors for robustness */
            .${flairTag}:hover .dismiss-flair,
            .${multiAirportFlairClass}:hover .dismiss-flair {
                opacity: 0.85;
                pointer-events: auto;
                transition: opacity 0.15s ease-in-out, background-color 0.15s ease-in-out;
            }
            .${flairTag} .dismiss-flair:hover,
            .${multiAirportFlairClass} .dismiss-flair:hover {
                opacity: 1;
                background-color: rgba(51, 51, 51, 0.85);
                transition: opacity 0.15s ease-in-out, background-color 0.15s ease-in-out;
            }
        `;
    }

    function injectStyles() {
        // Remove existing styles if they exist to update dynamic ones (like dark mode)
        const existingStyleId = 'airport-flair-styles';
        let existingStyleElement = document.getElementById(existingStyleId);
        if (existingStyleElement) {
            existingStyleElement.remove();
        }
        GM_addStyle(getDynamicStyles());
        // Add an ID to the style element for easy removal/update if needed
        const styleElements = document.head.getElementsByTagName('style');
        if(styleElements.length > 0) {
             // Assume the last style element added by GM_addStyle is ours if we don't have a direct way to ID it from GM_addStyle
            styleElements[styleElements.length - 1].id = existingStyleId;
        }
    }

    // --- DOM Interaction & Manipulation ---
    function createFlairElement(code, airportInfo, originalCasing = null) {
        const anchor = document.createElement('a');
        anchor.href = `https://www.google.com/search?q=airport+${encodeURIComponent(code)}`;
        anchor.target = '_blank';
        anchor.rel = 'noopener noreferrer';
        anchor.classList.add(flairTag); // Standard flair tag
        anchor.classList.add(processedMark);

        let titleText = `${airportInfo.name} (${code})`;
        if (airportInfo.municipality) titleText += `, ${airportInfo.municipality}`;
        if (airportInfo.iso_country) titleText += `, ${airportInfo.iso_country}`;
        anchor.title = titleText;

        const codeTextNode = document.createTextNode(code);
        anchor.appendChild(codeTextNode); // Text first

        if (airportInfo.iso_country) {
            const flagImg = document.createElement('img');
            flagImg.src = `https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/4.1.5/flags/4x3/${airportInfo.iso_country.toLowerCase()}.svg`;
            flagImg.alt = `${airportInfo.iso_country} flag`;
            flagImg.classList.add('country-flag');
            anchor.appendChild(flagImg); // Flag after text
        }

        // Dismiss Button for single airport flairs
        const dismissBtn = document.createElement('span');
        dismissBtn.classList.add('dismiss-flair');
        dismissBtn.innerHTML = '&times;';
        dismissBtn.dataset.code = code;
        dismissBtn.dataset.type = 'single-airport'; // Explicitly mark type

        if (originalCasing && originalCasing !== code) {
            dismissBtn.dataset.originalCasing = originalCasing;
            dismissBtn.title = `Revert to '${originalCasing}' (potential code)`;
        } else {
            dismissBtn.title = `Revert to '${code}' (potential code)`;
        }

        dismissBtn.addEventListener('click', function(event) {
            console.log("[Airport Flair] Single Airport Dismiss button clicked:", event.target.dataset.code);
            event.preventDefault();
            event.stopPropagation();

            const currentCode = event.target.dataset.code;
            const originalMixedCasing = event.target.dataset.originalCasing;
            const currentFlairElement = event.target.closest('.' + flairTag);

            const codeForPotential = originalMixedCasing || currentCode;
            const airportInfoForReversion = airportData[codeForPotential.toUpperCase()];

            if (currentFlairElement && currentFlairElement.parentNode) {
                if (airportInfoForReversion) {
                    const potentialElement = createPotentialFlairElement(codeForPotential, airportInfoForReversion, false);
                    currentFlairElement.parentNode.replaceChild(potentialElement, currentFlairElement);
                } else {
                    const revertedTextSpan = document.createElement('span');
                    revertedTextSpan.textContent = codeForPotential;
                    revertedTextSpan.classList.add(processedMark);
                    currentFlairElement.parentNode.replaceChild(revertedTextSpan, currentFlairElement);
                }
            }
        });
        anchor.appendChild(dismissBtn);
        return anchor;
    }

    // Modified createPotentialFlairElement to handle both single and multi-airport types
    function createPotentialFlairElement(originalCode, info, isMultiAirport = false) {
        const span = document.createElement('span');
        span.classList.add(potentialFlairClass);
        span.classList.add(processedMark);
        span.textContent = originalCode;

        let titleText = `Recognize ${originalCode.toUpperCase()} as an `;
        if (isMultiAirport && info && info.name) {
            titleText += `area? (${info.name})`;
        } else if (!isMultiAirport && info && info.name) {
            titleText += `airport? (${info.name})`;
        } else {
            titleText += `code?`; // Fallback
        }
        span.title = titleText;

        span.dataset.originalCode = originalCode;
        span.dataset.isMulti = isMultiAirport ? "true" : "false";

        const clickListener = function(event) {
            event.preventDefault();
            event.stopPropagation();

            const codeToConvert = event.target.dataset.originalCode;
            const wasMulti = event.target.dataset.isMulti === "true";
            const uppercaseCode = codeToConvert.toUpperCase();

            let fullFlairElement;
            if (wasMulti) {
                const maInfo = multiAirportData[uppercaseCode];
                if (maInfo) {
                    fullFlairElement = createMultiAirportFlairElement(uppercaseCode, maInfo, codeToConvert);
                }
            } else {
                const airportInfo = airportData[uppercaseCode];
                if (airportInfo) {
                    fullFlairElement = createFlairElement(uppercaseCode, airportInfo, codeToConvert);
                }
            }

            if (fullFlairElement && event.target.parentNode) {
                event.target.parentNode.replaceChild(fullFlairElement, event.target);
            }
        };
        span.addEventListener('click', clickListener, { once: true });
        return span;
    }

    function replaceTextWithFlair(textNode) {
        if (!textNode.parentNode || textNode.parentNode.classList.contains(processedMark) ||
            textNode.parentNode.closest('a, script, style, input, textarea, [contenteditable="true"], .poll-container, .option-text, .' + flairTag + ', .' + potentialFlairClass + ', .' + multiAirportFlairClass)) {
            return;
        }

        const text = textNode.nodeValue;
        let match;
        let lastIndex = 0;
        const fragment = document.createDocumentFragment();
        let foundMatch = false;

        while ((match = iataRegex.exec(text)) !== null) {
            const originalCode = match[1];
            const uppercaseCode = originalCode.toUpperCase();

            const airportInfo = airportData[uppercaseCode];
            const maInfo = multiAirportData[uppercaseCode];

            if (COMMON_TLA_BLOCKLIST.has(uppercaseCode)) {
                if (airportInfo || maInfo) { // It's a common TLA but also a valid airport/MA code
                    foundMatch = true;
                    if (match.index > lastIndex) {
                        fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
                    }
                    // Always create potential for blocklisted items that are also airports
                    fragment.appendChild(createPotentialFlairElement(originalCode, airportInfo || maInfo, !!maInfo));
                    lastIndex = iataRegex.lastIndex;
                }
                // If it's in the blocklist and NOT an airport, we do nothing, effectively skipping it.
                // The loop will continue, and this part of the text remains unchanged.
            } else if (airportInfo || maInfo) { // Not in blocklist, but is an airport/MA code
                foundMatch = true;
                if (match.index > lastIndex) {
                    fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
                }

                if (originalCode === uppercaseCode) { // Already all caps
                    if (airportInfo) {
                        fragment.appendChild(createFlairElement(uppercaseCode, airportInfo));
                    } else { // maInfo must be true
                        fragment.appendChild(createMultiAirportFlairElement(uppercaseCode, maInfo));
                    }
                } else { // Mixed or lowercase - create potential
                    if (airportInfo) {
                        fragment.appendChild(createPotentialFlairElement(originalCode, airportInfo, false));
                    } else { // maInfo must be true
                        fragment.appendChild(createPotentialFlairElement(originalCode, maInfo, true));
                    }
                }
                lastIndex = iataRegex.lastIndex;
            }
            // If no conditions met (not blocklisted, not airport), loop continues, text remains.
        }

        if (foundMatch) {
            if (lastIndex < text.length) {
                fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
            }
            textNode.parentNode.replaceChild(fragment, textNode);
        }
    }

    function processNode(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            replaceTextWithFlair(node);
        }
        else if (node.nodeType === Node.ELEMENT_NODE &&
                 !node.classList.contains(processedMark) &&
                 !node.closest('a, script, style, input, textarea, [contenteditable="true"], .poll-container, .option-text, .' + flairTag + ', .' + potentialFlairClass + ', .' + multiAirportFlairClass)) {
             Array.from(node.childNodes).forEach(child => processNode(child));
        }
    }

    function processAddedNodes(mutationList) {
        for (const mutation of mutationList) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    // Check if the node itself has already been processed (e.g. if it's part of a flair we just added)
                    if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(processedMark)) {
                        return;
                    }
                    processNode(node);
                });
            }
        }
        // After processing mutations, it's good to re-evaluate dynamic styles like dark mode
        injectStyles();
    }

    function observeDOMChanges() {
        if (observer) observer.disconnect(); // Disconnect previous observer if any

        observer = new MutationObserver(processAddedNodes);
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        console.log("MutationObserver started.");
    }

    function processPage() {
        console.log("Processing page for airport codes...");
        injectStyles();
        // Initial scan of the entire body
        processNode(document.body);
        observeDOMChanges();
    }

    // --- Cleanup ---
    window.addEventListener('unload', () => {
        if (observer) {
            observer.disconnect();
            console.log("Disconnected MutationObserver.");
        }
    });

    // --- Create Flair Elements ---
    // Function to create the actual styled flair element for MULTI-AIRPORT codes
    function createMultiAirportFlairElement(code, maInfo, originalCasing = null) {
        const anchor = document.createElement('a');
        anchor.href = `https://www.google.com/search?q=${encodeURIComponent(maInfo.name)}`; // Search for the area name
        anchor.target = '_blank';
        anchor.rel = 'noopener noreferrer';
        anchor.classList.add(multiAirportFlairClass); // Use special class
        anchor.classList.add(processedMark);

        anchor.title = `${maInfo.name} (${code})`;

        // Flag logic: use primaryAirportForFlag from maInfo to look up in airportData
        if (maInfo.primaryAirportForFlag && airportData[maInfo.primaryAirportForFlag]) {
            const primaryAirportDetails = airportData[maInfo.primaryAirportForFlag];
            if (primaryAirportDetails.iso_country) {
                const flagImg = document.createElement('img');
                flagImg.src = `https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/4.1.5/flags/4x3/${primaryAirportDetails.iso_country.toLowerCase()}.svg`;
                flagImg.alt = `${primaryAirportDetails.iso_country} flag`;
                flagImg.classList.add('country-flag');
                anchor.appendChild(flagImg); // Flag first for multi-airport to differentiate?
            }
        }

        const codeTextNode = document.createTextNode(code);
        if (anchor.firstChild && anchor.firstChild.tagName === 'IMG') { // If flag was added, append text after
            anchor.appendChild(codeTextNode);
        } else { // Otherwise, text is first
            anchor.insertBefore(codeTextNode, anchor.firstChild);
        }

        // Add Dismiss Button
        const dismissBtn = document.createElement('span');
        dismissBtn.classList.add('dismiss-flair');
        dismissBtn.innerHTML = '&times;';
        dismissBtn.dataset.code = code; // Uppercase code of the flair
        dismissBtn.dataset.type = 'multi-airport'; // Mark type for potential differentiated dismiss logic

        // Determine title and if originalCasing needs to be stored for dismiss
        if (originalCasing && originalCasing !== code) {
            dismissBtn.dataset.originalCasing = originalCasing;
            dismissBtn.title = `Revert to '${originalCasing}' (potential code)`;
        } else {
            dismissBtn.title = `Revert to '${code}' (potential code)`;
        }

        dismissBtn.addEventListener('click', function(event) {
            console.log("[Airport Flair] Multi-Airport Dismiss button clicked:", event.target.dataset.code);
            event.preventDefault();
            event.stopPropagation();

            const currentCode = event.target.dataset.code;
            const originalMixedCasing = event.target.dataset.originalCasing;
            const currentFlairElement = event.target.closest('.' + multiAirportFlairClass);
            const codeForPotential = originalMixedCasing || currentCode;
            const maInfoForReversion = multiAirportData[codeForPotential.toUpperCase()];
            // Also check regular airport data in case it's an ambiguous code that got here
            const airportInfoForReversion = airportData[codeForPotential.toUpperCase()];

            if (currentFlairElement && currentFlairElement.parentNode) {
                const infoForPotential = maInfoForReversion || airportInfoForReversion; // Prefer MA info if available
                if (infoForPotential) {
                    const potentialElement = createPotentialFlairElement(codeForPotential, infoForPotential, !!maInfoForReversion);
                    currentFlairElement.parentNode.replaceChild(potentialElement, currentFlairElement);
                } else {
                    // Fallback to plain text if no info found (should be rare)
                    const revertedTextSpan = document.createElement('span');
                    revertedTextSpan.textContent = codeForPotential;
                    revertedTextSpan.classList.add(processedMark);
                    currentFlairElement.parentNode.replaceChild(revertedTextSpan, currentFlairElement);
                }
            }
        });
        anchor.appendChild(dismissBtn);
        return anchor;
    }

})();