미디어위키:NationsPanel.js: 두 판 사이의 차이

편집 요약 없음
태그: 되돌려진 기여
(Install package: clbiwiki-era-globe-multisymbol-runtime-hotfix-20260711 / js/NationsPanel.js)
 
(같은 사용자의 중간 판 67개는 보이지 않습니다)
1번째 줄: 1번째 줄:
/* =========================================
/* =========================================
COASTLINE: BLACK ICE - NationsGlobe
  COASTLINE: BLACK ICE - Nations Panel
Real-world prototype / QGIS-ready globe viewer
  시대 문서: 시대 전환 / 역사 연도 / 대륙 탭 제어
========================================= */
  ========================================= */
(function () {
    'use strict';


(function (mw, $) {
    var ENTRY_CONSUME_FIX_BUILD = '20260708-boot-skip-preview-and-manager-save-001';
'use strict';


var THREE_URL = 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
    var NATIONS_PANEL_BUILD = '20260710-interaction-budget-bg-parity-001';
var TOPOJSON_URL = 'https://cdn.jsdelivr.net/npm/topojson-client@3/+esm';
var DEFAULT_WORLD_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json';
var CELESTIAL_PLANET_TEXTURES = {
    mercury: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_mercury.jpg',
    venus: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_venus_surface.jpg',
    mars: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_mars.jpg',
    jupiter: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_jupiter.jpg',
    saturn: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_saturn.jpg',
    uranus: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_uranus.jpg',
    neptune: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_neptune.jpg'
};


var CELESTIAL_SUN_TEXTURE_URL = 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_sun.jpg';
    var DEFAULT_ERAS = [
var CELESTIAL_MOON_TEXTURE_URL = 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_moon.jpg';
        '1950', '1960', '1970', '1980', '1990', '2000', '2010', '2020',
var SATURN_RING_TEXTURE_URL = 'https://upload.wikimedia.org/wikipedia/commons/2/29/Solarsystemscope_texture_8k_saturn_ring_alpha.png';
        '2030', '2040', '2050', '2060', '2070', '2080', '2090', '2100'
    ];


/*
    var nationPanelLastDirectInteractionAt = 0;
* Decorative celestial model.
* This is not a precise ephemeris.  It keeps the planets on one distant
* ecliptic plane so they read as real 3D bodies instead of screen-composed PNGs.
*/
var CELESTIAL_ECLIPTIC = {
    depth: -22.0,
    tiltYPerX: 0.018,
    depthScale: 0.34
};


var CELESTIAL_PLANET_DEFS = [
     function markNationPanelDirectInteraction() {
     {
         nationPanelLastDirectInteractionAt = Date.now();
         key: 'mercury',
        radius: 0.052,
        orbitRadius: 4.15,
        orbitAngle: 2.03,
        inclination: -0.018,
        opacity: 0.36,
        rotationSpeed: 0.000056,
        fallbackA: '#7f786d',
        fallbackB: '#24211d'
    },
    {
        key: 'venus',
        radius: 0.086,
        orbitRadius: 4.65,
        orbitAngle: 2.96,
        inclination: 0.024,
        opacity: 0.36,
        rotationSpeed: 0.000022,
        fallbackA: '#b99765',
        fallbackB: '#4f341a'
    },
    {
        key: 'mars',
        radius: 0.105,
        orbitRadius: 5.25,
        orbitAngle: 3.84,
        inclination: 0.032,
        opacity: 0.42,
        rotationSpeed: 0.000040,
        fallbackA: '#8f4938',
        fallbackB: '#3a1712'
    },
    {
        key: 'jupiter',
        radius: 0.235,
        orbitRadius: 8.20,
        orbitAngle: 5.64,
        inclination: 0.023,
        opacity: 0.34,
        rotationSpeed: 0.000024,
        fallbackA: '#a98262',
        fallbackB: '#4c3628'
    },
    {
        key: 'saturn',
        radius: 0.225,
        orbitRadius: 9.15,
        orbitAngle: 4.76,
        inclination: -0.018,
        opacity: 0.44,
        rotationSpeed: 0.000022,
        fallbackA: '#b89a70',
        fallbackB: '#4d3724',
        ring: true,
        ringOpacity: 0.86,
        ringInnerScale: 1.34,
        ringOuterScale: 2.58,
        ringTiltX: 1.08,
        ringTiltY: 0.18,
        ringTiltZ: -0.34,
        ringTextureUrl: SATURN_RING_TEXTURE_URL
    },
    {
        key: 'uranus',
        radius: 0.116,
        orbitRadius: 10.70,
        orbitAngle: 1.43,
        inclination: -0.026,
        opacity: 0.29,
        rotationSpeed: 0.000030,
        fallbackA: '#7fb5bd',
        fallbackB: '#24424b'
    },
    {
        key: 'neptune',
        radius: 0.100,
        orbitRadius: 11.75,
        orbitAngle: 0.66,
        inclination: 0.031,
        opacity: 0.28,
        rotationSpeed: 0.000032,
        fallbackA: '#43658f',
        fallbackB: '#151d35'
     }
     }
];


var CELESTIAL_SUN_DEF = {
    function hasRecentNationPanelDirectInteraction(windowMs) {
    radius: 0.35,
        return Date.now() - Number(nationPanelLastDirectInteractionAt || 0) < Math.max(120, Number(windowMs || 900));
    orbitRadius: 12.4,
     }
    orbitAngle: 2.72,
    inclination: 0.000,
    color: 0xffddb4,
    coreOpacity: 0.90,
    innerGlowOpacity: 0.74,
     innerGlowScale: 4.9,
    outerGlowOpacity: 0.24,
    outerGlowScale: 8.6,
    pulseA: 0.010,
    pulseB: 0.014,
    textureScrollSpeed: 0.000014
};


var CELESTIAL_MOON_DEF = {
    function isNationsGlobeBusyForPanelWork() {
    radius: 0.145,
        var globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
    position: [1.72, 0.28, -0.62],
        var instance;
    opacity: 0.72,
    rotationSpeed: 0.000018,
    fallbackA: '#8f8f8f',
    fallbackB: '#343434'
};
var instances = [];
var threeModulePromise = null;
var topoModulePromise = null;


function escapeHtml(value) {
        if (!globe) return false;
    return String(value == null ? '' : value)
        if (globe.classList && globe.classList.contains('is-dragging')) return true;
         .replace(/&/g, '&amp;')
         if (globe.getAttribute && globe.getAttribute('data-clbi-globe-bg-busy') === '1') return true;
        .replace(/</g, '&lt;')
         instance = globe.CLBI_NationsGlobeInstance || null;
        .replace(/>/g, '&gt;')
         return !!(instance && typeof instance.isHalftoneBackgroundBusy === 'function' && instance.isHalftoneBackgroundBusy());
         .replace(/"/g, '&quot;')
    }
         .replace(/'/g, '&#039;');
}


function loadThree() {
    function toArray(list) {
    if (window.CLBI_NATIONS_GLOBE_THREE) {
         return Array.prototype.slice.call(list || []);
         return Promise.resolve(window.CLBI_NATIONS_GLOBE_THREE);
     }
     }


     if (!threeModulePromise) {
     function unique(values) {
         threeModulePromise = import(THREE_URL).then(function (module) {
         var seen = {};
             window.CLBI_NATIONS_GLOBE_THREE = module;
        var out = [];
             return module;
 
        values.forEach(function (value) {
             value = String(value || '').trim();
            if (!value || seen[value]) return;
            seen[value] = true;
             out.push(value);
         });
         });
        return out;
     }
     }


     return threeModulePromise;
     function parseEraList(stack) {
}
        var raw = stack ? stack.getAttribute('data-nations-era-years') : '';
        var values = raw ? raw.split(/[\s,|]+/) : [];
 
        values = unique(values.filter(function (value) {
            return /^\d{4}$/.test(value);
        }));
 
        return values.length ? values : DEFAULT_ERAS.slice();
    }


function loadTopojson() {
    function getEraIndex(eras, era) {
    if (window.CLBI_NATIONS_GLOBE_TOPOJSON) {
        var index = eras.indexOf(String(era || ''));
         return Promise.resolve(window.CLBI_NATIONS_GLOBE_TOPOJSON);
         return index >= 0 ? index : 0;
     }
     }


     if (!topoModulePromise) {
     function getWrappedEra(eras, index) {
         topoModulePromise = import(TOPOJSON_URL).then(function (module) {
        if (!eras.length) return '';
            window.CLBI_NATIONS_GLOBE_TOPOJSON = module;
         index = ((index % eras.length) + eras.length) % eras.length;
            return module;
        return eras[index];
        });
     }
     }


     return topoModulePromise;
     function setHidden(element, hidden) {
}
        var active = !hidden;
 
        if (!element) return;
        if (element.classList.contains('is-active') === active &&
            element.hasAttribute('hidden') === !!hidden &&
            element.getAttribute('aria-hidden') === (hidden ? 'true' : 'false')) return;
 
        if (hidden) {
            element.setAttribute('hidden', 'hidden');
            element.setAttribute('aria-hidden', 'true');
            element.classList.remove('is-active');
        } else {
            element.removeAttribute('hidden');
            element.setAttribute('aria-hidden', 'false');
            element.classList.add('is-active');
        }
    }


function toNumber(value, fallback) {
    function setActiveButton(buttons, activeButton) {
    var n = parseFloat(value);
        buttons.forEach(function (button) {
    return Number.isFinite(n) ? n : fallback;
            var active = button === activeButton;
}


function lonLatToVector3(THREE, lon, lat, radius) {
            if (button.classList.contains('is-active') !== active) {
    var phi = (90 - lat) * Math.PI / 180;
                button.classList.toggle('is-active', active);
    var theta = (lon + 180) * Math.PI / 180;
            }
    var sinPhi = Math.sin(phi);
            if (button.getAttribute('aria-selected') !== (active ? 'true' : 'false')) {
                button.setAttribute('aria-selected', active ? 'true' : 'false');
            }
            if (button.getAttribute('tabindex') !== (active ? '0' : '-1')) {
                button.setAttribute('tabindex', active ? '0' : '-1');
            }
        });
    }


     return new THREE.Vector3(
     function activatePanelsByAttribute(panels, attrName, value) {
         -radius * sinPhi * Math.cos(theta),
         panels.forEach(function (panel) {
        radius * Math.cos(phi),
            setHidden(panel, panel.getAttribute(attrName) !== value);
        radius * sinPhi * Math.sin(theta)
        });
    );
    }
}


function vectorToLonLat(vec) {
    function activatePersistentPanelsByAttribute(panels, attrName, value) {
    var n = vec.clone().normalize();
        panels.forEach(function (panel) {
    var lat = Math.asin(Math.max(-1, Math.min(1, n.y))) * 180 / Math.PI;
            var active = panel.getAttribute(attrName) === value;
    var lon = Math.atan2(n.z, -n.x) * 180 / Math.PI - 180;
            var aria = active ? 'false' : 'true';


    while (lon < -180) lon += 360;
            if (panel.hasAttribute('hidden')) panel.removeAttribute('hidden');
    while (lon > 180) lon -= 360;
            if (panel.classList.contains('is-active') !== active) {
                panel.classList.toggle('is-active', active);
            }
            if (panel.getAttribute('aria-hidden') !== aria) {
                panel.setAttribute('aria-hidden', aria);
            }
            if (panel.getAttribute('data-clbi-panel-warm') !== '1') {
                panel.setAttribute('data-clbi-panel-warm', '1');
            }
        });
    }


     return {
     function getIdleScheduler() {
        lon: lon,
        return window.requestIdleCallback || function (callback) {
        lat: lat
            return window.setTimeout(function () {
    };
                callback({
}
                    didTimeout: false,
                    timeRemaining: function () { return 12; }
                });
            }, 96);
        };
    }


    function getIdleCanceller() {
        return window.cancelIdleCallback || window.clearTimeout;
    }


function ensureNationsGlobeInteractionStyle() {
    function scheduleIdle(callback, options) {
    if (document.getElementById('nation-globe-interaction-style')) return;
        return getIdleScheduler()(callback, options || { timeout: 650 });
    }


     var style = document.createElement('style');
     function cancelIdle(handle) {
    style.id = 'nation-globe-interaction-style';
         if (handle == null) return;
    style.textContent = [
         getIdleCanceller()(handle);
        '.clbi-nations-globe-window.has-nation-hover .clbi-nations-globe-stage{cursor:pointer;}',
    }
        '.clbi-nations-globe-window.is-dragging .clbi-nations-globe-stage{cursor:grabbing;}',
         '.clbi-nations-globe-nation-tooltip{position:absolute;z-index:8;min-width:72px;max-width:220px;pointer-events:none;display:none;padding:4px 7px;background:rgba(5,5,5,.92);border:1px solid #000;color:#e2e2e2;font-size:11px;line-height:1.25;box-shadow:inset 0 1px 0 #333,inset 0 -1px 0 #111,0 4px 12px rgba(0,0,0,.45);white-space:nowrap;}',
        '.clbi-nations-globe-nation-tooltip.is-visible{display:block;}',
        '.clbi-nations-globe-nation-tooltip .nation-tooltip-name{font-weight:700;letter-spacing:.02em;}',
        '.clbi-nations-globe-nation-tooltip .nation-tooltip-code{margin-left:6px;color:#888;font-size:10px;font-weight:500;}',
         '.clbi-nations-globe-nation-tooltip .nation-tooltip-link{display:block;margin-top:2px;color:#a8a8a8;font-size:10px;}'
    ].join('\n');
    document.head.appendChild(style);
}


function getFeaturePage(feature) {
    function replaceYearTemplate(template, era) {
    return feature && feature.properties ? String(feature.properties.page || '').trim() : '';
        return String(template || '').replace(/\{year\}/g, String(era || ''));
}
    }


function calculateGeometryBBox(geometry) {
    function readTemplate(root, attrName, fallback) {
     var bbox = [Infinity, Infinity, -Infinity, -Infinity];
        if (!root) return fallback;
        return root.getAttribute(attrName) || fallback;
     }


     function visit(coords) {
     function setAttributeFromTemplate(root, attrName, templateName, fallback, era) {
         if (!Array.isArray(coords)) return;
         var template = readTemplate(root, templateName, fallback);
         if (coords.length >= 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') {
         if (!template) {
             bbox[0] = Math.min(bbox[0], coords[0]);
             root.removeAttribute(attrName);
            bbox[1] = Math.min(bbox[1], coords[1]);
            bbox[2] = Math.max(bbox[2], coords[0]);
            bbox[3] = Math.max(bbox[3], coords[1]);
             return;
             return;
         }
         }
         coords.forEach(visit);
         root.setAttribute(attrName, replaceYearTemplate(template, era));
     }
     }


     if (geometry) visit(geometry.coordinates);
     function resetGlobeDom(globe) {
    if (!Number.isFinite(bbox[0])) return [-180, -90, 180, 90];
        var instance;
    return bbox;
 
}
        if (!globe) return;
 
        instance = globe.CLBI_NationsGlobeInstance || null;
        if (instance && typeof instance.dispose === 'function') {
            try { instance.dispose(); } catch (err) {}
        }
 
        globe.CLBI_NationsGlobeInstance = null;
        globe.removeAttribute('data-nations-globe-ready');
        globe.classList.remove('is-ready', 'has-error', 'is-dragging');
        globe.innerHTML = '';
    }


function isLonLatInsideBbox(lon, lat, bbox) {
    function reloadGlobe(globe) {
    if (!bbox || bbox.length < 4) return true;
        if (!globe) return;
    if (lat < bbox[1] || lat > bbox[3]) return false;
    if ((bbox[2] - bbox[0]) >= 300) return true;
    return lon >= bbox[0] && lon <= bbox[2];
}


function unwrapLonNear(lon, refLon) {
        if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.reload === 'function') {
    var x = lon;
            window.CLBI_NationsGlobe.reload(globe);
    while (x - refLon > 180) x -= 360;
            return;
    while (x - refLon < -180) x += 360;
        }
    return x;
}


function pointInRing(lon, lat, ring) {
        resetGlobeDom(globe);
    var inside = false;
    var i;
    var j;
    var xi;
    var yi;
    var xj;
    var yj;
    var intersect;


    if (!Array.isArray(ring) || ring.length < 3) return false;
        if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function') {
            window.CLBI_NationsGlobe.init(globe.parentNode || document);
            return;
        }


    for (i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
        window.setTimeout(function () {
        if (!ring[i] || !ring[j]) continue;
            if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function') {
        xi = unwrapLonNear(Number(ring[i][0]), lon);
                window.CLBI_NationsGlobe.init(globe.parentNode || document);
        yi = Number(ring[i][1]);
            }
        xj = unwrapLonNear(Number(ring[j][0]), lon);
         }, 0);
        yj = Number(ring[j][1]);
        if (!Number.isFinite(xi) || !Number.isFinite(yi) || !Number.isFinite(xj) || !Number.isFinite(yj)) continue;
        intersect = ((yi > lat) !== (yj > lat)) && (lon < (xj - xi) * (lat - yi) / ((yj - yi) || 1e-12) + xi);
         if (intersect) inside = !inside;
     }
     }


     return inside;
     function applyGlobeEra(globe, era, reload) {
}
        if (!globe || !era) return;


function pointInPolygonCoordinates(lon, lat, polygon) {
        globe.setAttribute('data-era-active', era);
    var i;
        globe.setAttribute('data-era', replaceYearTemplate(
            readTemplate(globe, 'data-era-readout-template', '{year}'),
            era
        ));


    if (!Array.isArray(polygon) || !polygon.length) return false;
        setAttributeFromTemplate(
    if (!pointInRing(lon, lat, polygon[0])) return false;
            globe,
            'data-geojson-url',
            'data-era-border-url-template',
            '/index.php/Special:Redirect/file/{year}_Border_Lines.geojson',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-polygon-overlay-url',
            'data-era-polygon-overlay-url-template',
            '/index.php/Special:Redirect/file/{year}_Polygon_Overlay.webp',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-polygon-hit-url',
            'data-era-polygon-hit-url-template',
            '/index.php/Special:Redirect/file/{year}_Polygon_Hit.geojson',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-nation-list-url',
            'data-era-nation-list-url-template',
            '/index.php?title=MediaWiki:{year}_Nation_List.json&action=raw&ctype=application/json',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-nation-link-map-url',
            'data-era-nation-link-map-url-template',
            '/index.php?title=MediaWiki:{year}_Nation_Link_Map.json&action=raw&ctype=application/json',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-lakes-overlay-url',
            'data-era-lakes-overlay-url-template',
            '/index.php/Special:Redirect/file/{year}_Lakes_Overlay.webp',
            era
        );


    for (i = 1; i < polygon.length; i += 1) {
        if (reload) {
        if (pointInRing(lon, lat, polygon[i])) return false;
            reloadGlobe(globe);
        }
     }
     }


    return true;
}


function pointInGeometry(lon, lat, geometry) {
    var NATION_CONTINENT_FALLBACK = [
     var i;
        { key: 'america', label: '아메리카', regions: ['북아메리카', '남아메리카'] },
        { key: 'europe', label: '유럽', regions: ['북유럽', '서유럽', '중앙유럽', '동유럽', '동남유럽', '남유럽', '남서유럽'] },
        { key: 'africa', label: '아프리카', regions: ['북아프리카', '서아프리카', '중앙아프리카', '동아프리카', '남아프리카'] },
        { key: 'asia', label: '아시아', regions: ['서아시아', '중앙아시아', '북아시아', '남아시아', '동아시아', '동남아시아'] },
        { key: 'oceania', label: '오세아니아', regions: ['오세아니아'] },
        { key: 'antarctica', label: '남극', regions: ['남극'] }
    ];
 
    var nationListCache = {};
    var nationLinkMapCache = {};
    var historyEventsCache = {};
    var nationRuntimeCacheBust = 'revision-ledger';
    var nationResolvedFileUrlCache = {};
 
    var PanelPerf = (function () {
        var startedAt = Date.now();
        var sequence = 1;
        var active = {};
        var entries = [];
 
        function cloneMeta(meta) {
            var copy = {};
            if (!meta || typeof meta !== 'object') return copy;
            Object.keys(meta).forEach(function (key) {
                var value = meta[key];
                if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
                    copy[key] = value;
                } else if (Array.isArray(value)) {
                    copy[key] = value.length <= 12 ? value.slice() : value.slice(0, 12).concat(['...+' + (value.length - 12)]);
                } else {
                    try { copy[key] = JSON.parse(JSON.stringify(value)); }
                    catch (err) { copy[key] = String(value); }
                }
            });
            return copy;
        }
 
        function mergeMeta(a, b) {
            var out = cloneMeta(a);
            b = cloneMeta(b);
            Object.keys(b).forEach(function (key) { out[key] = b[key]; });
            return out;
        }
 
        function start(name, meta) {
            var id = sequence++;
            active[id] = {
                id: id,
                name: String(name || 'step'),
                startedAt: Date.now(),
                offset: Date.now() - startedAt,
                meta: cloneMeta(meta)
            };
            return id;
        }
 
        function end(id, meta) {
            var item = active[id];
            if (!item) return null;
            delete active[id];
            item.ms = Date.now() - item.startedAt;
            item.meta = mergeMeta(item.meta, meta || { ok: true });
            entries.push({
                offset: item.offset,
                ms: item.ms,
                name: item.name,
                meta: item.meta
            });
            return item;
        }
 
        function mark(name, meta) {
            entries.push({
                offset: Date.now() - startedAt,
                ms: 0,
                name: String(name || 'mark'),
                meta: cloneMeta(meta)
            });
        }
 
        function measure(name, meta, callback) {
            var id = start(name, meta);
            var value;
            try {
                value = callback();
            } catch (err) {
                end(id, { ok: false, error: err && err.message ? err.message : String(err) });
                throw err;
            }
            if (value && typeof value.then === 'function') {
                return value.then(function (result) {
                    end(id, { ok: true });
                    return result;
                }, function (err) {
                    end(id, { ok: false, error: err && err.message ? err.message : String(err) });
                    throw err;
                });
            }
            end(id, { ok: true });
            return value;
        }
 
        function summary() {
            return {
                build: NATIONS_PANEL_BUILD,
                startedAt: startedAt,
                totalMs: Date.now() - startedAt,
                entries: entries.slice().sort(function (a, b) {
                    if (a.offset !== b.offset) return a.offset - b.offset;
                    return b.ms - a.ms;
                }),
                active: Object.keys(active).map(function (id) {
                    var item = active[id];
                    return {
                        offset: item.offset,
                        ms: Date.now() - item.startedAt,
                        name: item.name,
                        meta: cloneMeta(item.meta)
                    };
                })
            };
        }
 
        function table() {
            var data = summary().entries.map(function (entry) {
                return { name: entry.name, ms: entry.ms, offset: entry.offset, meta: entry.meta };
            });
            if (window.console && console.table) console.table(data);
            return data;
        }
 
        return {
            build: NATIONS_PANEL_BUILD,
            start: start,
            end: end,
            mark: mark,
            measure: measure,
            summary: summary,
            table: table
        };
     }());


     if (!geometry) return false;
     window.NationsPanelPerf = PanelPerf;


     if (geometry.type === 'Polygon') {
     function interactionPerfMeasure(name, meta, fn) {
        return pointInPolygonCoordinates(lon, lat, geometry.coordinates);
        var perf = window.InteractionPerf;
        if (perf && typeof perf.measureSync === 'function') {
            return perf.measureSync(name, meta || {}, fn);
        }
        return fn();
     }
     }


     if (geometry.type === 'MultiPolygon') {
     function interactionPerfMark(name, meta) {
         for (i = 0; i < geometry.coordinates.length; i += 1) {
         var perf = window.InteractionPerf;
             if (pointInPolygonCoordinates(lon, lat, geometry.coordinates[i])) return true;
        if (perf && typeof perf.mark === 'function') {
             perf.mark(name, meta || {});
         }
         }
     }
     }


     return false;
     function countNationListItems(data) {
}
        var continents = data && Array.isArray(data.continents) ? data.continents : [];
        var regions = 0;
        var items = 0;
        continents.forEach(function (continent) {
            (continent.regions || []).forEach(function (region) {
                regions += 1;
                items += Array.isArray(region.items) ? region.items.length : 0;
            });
        });
        return { continents: continents.length, regions: regions, items: items };
    }
 
    function extractRawTitleFromUrl(url) {
        var text = String(url || '').trim();
        var match;
        if (!text) return '';
        match = text.match(/[?&]title=([^&#]+)/i);
        if (match) {
            try { return decodeURIComponent(match[1].replace(/\+/g, ' ')).replace(/_/g, ' ').trim(); }
            catch (err) { return match[1].replace(/_/g, ' ').trim(); }
        }
        return '';
    }


function normalizeNationLinkMap(payload) {
    function addNationCacheBust(url, ref) {
    var source = payload && payload.items ? payload.items : (payload || {});
        url = String(url || '').trim();
    var map = {};
        ref = String(ref || extractRawTitleFromUrl(url) || '').trim();
        if (!url) return '';
        if (/[?&](?:_clbiBust|_|_entryRev)=/.test(url)) return url;
        if (ref && window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
            return window.RevisionManifest.addRevisionParam(url, ref);
        }
        return url;
    }


     Object.keys(source).forEach(function (key) {
     function fetchNationJson(url, fallbackRef) {
         var entry = source[key];
         var ref = String(extractRawTitleFromUrl(url) || fallbackRef || '').trim();
        var page = String((entry && entry.page) || key || '').trim();
         var requestUrl;
         if (!page) return;
         if (ref && window.EntryStore && typeof window.EntryStore.fetchJsonRef === 'function') {
         if (typeof entry === 'string') {
             return PanelPerf.measure('fetch nation json via EntryStore', { ref: ref }, function () {
             map[page] = {
                 return window.EntryStore.fetchJsonRef(ref, { noStore: false });
                page: page,
             });
                name_ko: entry,
                wiki_title: entry,
                enabled: true
            };
        } else {
            map[page] = Object.assign({
                page: page,
                name_ko: '',
                wiki_title: '',
                 enabled: true,
                note: ''
             }, entry || {}, { page: page });
         }
         }
     });
        requestUrl = addNationCacheBust(url, ref);
        return PanelPerf.measure('fetch nation json via fetch', { ref: ref, url: requestUrl }, function () {
            return fetch(requestUrl, { credentials: 'same-origin', cache: 'force-cache' })
                .then(function (res) {
                    if (!res.ok) throw new Error('HTTP ' + res.status);
                    return res.text();
                })
                .then(function (text) {
                    var trimmed = String(text || '').trim();
                    if (!trimmed) throw new Error('empty JSON response: ' + requestUrl);
                    return JSON.parse(trimmed);
                });
        });
    }
 
     function escapeHtml(value) {
        return String(value == null ? '' : value)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }
 
    function encodeWikiTitle(title) {
        return String(title || '')
            .trim()
            .replace(/ /g, '_')
            .split('/')
            .map(function (part) { return encodeURIComponent(part); })
            .join('/');
    }
 
    function normalizeNationFile(value) {
        var text = String(value || '').trim();
        var titleMatch;
        var redirectMatch;
        var previous;
        var i;
        var j;
 
        if (!text) return '';
 
        text = text
            .replace(/^\[\[:?\s*/i, '')
            .replace(/\]\]$/i, '')
            .trim();
 
        for (i = 0; i < 8; i += 1) {
            previous = text;
 
            for (j = 0; j < 3; j += 1) {
                try {
                    if (/%[0-9a-f]{2}/i.test(text)) {
                        text = decodeURIComponent(text);
                    }
                } catch (err) {
                    break;
                }
            }
 
            text = text.replace(/\\/g, '/').trim();
 
            titleMatch = text.match(/[?&]title=([^&#]+)/i);
            if (titleMatch) {
                text = titleMatch[1];
                continue;
            }
 
            text = text
                .replace(/[?#].*$/g, '')
                .replace(/^https?:\/\/[^/]+/i, '')
                .replace(/^\/+/, '')
                .replace(/^index\.php\/?/i, '')
                .replace(/^wiki\/?/i, '')
                .trim();


    return map;
            for (j = 0; j < 6; j += 1) {
}
                var stripped = text
                    .replace(/^\s*파일\s*:/i, '')
                    .replace(/^\s*File\s*:/i, '')
                    .replace(/^\s*이미지\s*:/i, '')
                    .replace(/^\s*Image\s*:/i, '')
                    .replace(/^:+/, '')
                    .trim();
                if (stripped === text) break;
                text = stripped;
            }


function encodeWikiTitle(title) {
            redirectMatch = text.match(/^(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
    return String(title || '')
                text.match(/^(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
        .trim()
                text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i);
        .replace(/ /g, '_')
        .split('/')
        .map(function (part) { return encodeURIComponent(part); })
        .join('/');
}


function normalizeLonDelta(a, b) {
            if (redirectMatch) {
    var d = b - a;
                text = redirectMatch[1].trim();
    if (d > 180) d -= 360;
                continue;
    if (d < -180) d += 360;
            }
    return d;
}


function pushLineSegment(THREE, positions, a, b, radius) {
            if (text === previous) break;
    if (!a || !b || a.length < 2 || b.length < 2) return;
        }


    var lonA = toNumber(a[0], 0);
        text = text.replace(/^\/+/, '').trim();
    var latA = toNumber(a[1], 0);
    var lonB = toNumber(b[0], 0);
    var latB = toNumber(b[1], 0);
    var dLon = normalizeLonDelta(lonA, lonB);
    var dLat = latB - latA;
    var steps = Math.max(1, Math.ceil(Math.max(Math.abs(dLon), Math.abs(dLat)) / 3));
    var i;


    for (i = 0; i < steps; i += 1) {
         return text;
         var t0 = i / steps;
    }
        var t1 = (i + 1) / steps;
        var p0 = lonLatToVector3(THREE, lonA + dLon * t0, latA + dLat * t0, radius);
        var p1 = lonLatToVector3(THREE, lonA + dLon * t1, latA + dLat * t1, radius);


         positions.push(p0.x, p0.y, p0.z, p1.x, p1.y, p1.z);
    function normalizeNationFileKey(file) {
         return normalizeNationFile(file).replace(/_/g, ' ').trim().toLowerCase();
     }
     }
}


function pushLineString(THREE, positions, coords, radius) {
    function getWikiApiUrl(params) {
    var i;
        var query = [];
    if (!Array.isArray(coords) || coords.length < 2) return;
        Object.keys(params || {}).forEach(function (key) {
            if (params[key] == null) return;
            query.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(params[key])));
        });
        return (window.mw && mw.util && mw.util.wikiScript ? mw.util.wikiScript('api') : '/api.php') + '?' + query.join('&');
    }


     for (i = 0; i < coords.length - 1; i += 1) {
     function getWikiApiEndpoint() {
         pushLineSegment(THREE, positions, coords[i], coords[i + 1], radius);
         return (window.mw && mw.util && mw.util.wikiScript ? mw.util.wikiScript('api') : '/api.php');
     }
     }
}


function pushPolygonRings(THREE, positions, rings, radius) {
    function fetchWikiApi(params) {
    if (!Array.isArray(rings)) return;
        var body;
 
        try {
            body = new URLSearchParams();
            Object.keys(params || {}).forEach(function (key) {
                if (params[key] == null) return;
                body.append(key, String(params[key]));
            });
            return fetch(getWikiApiEndpoint(), {
                method: 'POST',
                credentials: 'same-origin',
                cache: 'no-store',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                },
                body: body.toString()
            }).then(function (res) {
                if (!res.ok) throw new Error('wiki api failed: ' + res.status);
                return res.json();
            });
        } catch (err) {
            return fetch(getWikiApiUrl(params), { credentials: 'same-origin', cache: 'no-store' }).then(function (res) {
                if (!res.ok) throw new Error('wiki api failed: ' + res.status);
                return res.json();
            });
        }
    }


     rings.forEach(function (ring) {
     function buildNationFileUrl(file) {
         pushLineString(THREE, positions, ring, radius);
         var clean = normalizeNationFile(file);
    });
        var key = normalizeNationFileKey(clean);
}
        var resolved;


function pushGeometryLines(THREE, positions, geometry, radius) {
        if (!clean || !key) return '';
    if (!geometry) return;


    if (geometry.type === 'LineString') {
        /*
        pushLineString(THREE, positions, geometry.coordinates, radius);
          Render nation-list flags through MediaWiki's own file thumbnail pipeline.
         return;
          This mirrors [[파일:Name.svg|16px]] far more closely than using the raw
          SVG URL or Special:Redirect/file path directly. If MediaWiki cannot
          resolve the file title, return an empty URL and hide the flag slot.
        */
        resolved = nationResolvedFileUrlCache[key];
        /* Entry full-load prewarms the exact thumbnail URL.  Adding a Date-based cache
          buster here would create a new URL and make the preloaded flag useless.
          When EntryCache has a same-revision blob URL, render that blob URL so a
          second tab consumes the persistent cache instead of relying on a fresh <img>
          request path. */
        if (resolved && window.EntryStore && typeof window.EntryStore.getImageDisplayUrl === 'function') {
            return window.EntryStore.getImageDisplayUrl(resolved) || resolved;
        }
         return resolved || '';
     }
     }


     if (geometry.type === 'MultiLineString') {
     function collectNationFlagFileNames(data, options) {
         geometry.coordinates.forEach(function (line) {
        var files = [];
             pushLineString(THREE, positions, line, radius);
        var continentKeys = options && options.continentKeys;
 
        function addFlag(flag) {
            var file = normalizeNationFile(flag && (flag.file || flag.flag_file || flag.flag || flag.flag_title || flag));
            if (file) files.push(file);
        }
        function scanItems(items) {
            (items || []).forEach(function (item) {
                if (!item || typeof item !== 'object') return;
                if (Array.isArray(item.flags)) item.flags.forEach(addFlag);
                if (Array.isArray(item.sybs)) item.sybs.forEach(addFlag);
                if (Array.isArray(item.symbols)) item.symbols.forEach(addFlag);
                addFlag(item.flag_file || item.flag || item.flag_title || '');
                addFlag(item.syb_file || item.syb || item.symbol_file || item.symbol || '');
            });
        }
         (data && data.continents || []).forEach(function (continent) {
             var key = String((continent && continent.key) || '').trim();
            if (continentKeys && key && !continentKeys[key]) return;
            if (continentKeys && !key) return;
            (continent.regions || []).forEach(function (region) {
                scanItems(region.items || []);
            });
         });
         });
         return;
         return unique(files);
    }
 
    function getInitialContinentKey(panel, data) {
        var activeTab = panel && panel.querySelector ? panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]') : null;
        var activeKey = activeTab ? String(activeTab.getAttribute('data-continent') || '').trim() : '';
        var continents = data && Array.isArray(data.continents) ? data.continents : [];
 
        if (!activeKey && continents.length) activeKey = String(continents[0].key || '').trim();
        return activeKey;
     }
     }


     if (geometry.type === 'Polygon') {
     function collectInitialNationFlagFileNames(data, panel) {
         pushPolygonRings(THREE, positions, geometry.coordinates, radius);
        var activeKey = getInitialContinentKey(panel, data);
         return;
        var map = {};
 
         if (!activeKey) return collectNationFlagFileNames(data);
        map[activeKey] = true;
         return collectNationFlagFileNames(data, { continentKeys: map });
     }
     }


     if (geometry.type === 'MultiPolygon') {
     function subtractNationFlagFiles(allFiles, initialFiles) {
         geometry.coordinates.forEach(function (polygon) {
        var initial = {};
             pushPolygonRings(THREE, positions, polygon, radius);
 
         (initialFiles || []).forEach(function (file) {
             var key = normalizeNationFileKey(file);
            if (key) initial[key] = true;
         });
         });
         return;
 
         return unique((allFiles || []).filter(function (file) {
            var key = normalizeNationFileKey(file);
            return key && !initial[key];
        }));
     }
     }


     if (geometry.type === 'GeometryCollection') {
     function resolveNationFlagFileUrls(files) {
         geometry.geometries.forEach(function (child) {
         var cleanFiles = unique((files || []).map(normalizeNationFile).filter(Boolean));
             pushGeometryLines(THREE, positions, child, radius);
 
        cleanFiles.forEach(function (file) {
             var key = normalizeNationFileKey(file);
            var readyUrl = window.EntryStore && typeof window.EntryStore.getFlagUrl === 'function' ? window.EntryStore.getFlagUrl(file) : '';
            if (key && readyUrl && nationResolvedFileUrlCache[key] === undefined) nationResolvedFileUrlCache[key] = readyUrl;
         });
         });
    }
}


function extractGeojsonFromPayload(payload, topojson, objectName) {
        var pending = cleanFiles.filter(function (file) {
    if (!payload) throw new Error('empty data');
            var key = normalizeNationFileKey(file);
            return key && nationResolvedFileUrlCache[key] === undefined;
        });
        var chunks = [];


    if (payload.type === 'Topology') {
        while (pending.length) chunks.push(pending.splice(0, 25));
         if (!topojson || typeof topojson.mesh !== 'function') {
         if (!chunks.length) {
             throw new Error('topojson-client unavailable');
             PanelPerf.mark('resolve nation flag URLs', { count: cleanFiles.length, pending: 0, cached: cleanFiles.length });
            return Promise.resolve(nationResolvedFileUrlCache);
         }
         }


         var objects = payload.objects || {};
         return PanelPerf.measure('resolve nation flag URLs', { count: cleanFiles.length, pending: pending.length, chunks: chunks.length }, function () {
        var target = objects[objectName] || objects.countries || objects.land || objects[Object.keys(objects)[0]];
            return Promise.all(chunks.map(function (chunk) {
            var titleToKey = {};
            var titles = [];
 
            chunk.forEach(function (file) {
                var key = normalizeNationFileKey(file);
                var canonical = 'File:' + file;
                var local = '파일:' + file;
 
                if (!key) return;
                titleToKey[normalizeNationFileKey(canonical)] = key;
                titleToKey[normalizeNationFileKey(local)] = key;
                titles.push(canonical);
                titles.push(local);
            });
 
            return fetchWikiApi({
                action: 'query',
                format: 'json',
                formatversion: '2',
                redirects: '1',
                prop: 'imageinfo',
                iiprop: 'url|sha1|timestamp|size',
                iiurlwidth: '16',
                titles: titles.join('|')
            }).then(function (json) {
                var pages = (json && json.query && json.query.pages) || [];
                var seen = {};


        if (!target) throw new Error('topology object not found');
                pages.forEach(function (page) {
                    var file = normalizeNationFile(page && page.title);
                    var pageKey = normalizeNationFileKey(file);
                    var originalKey = titleToKey[pageKey] || pageKey;
                    var imageinfo = page && page.imageinfo && page.imageinfo[0];
                    if (!originalKey) return;
                    seen[originalKey] = true;
                    if (imageinfo && (imageinfo.thumburl || imageinfo.url)) {
                        var resolvedUrl = imageinfo.thumburl || imageinfo.url;
                        var rev = imageinfo.sha1 || imageinfo.timestamp || imageinfo.size || '';
                        var sep;
                        if (rev && !/[?&]_entryFileRev=/.test(resolvedUrl)) {
                            sep = resolvedUrl.indexOf('?') === -1 ? '?' : '&';
                            resolvedUrl += sep + '_entryFileRev=' + encodeURIComponent(String(rev));
                        }
                        nationResolvedFileUrlCache[originalKey] = resolvedUrl;
                        if (pageKey) nationResolvedFileUrlCache[pageKey] = resolvedUrl;
                        if (window.EntryStore && typeof window.EntryStore.setFlagUrl === 'function') window.EntryStore.setFlagUrl(file, resolvedUrl);
                    } else if (nationResolvedFileUrlCache[originalKey] === undefined) {
                        nationResolvedFileUrlCache[originalKey] = '';
                    }
                });


        return topojson.mesh(payload, target, function (a, b) {
                chunk.forEach(function (file) {
             return a !== b;
                    var key = normalizeNationFileKey(file);
                    if (key && !seen[key] && nationResolvedFileUrlCache[key] === undefined) {
                        nationResolvedFileUrlCache[key] = '';
                    }
                });
             }).catch(function () {
                chunk.forEach(function (file) {
                    var key = normalizeNationFileKey(file);
                    if (key && nationResolvedFileUrlCache[key] === undefined) nationResolvedFileUrlCache[key] = '';
                });
            });
            })).then(function () {
                return nationResolvedFileUrlCache;
            });
         });
         });
     }
     }


     if (payload.type === 'FeatureCollection') {
 
         return payload;
     function isLegacyNationIndexFileUrl(src) {
        src = String(src || '');
        if (!src) return false;
        if (!/\.svg(?:[?#]|$)/i.test(src)) return false;
        if (/\/images\//i.test(src)) return false;
         return /(?:Special|%ED%8A%B9%EC%88%98|특수).*?(?:Redirect|%EB%84%98%EA%B2%A8%EC%A3%BC%EA%B8%B0|넘겨주기).*?file/i.test(src) || /\/index\.php\//i.test(src);
     }
     }


     if (payload.type === 'Feature') {
     function purgeLegacyNationIndexFileImages(scope) {
        return {
        var root = scope || document;
             type: 'FeatureCollection',
        toArray(root.querySelectorAll ? root.querySelectorAll('.clbi-nations-tabpanel-body img, .clbi-nations-tabpanel img') : []).forEach(function (img) {
             features: [payload]
            var src = img.currentSrc || img.getAttribute('src') || '';
         };
            if (!isLegacyNationIndexFileUrl(src)) return;
             img.removeAttribute('src');
             img.setAttribute('data-clbi-legacy-src-removed', src);
         });
     }
     }


     if (payload.type && payload.coordinates) {
     function getNationFlagOffsetY(flag) {
         return payload;
        var value;
 
        if (!flag || typeof flag !== 'object') return 0;
 
        value = flag.offsetY;
        if (value == null) value = flag.offset_y;
        if (value == null) value = flag.flagOffsetY;
        if (value == null && flag.decorator === 'top') value = -1;
 
        value = parseFloat(value);
        if (!isFinite(value)) value = 0;
 
         return Math.max(-3, Math.min(3, Math.round(value)));
     }
     }


     throw new Error('unsupported map data');
     function buildNationPageUrl(page) {
}
        page = String(page || '').trim();
        return page ? '/index.php/' + encodeWikiTitle(page) : '';
    }


function buildLineGeometryFromGeojson(THREE, geojson, radius) {
    function getPanelEra(panel) {
    var positions = [];
        var eraPanel = panel && panel.closest ? panel.closest('.clbi-nations-era-content[data-era-content]') : null;
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;


    if (geojson.type === 'FeatureCollection') {
        return (eraPanel && eraPanel.getAttribute('data-era-content')) ||
        geojson.features.forEach(function (feature) {
             (panel && panel.getAttribute('data-era')) ||
             pushGeometryLines(THREE, positions, feature.geometry, radius);
            (stack && (stack.getAttribute('data-nations-current-era') || stack.getAttribute('data-era'))) ||
        });
            '1950';
    } else if (geojson.type === 'Feature') {
        pushGeometryLines(THREE, positions, geojson.geometry, radius);
    } else {
        pushGeometryLines(THREE, positions, geojson, radius);
     }
     }


     var geometry = new THREE.BufferGeometry();
     function getNationListUrl(panel, era) {
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
    return {
        var template = (panel && panel.getAttribute('data-nation-list-url-template')) ||
        geometry: geometry,
            (stack && stack.getAttribute('data-nation-list-url-template')) ||
        segmentCount: positions.length / 6
            '/index.php?title=MediaWiki:{year}_Nation_List.json&action=raw&ctype=application/json';
    };
}


function createEarthTexture(THREE) {
        return (panel && panel.getAttribute('data-nation-list-url')) || replaceYearTemplate(template, era);
    var canvas = document.createElement('canvas');
     }
     var size = 1024;
    var ctx;
    var gradient;
    var i;


    canvas.width = size;
    canvas.height = size / 2;
    ctx = canvas.getContext('2d');


     ctx.fillStyle = '#08111a';
     function getNationLinkMapUrl(panel, era) {
    ctx.fillRect(0, 0, canvas.width, canvas.height);
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
        var template = (panel && panel.getAttribute('data-nation-link-map-url-template')) ||
            (stack && stack.getAttribute('data-nation-link-map-url-template')) ||
            '/index.php?title=MediaWiki:{year}_Nation_Link_Map.json&action=raw&ctype=application/json';


    gradient = ctx.createRadialGradient(size * 0.42, size * 0.20, 10, size * 0.45, size * 0.28, size * 0.50);
        return (panel && panel.getAttribute('data-nation-link-map-url')) || replaceYearTemplate(template, era);
    gradient.addColorStop(0, 'rgba(100,135,160,0.38)');
     }
    gradient.addColorStop(0.28, 'rgba(32,57,74,0.28)');
    gradient.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.fillStyle = gradient;
     ctx.fillRect(0, 0, canvas.width, canvas.height);


     ctx.strokeStyle = 'rgba(255,255,255,0.030)';
     function getHistoryEventsUrl(panel, era) {
    ctx.lineWidth = 1;
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
        var template = (panel && panel.getAttribute('data-history-events-url-template')) ||
            (stack && stack.getAttribute('data-history-events-url-template')) ||
            '/index.php?title=MediaWiki:{year}_History_Events.json&action=raw&ctype=application/json';


    for (i = 0; i <= 12; i += 1) {
         return (panel && panel.getAttribute('data-history-events-url')) || replaceYearTemplate(template, era);
         var y = Math.round(i * canvas.height / 12) + 0.5;
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(canvas.width, y);
        ctx.stroke();
     }
     }


     for (i = 0; i <= 24; i += 1) {
     function makeDefaultHistoryYears(era) {
        var x = Math.round(i * canvas.width / 24) + 0.5;
        var start = parseInt(era || '1950', 10);
        ctx.beginPath();
        var years = [];
        ctx.moveTo(x, 0);
        var i;
         ctx.lineTo(x, canvas.height);
 
         ctx.stroke();
        if (!isFinite(start)) start = 1950;
        for (i = 0; i < 10; i += 1) {
            years.push({ year: String(start + i), label: String(start + i), items: [] });
         }
         return years;
     }
     }


     for (i = 0; i < 220; i += 1) {
     function normalizeHistoryEventItem(item) {
         var px = Math.random() * canvas.width;
         item = item && typeof item === 'object' ? item : {};
         var py = Math.random() * canvas.height;
         return {
        var r = 1 + Math.random() * 2.8;
            period: String(item.period || item.date || item.range || item.year || '').trim(),
        ctx.fillStyle = Math.random() > 0.5 ? 'rgba(80,105,93,0.18)' : 'rgba(95,83,70,0.16)';
            title: String(item.title || item.event || item.name || item.label || '').trim(),
        ctx.beginPath();
            page: String(item.page || item.wiki_title || item.title_page || '').trim(),
        ctx.ellipse(px, py, r * 6, r * 1.7, Math.random() * Math.PI, 0, Math.PI * 2);
            external_url: String(item.external_url || item.url || '').trim(),
         ctx.fill();
            major: item.major === true || item.type === 'major',
            note: String(item.note || '').trim(),
            enabled: item.enabled !== false
         };
     }
     }


     var texture = new THREE.CanvasTexture(canvas);
     function normalizeHistoryEvents(payload, era) {
    texture.colorSpace = THREE.SRGBColorSpace;
        var sourceYears = [];
    texture.needsUpdate = true;
        var normalized;
    return texture;
}


        if (payload && Array.isArray(payload.years)) {
            sourceYears = payload.years;
        } else if (payload && payload.events && typeof payload.events === 'object') {
            Object.keys(payload.events).forEach(function (year) {
                sourceYears.push({ year: year, label: year, items: payload.events[year] });
            });
        }


function loadImageTexture(THREE, url) {
        normalized = sourceYears.map(function (row) {
    return new Promise(function (resolve, reject) {
            var year;
        var loader = new THREE.TextureLoader();
            var items;


        loader.load(
             if (typeof row === 'string' || typeof row === 'number') {
             url,
                 row = { year: String(row), items: [] };
            function (texture) {
                texture.colorSpace = THREE.SRGBColorSpace;
                texture.wrapS = THREE.RepeatWrapping;
                texture.wrapT = THREE.ClampToEdgeWrapping;
                texture.anisotropy = 4;
                 texture.needsUpdate = true;
                resolve(texture);
            },
            undefined,
            function (err) {
                reject(err || new Error('texture load failed: ' + url));
             }
             }
        );
            row = row && typeof row === 'object' ? row : {};
    });
            year = String(row.year || row.key || row.label || '').trim();
}
            items = Array.isArray(row.items) ? row.items : (Array.isArray(row.events) ? row.events : []);


function loadImageElement(url) {
            return {
    return new Promise(function (resolve, reject) {
                year: year,
         var image = new Image();
                label: String(row.label || year).trim() || year,
                items: items.map(normalizeHistoryEventItem).filter(function (item) {
                    return item.enabled !== false;
                })
            };
        }).filter(function (row) {
            return !!row.year;
         });


         image.crossOrigin = 'anonymous';
         if (!normalized.length) normalized = makeDefaultHistoryYears(era);
        image.decoding = 'async';


         image.onload = function () {
         return {
             resolve(image);
            version: payload && payload.version ? payload.version : 1,
            era: payload && (payload.era || payload.year) ? String(payload.era || payload.year) : String(era || '1950'),
             description: payload && payload.description ? String(payload.description) : '',
            years: normalized
         };
         };
    }
    function fetchHistoryEvents(panel, era) {
        var url = getHistoryEventsUrl(panel, era);
        var cacheKey = era + '|' + url;
        var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_History_Events.json');
        if (historyEventsCache[cacheKey]) return historyEventsCache[cacheKey];
        if (cached) {
            historyEventsCache[cacheKey] = Promise.resolve(normalizeHistoryEvents(cached, era));
            return historyEventsCache[cacheKey];
        }
        historyEventsCache[cacheKey] = fetchNationJson(url, 'MediaWiki:' + era + '_History_Events.json')
            .then(function (payload) {
                return normalizeHistoryEvents(payload, era);
            });
        return historyEventsCache[cacheKey];
    }
    function buildHistoryPageUrl(page) {
        page = String(page || '').trim();
        return page ? '/index.php/' + encodeWikiTitle(page) : '';
    }
    function renderInlineWikiLinks(text) {
        var source = String(text || '');
        var out = '';
        var lastIndex = 0;
        var match;
        var pattern = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
        pattern.lastIndex = 0;
        while ((match = pattern.exec(source))) {
            var rawPage = String(match[1] || '').trim();
            var rawLabel = String(match[2] || rawPage).trim();
            out += escapeHtml(source.slice(lastIndex, match.index));
            if (rawPage) {
                out += '<a href="' + escapeHtml(buildHistoryPageUrl(rawPage)) + '">' + escapeHtml(rawLabel) + '</a>';
            } else {
                out += escapeHtml(match[0]);
            }
            lastIndex = pattern.lastIndex;
        }
        out += escapeHtml(source.slice(lastIndex));
        return out;
    }
    function hasInlineWikiLink(text) {
        return /\[\[[^\]]+\]\]/.test(String(text || ''));
    }
    function renderHistoryEventTitle(item) {
        var title = item.title || item.page || item.external_url || '미지정';
        var href;


         image.onerror = function () {
         if (hasInlineWikiLink(title)) return renderInlineWikiLinks(title);
            reject(new Error('image load failed: ' + url));
 
        };
        href = item.external_url || buildHistoryPageUrl(item.page || '');
        return href ? '<a href="' + escapeHtml(href) + '">' + escapeHtml(title) + '</a>' : escapeHtml(title);
    }


         image.src = url;
    function renderHistoryEventItem(item) {
    });
         var classes = 'clbi-nations-history-item' + (item.major ? ' clbi-nations-history-item-major' : '');
}
        var nameHtml = renderHistoryEventTitle(item);


function createCompositedEarthTextureLayers(THREE, baseUrl, layers, maxWidth) {
        return '<div class="' + classes + '">' +
    var effectiveLayers = Array.isArray(layers) ? layers.filter(function (layer) {
            '<span class="clbi-nations-history-period">' + escapeHtml(item.period || '') + '</span>' +
         return !!(layer && layer.url);
            '<span class="clbi-nations-history-event-name">' + nameHtml + '</span>' +
     }) : [];
         '</div>';
     }


     return loadImageElement(baseUrl).then(function (base) {
     function renderHistoryEmpty() {
         return Promise.all(effectiveLayers.map(function (layer) {
         return '<div class="clbi-nations-history-empty"><span class="clbi-nations-history-empty-ko">작업 중...</span><span class="clbi-nations-history-empty-en">Working in Progress...</span></div>';
            return loadImageElement(layer.url)
    }
                .then(function (image) {
                    return {
                        layer: layer,
                        image: image
                    };
                })
                .catch(function (err) {
                    if (window.console && console.warn) {
                        console.warn('CLBI earth composite layer skipped:', {
                            label: layer.label || 'overlay',
                            url: layer.url,
                            error: err
                        });
                    }
                    return null;
                });
        })).then(function (loadedLayers) {
            var width = Math.max(1024, Math.min(8192, Math.floor(maxWidth || 4096)));
            var height = Math.max(512, Math.floor(width / 2));
            var canvas = document.createElement('canvas');
            var ctx;
            var texture;


            canvas.width = width;
    function renderHistoryPanel(panel, data) {
            canvas.height = height;
        var body = panel && panel.querySelector ? panel.querySelector('.clbi-nations-history-body') : null;
            ctx = canvas.getContext('2d');
        var years = data && Array.isArray(data.years) ? data.years : [];
        var activeTab = panel.querySelector('.clbi-nations-history-year-button.is-active[data-year], .clbi-nations-history-year-button[aria-selected="true"][data-year]');
        var activeYear = activeTab ? activeTab.getAttribute('data-year') : '';
        var pages;
        var tabs;


            ctx.imageSmoothingEnabled = true;
        if (!body || !years.length) return;
            ctx.imageSmoothingQuality = 'high';
        if (!activeYear || !years.some(function (row) { return row.year === activeYear; })) activeYear = years[0].year;


             ctx.clearRect(0, 0, width, height);
        pages = years.map(function (row) {
             ctx.globalCompositeOperation = 'source-over';
            var active = row.year === activeYear;
             ctx.globalAlpha = 1;
             var items = (row.items || []).filter(function (item) { return item && item.enabled !== false; });
            ctx.drawImage(base, 0, 0, width, height);
             return '<div class="clbi-nations-history-list clbi-nations-history-page' + (active ? ' is-active' : '') + '" data-year-panel="' + escapeHtml(row.year) + '"' + (active ? '' : ' hidden="hidden"') + '>' +
                (items.length ? items.map(renderHistoryEventItem).join('') : renderHistoryEmpty()) +
             '</div>';
        }).join('');


            loadedLayers.forEach(function (entry) {
        tabs = years.map(function (row) {
                var layer;
            var active = row.year === activeYear;
                var opacity;
            return '<span class="clbi-nations-history-year-button' + (active ? ' is-active' : '') + '" role="tab" tabindex="' + (active ? '0' : '-1') + '" aria-selected="' + (active ? 'true' : 'false') + '" data-year="' + escapeHtml(row.year) + '">' + escapeHtml(row.label || row.year) + '</span>';
                var blend;
        }).join('');


                if (!entry || !entry.image) return;
        body.innerHTML = '<div class="clbi-nations-history-pages">' + pages + '</div>' +
            '<div class="clbi-nations-history-year-well"><div class="clbi-nations-history-year-list" role="tablist" aria-label="역사적 사건 연도 선택">' + tabs + '</div></div>';


                layer = entry.layer;
        panel.removeAttribute('data-nations-history-ready');
                opacity = Math.max(0, Math.min(1.4, layer.opacity == null ? 1 : layer.opacity));
        panel.classList.add('clbi-nations-history-json-ready');
                blend = String(layer.blend || 'source-over').toLowerCase();
        panel.setAttribute('data-history-events-year-loaded', String(data.era || getPanelEra(panel)));
        initHistoryYearPanel(panel);
        scheduleHistoryItemSlides(panel);
    }


                ctx.globalCompositeOperation = blend;
    function initHistoryEventsPanel(panel) {
                ctx.globalAlpha = opacity;
        var era;
                if (layer.filter && typeof ctx.filter !== 'undefined') {
        var promise;
                    ctx.filter = String(layer.filter);
                } else if (typeof ctx.filter !== 'undefined') {
                    ctx.filter = 'none';
                }
                ctx.drawImage(entry.image, 0, 0, width, height);
            });


            ctx.globalCompositeOperation = 'source-over';
        if (!panel || isNationListPanelHidden(panel)) return Promise.resolve(null);
            ctx.globalAlpha = 1;
        if (panel.getAttribute('data-history-events-year-loaded') === getPanelEra(panel)) return Promise.resolve(panel);
            if (typeof ctx.filter !== 'undefined') {
        if (panel.getAttribute('data-history-events-loading') === '1' && panel.HistoryEventsLoadPromise) return panel.HistoryEventsLoadPromise;
                ctx.filter = 'none';
            }


            texture = new THREE.CanvasTexture(canvas);
        era = getPanelEra(panel);
            texture.colorSpace = THREE.SRGBColorSpace;
        panel.setAttribute('data-history-events-loading', '1');
            texture.wrapS = THREE.RepeatWrapping;
            texture.wrapT = THREE.ClampToEdgeWrapping;
            texture.anisotropy = 6;
            texture.needsUpdate = true;


             return texture;
        promise = fetchHistoryEvents(panel, era).then(function (data) {
            renderHistoryPanel(panel, data);
            return panel;
        }).catch(function () {
            panel.classList.add('clbi-nations-history-json-fallback');
             return panel;
        }).finally(function () {
            panel.removeAttribute('data-history-events-loading');
            panel.HistoryEventsLoadPromise = null;
         });
         });
    });
}


function createFallbackPlanetTexture(THREE, colorA, colorB) {
        panel.HistoryEventsLoadPromise = promise;
    var canvas = document.createElement('canvas');
        return promise;
    var size = 512;
     }
    var ctx;
     var gradient;
    var i;


     canvas.width = size;
     function getEntryStoreJson(url) {
    canvas.height = size / 2;
        if (!window.EntryStore || typeof window.EntryStore.getJsonSync !== 'function') return null;
    ctx = canvas.getContext('2d');
        return window.EntryStore.getJsonSync(url);
    }


     gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
     function normalizeNationTag(value) {
    gradient.addColorStop(0, colorA || '#777777');
        value = String(value == null ? '' : value).trim().toUpperCase();
     gradient.addColorStop(1, colorB || '#151515');
        return /^[A-Z0-9]{3}$/.test(value) ? value : '';
     }


     ctx.fillStyle = gradient;
     function normalizeLookupKey(value) {
    ctx.fillRect(0, 0, canvas.width, canvas.height);
        return String(value == null ? '' : value)
            .trim()
            .replace(/_/g, ' ')
            .replace(/\s+/g, ' ')
            .toLowerCase();
    }


     for (i = 0; i < 18; i += 1) {
     function getNationItemTag(item) {
         ctx.fillStyle = i % 2 ? 'rgba(255,255,255,0.055)' : 'rgba(0,0,0,0.095)';
         if (!item || typeof item !== 'object') return '';
         ctx.fillRect(0, Math.round(i * canvas.height / 18), canvas.width, Math.max(1, Math.round(canvas.height / 46)));
         return normalizeNationTag(item.tag || item.code || item.map_tag || item.mapTag || '');
     }
     }


     var texture = new THREE.CanvasTexture(canvas);
     function collectNationItemFlags(item) {
    texture.colorSpace = THREE.SRGBColorSpace;
        var flags = [];
    texture.wrapS = THREE.RepeatWrapping;
 
    texture.wrapT = THREE.ClampToEdgeWrapping;
        if (!item || typeof item !== 'object') return flags;
    texture.needsUpdate = true;
 
    return texture;
        if (Array.isArray(item.flags)) {
}
            item.flags.forEach(function (flag) {
                if (!flag) return;
                if (typeof flag === 'string') {
                    flag = { file: flag };
                }
                if (typeof flag !== 'object') return;
                if (normalizeNationFile(flag.file || flag.flag_file || flag.flag || '')) {
                    flags.push(flag);
                }
            });
        }
 
        if (!flags.length && (item.flag_file || item.flag || item.flag_title)) {
            flags.push({
                file: item.flag_file || item.flag || item.flag_title,
                width: item.flag_width || '16px',
                border: item.border !== false,
                offsetMode: item.offsetMode || item.offset_mode || 'auto',
                offsetY: item.offsetY || item.offset_y || 0
            });
        }


function createSaturnRingTexture(THREE) {
        return flags;
    var canvas = document.createElement('canvas');
     }
    var size = 512;
    var ctx;
    var image;
    var data;
    var cx = size * 0.5;
    var cy = size * 0.5;
    var maxR = size * 0.5;
     var texture;
    var x;
    var y;


     canvas.width = size;
     function collectNationItemSybs(item) {
    canvas.height = size;
        var sybs = [];
    ctx = canvas.getContext('2d');
    image = ctx.createImageData(size, size);
    data = image.data;


    for (y = 0; y < size; y += 1) {
         if (!item || typeof item !== 'object') return sybs;
         for (x = 0; x < size; x += 1) {
            var dx = (x - cx) / maxR;
            var dy = (y - cy) / maxR;
            var r = Math.sqrt(dx * dx + dy * dy);
            var idx = (y * size + x) * 4;
            var band;
            var gap;
            var alpha;
            var warm;


             if (r < 0.200 || r > 0.500) {
        function addSymbol(symbol) {
                 data[idx + 3] = 0;
             if (!symbol) return;
                continue;
            if (typeof symbol === 'string') symbol = { file: symbol };
            if (typeof symbol !== 'object') return;
            if (normalizeNationFile(symbol.file || symbol.syb_file || symbol.syb || symbol.symbol_file || symbol.symbol || '')) {
                 sybs.push(symbol);
             }
             }
        }


            band = 0.58
        if (Array.isArray(item.sybs)) item.sybs.forEach(addSymbol);
                + 0.22 * Math.sin(r * 92.0)
        if (Array.isArray(item.symbols)) item.symbols.forEach(addSymbol);
                + 0.13 * Math.sin(r * 177.0 + 0.6)
                + 0.07 * Math.sin(r * 317.0 - 1.1);
            gap = (r > 0.365 && r < 0.386) ? 0.22 : 1.0;
            alpha = Math.max(0, Math.min(1, (band * 1.28 + 0.08) * gap));
            warm = Math.max(0, Math.min(1, (0.50 - r) / 0.300));


            data[idx + 0] = Math.round(216 + warm * 38);
        if (!sybs.length && (item.syb_file || item.syb || item.symbol_file || item.symbol)) {
             data[idx + 1] = Math.round(186 + warm * 44);
             sybs.push({
            data[idx + 2] = Math.round(126 + warm * 48);
                file: item.syb_file || item.syb || item.symbol_file || item.symbol,
             data[idx + 3] = Math.round(alpha * 248);
                width: item.syb_width || item.symbol_width || '16px',
                border: item.syb_border === true || item.symbol_border === true
             });
         }
         }
        return sybs;
     }
     }


     ctx.putImageData(image, 0, 0);
     function collectNationItemSymbols(item) {
    texture = new THREE.CanvasTexture(canvas);
        var symbols = [];
    texture.colorSpace = THREE.SRGBColorSpace;
        var legacySeen = {};
    texture.needsUpdate = true;
 
    return texture;
        function add(symbol, dedupeLegacy) {
}
            var file = normalizeNationSymbolFile(symbol);
            var key = normalizeNationFileKey(file);
            var out;
 
            if (!file) return;
            if (dedupeLegacy && key) {
                if (legacySeen[key]) return;
                legacySeen[key] = true;
            }
 
            if (typeof symbol === 'string') {
                out = { file: file };
            } else {
                out = Object.assign({}, symbol || {});
                out.file = file;
            }


            if (out.border == null && out.symbol_border != null) out.border = out.symbol_border !== false;
            if (out.border == null && out.syb_border != null) out.border = out.syb_border === true;
            if (out.border == null) out.border = !isNationSybFile(file);


function configureSaturnRingMap(THREE, texture) {
            symbols.push(out);
    if (!texture) return texture;
        }


    texture.colorSpace = THREE.SRGBColorSpace;
        if (!item || typeof item !== 'object') return symbols;
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping;
    texture.anisotropy = 6;
    texture.needsUpdate = true;
    return texture;
}


function loadSaturnRingTexture(THREE, url) {
        if (Array.isArray(item.symbols)) {
    return loadLimitedImageTexture(THREE, url, 2048).then(function (texture) {
            item.symbols.slice(0, 3).forEach(function (symbol) {
        return configureSaturnRingMap(THREE, texture);
                add(symbol, false);
    });
            });
}
            if (symbols.length) return symbols.slice(0, 3);
        }


function createSaturnRingGeometry(THREE, innerRadius, outerRadius) {
        collectNationItemFlags(item).forEach(function (symbol) { add(symbol, true); });
    var geometry = new THREE.RingGeometry(innerRadius, outerRadius, 256, 32);
        collectNationItemSybs(item).forEach(function (symbol) { add(symbol, true); });
    var position = geometry.attributes.position;
    var uv = geometry.attributes.uv;
    var range = Math.max(0.0001, outerRadius - innerRadius);
    var i;


    /*
        if (!symbols.length) {
    * The Solar System Scope / Commons file is a radial strip: left-to-right is
            add(item.flag_file || item.flag || item.flag_title || '', true);
    * inner-to-outer ring structure.  Three.js RingGeometry's default UVs map
            add(item.syb_file || item.syb || item.symbol_file || item.symbol || '', true);
    * the whole square, which turns the strip into a flat painted oval.  Rewrite
         }
    * the U coordinate from each vertex's local radius so the real ring bands
        return symbols.slice(0, 3);
    * wrap around Saturn concentrically.
    */
    for (i = 0; i < position.count; i += 1) {
        var x = position.getX(i);
        var y = position.getY(i);
        var r = Math.sqrt(x * x + y * y);
        var u = Math.max(0, Math.min(1, (r - innerRadius) / range));
         uv.setXY(i, u, 0.5);
     }
     }


     uv.needsUpdate = true;
     function normalizeNationLinkMap(payload) {
    return geometry;
        var source = payload && payload.items ? payload.items : {};
}
        var index = { byTag: {}, byPage: {}, byLabel: {} };
 
        Object.keys(source || {}).forEach(function (key) {
            var entry = source[key];
            var tag;
            var pageKey;
            var labelKey;


function createSolarInnerGlowTexture(THREE) {
            if (!entry || typeof entry !== 'object') return;
    var canvas = document.createElement('canvas');
            tag = normalizeNationTag(entry.tag || key || entry.page || '');
    var size = 384;
            pageKey = normalizeLookupKey(entry.wiki_title || entry.page_title || entry.title || '');
    var ctx = canvas.getContext('2d');
            labelKey = normalizeLookupKey(entry.name_ko || entry.label || entry.page_name || '');
    var gradient;
 
    var texture;
            if (tag && !index.byTag[tag]) index.byTag[tag] = entry;
            if (pageKey && !index.byPage[pageKey]) index.byPage[pageKey] = entry;
            if (labelKey && !index.byLabel[labelKey]) index.byLabel[labelKey] = entry;
        });


    canvas.width = size;
        return index;
     canvas.height = size;
     }


     gradient = ctx.createRadialGradient(
     function fetchNationLinkMap(panel, era) {
         size * 0.5, size * 0.5, 0,
        var url = getNationLinkMapUrl(panel, era);
         size * 0.5, size * 0.5, size * 0.5
         var cacheKey = era + '|' + url;
    );
         var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_Nation_Link_Map.json');


    gradient.addColorStop(0.000, 'rgba(255,252,232,1.00)');
        if (nationLinkMapCache[cacheKey]) {
    gradient.addColorStop(0.090, 'rgba(255,232,168,0.96)');
            PanelPerf.mark('fetch nation link map cache promise hit', { era: era });
    gradient.addColorStop(0.240, 'rgba(255,182,82,0.70)');
            return nationLinkMapCache[cacheKey];
    gradient.addColorStop(0.430, 'rgba(255,126,28,0.36)');
        }
    gradient.addColorStop(0.660, 'rgba(232,96,16,0.12)');
        if (cached) {
    gradient.addColorStop(1.000, 'rgba(0,0,0,0.00)');
            PanelPerf.mark('fetch nation link map EntryStore hit', { era: era });
            nationLinkMapCache[cacheKey] = Promise.resolve(PanelPerf.measure('normalize nation link map', { era: era, source: 'EntryStore' }, function () {
                return normalizeNationLinkMap(cached);
            }));
            return nationLinkMapCache[cacheKey];
        }


    ctx.fillStyle = gradient;
        nationLinkMapCache[cacheKey] = PanelPerf.measure('fetch nation link map remote', { era: era }, function () {
    ctx.fillRect(0, 0, size, size);
            return fetchNationJson(url, 'MediaWiki:' + era + '_Nation_Link_Map.json')
                .then(function (payload) {
                    return PanelPerf.measure('normalize nation link map', { era: era, source: 'remote' }, function () {
                        return normalizeNationLinkMap(payload);
                    });
                })
                .catch(function () {
                    return normalizeNationLinkMap(null);
                });
        });


    texture = new THREE.CanvasTexture(canvas);
        return nationLinkMapCache[cacheKey];
    texture.colorSpace = THREE.SRGBColorSpace;
     }
     texture.needsUpdate = true;
    return texture;
}


function createSolarCoronaTexture(THREE) {
    function findNationLinkEntryForItem(item, index) {
    var canvas = document.createElement('canvas');
        var tag;
    var size = 512;
        var pageKey;
    var ctx;
        var labelKey;
    var image;
    var data;
    var cx = size * 0.5;
    var cy = size * 0.5;
    var maxR = size * 0.5;
    var texture;
    var x;
    var y;


    canvas.width = size;
        if (!item || !index) return null;
    canvas.height = size;
    ctx = canvas.getContext('2d');
    image = ctx.createImageData(size, size);
    data = image.data;


    for (y = 0; y < size; y += 1) {
         tag = getNationItemTag(item);
         for (x = 0; x < size; x += 1) {
        if (tag && index.byTag && index.byTag[tag]) return index.byTag[tag];
            var dx = (x - cx) / maxR;
            var dy = (y - cy) / maxR;
            var r = Math.sqrt(dx * dx + dy * dy);
            var idx = (y * size + x) * 4;
            var angle;
            var noise;
            var base;
            var alpha;
            var warm;


            if (r >= 1) {
        pageKey = normalizeLookupKey(item.page || item.wiki_title || '');
                data[idx + 3] = 0;
        if (pageKey && index.byPage && index.byPage[pageKey]) return index.byPage[pageKey];
                continue;
            }


            angle = Math.atan2(dy, dx);
        labelKey = normalizeLookupKey(item.label || item.name || item.name_ko || '');
            noise = 0.90
        if (labelKey && index.byLabel && index.byLabel[labelKey]) return index.byLabel[labelKey];
                + 0.10 * Math.sin(angle * 7.0 + r * 9.0)
                + 0.06 * Math.sin(angle * 13.0 - r * 16.0)
                + 0.04 * Math.sin(angle * 29.0 + r * 25.0);
            base = Math.pow(Math.max(0, 1 - r), 2.65);
            alpha = Math.max(0, Math.min(1, base * noise));
            warm = Math.max(0, Math.min(1, 1 - r * 0.9));


            data[idx + 0] = Math.round(255);
        return null;
            data[idx + 1] = Math.round(170 + 45 * warm);
            data[idx + 2] = Math.round(72 + 26 * warm);
            data[idx + 3] = Math.round(alpha * 255);
        }
     }
     }


     ctx.putImageData(image, 0, 0);
     function applyNationLinkMapFlagFallback(data, linkMapIndex) {
    texture = new THREE.CanvasTexture(canvas);
        if (!data || !Array.isArray(data.continents)) return data;
    texture.colorSpace = THREE.SRGBColorSpace;
    texture.needsUpdate = true;
    return texture;
}


function loadLimitedImageTexture(THREE, url, maxWidth) {
        data.continents.forEach(function (continent) {
    return new Promise(function (resolve, reject) {
            (continent.regions || []).forEach(function (region) {
        var image = new Image();
                (region.items || []).forEach(function (item) {
        var limit = Math.max(64, Math.min(1024, Math.floor(maxWidth || 512)));
                    var entry;
                    var file;


        image.crossOrigin = 'anonymous';
                    if (!item || collectNationItemFlags(item).length) return;
        image.decoding = 'async';
                    entry = findNationLinkEntryForItem(item, linkMapIndex);
                    file = entry ? normalizeNationFile(entry.flag_file || entry.flag || entry.flag_title || '') : '';
                    if (!file) return;


        image.onload = function () {
                    item.flags = [{
            var naturalW = image.naturalWidth || image.width || limit;
                        file: file,
            var naturalH = image.naturalHeight || image.height || Math.max(1, Math.floor(limit / 2));
                        width: '16px',
            var scale = Math.min(1, limit / naturalW);
                        border: true,
            var width = Math.max(1, Math.round(naturalW * scale));
                        offsetMode: 'auto',
            var height = Math.max(1, Math.round(naturalH * scale));
                        offsetY: 0,
            var canvas = document.createElement('canvas');
                        source: 'Nation_Link_Map'
             var ctx;
                    }];
            var texture;
                });
             });
        });


            canvas.width = width;
        return data;
            canvas.height = height;
    }
            ctx = canvas.getContext('2d');
            ctx.drawImage(image, 0, 0, width, height);


             texture = new THREE.CanvasTexture(canvas);
    function getNationListFallback() {
            texture.colorSpace = THREE.SRGBColorSpace;
        return {
            texture.wrapS = THREE.RepeatWrapping;
            version: 1,
            texture.wrapT = THREE.ClampToEdgeWrapping;
            year: 1950,
            texture.anisotropy = 2;
             continents: NATION_CONTINENT_FALLBACK.map(function (continent) {
            texture.needsUpdate = true;
                return {
             resolve(texture);
                    key: continent.key,
                    label: continent.label,
                    regions: continent.regions.map(function (region) {
                        return { key: region, label: region, items: [] };
                    })
                };
             })
         };
         };
    }


        image.onerror = function () {
    function normalizeNationList(payload, era) {
            reject(new Error('limited texture load failed: ' + url));
        var fallback = getNationListFallback();
         };
        var sourceContinents = payload && Array.isArray(payload.continents) ? payload.continents : [];
         var sourceByKey = {};


         image.src = url;
         sourceContinents.forEach(function (continent) {
    });
            if (continent && continent.key) sourceByKey[continent.key] = continent;
}
        });


function clamp01(value) {
        return {
    return Math.max(0, Math.min(1, value));
            version: payload && payload.version ? payload.version : 1,
}
            year: payload && payload.year ? payload.year : parseInt(era || '1950', 10),
            description: payload && payload.description ? payload.description : '',
            continents: fallback.continents.map(function (base) {
                var source = sourceByKey[base.key] || {};
                var regionsByLabel = {};


function easeOutCubic(t) {
                if (Array.isArray(source.regions)) {
    t = clamp01(t);
                    source.regions.forEach(function (region) {
    return 1 - Math.pow(1 - t, 3);
                        if (!region) return;
}
                        if (region.key) regionsByLabel[region.key] = region;
                        if (region.label) regionsByLabel[region.label] = region;
                    });
                }


function nearestEquivalentAngle(current, target) {
                return {
    var full = Math.PI * 2;
                    key: base.key,
    return target + Math.round((current - target) / full) * full;
                    label: source.label || base.label,
}
                    regions: base.regions.map(function (baseRegion) {
                        var region = regionsByLabel[baseRegion.key] || regionsByLabel[baseRegion.label] || baseRegion;
                        return {
                            key: region.key || baseRegion.key,
                            label: region.label || baseRegion.label,
                            items: Array.isArray(region.items) ? region.items.filter(function (item) {
                                return item && item.enabled !== false;
                            }) : []
                        };
                    })
                };
            })
        };
    }


function canonicalAngle(value) {
    function fetchNationList(panel, era) {
    var full = Math.PI * 2;
        var url = getNationListUrl(panel, era);
    var angle = value % full;
        var cacheKey = era + '|' + url;
        var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_Nation_List.json');


    if (angle > Math.PI) angle -= full;
        if (nationListCache[cacheKey]) {
    if (angle < -Math.PI) angle += full;
            PanelPerf.mark('fetch nation list cache promise hit', { era: era });
            return nationListCache[cacheKey];
        }
        if (cached) {
            PanelPerf.mark('fetch nation list EntryStore hit', { era: era });
            nationListCache[cacheKey] = Promise.resolve(PanelPerf.measure('normalize nation list', { era: era, source: 'EntryStore' }, function () {
                return normalizeNationList(cached, era);
            }));
            return nationListCache[cacheKey];
        }


    return angle;
        nationListCache[cacheKey] = PanelPerf.measure('fetch nation list remote', { era: era }, function () {
}
            return fetchNationJson(url, 'MediaWiki:' + era + '_Nation_List.json')
                .then(function (payload) {
                    return PanelPerf.measure('normalize nation list', { era: era, source: 'remote' }, function () {
                        return normalizeNationList(payload, era);
                    });
                });
        });


function inverseLerp(a, b, value) {
        return nationListCache[cacheKey];
    if (a === b) return 0;
     }
     return (value - a) / (b - a);
}


function createStars(THREE, count) {
    function normalizeNationFlagMaxWidth(value) {
    var geometry = new THREE.BufferGeometry();
        var raw = String(value || '').trim();
    var positions = [];
        var match = raw.match(/^(\d+(?:\.\d+)?)\s*(px)?$/i);
    var i;
        var n = match ? parseFloat(match[1]) : 16;


    for (i = 0; i < count; i += 1) {
        if (!isFinite(n)) n = 16;
        var r = 8 + Math.random() * 16;
         if (n < 8) n = 8;
         var theta = Math.random() * Math.PI * 2;
         if (n > 32) n = 32;
         var phi = Math.acos((Math.random() * 2) - 1);


         positions.push(
         return Math.round(n * 100) / 100 + 'px';
            r * Math.sin(phi) * Math.cos(theta),
            r * Math.cos(phi),
            r * Math.sin(phi) * Math.sin(theta)
        );
     }
     }


     geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
     function isNationSybFile(file) {
        return /^Syb-/i.test(normalizeNationFile(file).replace(/_/g, ' ').trim());
    }


     return new THREE.Points(
     function getNationSymbolBorder(symbol, file) {
         geometry,
        if (!symbol || typeof symbol !== 'object') return !isNationSybFile(file);
         new THREE.PointsMaterial({
         if (symbol.border != null) return symbol.border !== false;
            color: 0xd8d8d8,
         if (symbol.symbol_border != null) return symbol.symbol_border !== false;
            size: 0.018,
         if (symbol.syb_border != null) return symbol.syb_border === true;
            transparent: true,
        return !isNationSybFile(file);
            opacity: 0.62,
    }
            depthWrite: false
         })
    );
}


function NationsGlobe(root) {
    function normalizeNationSymbolFile(symbol) {
    this.root = root;
        if (typeof symbol === 'string') return normalizeNationFile(symbol);
    this.stage = null;
        if (!symbol || typeof symbol !== 'object') return '';
    this.loading = null;
        return normalizeNationFile(symbol.file || symbol.flag_file || symbol.flag || symbol.flag_title || symbol.syb_file || symbol.syb || symbol.symbol_file || symbol.symbol || '');
    this.error = null;
    this.status = null;
    this.readout = null;
    this.autoButton = null;
    this.resetButton = null;
    this.THREE = null;
    this.scene = null;
    this.camera = null;
    this.renderer = null;
    this.group = null;
    this.starGroup = null;
    this.celestialGroup = null;
    this.celestialPlanets = [];
    this.starDriftY = 0;
    this.celestialDriftY = 0;
    this.celestialSun = null;
    this.celestialSunGlowA = null;
    this.celestialSunGlowB = null;
    this.celestialSunCorona = null;
    this.sunOccultRendered = 0;
    this.sunOccultMotion = null;
    this.moon = null;
    this.moonShell = null;
    this.earth = null;
    this.earthMaterial = null;
    this.clouds = null;
    this.cloudMaterial = null;
    this.lakesOverlay = null;
    this.raycaster = null;
    this.pointer = null;
    this.pointerInside = false;
    this.pointerClientX = null;
    this.pointerClientY = null;
    this.pointerDownX = null;
    this.pointerDownY = null;
    this.pointerDownNationPage = null;
    this.clickMoveTolerance = toNumber(root.getAttribute('data-nation-click-tolerance'), 5);
    this.nationHitUrl = root.getAttribute('data-polygon-hit-url') || '';
    this.nationLinkMapUrl = root.getAttribute('data-nation-link-map-url') || '';
    this.nationLinkBase = root.getAttribute('data-nation-link-base') || '/index.php/';
    this.nationHitFeatures = [];
    this.nationLinkMap = {};
    this.currentNation = null;
    this.nationTooltip = null;
    this.radius = 1;
    this.rotationX = toNumber(root.getAttribute('data-rotation-x'), -0.18);
    this.rotationY = toNumber(root.getAttribute('data-rotation-y'), -0.48);
    this.cameraZ = toNumber(root.getAttribute('data-camera-z'), 3.75);
    this.cameraZMin = toNumber(root.getAttribute('data-camera-z-min'), 1.23);
    this.cameraZMax = toNumber(root.getAttribute('data-camera-z-max'), 5.2);
    this.resetRotationX = toNumber(root.getAttribute('data-reset-rotation-x'), this.rotationX);
    this.resetRotationY = toNumber(root.getAttribute('data-reset-rotation-y'), this.rotationY);
    this.resetCameraZ = toNumber(root.getAttribute('data-reset-camera-z'), this.cameraZ);
    if (!root.getAttribute('data-camera-z') && root.getAttribute('data-reset-camera-z') === '3.05') {
        this.resetCameraZ = this.cameraZ;
     }
     }
    this.resetDuration = toNumber(root.getAttribute('data-reset-duration'), 520);
    this.resetMotion = null;
    this.terrainUrl = root.getAttribute('data-terrain-url') || '';
    this.cloudsUrl = root.getAttribute('data-clouds-url') || '';
    this.boundarySurfaceOffset = toNumber(root.getAttribute('data-boundary-surface-offset'), 0.0022);
    this.polygonOverlayEnabled = root.getAttribute('data-polygon-overlay') !== 'false';
    this.polygonOverlayUrl = root.getAttribute('data-polygon-overlay-url') || '';
    this.polygonOverlaySize = toNumber(root.getAttribute('data-polygon-overlay-size'), 4096);
    this.earthCompositeSize = toNumber(root.getAttribute('data-earth-composite-size'), this.polygonOverlaySize || 4096);
    this.polygonOverlayOpacity = toNumber(root.getAttribute('data-polygon-overlay-opacity'), 1);
    this.polygonOverlayBlend = root.getAttribute('data-polygon-overlay-blend') || 'source-over';
    this.polygonOverlayFilter = root.getAttribute('data-polygon-overlay-filter') || 'none';
    this.reliefOverlayUrl = root.getAttribute('data-relief-overlay-url') || '';
    this.reliefOverlayOpacity = toNumber(root.getAttribute('data-relief-overlay-opacity'), 1);
    this.reliefOverlayBlend = root.getAttribute('data-relief-overlay-blend') || 'source-over';
    this.reliefOverlayFilter = root.getAttribute('data-relief-overlay-filter') || 'none';
    this.lakesOverlayUrl = root.getAttribute('data-lakes-overlay-url') || '';
    this.lakesOverlayOpacity = toNumber(root.getAttribute('data-lakes-overlay-opacity'), 1);
    this.lakesSurfaceOffset = toNumber(root.getAttribute('data-lakes-surface-offset'), 0.0029);
    this.cloudOpacityNear = toNumber(root.getAttribute('data-cloud-opacity-near'), 0.10);
    this.cloudOpacityFar = toNumber(root.getAttribute('data-cloud-opacity-far'), 0.90);
    this.cloudDriftSpeed = toNumber(root.getAttribute('data-cloud-drift-speed'), 0.000010);
    this.cloudTiltDrift = toNumber(root.getAttribute('data-cloud-tilt-drift'), 0.006);
    this.celestialEnabled = root.getAttribute('data-celestial-enabled') !== 'false';
    this.celestialTextureSize = toNumber(root.getAttribute('data-celestial-texture-size'), 512);
    this.sunTextureSize = toNumber(root.getAttribute('data-sun-texture-size'), 512);
    this.moonTextureSize = toNumber(root.getAttribute('data-moon-texture-size'), 512);
    this.moonOpacityFar = toNumber(root.getAttribute('data-moon-opacity-far'), 1.00);
    this.moonOpacityNear = toNumber(root.getAttribute('data-moon-opacity-near'), 0.00);
    this.moonShellOpacityFar = toNumber(root.getAttribute('data-moon-shell-opacity-far'), 0.040);
    this.moonShellOpacityNear = toNumber(root.getAttribute('data-moon-shell-opacity-near'), 0.00);
    this.moonVanishZ = toNumber(root.getAttribute('data-moon-vanish-z'), 3.15);
    this.moonFullVisibleZ = toNumber(root.getAttribute('data-moon-full-visible-z'), 4.20);
    this.moonEarthFadeOpacity = toNumber(root.getAttribute('data-moon-earth-fade-opacity'), 0.08);
    this.moonEarthFadeMargin = toNumber(root.getAttribute('data-moon-earth-fade-margin'), 28);
    this.moonFadeDuration = toNumber(root.getAttribute('data-moon-fade-duration'), this.resetDuration);
    this.sunOccultFadeDuration = toNumber(root.getAttribute('data-sun-occult-fade-duration'), this.moonFadeDuration);
    this.moonRenderedOpacity = null;
    this.moonRenderedShellOpacity = null;
    this.moonFadeMotion = null;
    this.celestialParallax = toNumber(root.getAttribute('data-celestial-parallax'), 0.070);
    this.starParallax = toNumber(root.getAttribute('data-star-parallax'), 0.012);
    this.starDriftSpeed = toNumber(root.getAttribute('data-star-drift-speed'), 0);
    this.celestialDriftSpeed = toNumber(root.getAttribute('data-celestial-drift-speed'), 0);
    this.autoRotate = root.getAttribute('data-auto-rotate') !== 'false';
    this.resetAutoRotate = this.autoRotate;
    this.autoRotateSpeed = toNumber(root.getAttribute('data-auto-rotate-speed'), 0.000085);
    this.autoResumeDuration = toNumber(root.getAttribute('data-auto-resume-duration'), Math.max(1600, this.resetDuration * 3.5));
    this.autoResumeMinFactor = toNumber(root.getAttribute('data-auto-resume-min-factor'), 0.06);
    this.autoResumeMotion = null;
    this.dragging = false;
    this.lastX = 0;
    this.lastY = 0;
    this.lastMoveTime = 0;
    this.velocityX = 0;
    this.velocityY = 0;
    this.dragSensitivityY = toNumber(root.getAttribute('data-drag-sensitivity-y'), 0.0030);
    this.dragSensitivityX = toNumber(root.getAttribute('data-drag-sensitivity-x'), 0.0020);
    this.inertiaStrength = toNumber(root.getAttribute('data-inertia-strength'), 0.58);
    this.inertiaDamping = toNumber(root.getAttribute('data-inertia-damping'), 0.993);
    this.inertiaMinVelocity = toNumber(root.getAttribute('data-inertia-min-velocity'), 0.000085);
    this.inertiaReleaseWindow = toNumber(root.getAttribute('data-inertia-release-window'), 150);
    this.stageEventsBound = false;
    this.frame = null;
    this.disposed = false;
    this.segmentCount = 0;
    this.lastTime = performance.now();
}


NationsGlobe.prototype.ensureDom = function () {
    function renderNationFlag(flag) {
    var root = this.root;
        var file;
    var title = root.getAttribute('data-title') || 'TERRITORY GLOBE';
        var url;
    var era = root.getAttribute('data-era') || 'REAL WORLD / 110M';
        var width;
    var topbarHtml;
        var classes;
     var bottombarHtml;
        var offsetY;
 
        if (!flag) return '';
        file = normalizeNationFile(flag.file || flag.flag_file || flag.flag || '');
        if (!file) return '';
 
        url = buildNationFileUrl(file);
        width = normalizeNationFlagMaxWidth(flag.width || flag.flag_width || '24px');
        offsetY = getNationFlagOffsetY(flag);
        classes = 'clbi-nations-list-flag' + (flag.border !== false ? ' has-border' : '');
 
        if (!url) {
            return '<span class="clbi-nations-list-flag-slot is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '" title="국기 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
        }
 
        return '<span class="clbi-nations-list-flag-slot" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '">' +
            '<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
        '</span>';
     }


     root.classList.add('clbi-nations-globe-window');
     function renderNationSyb(syb) {
        var file;
        var url;
        var width;
        var classes;


    topbarHtml =
         if (!syb) return '';
         '<div class="clbi-nations-globe-topbar">' +
        file = normalizeNationFile(syb.file || syb.syb_file || syb.syb || syb.symbol_file || syb.symbol || '');
            '<span class="clbi-nations-globe-title">' + escapeHtml(title) + '</span>' +
         if (!file) return '';
            '<span class="clbi-nations-globe-status">SYNC</span>' +
         '</div>';


    bottombarHtml =
        url = buildNationFileUrl(file);
         '<div class="clbi-nations-globe-bottombar">' +
         width = normalizeNationFlagMaxWidth(syb.width || syb.syb_width || syb.symbol_width || '16px');
            '<span class="clbi-nations-globe-readout">' + escapeHtml(era) + '</span>' +
        classes = 'clbi-nations-list-flag clbi-nations-list-syb' + (syb.border === true ? ' has-border' : '');
            '<span class="clbi-nations-globe-controls">' +
                '<button type="button" class="clbi-nations-globe-btn clbi-nations-globe-auto">AUTO</button>' +
                '<button type="button" class="clbi-nations-globe-btn clbi-nations-globe-reset">RESET</button>' +
            '</span>' +
        '</div>';


    if (!root.querySelector('.clbi-nations-globe-stage')) {
        if (!url) {
        root.innerHTML =
             return '<span class="clbi-nations-list-flag-slot clbi-nations-list-syb-slot is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:0px;" data-flag-file="' + escapeHtml(file) + '" title="SYB 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
             '<div class="clbi-nations-globe-stage" aria-label="3D globe viewport"></div>' +
            '<div class="clbi-nations-globe-vignette" aria-hidden="true"></div>' +
            topbarHtml +
            bottombarHtml +
            '<div class="clbi-nations-globe-loading">LOADING REAL-WORLD TERRITORY DATA</div>' +
            '<div class="clbi-nations-globe-error"></div>';
    } else {
        if (!root.querySelector('.clbi-nations-globe-vignette')) {
            root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-vignette" aria-hidden="true"></div>');
         }
         }


         if (!root.querySelector('.clbi-nations-globe-topbar')) {
         return '<span class="clbi-nations-list-flag-slot clbi-nations-list-syb-slot" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:0px;" data-flag-file="' + escapeHtml(file) + '">' +
             root.insertAdjacentHTML('beforeend', topbarHtml);
             '<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
         }
        '</span>';
    }
 
    function renderNationSymbol(symbol) {
        var file;
         var url;
        var width;
        var classes;
        var offsetY;
        var isSyb;
        var hasBorder;


         if (!root.querySelector('.clbi-nations-globe-bottombar')) {
         if (!symbol) return '';
            root.insertAdjacentHTML('beforeend', bottombarHtml);
        file = normalizeNationSymbolFile(symbol);
        }
        if (!file) return '';


         if (!root.querySelector('.clbi-nations-globe-loading')) {
         isSyb = isNationSybFile(file);
            root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-loading">LOADING REAL-WORLD TERRITORY DATA</div>');
        url = buildNationFileUrl(file);
        }
        width = normalizeNationFlagMaxWidth(symbol.width || symbol.flag_width || symbol.syb_width || symbol.symbol_width || (isSyb ? '16px' : '24px'));
        offsetY = getNationFlagOffsetY(symbol);
        hasBorder = getNationSymbolBorder(symbol, file);
        classes = 'clbi-nations-list-flag clbi-nations-list-symbol' + (isSyb ? ' clbi-nations-list-syb' : '') + (hasBorder ? ' has-border' : '');


         if (!root.querySelector('.clbi-nations-globe-error')) {
         if (!url) {
            root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-error"></div>');
            return '<span class="clbi-nations-list-flag-slot clbi-nations-list-symbol-slot' + (isSyb ? ' clbi-nations-list-syb-slot' : '') + ' is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '" title="상징물 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
         }
         }
    }


    this.stage = root.querySelector('.clbi-nations-globe-stage');
        return '<span class="clbi-nations-list-flag-slot clbi-nations-list-symbol-slot' + (isSyb ? ' clbi-nations-list-syb-slot' : '') + '" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '">' +
    this.loading = root.querySelector('.clbi-nations-globe-loading');
            '<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
    this.error = root.querySelector('.clbi-nations-globe-error');
        '</span>';
    this.status = root.querySelector('.clbi-nations-globe-status');
    this.readout = root.querySelector('.clbi-nations-globe-readout');
    if (!root.querySelector('.clbi-nations-globe-nation-tooltip')) {
        root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-nation-tooltip" aria-hidden="true"></div>');
     }
     }


     this.autoButton = root.querySelector('.clbi-nations-globe-auto');
     function bindNationFlagFallbacks(scope) {
    this.resetButton = root.querySelector('.clbi-nations-globe-reset');
        var root = scope || document;
    this.nationTooltip = root.querySelector('.clbi-nations-globe-nation-tooltip');
    ensureNationsGlobeInteractionStyle();


    this.syncAutoButton();
        toArray(root.querySelectorAll('.clbi-nations-list-flag-slot .clbi-nations-list-flag')).forEach(function (img) {
            if (img.getAttribute('data-flag-missing-handler') === '1') return;
            img.setAttribute('data-flag-missing-handler', '1');
            img.addEventListener('error', function () {
                var slot = img.closest ? img.closest('.clbi-nations-list-flag-slot') : null;
                if (slot) {
                    slot.classList.add('is-missing');
                    slot.setAttribute('title', '국기 파일 로드 실패: ' + (slot.getAttribute('data-flag-file') || ''));
                }
                img.removeAttribute('src');
            });
        });
    }


     if (this.readout && !this.readout.textContent) {
     function getResolvedFlagUrl(file) {
         this.readout.textContent = era;
         var key = normalizeNationFileKey(file);
        return key ? (nationResolvedFileUrlCache[key] || '') : '';
     }
     }
};


NationsGlobe.prototype.rehydrateDom = function () {
    function collectResolvedNationFlagUrls(data) {
    if (!this.root || !document.body.contains(this.root)) return false;
        var files = collectNationFlagFileNames(data);
        var seen = {};
        var urls = [];


    this.ensureDom();
        files.forEach(function (file) {
    this.bindControlEvents();
            var url = getResolvedFlagUrl(file);
    this.resize();
            if (!url || seen[url]) return;
            seen[url] = true;
            urls.push(url);
        });


    return true;
        return urls;
};
    }


    function collectResolvedNationFlagUrlsForFiles(files) {
        var seen = {};
        var urls = [];


NationsGlobe.prototype.setStatus = function (text) {
        (files || []).forEach(function (file) {
    if (this.status) this.status.textContent = text || '';
            var url = getResolvedFlagUrl(file);
};
            if (!url || seen[url]) return;
            seen[url] = true;
            urls.push(url);
        });


NationsGlobe.prototype.setReadout = function (text) {
        return urls;
    if (this.readout) this.readout.textContent = text || '';
     }
};
 
NationsGlobe.prototype.setError = function (err) {
    var message = err && err.message ? err.message : String(err || 'unknown error');
    this.root.classList.add('has-error');
    this.root.classList.remove('is-ready');
     if (this.error) this.error.textContent = 'GLOBE ERROR: ' + message;
    this.setStatus('ERROR');
};


NationsGlobe.prototype.init = function () {
    function warmNationFlagUrlsToCompletion(urls, owner, options) {
    var self = this;
        var queue = unique((urls || []).filter(Boolean));
        var index = 0;
        var active = 0;
        var done = 0;
        var ok = 0;
        var fail = 0;
        var concurrency = Math.max(1, Math.min(32, Number(options && options.concurrency) || 18));
        var storeKey = options && options.storeKey ? String(options.storeKey) : 'CLBI_NationsFlagWarmImages';
        var meta = options && options.meta ? options.meta : {};
        var startId;


    this.ensureDom();
        if (!queue.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
    this.setStatus('LIBRARY');


    return Promise.all([loadThree(), loadTopojson()])
         if (owner) owner[storeKey] = [];
         .then(function (libs) {
         startId = PanelPerf.start('background nation flag image load', Object.assign({}, meta, { count: queue.length, concurrency: concurrency }));
            self.THREE = libs[0];
            self.topojson = libs[1];
            self.createScene();
            self.bindEvents();
            return self.loadOptionalTextures().then(function () {
                return self.loadBoundaryData();
            }).then(function () {
                return self.loadNationInteractionData();
            });
         })
        .then(function () {
            self.root.classList.add('is-ready');
            self.setStatus('READY');
            self.animate(performance.now());
        })
        .catch(function (err) {
            self.setError(err);
        });
};


        return new Promise(function (resolve) {
            function settle(success, img) {
                active -= 1;
                done += 1;
                if (success) ok += 1;
                else fail += 1;


NationsGlobe.prototype.addMoon = function (texture) {
                if (owner && owner[storeKey]) {
    var THREE = this.THREE;
                    var idx = owner[storeKey].indexOf(img);
    var def = CELESTIAL_MOON_DEF;
                    if (idx >= 0) owner[storeKey].splice(idx, 1);
    var material;
                }
    var mesh;
    var shell;


    if (!this.group || !texture) return;
                pump();
            }


    material = new THREE.MeshPhongMaterial({
            function startOne(url) {
        map: texture,
                var img = new Image();
        color: 0xffffff,
                var finished = false;
        transparent: true,
        opacity: this.moonOpacityFar,
        depthWrite: false,
        depthTest: true,
        shininess: 2,
        specular: new THREE.Color(0x050505),
        emissive: new THREE.Color(0x010101)
    });


    mesh = new THREE.Mesh(
                active += 1;
        new THREE.SphereGeometry(def.radius, 42, 28),
                img.decoding = 'sync';
        material
                img.loading = 'eager';
    );
                try { img.fetchPriority = 'high'; } catch (ignorePriority) {}
                if (owner && owner[storeKey]) owner[storeKey].push(img);


    mesh.position.set(def.position[0], def.position[1], def.position[2]);
                function finish(success) {
    mesh.renderOrder = 20;
                    if (finished) return;
                    finished = true;
                    settle(success, img);
                }


    shell = new THREE.Mesh(
                img.onload = function () {
        new THREE.SphereGeometry(def.radius * 1.022, 36, 24),
                    if (img.decode) {
        new THREE.MeshBasicMaterial({
                        img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
            color: 0xd8d8d8,
                    } else {
            transparent: true,
                        finish(true);
            opacity: this.moonShellOpacityFar,
                    }
            side: THREE.BackSide,
                };
            depthWrite: false
                img.onerror = function () { finish(false); };
        })
                img.src = url;
    );
    shell.position.copy(mesh.position);
    shell.renderOrder = 19;


    this.group.add(shell);
                if (img.complete) {
    this.group.add(mesh);
                    scheduleIdle(function () { finish(img.naturalWidth > 0); }, { timeout: 250 });
                }
            }


    this.moon = mesh;
            function pump() {
    this.moonShell = shell;
                while (active < concurrency && index < queue.length) {
    this.updateMoonOpacity();
                    startOne(queue[index++]);
};
                }


NationsGlobe.prototype.loadMoon = function () {
                if (done >= queue.length) {
    var self = this;
                    PanelPerf.end(startId, Object.assign({}, meta, { count: queue.length, ok: ok, fail: fail }));
    var THREE = this.THREE;
                    resolve({ total: queue.length, ok: ok, fail: fail });
    var textureSize = Math.max(64, Math.min(1024, Math.floor(this.moonTextureSize || 512)));
                }
            }


    loadLimitedImageTexture(THREE, CELESTIAL_MOON_TEXTURE_URL, textureSize)
            scheduleIdle(pump, { timeout: 300 });
        .then(function (texture) {
            self.addMoon(texture);
        })
        .catch(function (err) {
            console.warn('CLBI moon texture failed:', err);
            self.addMoon(createFallbackPlanetTexture(THREE, CELESTIAL_MOON_DEF.fallbackA, CELESTIAL_MOON_DEF.fallbackB));
         });
         });
};
    }
 
    function prewarmImageUrls(urls, limit) {
        var index = 0;
        var max = Math.max(0, limit || 64);


NationsGlobe.prototype.resolveCelestialEclipticPosition = function (def) {
        urls = (urls || []).filter(Boolean).slice(0, max);
    var plane = CELESTIAL_ECLIPTIC;
        if (!urls.length) return;
    var orbitRadius = def.orbitRadius || 0;
    var angle = def.orbitAngle || 0;
    var x = Math.cos(angle) * orbitRadius;
    var z = plane.depth + Math.sin(angle) * orbitRadius * plane.depthScale;
    var y = x * plane.tiltYPerX + Math.sin(angle) * orbitRadius * (def.inclination || 0);


    return [x, y, z];
        function step(deadline) {
};
            var started = 0;


NationsGlobe.prototype.addCelestialPlanet = function (def, texture) {
            while (index < urls.length && started < 6 && (!deadline || deadline.timeRemaining() > 4 || deadline.didTimeout)) {
    var THREE = this.THREE;
                var img = new Image();
    var self = this;
                var url = urls[index++];
    var mesh;
    var material;
    var shell;
    var shellColor;
    var ring;
    var ringGlow;
    var ringMaterial;
    var position;


    if (!this.celestialGroup || !def || !texture) return;
                started += 1;
                img.decoding = 'async';
                img.loading = 'eager';
                img.src = url;


    material = new THREE.MeshPhongMaterial({
                if (img.decode) {
        map: texture,
                    img.decode().catch(function () {});
        color: 0xffffff,
                }
        transparent: true,
            }
        opacity: def.opacity,
        depthWrite: false,
        depthTest: true,
        shininess: 3,
        specular: new THREE.Color(0x050505),
        emissive: new THREE.Color(0x020202)
    });


    mesh = new THREE.Mesh(
            if (index < urls.length) scheduleIdle(step, { timeout: 850 });
        new THREE.SphereGeometry(def.radius, 42, 28),
         }
         material
    );


    position = this.resolveCelestialEclipticPosition(def);
        scheduleIdle(step, { timeout: 850 });
    mesh.position.set(position[0], position[1], position[2]);
     }
     mesh.renderOrder = 1;
    this.celestialGroup.add(mesh);


     if (def.ring) {
     function prewarmRenderedNationFlags(scope) {
         var ringInnerRadius = def.radius * (def.ringInnerScale || 1.34);
         var urls = [];
         var ringOuterRadius = def.radius * (def.ringOuterScale || 2.58);
        var seen = {};
        var perf = window.InteractionPerf;
         var perfId = perf && typeof perf.start === 'function' ? perf.start('panel collect rendered flag prewarm urls', {
            scope: scope === document ? 'document' : 'panel'
        }) : null;


         ringMaterial = new THREE.MeshBasicMaterial({
         toArray((scope || document).querySelectorAll ? (scope || document).querySelectorAll('.clbi-nations-list-flag[src]') : []).forEach(function (img) {
            map: configureSaturnRingMap(THREE, createSaturnRingTexture(THREE)),
             var url = img.currentSrc || img.getAttribute('src') || '';
             color: 0xffffff,
             if (!url || seen[url]) return;
            transparent: true,
             seen[url] = true;
            opacity: def.ringOpacity || 0.86,
             urls.push(url);
             side: THREE.DoubleSide,
             depthWrite: false,
            depthTest: true,
             blending: THREE.NormalBlending
         });
         });


         ring = new THREE.Mesh(
         if (perf && typeof perf.end === 'function' && perfId) {
            createSaturnRingGeometry(THREE, ringInnerRadius, ringOuterRadius),
             perf.end(perfId, { urls: urls.length });
            ringMaterial
        );
        ring.position.copy(mesh.position);
        ring.rotation.x = def.ringTiltX || 1.08;
        ring.rotation.y = def.ringTiltY || 0.18;
        ring.rotation.z = def.ringTiltZ || -0.34;
        ring.renderOrder = 1.8;
        this.celestialGroup.add(ring);
 
        if (def.ringTextureUrl) {
            loadSaturnRingTexture(THREE, def.ringTextureUrl).then(function (ringTexture) {
                if (!ringMaterial) return;
                ringMaterial.map = ringTexture;
                ringMaterial.needsUpdate = true;
             }).catch(function (err) {
                console.warn('CLBI Saturn ring texture failed:', err);
            });
         }
         }
        prewarmImageUrls(urls, 80);
     }
     }


     shellColor = new THREE.Color(def.fallbackA || '#707070');
     function hydrateResolvedFlagSlots(scope) {
        var root = scope || document;
        var hydrated = 0;
        var scanned = 0;
        var perf = window.InteractionPerf;
        var perfId = perf && typeof perf.start === 'function' ? perf.start('panel hydrate resolved flag slots', {
            scope: root === document ? 'document' : 'panel'
        }) : null;


    shell = new THREE.Mesh(
        try {
         new THREE.SphereGeometry(def.radius * 1.028, 36, 24),
         toArray(root.querySelectorAll ? root.querySelectorAll('.clbi-nations-list-flag-slot[data-flag-file]') : []).forEach(function (slot) {
        new THREE.MeshBasicMaterial({
             scanned += 1;
             color: shellColor,
             var file = normalizeNationFile(slot.getAttribute('data-flag-file') || '');
             transparent: true,
             var url = buildNationFileUrl(file);
            opacity: Math.max(0.05, Math.min(0.12, (def.opacity || 0.4) * 0.20)),
             var img;
             side: THREE.BackSide,
            var isSyb;
             depthWrite: false
            var isSymbol;
        })
            var classes;
    );
    shell.position.copy(mesh.position);
    shell.renderOrder = 0;
    this.celestialGroup.add(shell);


    this.celestialPlanets.push({
            if (!file || !url) return;
        mesh: mesh,
            if (slot.querySelector && slot.querySelector('.clbi-nations-list-flag[src]')) return;
        shell: shell,
        ring: ring,
        ringGlow: ringGlow,
        speed: def.rotationSpeed || 0
    });
};


NationsGlobe.prototype.addCelestialSun = function (texture) {
            isSyb = slot.classList.contains('clbi-nations-list-syb-slot') || isNationSybFile(file);
    var THREE = this.THREE;
            isSymbol = slot.classList.contains('clbi-nations-list-symbol-slot');
    var def = CELESTIAL_SUN_DEF;
            classes = 'clbi-nations-list-flag' +
    var position = this.resolveCelestialEclipticPosition(def);
                (isSymbol ? ' clbi-nations-list-symbol' : '') +
    var core;
                (isSyb ? ' clbi-nations-list-syb' : '') +
    var innerGlow;
                (!isSyb ? ' has-border' : '');
    var outerGlow;
    var coreMaterial;
    var innerGlowMaterial;
    var outerGlowMaterial;


    if (!this.celestialGroup) return;
            slot.classList.remove('is-missing');
            slot.removeAttribute('title');
            slot.innerHTML = '';
            img = document.createElement('img');
            img.className = classes;
            img.alt = '';
            img.loading = 'eager';
            img.decoding = 'sync';
            try { img.fetchPriority = 'high'; } catch (ignorePriority) {}
            img.src = url;
            slot.appendChild(img);
            hydrated += 1;
        });


    coreMaterial = new THREE.MeshBasicMaterial({
        if (hydrated) bindNationFlagFallbacks(root);
         map: texture || null,
         return hydrated;
         color: def.color,
         } finally {
        transparent: true,
            if (perf && typeof perf.end === 'function' && perfId) {
        opacity: def.coreOpacity,
                perf.end(perfId, { scanned: scanned, hydrated: hydrated });
         depthWrite: false
            }
     });
         }
     }


    core = new THREE.Mesh(
        new THREE.SphereGeometry(def.radius, 48, 32),
        coreMaterial
    );
    core.position.set(position[0], position[1], position[2]);
    core.renderOrder = 3;


     innerGlowMaterial = new THREE.SpriteMaterial({
     function scheduleResolvedFlagHydrationSweeps(panel, options) {
         map: createSolarInnerGlowTexture(THREE),
         var delays = options && options.delays ? options.delays : [0, 16, 80, 180, 360, 720, 1200, 2000];
        color: 0xffffff,
         var era = options && options.era ? String(options.era) : getPanelEra(panel);
        transparent: true,
        opacity: def.innerGlowOpacity,
        depthWrite: false,
        depthTest: true,
         blending: THREE.AdditiveBlending
    });


    innerGlow = new THREE.Sprite(innerGlowMaterial);
        if (!panel || !panel.querySelector) return;
    innerGlow.position.copy(core.position);
        if (!panel.CLBI_NationsFlagHydrationSweepTimers) panel.CLBI_NationsFlagHydrationSweepTimers = [];
    innerGlow.scale.set(def.radius * def.innerGlowScale, def.radius * def.innerGlowScale, 1);
    innerGlow.renderOrder = 2;


    outerGlowMaterial = new THREE.SpriteMaterial({
        panel.CLBI_NationsFlagHydrationSweepTimers.forEach(function (timer) {
        map: createSolarCoronaTexture(THREE),
            try { window.clearTimeout(timer); } catch (ignoreClear) {}
         color: 0xffffff,
         });
        transparent: true,
         panel.CLBI_NationsFlagHydrationSweepTimers = [];
         opacity: def.outerGlowOpacity,
        depthWrite: false,
        depthTest: true,
        blending: THREE.AdditiveBlending
    });


    outerGlow = new THREE.Sprite(outerGlowMaterial);
        delays.forEach(function (delay, index) {
    outerGlow.position.copy(core.position);
            var timer = window.setTimeout(function () {
    outerGlow.scale.set(def.radius * def.outerGlowScale, def.radius * def.outerGlowScale, 1);
                var hydrated;
     outerGlow.renderOrder = 1;
                if (!document.body.contains(panel)) return;
                hydrated = hydrateResolvedFlagSlots(panel);
                if (hydrated) {
                    prewarmRenderedNationFlags(panel);
                    PanelPerf.mark('remaining nation flags hydration sweep', {
                        era: era,
                        pass: index,
                        delay: delay,
                        hydrated: hydrated
                    });
                }
            }, Math.max(0, delay));
            panel.CLBI_NationsFlagHydrationSweepTimers.push(timer);
        });
     }


     this.celestialGroup.add(outerGlow);
     function bindRemainingFlagHydrationWatcher(panel) {
    this.celestialGroup.add(innerGlow);
        if (!panel || panel.CLBI_NationsRemainingFlagHydrationWatcherBound) return;
    this.celestialGroup.add(core);
        panel.CLBI_NationsRemainingFlagHydrationWatcherBound = true;


    this.celestialSun = core;
        if (typeof MutationObserver === 'function') {
    this.celestialSunGlowA = innerGlow;
            panel.CLBI_NationsRemainingFlagObserver = new MutationObserver(function (mutations) {
    this.celestialSunGlowB = outerGlow;
                var shouldHydrate = false;
    this.celestialSunCorona = null;
                mutations.forEach(function (mutation) {
};
                    if (shouldHydrate) return;
                    toArray(mutation.addedNodes || []).forEach(function (node) {
                        if (shouldHydrate || !node || node.nodeType !== 1) return;
                        if ((node.matches && node.matches('.clbi-nations-list-flag-slot[data-flag-file]')) ||
                            (node.querySelector && node.querySelector('.clbi-nations-list-flag-slot[data-flag-file]'))) {
                            shouldHydrate = true;
                        }
                    });
                });
                if (shouldHydrate && panel.CLBI_NationsRemainingFlagsResolved) {
                    scheduleResolvedFlagHydrationSweeps(panel, {
                        era: getPanelEra(panel),
                        delays: [0, 60, 180, 420]
                    });
                }
            });
            try {
                panel.CLBI_NationsRemainingFlagObserver.observe(panel, { childList: true, subtree: true });
            } catch (ignoreObserve) {}
        }


        panel.addEventListener('click', function (event) {
            var target = event && event.target;
            if (!panel.CLBI_NationsRemainingFlagsResolved) return;
            if (target && target.closest && target.closest('.clbi-nations-tabpanel-tab, [data-continent], .clbi-nations-era-button')) {
                scheduleResolvedFlagHydrationSweeps(panel, {
                    era: getPanelEra(panel),
                    delays: [0, 40, 120, 260]
                });
            }
        }, true);
    }


NationsGlobe.prototype.loadCelestialSun = function () {
    function scheduleAllNationFlagBackgroundWarm(panel, data) {
    var self = this;
        var urls;
    var THREE = this.THREE;
        var era;
    var textureSize = Math.max(64, Math.min(1024, Math.floor(this.sunTextureSize || 512)));


    loadLimitedImageTexture(THREE, CELESTIAL_SUN_TEXTURE_URL, textureSize)
         if (!panel || !data) return;
         .then(function (texture) {
         if (panel.CLBI_NationsAllFlagBackgroundWarmStarted) return;
            self.addCelestialSun(texture);
         })
        .catch(function (err) {
            console.warn('CLBI sun texture failed:', err);
            self.addCelestialSun(createFallbackPlanetTexture(THREE, '#d08b3e', '#5f240b'));
        });
};


        urls = collectResolvedNationFlagUrls(data);
        if (!urls.length) return;


        panel.CLBI_NationsAllFlagBackgroundWarmStarted = true;
        era = String(data && (data.year || getPanelEra(panel)) || '');


NationsGlobe.prototype.loadCelestialPlanets = function () {
        window.setTimeout(function () {
    var self = this;
            if (!document.body.contains(panel)) return;
    var THREE = this.THREE;
            warmNationFlagUrlsToCompletion(urls, panel, {
    var textureSize = Math.max(64, Math.min(1024, Math.floor(this.celestialTextureSize || 512)));
                storeKey: 'CLBI_NationsAllContinentFlagWarmImages',
                concurrency: 24,
                meta: { era: era, phase: 'all-continent-prebuilt-flags' }
            }).then(function (result) {
                PanelPerf.mark('all continent flag background load complete', {
                    era: era,
                    total: result.total,
                    ok: result.ok,
                    fail: result.fail
                });
            });
        }, 32);
    }


     if (!this.celestialEnabled || !this.celestialGroup) return;
     function scheduleRemainingNationFlagResolution(panel, data, initialFiles) {
        var allFiles = collectNationFlagFileNames(data);
        var remainingFiles = subtractNationFlagFiles(allFiles, initialFiles);
        var handle;


    CELESTIAL_PLANET_DEFS.forEach(function (def) {
        if (!panel || !remainingFiles.length) return;
         var url = CELESTIAL_PLANET_TEXTURES[def.key];
        panel.CLBI_NationsRemainingFlagFiles = remainingFiles.slice();
         panel.CLBI_NationsRemainingFlagsResolved = false;
        bindRemainingFlagHydrationWatcher(panel);


         if (!url) {
        handle = panel.CLBI_NationsRemainingFlagResolveHandle;
             self.addCelestialPlanet(def, createFallbackPlanetTexture(THREE, def.fallbackA, def.fallbackB));
         if (handle != null) {
             return;
             try { window.clearTimeout(handle); } catch (ignoreClear) {}
             try { cancelIdle(handle); } catch (ignoreIdleClear) {}
         }
         }


         loadLimitedImageTexture(THREE, url, textureSize)
         /*
            .then(function (texture) {
          The first visible continent is part of the entry-ready contract.  The
                 self.addCelestialPlanet(def, texture);
          rest of the flags must not wait until the user opens another tab:
             })
          start resolving them right after the first paint, inject eager images
            .catch(function (err) {
          into every already-rendered slot, and keep explicit Image() objects
                console.warn('CLBI celestial texture failed:', err);
          alive until load/decode completes so a fast tab switch still sees
                self.addCelestialPlanet(def, createFallbackPlanetTexture(THREE, def.fallbackA, def.fallbackB));
          warm flags instead of blank placeholders.
        */
        panel.CLBI_NationsRemainingFlagResolveHandle = window.setTimeout(function () {
            panel.CLBI_NationsRemainingFlagResolveHandle = null;
            PanelPerf.measure('resolve remaining nation flag URLs', {
                 era: String(data && (data.year || getPanelEra(panel)) || ''),
                count: remainingFiles.length,
                mode: 'post-visible-aggressive'
             }, function () {
                return resolveNationFlagFileUrls(remainingFiles).then(function () {
                    var hydrated;
                    var urls = collectResolvedNationFlagUrlsForFiles(remainingFiles);
                    var era = String(data && (data.year || getPanelEra(panel)) || '');
                    panel.CLBI_NationsRemainingFlagsResolved = true;
                    hydrated = hydrateResolvedFlagSlots(panel);
                    scheduleResolvedFlagHydrationSweeps(panel, {
                        era: era,
                        delays: [0, 16, 80, 180, 360, 720, 1200, 2000, 3200]
                    });
                    PanelPerf.mark('hydrate remaining nation flags', {
                        era: era,
                        files: remainingFiles.length,
                        hydrated: hydrated,
                        urls: urls.length,
                        mode: 'post-visible-aggressive'
                    });
                    prewarmRenderedNationFlags(panel);
                    return warmNationFlagUrlsToCompletion(urls, panel, {
                        storeKey: 'CLBI_NationsRemainingFlagWarmImages',
                        concurrency: 24,
                        meta: { era: era, phase: 'remaining-flags-aggressive' }
                    }).then(function (result) {
                        var hydratedAgain = hydrateResolvedFlagSlots(panel);
                        scheduleResolvedFlagHydrationSweeps(panel, {
                            era: era,
                            delays: [0, 100, 300, 700]
                        });
                        PanelPerf.mark('remaining nation flag background load complete', {
                            era: era,
                            total: result.total,
                            ok: result.ok,
                            fail: result.fail,
                            hydratedAgain: hydratedAgain
                        });
                    });
                });
             });
             });
    });
        }, 32);
};
    }


NationsGlobe.prototype.createCelestialBackdrop = function () {
    function waitForRenderedFlagImages(scope, options) {
    var THREE = this.THREE;
        var root = scope || document;
        var imageRoot = root;
        var images;
        var timeoutMs = options && options.timeoutMs ? Number(options && options.timeoutMs) : 15000;
        var started = Date.now();


    this.starGroup = new THREE.Group();
        if (options && options.activeOnly && root && root.querySelector) {
    this.celestialGroup = new THREE.Group();
            imageRoot = root.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]') || root;
        }
        images = toArray(imageRoot.querySelectorAll ? imageRoot.querySelectorAll('.clbi-nations-list-flag[src]') : []);


    this.scene.add(this.starGroup);
        function waitOne(img) {
    this.scene.add(this.celestialGroup);
            var src = img.currentSrc || img.getAttribute('src') || '';
            if (!src) return Promise.resolve(false);
            if (img.complete && img.naturalWidth > 0) {
                if (img.decode) return img.decode().then(function () { return true; }).catch(function () { return true; });
                return Promise.resolve(true);
            }
            return new Promise(function (resolve) {
                var done = false;
                var timer;
                function finish(ok) {
                    if (done) return;
                    done = true;
                    if (timer) window.clearTimeout(timer);
                    resolve(!!ok);
                }
                timer = window.setTimeout(function () { finish(false); }, Math.max(500, timeoutMs - (Date.now() - started)));
                img.addEventListener('load', function () {
                    if (img.decode) img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                    else finish(true);
                }, { once: true });
                img.addEventListener('error', function () { finish(false); }, { once: true });
            });
        }


    this.starGroup.add(createStars(THREE, 780));
        if (!images.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
        return Promise.all(images.map(waitOne)).then(function (results) {
            var ok = results.filter(Boolean).length;
            return { total: images.length, ok: ok, fail: images.length - ok };
        });
    }


     this.loadCelestialSun();
     function renderNationItem(item, index, items) {
    this.loadCelestialPlanets();
        var symbols = collectNationItemSymbols(item);
};
        var symbolHtml = symbols.map(renderNationSymbol).join('');
        var label = item.label || item.name || item.name_ko || item.page || item.external_url || '미지정';
        var href = item.external_url || buildNationPageUrl(item.page || item.wiki_title || '');
        var tag = String(item.tag || item.code || item.page_tag || '').trim();
        var page = String(item.page || item.wiki_title || '').trim();
        var linkHtml = href ? '<a class="clbi-nations-list-link" href="' + escapeHtml(href) + '">' + escapeHtml(label) + '</a>' : '<span class="clbi-nations-list-label">' + escapeHtml(label) + '</span>';
        var divider = index < items.length - 1 ? '<span class="clbi-nations-list-divider" aria-hidden="true" draggable="false">·</span>' : '';


NationsGlobe.prototype.updateCelestialBackdrop = function (dt, now) {
        return '<span class="clbi-nations-list-item" data-nation-tag="' + escapeHtml(tag) + '" data-nation-page="' + escapeHtml(page) + '" data-nation-label="' + escapeHtml(label) + '">' + symbolHtml + linkHtml + '</span>' + divider;
    var i;
     }
    var planet;
    var def = CELESTIAL_SUN_DEF;
     var pulseA;
    var pulseB;
    var sunOccult = 0;


     if (!this.resetMotion) {
     function renderNationEmpty() {
         this.starDriftY += dt * this.starDriftSpeed;
         return '<span class="clbi-nations-era-wip-message"><span class="clbi-nations-era-wip-message-ko">작업 중...</span><span class="clbi-nations-era-wip-message-en">Working in Progress...</span></span>';
        this.celestialDriftY += dt * this.celestialDriftSpeed;
     }
     }


     if (this.starGroup) {
     function getElementOuterWidth(element) {
         this.starGroup.rotation.x = -this.rotationX * this.starParallax;
         var rect;
         this.starGroup.rotation.y = -this.rotationY * this.starParallax + this.starDriftY;
         var style;
    }
        var width;


    if (this.celestialGroup) {
        if (!element || !element.getBoundingClientRect) return 0;
         this.celestialGroup.rotation.x = -this.rotationX * this.celestialParallax;
         rect = element.getBoundingClientRect();
         this.celestialGroup.rotation.y = -this.rotationY * this.celestialParallax + this.celestialDriftY;
        style = window.getComputedStyle ? window.getComputedStyle(element) : null;
    }
         width = rect && isFinite(rect.width) ? rect.width : (element.offsetWidth || 0);


    for (i = 0; i < this.celestialPlanets.length; i += 1) {
         if (style) {
        planet = this.celestialPlanets[i];
             width += parseFloat(style.marginLeft) || 0;
         if (planet && planet.mesh) {
             width += parseFloat(style.marginRight) || 0;
             planet.mesh.rotation.y += dt * planet.speed;
            if (planet.shell) planet.shell.rotation.y += dt * planet.speed * 0.35;
             if (planet.ring) planet.ring.rotation.z += dt * planet.speed * 0.10;
         }
         }
        return width;
     }
     }


     if (this.celestialSun) {
     function getElementContentWidth(element) {
         this.celestialSun.rotation.y += dt * def.textureScrollSpeed;
        var style;
         if (this.celestialSun.material) {
        var width;
             this.celestialSun.material.opacity = def.coreOpacity * (1 - sunOccult);
 
        if (!element) return 0;
         style = window.getComputedStyle ? window.getComputedStyle(element) : null;
        width = element.clientWidth || 0;
 
         if (style) {
             width -= parseFloat(style.paddingLeft) || 0;
            width -= parseFloat(style.paddingRight) || 0;
         }
         }
        return Math.max(0, Math.floor(width));
     }
     }


     if (this.celestialSunGlowA) {
     function isNationListElement(node, className) {
         pulseA = def.radius * def.innerGlowScale * (1 + Math.sin(now * 0.00110) * def.pulseA);
         return !!(node && node.nodeType === 1 && node.classList && node.classList.contains(className));
        this.celestialSunGlowA.scale.set(pulseA, pulseA, 1);
        if (this.celestialSunGlowA.material) {
            this.celestialSunGlowA.material.opacity = def.innerGlowOpacity * (1 + Math.sin(now * 0.00082 + 1.7) * (def.pulseB * 0.35)) * (1 - sunOccult);
        }
     }
     }


     if (this.celestialSunGlowB) {
     function collectNationListSnapGroups(well) {
         pulseB = def.radius * def.outerGlowScale * (1 + Math.sin(now * 0.00057 + 0.9) * def.pulseB);
         var nodes = toArray(well ? well.childNodes : []);
         this.celestialSunGlowB.scale.set(pulseB, pulseB, 1);
         var groups = [];
         if (this.celestialSunGlowB.material) {
        var index = 0;
             this.celestialSunGlowB.material.opacity = def.outerGlowOpacity * (1 + Math.sin(now * 0.00043 + 2.4) * (def.pulseA * 0.45)) * (1 - sunOccult);
 
         while (index < nodes.length) {
             var node = nodes[index];
            var group;
            var nextIndex;
            var nextNode;
 
            if (!isNationListElement(node, 'clbi-nations-list-item')) {
                index += 1;
                continue;
            }
 
            group = [node];
            nextIndex = index + 1;
            while (nextIndex < nodes.length && nodes[nextIndex].nodeType === 3 && !String(nodes[nextIndex].nodeValue || '').trim()) {
                nextIndex += 1;
            }
 
            nextNode = nodes[nextIndex];
            if (isNationListElement(nextNode, 'clbi-nations-list-divider')) {
                group.push(nextNode);
                index = nextIndex + 1;
            } else {
                index += 1;
            }
 
            groups.push(group);
         }
         }
        return groups;
     }
     }


     if (this.moon) {
     function measureNationListGroup(group) {
         this.moon.rotation.y += dt * CELESTIAL_MOON_DEF.rotationSpeed;
         return (group || []).reduce(function (sum, node) {
         if (this.moonShell) this.moonShell.rotation.y += dt * CELESTIAL_MOON_DEF.rotationSpeed * 0.35;
            return sum + getElementOuterWidth(node);
         }, 0);
     }
     }


     this.updateMoonOpacity(dt);
     function positionNationListSnapLines(well) {
};
        var contentWidth = getElementContentWidth(well);
 
        if (!well || !contentWidth) return;
 
        toArray(well.querySelectorAll('.nations-list-line')).forEach(function (line) {
            var inner = line.querySelector('.nations-list-line-inner');
            var rect = inner && inner.getBoundingClientRect ? inner.getBoundingClientRect() : null;
            var width = rect && isFinite(rect.width) ? rect.width : (inner ? inner.offsetWidth : 0);
            var left = Math.max(0, Math.round((contentWidth - width) / 2));
 
            /*
            * Keep visual centering, but snap the row start to the integer
            * pixel grid.  The stylesheet intentionally zeroes row padding
            * with !important, so a plain inline paddingLeft is ignored.
            * Use setProperty(..., 'important') or the row falls back to
            * left alignment even though the centering math is correct.
            */
            line.style.setProperty('padding-left', left + 'px', 'important');
        });
    }


NationsGlobe.prototype.createScene = function () {
    function snapNationListWellRows(well) {
    var THREE = this.THREE;
        var contentWidth;
    var rect = this.stage.getBoundingClientRect();
        var groups;
    var width = Math.max(320, Math.floor(rect.width || this.root.clientWidth || 960));
        var lines = [];
    var height = Math.max(240, Math.floor(rect.height || this.root.clientHeight || 360));
        var current = [];
    var texture = createEarthTexture(THREE);
        var currentWidth = 0;
    var atmosphere;
        var container;
    var light;
    var ambient;


    this.scene = new THREE.Scene();
        if (!well || !well.querySelector || !well.querySelector('.clbi-nations-list-item')) return;
    this.camera = new THREE.PerspectiveCamera(38, width / height, 0.1, 80);
    this.camera.position.set(0, 0, this.cameraZ);


    this.renderer = new THREE.WebGLRenderer({
        contentWidth = getElementContentWidth(well);
        antialias: true,
        if (!contentWidth) return;
        alpha: true,
        powerPreference: 'high-performance'
    });
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.7));
    this.renderer.setSize(width, height, false);
    this.renderer.setClearColor(0x000000, 0);
    this.stage.appendChild(this.renderer.domElement);


    this.group = new THREE.Group();
        if (!well.NationListRowSnapSourceHtml || !well.classList.contains('is-row-snapped')) {
    this.scene.add(this.group);
            well.NationListRowSnapSourceHtml = well.innerHTML;
        } else {
            well.innerHTML = well.NationListRowSnapSourceHtml;
            bindNationFlagFallbacks(well);
        }


    this.earthMaterial = new THREE.MeshPhongMaterial({
        well.classList.remove('is-row-snapped');
         map: texture,
         groups = collectNationListSnapGroups(well);
         color: 0x9fb3bb,
         if (!groups.length) return;
        emissive: 0x05080a,
        shininess: 10,
        transparent: false
    });


    this.earth = new THREE.Mesh(
        groups.forEach(function (group) {
        new THREE.SphereGeometry(this.radius, 96, 64),
            var width = Math.ceil(measureNationListGroup(group));
        this.earthMaterial
    );
    this.group.add(this.earth);


    atmosphere = new THREE.Mesh(
            if (current.length && currentWidth + width > contentWidth) {
        new THREE.SphereGeometry(this.radius * 1.035, 96, 64),
                lines.push(current);
        new THREE.MeshBasicMaterial({
                current = [];
            color: 0x5f88a8,
                currentWidth = 0;
            transparent: true,
             }
            opacity: 0.115,
             side: THREE.BackSide,
            depthWrite: false
        })
    );
    this.group.add(atmosphere);


    this.loadMoon();
            current.push(group);
            currentWidth += width;
        });


    light = new THREE.DirectionalLight(0xffffff, 2.2);
        if (current.length) lines.push(current);
    light.position.set(3.2, 2.4, 4.8);
    this.scene.add(light);


    ambient = new THREE.AmbientLight(0x607080, 0.66);
        container = document.createElement('div');
    this.scene.add(ambient);
        container.className = 'nations-list-lines';


    this.createCelestialBackdrop();
        lines.forEach(function (lineGroups) {
            var line = document.createElement('span');
            var inner = document.createElement('span');


    this.raycaster = new THREE.Raycaster();
            line.className = 'nations-list-line';
    this.pointer = new THREE.Vector2();
            inner.className = 'nations-list-line-inner';


    this.applyRotation();
            lineGroups.forEach(function (group, groupIndex) {
};
                var isLastGroupInLine = groupIndex === lineGroups.length - 1;


                group.forEach(function (node) {
                    if (isLastGroupInLine && isNationListElement(node, 'clbi-nations-list-divider')) {
                        return;
                    }
                    inner.appendChild(node);
                });
            });


NationsGlobe.prototype.updateCloudOpacity = function () {
            line.appendChild(inner);
    if (!this.cloudMaterial) return;
            container.appendChild(line);
        });


    var minZ = this.cameraZMin;
        well.innerHTML = '';
    var maxZ = this.cameraZMax;
        well.appendChild(container);
    var t = clamp01((this.cameraZ - minZ) / (maxZ - minZ));
        well.classList.add('is-row-snapped');
    var nearOpacity = Math.max(0, Math.min(1, this.cloudOpacityNear));
        positionNationListSnapLines(well);
    var farOpacity = Math.max(0, Math.min(1, this.cloudOpacityFar));
    }


     this.cloudMaterial.opacity = nearOpacity + (farOpacity - nearOpacity) * t;
     function snapNationListRows(scope) {
    this.cloudMaterial.needsUpdate = true;
        var root = scope || document;
};


        if (!root || !root.querySelectorAll) return;
        if (root !== document && isNationListPanelHidden(root)) return;
        toArray(root.querySelectorAll('.clbi-nations-tabpanel-well')).forEach(snapNationListWellRows);
    }


NationsGlobe.prototype.getProjectedScreenRadius = function (object3D, radius) {
    function shouldDelayNationListRowSnap(root, reason) {
    var THREE = this.THREE;
        if (reason === 'render') return false;
    var center;
        if (hasRecentNationPanelDirectInteraction(900)) return true;
    var edge;
        if (isNationsGlobeBusyForPanelWork()) return true;
    var rect;
        return false;
    var cx;
     }
    var cy;
     var ex;
    var ey;


     if (!this.camera || !this.renderer || !object3D || !THREE) return null;
     function scheduleNationListRowSnap(scope, options) {
        var root = scope || document;
        var force = !!(options && options.force);
        var reason = options && options.reason ? String(options.reason) : '';
        var delay;


    center = new THREE.Vector3();
        if (!root) return;
    edge = new THREE.Vector3();


    object3D.getWorldPosition(center);
        /*
    edge.copy(center).add(new THREE.Vector3(radius, 0, 0));
        * Row snap scheduler — 20260710.
        *
        * The row snap pass rewrites well contents and forces layout.  It must
        * run for the initial rendered panel and real viewport/font changes, but
        * it must not compete with continent clicks or globe manipulation.  Keep
        * the same generated DOM/classes, collapse duplicates, and postpone late
        * maintenance until the user has stopped interacting for a short window.
        */
        if (root !== document && !document.body.contains(root)) return;
        if (root !== document && isNationListPanelHidden(root)) return;
        if (!force && root.NationListRowSnapSettled) return;
        if (root.NationListRowSnapRaf || root.NationListRowSnapTimer) return;


    center.project(this.camera);
        if (force && root.NationListRowSnapSettled && reason !== 'resize' && reason !== 'font') return;
    edge.project(this.camera);


    rect = this.renderer.domElement.getBoundingClientRect();
        if (shouldDelayNationListRowSnap(root, reason)) {
    cx = (center.x * 0.5 + 0.5) * rect.width;
            delay = reason === 'font' ? 720 : 360;
    cy = (-center.y * 0.5 + 0.5) * rect.height;
            root.NationListRowSnapTimer = window.setTimeout(function () {
    ex = (edge.x * 0.5 + 0.5) * rect.width;
                root.NationListRowSnapTimer = null;
    ey = (-edge.y * 0.5 + 0.5) * rect.height;
                scheduleNationListRowSnap(root, { force: force, reason: reason || 'deferred' });
            }, delay);
            return;
        }


    return {
        root.NationListRowSnapRaf = window.requestAnimationFrame(function () {
        x: cx,
            root.NationListRowSnapRaf = null;
        y: cy,
            if (root !== document && (!document.body.contains(root) || isNationListPanelHidden(root))) return;
        r: Math.hypot(ex - cx, ey - cy)
            interactionPerfMeasure('panel row snap frame', { pass: 1, scope: root === document ? 'document' : 'panel', reason: reason || 'auto' }, function () {
    };
                snapNationListRows(root);
};
            });


NationsGlobe.prototype.getMoonEarthOverlapFactor = function () {
            window.requestAnimationFrame(function () {
    var earthScreen;
                if (root !== document && (!document.body.contains(root) || isNationListPanelHidden(root))) return;
    var moonScreen;
                interactionPerfMeasure('panel row snap frame', { pass: 2, scope: root === document ? 'document' : 'panel', reason: reason || 'auto' }, function () {
    var distance;
                    snapNationListRows(root);
    var fadeStart;
                });
    var fadeEnd;
                root.NationListRowSnapSettled = true;
            });
        });


    if (!this.moon || !this.camera || !this.renderer || !this.group) return 0;
        if (document.fonts && document.fonts.ready && !root.NationListFontSnapBound) {
            root.NationListFontSnapBound = true;
            document.fonts.ready.then(function () {
                if (!root || (root !== document && !document.body.contains(root))) return;
                if (root.NationListFontSnapSettled) return;
                root.NationListFontSnapSettled = true;
                root.NationListRowSnapSettled = false;
                scheduleNationListRowSnap(root, { force: true, reason: 'font' });
            }).catch(function () {});
        }
    }


     earthScreen = this.getProjectedScreenRadius(this.group, this.radius);
     function bindNationListRowSnapResize() {
    moonScreen = this.getProjectedScreenRadius(this.moon, CELESTIAL_MOON_DEF.radius);
        if (window.NationListRowSnapResizeBound) return;
        window.NationListRowSnapResizeBound = true;


    if (!earthScreen || !moonScreen) return 0;
        window.addEventListener('resize', function () {
            if (window.NationListRowSnapResizeRaf) {
                window.cancelAnimationFrame(window.NationListRowSnapResizeRaf);
            }


    distance = Math.hypot(moonScreen.x - earthScreen.x, moonScreen.y - earthScreen.y);
            window.NationListRowSnapResizeRaf = window.requestAnimationFrame(function () {
    fadeStart = earthScreen.r + moonScreen.r + this.moonEarthFadeMargin;
                var panel;
    fadeEnd = Math.max(0, earthScreen.r - moonScreen.r);


    if (distance >= fadeStart) return 0;
                window.NationListRowSnapResizeRaf = null;
    if (distance <= fadeEnd) return 1;
                panel = findActiveContinentPanel();
                if (!panel) return;
                panel.NationListRowSnapSettled = false;
                scheduleNationListRowSnap(panel, { force: true, reason: 'resize' });
            });
        });
    }


    return clamp01(1 - inverseLerp(fadeEnd, fadeStart, distance));
};


NationsGlobe.prototype.getMoonSunOccultFactor = function () {
    function ensureHistoryScrollText(target) {
    var THREE = this.THREE;
        var direct;
    var moonScreen;
        var wrap;
    var sunScreen;
    var moonWorld;
    var sunWorld;
    var moonCamera;
    var sunCamera;
    var distance;
    var fadeStart;
    var fadeEnd;
    var geometryOccult;
    var moonOpacityFactor;


    if (!this.moon || !this.celestialSun || !this.camera || !this.renderer || !THREE) return 0;
        if (!target || !target.querySelector) return null;
    moonOpacityFactor = Math.max(0, Math.min(1, this.moonRenderedOpacity == null ? 1 : this.moonRenderedOpacity));


    /*
        direct = target.querySelector(':scope > .clbi-nations-history-scroll-text');
    * A faded moon is a weaker occluder, but it must not suddenly release the sun.
        if (direct) return direct;
    * The final reveal is smoothed in updateSunOccultFactor.
    */


    moonWorld = new THREE.Vector3();
        if (target.children.length === 1 && target.firstElementChild && target.firstElementChild.tagName && target.firstElementChild.tagName.toLowerCase() === 'a') {
    sunWorld = new THREE.Vector3();
            target.firstElementChild.classList.add('clbi-nations-history-scroll-text');
            return target.firstElementChild;
        }


    this.moon.getWorldPosition(moonWorld);
        wrap = document.createElement('span');
    this.celestialSun.getWorldPosition(sunWorld);
        wrap.className = 'clbi-nations-history-scroll-text';
        while (target.firstChild) wrap.appendChild(target.firstChild);
        target.appendChild(wrap);
        return wrap;
    }


     moonCamera = moonWorld.clone().applyMatrix4(this.camera.matrixWorldInverse);
     function updateHistorySlideTarget(target) {
    sunCamera = sunWorld.clone().applyMatrix4(this.camera.matrixWorldInverse);
        var text;
        var wrapW;
        var textW;
        var scrollDist;
        var duration;
        var scrollKey;


    /*
        if (!target || !target.getBoundingClientRect) return;
    * In camera space, objects in front of the camera have negative z values.
        if (target.offsetParent === null && !target.getClientRects().length) return;
    * The less negative value is closer.  Only the closer moon can occult the sun.
    */
    if (moonCamera.z <= sunCamera.z) return 0;


    moonScreen = this.getProjectedScreenRadius(this.moon, CELESTIAL_MOON_DEF.radius);
        text = ensureHistoryScrollText(target);
    sunScreen = this.getProjectedScreenRadius(this.celestialSun, CELESTIAL_SUN_DEF.radius * 1.18);
        if (!text) return;


    if (!moonScreen || !sunScreen) return 0;
        target.classList.add('is-history-scroll-viewport');
        wrapW = Math.floor(target.clientWidth || 0);
        textW = Math.ceil(text.scrollWidth || 0);


    distance = Math.hypot(moonScreen.x - sunScreen.x, moonScreen.y - sunScreen.y);
        if (!wrapW || !textW || textW <= wrapW + 1) {
    fadeStart = moonScreen.r + sunScreen.r;
            target.classList.remove('is-history-scrolling');
    fadeEnd = Math.max(0, moonScreen.r - sunScreen.r * 0.35);
            text.style.animation = '';
            text.style.animationDelay = '';
            text.style.removeProperty('--scroll-dist');
            text.removeAttribute('data-history-scroll-key');
            return;
        }


    if (distance >= fadeStart) return 0;
        scrollDist = '-' + Math.max(0, textW - wrapW + 8) + 'px';
        duration = Math.max(4.5, textW / 40);
        scrollKey = scrollDist + '|' + duration;


    geometryOccult = distance <= fadeEnd
         target.classList.add('is-history-scrolling');
         ? 1
        : clamp01(1 - inverseLerp(fadeEnd, fadeStart, distance));


    return geometryOccult * moonOpacityFactor;
        if (text.getAttribute('data-history-scroll-key') === scrollKey) return;
};
        text.setAttribute('data-history-scroll-key', scrollKey);
        text.style.setProperty('--scroll-dist', scrollDist);
        text.style.animation = 'clbi-scroll ' + duration + 's linear infinite';
        text.style.animationDelay = '0.4s';
    }


NationsGlobe.prototype.updateSunOccultFactor = function (targetOccult, dt) {
    function updateHistoryItemSlides(scope) {
    var target = Math.max(0, Math.min(1, targetOccult || 0));
        var root = scope || document;
    var duration = Math.max(120, this.sunOccultFadeDuration || this.moonFadeDuration || this.resetDuration || 520);
    var step = dt == null ? 16.6667 : Math.max(0, Math.min(80, dt));
    var alpha = 1 - Math.pow(1 - clamp01(step / duration), 3);


    /*
        if (!root || !root.querySelectorAll) return;
    * Do not create a new transition every time the target changes.
    * The moon is fading every frame, so targetOccult changes every frame too.
    * A continuous low-pass interpolation prevents the sun from popping out
    * when the moon finally stops acting as an occluder.
    */
    this.sunOccultRendered = (this.sunOccultRendered || 0) + (target - (this.sunOccultRendered || 0)) * alpha;


    if (Math.abs(this.sunOccultRendered - target) < 0.0008) {
        toArray(root.querySelectorAll('.clbi-nations-history-period, .clbi-nations-history-event-name')).forEach(updateHistorySlideTarget);
        this.sunOccultRendered = target;
     }
     }


     if (this.sunOccultRendered < 0.0008) this.sunOccultRendered = 0;
     function scheduleHistoryItemSlides(scope) {
    if (this.sunOccultRendered > 0.9992) this.sunOccultRendered = 1;
        var root = scope || document;


    return this.sunOccultRendered;
        if (!root) return;
};
        if (root.HistoryItemSlideRaf) {
            window.cancelAnimationFrame(root.HistoryItemSlideRaf);
            root.HistoryItemSlideRaf = null;
        }


NationsGlobe.prototype.updateMoonOpacity = function (dt) {
        root.HistoryItemSlideRaf = window.requestAnimationFrame(function () {
    var vanishZ = Math.max(this.cameraZMin, this.moonVanishZ);
            root.HistoryItemSlideRaf = null;
    var fullZ = Math.max(vanishZ + 0.01, this.moonFullVisibleZ);
            if (root !== document && !document.body.contains(root)) return;
    var t = clamp01((this.cameraZ - vanishZ) / (fullZ - vanishZ));
            updateHistoryItemSlides(root);
    var moonNear = Math.max(0, Math.min(1, this.moonOpacityNear));
    var moonFar = Math.max(0, Math.min(1, this.moonOpacityFar));
    var shellNear = Math.max(0, Math.min(1, this.moonShellOpacityNear));
    var shellFar = Math.max(0, Math.min(1, this.moonShellOpacityFar));
    var earthFadeOpacity = Math.max(0, Math.min(1, this.moonEarthFadeOpacity));
    var overlap = this.getMoonEarthOverlapFactor();
    var targetOpacity;
    var targetShellOpacity;
    var duration = Math.max(120, this.moonFadeDuration || this.resetDuration || 520);
    var step = dt == null ? 16.6667 : Math.max(0, Math.min(80, dt));
    var transition;
    var k;
    var isSolidMoon;


    if (this.cameraZ <= vanishZ) {
            window.requestAnimationFrame(function () {
        targetOpacity = 0;
                if (root !== document && !document.body.contains(root)) return;
        targetShellOpacity = 0;
                updateHistoryItemSlides(root);
    } else {
            });
        targetOpacity = moonNear + (moonFar - moonNear) * t;
         });
         targetShellOpacity = shellNear + (shellFar - shellNear) * t;


         if (overlap > 0) {
         if (document.fonts && document.fonts.ready && !root.HistoryItemSlideFontBound) {
             targetOpacity = targetOpacity + (earthFadeOpacity - targetOpacity) * overlap;
             root.HistoryItemSlideFontBound = true;
             targetShellOpacity = targetShellOpacity * (1 - overlap);
            document.fonts.ready.then(function () {
                scheduleHistoryItemSlides(root);
             }).catch(function () {});
         }
         }
     }
     }


     if (!this.moonFadeMotion ||
     function bindHistoryItemSlideResize() {
         Math.abs((this.moonFadeMotion.targetOpacity || 0) - targetOpacity) > 0.001 ||
         if (window.HistoryItemSlideResizeBound) return;
         Math.abs((this.moonFadeMotion.targetShellOpacity || 0) - targetShellOpacity) > 0.001) {
        window.HistoryItemSlideResizeBound = true;
 
         window.addEventListener('resize', function () {
            if (window.HistoryItemSlideResizeRaf) {
                window.cancelAnimationFrame(window.HistoryItemSlideResizeRaf);
            }


        this.moonFadeMotion = {
             window.HistoryItemSlideResizeRaf = window.requestAnimationFrame(function () {
            elapsed: 0,
                window.HistoryItemSlideResizeRaf = null;
             duration: duration,
                scheduleHistoryItemSlides(document);
            fromOpacity: this.moonRenderedOpacity === null ? targetOpacity : this.moonRenderedOpacity,
             });
            fromShellOpacity: this.moonRenderedShellOpacity === null ? targetShellOpacity : this.moonRenderedShellOpacity,
         });
            targetOpacity: targetOpacity,
             targetShellOpacity: targetShellOpacity
         };
     }
     }


     transition = this.moonFadeMotion;
     function playNationsPanelClickSound() {
        try {
            if (window && typeof window.playStaticSound === 'function') {
                window.playStaticSound();
            } else if (typeof playStaticSound === 'function') {
                playStaticSound();
            }
        } catch (err) {}
    }


     if (transition) {
     function getNationItemFromEventTarget(target, panel) {
         transition.elapsed += step;
         var item = target && target.closest ? target.closest('.clbi-nations-list-item[data-nation-tag]') : null;
         k = easeOutCubic(clamp01(transition.elapsed / transition.duration));
         if (!item || !panel || !panel.contains(item)) return null;
        return item;
    }


         this.moonRenderedOpacity = transition.fromOpacity + (transition.targetOpacity - transition.fromOpacity) * k;
    function getGlobeInstanceForNationItem(item) {
         this.moonRenderedShellOpacity = transition.fromShellOpacity + (transition.targetShellOpacity - transition.fromShellOpacity) * k;
         var stack = item && item.closest ? item.closest('.clbi-nations-panel-stack') : null;
         var globe = stack && stack.querySelector ? stack.querySelector('.clbi-nations-globe-window[data-nations-globe], .clbi-nations-globe-window') : null;
        var instance = globe && globe.CLBI_NationsGlobeInstance ? globe.CLBI_NationsGlobeInstance : null;


         if (transition.elapsed >= transition.duration) {
         if (instance && typeof instance.focusNationByPage === 'function') return instance;
             this.moonRenderedOpacity = transition.targetOpacity;
        if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function' && stack) {
             this.moonRenderedShellOpacity = transition.targetShellOpacity;
             window.CLBI_NationsGlobe.init(stack);
            this.moonFadeMotion = null;
             instance = globe && globe.CLBI_NationsGlobeInstance ? globe.CLBI_NationsGlobeInstance : null;
         }
         }
     } else {
 
         this.moonRenderedOpacity = targetOpacity;
        return instance && typeof instance.focusNationByPage === 'function' ? instance : null;
         this.moonRenderedShellOpacity = targetShellOpacity;
     }
 
    function clearNationPanelHoverState(panel, except) {
         if (!panel || !panel.querySelectorAll) return;
         toArray(panel.querySelectorAll('.clbi-nations-list-item.is-panel-hovered')).forEach(function (item) {
            if (item !== except) item.classList.remove('is-panel-hovered');
        });
     }
     }


     /*
     function cancelNationPanelPendingHoverFocus(panel) {
    * Do not hide at a visible 1% tail.  Keep rendering until the opacity is
        if (!panel) return;
    * effectively zero, so the final disappearance reads as a fade, not a pop.
 
    */
        panel.CLBI_NationHoverFocusToken = (panel.CLBI_NationHoverFocusToken || 0) + 1;
    if (this.moonRenderedOpacity < 0.0008) this.moonRenderedOpacity = 0;
 
    if (this.moonRenderedShellOpacity < 0.0008) this.moonRenderedShellOpacity = 0;
        if (panel.CLBI_NationHoverFocusTimer) {
            window.clearTimeout(panel.CLBI_NationHoverFocusTimer);
            panel.CLBI_NationHoverFocusTimer = null;
        }


    isSolidMoon = overlap <= 0.001 && this.moonRenderedOpacity >= 0.999;
        if (panel.CLBI_NationHoverFocusRaf) {
            window.cancelAnimationFrame(panel.CLBI_NationHoverFocusRaf);
            panel.CLBI_NationHoverFocusRaf = null;
        }


    if (this.moon && this.moon.material) {
         panel.CLBI_NationHoverFocusPendingTag = '';
        this.moon.material.transparent = !isSolidMoon;
        this.moon.material.opacity = isSolidMoon ? 1 : this.moonRenderedOpacity;
         this.moon.material.depthWrite = true;
        this.moon.visible = (isSolidMoon ? 1 : this.moonRenderedOpacity) > 0;
        this.moon.material.needsUpdate = true;
     }
     }


     if (this.moonShell && this.moonShell.material) {
     function clearNationPanelHoverFocus(panel) {
         this.moonShell.material.opacity = this.moonRenderedShellOpacity;
         cancelNationPanelPendingHoverFocus(panel);
         this.moonShell.visible = this.moonRenderedShellOpacity > 0;
         clearNationPanelHoverState(panel, null);
        this.moonShell.material.needsUpdate = true;
     }
     }
};


    function requestNationItemGlobeFocus(panel, item, immediate) {
        var tag;
        var delay;
        var token;
        if (!panel || !item) return;
        tag = String(item.getAttribute('data-nation-tag') || '').trim();
        if (!tag) return;
        cancelNationPanelPendingHoverFocus(panel);
        clearNationPanelHoverState(panel, item);
        item.classList.add('is-panel-hovered');
        delay = immediate ? 0 : 75;
        token = panel.CLBI_NationHoverFocusToken || 0;
        panel.CLBI_NationHoverFocusPendingTag = tag;
        panel.CLBI_NationHoverFocusTimer = window.setTimeout(function () {
            panel.CLBI_NationHoverFocusTimer = null;
            if (panel.CLBI_NationHoverFocusToken !== token) return;


NationsGlobe.prototype.loadOptionalTextures = function () {
            panel.CLBI_NationHoverFocusRaf = window.requestAnimationFrame(function () {
    var self = this;
                var instance;
    var THREE = this.THREE;
    var tasks = [];


    if (this.terrainUrl && this.earthMaterial) {
                panel.CLBI_NationHoverFocusRaf = null;
        var earthLayers = [];
                if (panel.CLBI_NationHoverFocusToken !== token) return;
                if (!document.body.contains(item) || !item.classList.contains('is-panel-hovered')) return;
                if (panel.CLBI_NationHoverFocusPendingTag !== tag) return;


        if (this.reliefOverlayUrl) {
                instance = getGlobeInstanceForNationItem(item);
            earthLayers.push({
                if (instance && typeof instance.focusNationByPage === 'function') {
                label: 'relief',
                    instance.focusNationByPage(tag, {
                url: this.reliefOverlayUrl,
                        source: 'nation-panel',
                opacity: this.reliefOverlayOpacity,
                        label: item.getAttribute('data-nation-label') || '',
                blend: this.reliefOverlayBlend || 'source-over',
                        duration: immediate ? 360 : null
                 filter: this.reliefOverlayFilter || 'none'
                    });
                 }
             });
             });
        }, delay);
    }
    function markNationContinentSwitching(panel) {
        var detail;
        if (!panel) return;
        panel.CLBI_NationContinentSwitchingUntil = Date.now() + 240;
        panel.classList.add('is-continent-switching');
        /*
        * Continent switch relief signal — 20260708.
        *
        * The visible tab/body swap is tiny, but the globe render loop, pointer
        * hit-test, and decorative maintenance can compete for the same frame.
        * Broadcast a short, explicit relief window so NationsGlobe can skip
        * non-essential resize/raycast work while the panel is repainting.  This
        * does not remove or reload globe assets; it only avoids doing expensive
        * housekeeping in the exact input frame where the user expects the tab to
        * respond instantly.
        */
        detail = { panel: panel, until: panel.CLBI_NationContinentSwitchingUntil, duration: 240 };
        try {
            window.dispatchEvent(new CustomEvent('clbi:nations-continent-switching', { detail: detail }));
        } catch (err) {}
        window.setTimeout(function () {
            if (!panel || Date.now() < (panel.CLBI_NationContinentSwitchingUntil || 0)) return;
            panel.classList.remove('is-continent-switching');
        }, 260);
    }
    function isNationContinentSwitching(panel) {
        return !!(panel && Date.now() < (panel.CLBI_NationContinentSwitchingUntil || 0));
    }
    var lastContinentPointerX = null;
    var lastContinentPointerY = null;
    var continentHoverGuardReady = false;
    function rememberContinentPointer(event) {
        if (!event) return;
        lastContinentPointerX = event.clientX;
        lastContinentPointerY = event.clientY;
    }
    function getContinentTabUnderPointer(panel) {
        var element;
        var tab = null;
        var hovered;
        if (!panel || !panel.querySelectorAll) return null;
        /*
        * Keyboard/shortcut continent switches do not move the physical mouse.
        * Browsers therefore keep the old CSS :hover target alive even after JS
        * changes the active tab.  We treat the tab under the last pointer
        * coordinate as a stale hover candidate and suppress only that visual
        * state until the pointer actually moves away or onto another tab.
        */
        if (lastContinentPointerX !== null && lastContinentPointerY !== null && typeof document.elementFromPoint === 'function') {
            element = document.elementFromPoint(lastContinentPointerX, lastContinentPointerY);
            tab = element && element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
            if (tab && panel.contains(tab)) return tab;
        }
        try {
            hovered = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]:hover'));
            if (hovered.length) return hovered[hovered.length - 1];
        } catch (err) {}
        return null;
    }
    function clearContinentKeyboardStaleHoverMarkers(panel) {
        if (!panel || !panel.querySelectorAll) return;
        toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab.is-keyboard-stale-hover')).forEach(function (tab) {
            tab.classList.remove('is-keyboard-stale-hover');
            tab.removeAttribute('data-clbi-keyboard-stale-hover');
        });
    }
    function clearContinentKeyboardHoverSuppress(panel) {
        if (!panel) return;
        panel.classList.remove('is-keyboard-switching');
        panel.removeAttribute('data-clbi-hover-suppress-continent');
        panel.removeAttribute('data-clbi-hover-suppress-source');
        clearContinentKeyboardStaleHoverMarkers(panel);
    }
    function syncContinentKeyboardHoverSuppress(panel, activeTab, force) {
        var staleTab;
        var focusedTab;
        var staleContinent;
        var active = activeTab;
        if (!panel) return;
        if (!active && panel.querySelector) {
            active = panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
        }
        staleTab = getContinentTabUnderPointer(panel);
        if (staleTab && active && staleTab === active) {
            clearContinentKeyboardHoverSuppress(panel);
            return;
         }
         }


         if (this.polygonOverlayEnabled && this.polygonOverlayUrl) {
         if (staleTab && (staleTab.classList.contains('is-active') || staleTab.getAttribute('aria-selected') === 'true')) {
            earthLayers.push({
             clearContinentKeyboardHoverSuppress(panel);
                label: 'polygon',
            return;
                url: this.polygonOverlayUrl,
                opacity: this.polygonOverlayOpacity,
                blend: this.polygonOverlayBlend || 'source-over',
                filter: this.polygonOverlayFilter || 'none'
             });
         }
         }


         if (earthLayers.length) {
        focusedTab = document.activeElement && document.activeElement.closest ?
             tasks.push(
            document.activeElement.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
                createCompositedEarthTextureLayers(THREE, this.terrainUrl, earthLayers, this.earthCompositeSize)
 
                    .then(function (texture) {
         if (focusedTab && panel.contains(focusedTab) && active && focusedTab !== active) {
                        if (!self.earthMaterial) return;
             /*
                        self.earthMaterial.map = texture;
            * A mouse click can leave keyboard focus on the old tab. If a later
                        self.earthMaterial.color.set(0xffffff);
            * Ctrl+Q/Ctrl+E switch moves the active state elsewhere, that stale
                        self.earthMaterial.emissive.set(0x020304);
            * :focus rule looks exactly like a stuck hover. Move focus to the
                        self.earthMaterial.shininess = 6;
            * newly active tab only when focus is already inside this continent
                        self.earthMaterial.needsUpdate = true;
            * tab strip; normal document focus outside the strip is not touched.
            */
            try { active.focus({ preventScroll: true }); } catch (err) { try { active.focus(); } catch (ignore) {} }
            if (!staleTab) staleTab = focusedTab;
        }


                        if (window.console && console.info) {
        if (!staleTab) {
                            console.info('CLBI earth composite applied:', {
             if (!force) clearContinentKeyboardHoverSuppress(panel);
                                terrainUrl: self.terrainUrl,
             return;
                                earthCompositeSize: self.earthCompositeSize,
                                reliefOverlayUrl: self.reliefOverlayUrl,
                                reliefOverlayOpacity: self.reliefOverlayOpacity,
                                reliefOverlayBlend: self.reliefOverlayBlend,
                                reliefOverlayFilter: self.reliefOverlayFilter,
                                polygonOverlayUrl: self.polygonOverlayUrl,
                                polygonOverlayOpacity: self.polygonOverlayOpacity,
                                polygonOverlayBlend: self.polygonOverlayBlend,
                                polygonOverlayFilter: self.polygonOverlayFilter
                            });
                        }
                    })
                    .catch(function (err) {
                        console.warn('CLBI earth composite failed, falling back to base terrain:', err);
                        return loadImageTexture(THREE, self.terrainUrl)
                            .then(function (texture) {
                                if (!self.earthMaterial) return;
                                self.earthMaterial.map = texture;
                                self.earthMaterial.color.set(0xffffff);
                                self.earthMaterial.emissive.set(0x020304);
                                self.earthMaterial.shininess = 6;
                                self.earthMaterial.needsUpdate = true;
                            });
                    })
            );
        } else {
             tasks.push(
                loadImageTexture(THREE, this.terrainUrl)
                    .then(function (texture) {
                        if (!self.earthMaterial) return;
                        self.earthMaterial.map = texture;
                        self.earthMaterial.color.set(0xffffff);
                        self.earthMaterial.emissive.set(0x020304);
                        self.earthMaterial.shininess = 6;
                        self.earthMaterial.needsUpdate = true;
                    })
                    .catch(function (err) {
                        console.warn('CLBI terrain texture failed:', err);
                    })
             );
         }
         }
        staleContinent = staleTab.getAttribute('data-continent') || '';
        /*
        * Stale hover/focus marker — 20260708.
        *
        * The panel-level is-keyboard-switching class is still useful, but it is
        * not specific enough after warm-cache/SAP prepaint fixes because click
        * and keyboard paths can now share the same live delegate.  Mark the
        * exact tab that remained under the physical mouse or retained focus.
        * CSS can then neutralize only that stale visual surface while the real
        * active tab stays highlighted.  Pointer movement, pointerleave, click,
        * and blur all clear this marker through clearContinentKeyboardHoverSuppress().
        */
        clearContinentKeyboardStaleHoverMarkers(panel);
        staleTab.classList.add('is-keyboard-stale-hover');
        staleTab.setAttribute('data-clbi-keyboard-stale-hover', '1');
        panel.classList.add('is-keyboard-switching');
        panel.setAttribute('data-clbi-hover-suppress-source', 'keyboard-continent-switch');
        if (staleContinent) panel.setAttribute('data-clbi-hover-suppress-continent', staleContinent);
        else panel.removeAttribute('data-clbi-hover-suppress-continent');
     }
     }


     if (this.lakesOverlayUrl && this.group) {
     function validateContinentKeyboardHoverSuppressions() {
        tasks.push(
        toArray(document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching')).forEach(function (panel) {
             loadImageTexture(THREE, this.lakesOverlayUrl)
            var suppressed = panel.getAttribute('data-clbi-hover-suppress-continent') || '';
                 .then(function (texture) {
             var hovered = getContinentTabUnderPointer(panel);
                    var material;
            var hoveredContinent = hovered ? (hovered.getAttribute('data-continent') || '') : '';
                    var mesh;
            var hoveredActive = hovered && (hovered.classList.contains('is-active') || hovered.getAttribute('aria-selected') === 'true');
                    var radius;
 
            if (!hovered || hoveredActive || (suppressed && hoveredContinent !== suppressed)) {
                 clearContinentKeyboardHoverSuppress(panel);
            } else if (hovered && suppressed && hoveredContinent === suppressed) {
                clearContinentKeyboardStaleHoverMarkers(panel);
                hovered.classList.add('is-keyboard-stale-hover');
                hovered.setAttribute('data-clbi-keyboard-stale-hover', '1');
            }
        });
    }


                    if (!self.group) return;
    function ensureContinentKeyboardHoverGuard() {
        if (continentHoverGuardReady) return;
        continentHoverGuardReady = true;


                    radius = self.radius + Math.max(0.0012, Math.min(0.008, self.lakesSurfaceOffset));
        document.addEventListener('pointermove', function (event) {
            rememberContinentPointer(event);
            validateContinentKeyboardHoverSuppressions();
        }, true);


                    material = new THREE.MeshBasicMaterial({
        document.addEventListener('pointerleave', function () {
                        map: texture,
            lastContinentPointerX = null;
                        transparent: true,
            lastContinentPointerY = null;
                        opacity: Math.max(0, Math.min(1.25, self.lakesOverlayOpacity)),
            validateContinentKeyboardHoverSuppressions();
                        depthWrite: false,
        }, true);
                        depthTest: true
                    });


                    mesh = new THREE.Mesh(
        window.addEventListener('blur', function () {
                        new THREE.SphereGeometry(radius, 128, 64),
            lastContinentPointerX = null;
                        material
            lastContinentPointerY = null;
                    );
            validateContinentKeyboardHoverSuppressions();
                    mesh.renderOrder = 12;
        });
                    self.group.add(mesh);
    }
                    self.lakesOverlay = mesh;


                    if (window.console && console.info) {
    function clearNationItemPanelHoverOnly(panel, item) {
                        console.info('CLBI 1950 lakes overlay applied:', {
        var tag;
                            lakesOverlayUrl: self.lakesOverlayUrl,
        if (!panel || !item) return;
                            lakesOverlayOpacity: self.lakesOverlayOpacity,
        tag = String(item.getAttribute('data-nation-tag') || '').trim();
                            lakesSurfaceOffset: self.lakesSurfaceOffset
        item.classList.remove('is-panel-hovered');
                        });
        if (panel.CLBI_NationHoverFocusPendingTag === tag) cancelNationPanelPendingHoverFocus(panel);
                    }
                })
                .catch(function (err) {
                    console.warn('CLBI lakes overlay failed:', err);
                })
        );
     }
     }


     if (this.cloudsUrl) {
     function clearNationItemGlobeFocus(panel, item) {
         tasks.push(
        var tag;
             loadImageTexture(THREE, this.cloudsUrl)
        var instance;
                .then(function (texture) {
        var timer;
                    var material = new THREE.MeshBasicMaterial({
 
                        map: texture,
        if (!panel || !item) return;
                        color: 0xffffff,
         if (isNationContinentSwitching(panel)) {
                        transparent: true,
             clearNationItemPanelHoverOnly(panel, item);
                        opacity: self.cloudOpacityFar,
            return;
                        depthWrite: false,
        }
                        side: THREE.DoubleSide
        tag = String(item.getAttribute('data-nation-tag') || '').trim();
                    });
 
        item.classList.remove('is-panel-hovered');
 
        timer = panel.CLBI_NationHoverFocusTimer || null;
        if (panel.CLBI_NationHoverFocusPendingTag === tag ||
            (!panel.CLBI_NationHoverFocusPendingTag && (timer || panel.CLBI_NationHoverFocusRaf))) {
            cancelNationPanelPendingHoverFocus(panel);
        }


                    self.cloudMaterial = material;
        if (!tag) return;
                    self.clouds = new THREE.Mesh(
        instance = getGlobeInstanceForNationItem(item);
                        new THREE.SphereGeometry(self.radius * 1.018, 96, 64),
        if (instance && typeof instance.clearPanelNationFocus === 'function') {
                        material
            instance.clearPanelNationFocus(tag);
                    );
        }
                    self.clouds.renderOrder = 4;
                    self.group.add(self.clouds);
                    self.updateCloudOpacity();
                })
                .catch(function (err) {
                    console.warn('CLBI cloud texture failed:', err);
                })
        );
     }
     }


    return Promise.all(tasks).then(function () {});
};


    function bindNationListHoverFocus(panel) {
        if (!panel || panel.CLBI_NationPanelHoverFocusBound) return;
        panel.CLBI_NationPanelHoverFocusBound = true;
        panel.setAttribute('data-nation-panel-hover-focus-ready', '1');
        panel.addEventListener('mouseover', function (event) {
            var item = getNationItemFromEventTarget(event.target, panel);
            if (!item) return;
            if (event.relatedTarget && item.contains(event.relatedTarget)) return;
            requestNationItemGlobeFocus(panel, item, false);
        });
        panel.addEventListener('mouseout', function (event) {
            var item = getNationItemFromEventTarget(event.target, panel);
            if (!item) return;
            if (event.relatedTarget && item.contains(event.relatedTarget)) return;
            clearNationItemGlobeFocus(panel, item);
        });


NationsGlobe.prototype.cancelResetMotion = function () {
        panel.addEventListener('focusin', function (event) {
    this.resetMotion = null;
            var item = getNationItemFromEventTarget(event.target, panel);
    this.autoResumeMotion = null;
            if (item) requestNationItemGlobeFocus(panel, item, true);
};
        });


NationsGlobe.prototype.startAutoResumeMotion = function () {
        panel.addEventListener('focusout', function (event) {
    if (!this.autoRotate) {
            var item = getNationItemFromEventTarget(event.target, panel);
        this.autoResumeMotion = null;
            if (item) clearNationItemGlobeFocus(panel, item);
         return;
         });
     }
     }


    this.autoResumeMotion = {
        elapsed: 0,
        duration: Math.max(900, this.autoResumeDuration || this.resetDuration || 1800),
        from: Math.max(0, Math.min(0.35, this.autoResumeMinFactor || 0.06)),
        to: 1
    };
};


NationsGlobe.prototype.getAutoRotateFactor = function (dt) {
    function buildNationTabSkeletonHtml(extraClass) {
    var motion = this.autoResumeMotion;
        var tabs = NATION_CONTINENT_FALLBACK.map(function (continent, index) {
    var t;
            var active = index === 0;
            return '<span class="clbi-nations-tabpanel-tab' + (active ? ' is-active' : '') + '" role="tab" tabindex="' + (active ? '0' : '-1') + '" aria-selected="' + (active ? 'true' : 'false') + '" data-continent="' + escapeHtml(continent.key) + '">' + escapeHtml(continent.label) + '</span>';
        }).join('');


    if (!motion) return 1;
        var bodies = NATION_CONTINENT_FALLBACK.map(function (continent, index) {
            var rows = continent.regions.map(function (region) {
                return '<div class="clbi-nations-tabpanel-row">' +
                    '<div class="clbi-nations-tabpanel-region">' + escapeHtml(region) + '</div>' +
                    '<div class="clbi-nations-tabpanel-well">' + renderNationEmpty() + '</div>' +
                '</div>';
            }).join('');


    motion.elapsed += dt;
            return '<div class="clbi-nations-tabpanel-continent' + (index === 0 ? ' is-active' : '') + '" data-continent-panel="' + escapeHtml(continent.key) + '" aria-hidden="' + (index === 0 ? 'false' : 'true') + '">' + rows + '</div>';
    t = clamp01(motion.elapsed / motion.duration);
        }).join('');


    /*
        return '<div class="clbi-nations-tabpanel' + (extraClass ? ' ' + extraClass : '') + '" data-clbi-nations-simple data-nation-list-source="1">' +
    * Speedometer-style ramp: no hidden threshold, no late kick.
            '<div class="clbi-nations-tabpanel-tabs" role="tablist" aria-label="대륙 선택">' +
    * The factor rises almost linearly from a very slow crawl to normal speed,
                tabs +
    * so the last frame before completion is already nearly identical to 1.0.
                '<span class="clbi-nations-tabpanel-title" aria-hidden="true">당대 존재 국가</span>' +
    */
            '</div>' +
    if (t >= 1) {
            '<div class="clbi-nations-tabpanel-body">' + bodies + '</div>' +
        this.autoResumeMotion = null;
         '</div>';
         return 1;
     }
     }


     return motion.from + (motion.to - motion.from) * t;
     function buildHistoryPanelSkeletonHtml(era) {
};
        var start = parseInt(era || '1950', 10);
        var years = [];


NationsGlobe.prototype.startResetMotion = function () {
        if (!isFinite(start)) start = 1950;
    var toY;


    this.velocityX = 0;
        for (var year = start; year < start + 10; year += 1) {
    this.velocityY = 0;
            years.push(String(year));
    this.dragging = false;
        }
    this.autoResumeMotion = null;
    this.autoRotate = this.resetAutoRotate;
    this.syncAutoButton();
    this.root.classList.remove('is-dragging');


    /*
        var pages = years.map(function (year, index) {
    * RESET restores the exact first-load state.
            return '<div class="clbi-nations-history-list clbi-nations-history-page' + (index === 0 ? ' is-active' : '') + '" data-year-panel="' + escapeHtml(year) + '"' + (index === 0 ? '' : ' hidden="hidden"') + '>' +
    * The earth still takes the nearest visual path back, but the sky drift is
                '<div class="clbi-nations-history-empty"><span class="clbi-nations-history-empty-ko">작업 중...</span><span class="clbi-nations-history-empty-en">Working in Progress...</span></div>' +
    * compensated during the animation so the celestial background lands on the
            '</div>';
    * same phase it had when the globe first opened.
        }).join('');
    */
    toY = nearestEquivalentAngle(this.rotationY, this.resetRotationY);


    this.resetMotion = {
        var tabs = years.map(function (year, index) {
        elapsed: 0,
            return '<span class="clbi-nations-history-year-button' + (index === 0 ? ' is-active' : '') + '" role="tab" tabindex="' + (index === 0 ? '0' : '-1') + '" aria-selected="' + (index === 0 ? 'true' : 'false') + '" data-year="' + escapeHtml(year) + '">' + escapeHtml(year) + '</span>';
        duration: Math.max(120, this.resetDuration),
        }).join('');
        fromX: this.rotationX,
        fromY: this.rotationY,
        fromZ: this.cameraZ,
        fromStarDriftY: this.starDriftY,
        fromCelestialDriftY: this.celestialDriftY,
        toX: this.resetRotationX,
        toY: toY,
        finalY: this.resetRotationY,
        toZ: this.resetCameraZ,
        toStarDriftY: (toY - this.resetRotationY) * this.starParallax,
        toCelestialDriftY: (toY - this.resetRotationY) * this.celestialParallax
    };
};


NationsGlobe.prototype.stepResetMotion = function (dt) {
        return '<div class="clbi-nations-forces-panel clbi-nations-module-panel clbi-nations-history-panel">' +
    var motion = this.resetMotion;
            '<div class="clbi-nations-module-title">역사적 사건</div>' +
    var t;
            '<div class="clbi-nations-module-body clbi-nations-history-body">' +
     var k;
                '<div class="clbi-nations-history-pages">' + pages + '</div>' +
                '<div class="clbi-nations-history-year-well"><div class="clbi-nations-history-year-list" data-decade-only="1" role="tablist" aria-label="역사적 사건 연도 선택">' + tabs + '</div></div>' +
            '</div>' +
        '</div>';
     }


     if (!motion) return false;
     function buildEraContentSkeletonHtml(era) {
        return '<div class="clbi-nations-era-content clbi-nations-era-wip" data-era-content="' + escapeHtml(era) + '" hidden="hidden">' +
            buildHistoryPanelSkeletonHtml(era) +
            buildNationTabSkeletonHtml('clbi-nations-era-wip-tabpanel') +
        '</div>';
    }


     motion.elapsed += dt;
     function ensureEraContentPanels(stack, eras) {
    t = clamp01(motion.elapsed / motion.duration);
        var insertAfter;
    k = easeOutCubic(t);


    this.rotationX = motion.fromX + (motion.toX - motion.fromX) * k;
        if (!stack || !eras || !eras.length) return;
    this.rotationY = motion.fromY + (motion.toY - motion.fromY) * k;
    this.cameraZ = motion.fromZ + (motion.toZ - motion.fromZ) * k;
    this.starDriftY = motion.fromStarDriftY + (motion.toStarDriftY - motion.fromStarDriftY) * k;
    this.celestialDriftY = motion.fromCelestialDriftY + (motion.toCelestialDriftY - motion.fromCelestialDriftY) * k;


    if (this.camera) this.camera.position.z = this.cameraZ;
        insertAfter = toArray(stack.querySelectorAll('.clbi-nations-era-content[data-era-content]')).pop() || stack.querySelector('.clbi-nations-map-row');
    this.updateCloudOpacity();
    this.updateMoonOpacity(dt);
    this.applyRotation();


    if (t >= 1) {
        eras.forEach(function (era) {
        this.rotationX = motion.toX;
            var selector = '.clbi-nations-era-content[data-era-content="' + String(era).replace(/"/g, '\\"') + '"]';
        this.rotationY = motion.finalY;
            var wrapper;
        this.cameraZ = motion.toZ;
            var panel;
        this.starDriftY = 0;
        this.celestialDriftY = 0;
        if (this.camera) this.camera.position.z = this.cameraZ;
        this.updateCloudOpacity();
        this.applyRotation();
        this.resetMotion = null;
        if (this.autoRotate) this.startAutoResumeMotion();
    }


    return true;
            if (stack.querySelector(selector)) return;
};


NationsGlobe.prototype.applyRotation = function () {
            wrapper = document.createElement('div');
    if (!this.group) return;
            wrapper.innerHTML = buildEraContentSkeletonHtml(era);
    this.rotationX = Math.max(-1.15, Math.min(1.15, this.rotationX));
            panel = wrapper.firstElementChild;
    this.group.rotation.x = this.rotationX;
    this.group.rotation.y = this.rotationY;
};


NationsGlobe.prototype.resize = function () {
            if (!panel) return;
    if (!this.renderer || !this.camera || !this.stage) return;


    var rect = this.stage.getBoundingClientRect();
            if (insertAfter && insertAfter.parentNode) {
    var width = Math.max(320, Math.floor(rect.width || 0));
                insertAfter.parentNode.insertBefore(panel, insertAfter.nextSibling);
    var height = Math.max(240, Math.floor(rect.height || 0));
            } else {
                stack.appendChild(panel);
            }


    if (!width || !height) return;
            insertAfter = panel;
        });
    }


     this.camera.aspect = width / height;
     function renderNationContinent(continent, activeKey) {
    this.camera.updateProjectionMatrix();
        var rows = (continent.regions || []).map(function (region) {
    this.renderer.setSize(width, height, false);
            var items = Array.isArray(region.items) ? region.items : [];
};
            var content = items.length ? items.map(function (item, index) {
                return renderNationItem(item, index, items);
            }).join(' ') : renderNationEmpty();


            return '' +
                '<div class="clbi-nations-tabpanel-row">' +
                    '<div class="clbi-nations-tabpanel-region">' + escapeHtml(region.label || region.key || '') + '</div>' +
                    '<div class="clbi-nations-tabpanel-well">' + content + '</div>' +
                '</div>';
        }).join('');


NationsGlobe.prototype.syncAutoButton = function () {
        return '<div class="clbi-nations-tabpanel-continent' + (continent.key === activeKey ? ' is-active' : '') + '" data-continent-panel="' + escapeHtml(continent.key) + '" aria-hidden="' + (continent.key === activeKey ? 'false' : 'true') + '">' + rows + '</div>';
    if (this.autoButton) {
        this.autoButton.classList.toggle('is-active', this.autoRotate);
     }
     }
};


NationsGlobe.prototype.bindControlEvents = function () {
    function renderNationListPanel(panel, data) {
    var self = this;
        var body = panel.querySelector('.clbi-nations-tabpanel-body');
        var activeTab = panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
        var activeKey = activeTab ? activeTab.getAttribute('data-continent') : '';
        var continents = data && Array.isArray(data.continents) ? data.continents : [];
        var stats = countNationListItems(data);
 
        if (!body || !continents.length) return;
        if (!activeKey) activeKey = continents[0].key;
 
        return PanelPerf.measure('render nation list panel', {
            era: String(data.year || getPanelEra(panel) || ''),
            continents: stats.continents,
            regions: stats.regions,
            items: stats.items
        }, function () {
            var html;
 
            PanelPerf.measure('render nation list panel: html build+insert', {
                era: String(data.year || getPanelEra(panel) || ''),
                continents: stats.continents,
                items: stats.items
            }, function () {
                html = continents.map(function (continent) {
                    return renderNationContinent(continent, activeKey);
                }).join('');
                body.innerHTML = html;
            });


    if (this.autoButton && this.autoButton.getAttribute('data-clbi-globe-control-ready') !== '1') {
            PanelPerf.measure('render nation list panel: bind flag fallbacks', {}, function () {
        this.autoButton.setAttribute('data-clbi-globe-control-ready', '1');
                bindNationFlagFallbacks(body);
        this.autoButton.addEventListener('click', function (e) {
            });
            self.cancelResetMotion();
            PanelPerf.measure('render nation list panel: bind hover focus', {}, function () {
             self.autoResumeMotion = null;
                bindNationListHoverFocus(panel);
             self.autoRotate = !self.autoRotate;
            });
            self.velocityX = 0;
            PanelPerf.measure('render nation list panel: prepare continent panels', {}, function () {
            self.velocityY = 0;
                preparePersistentContinentPanels(panel);
            self.syncAutoButton();
            });
            e.preventDefault();
            panel.classList.add('clbi-nations-list-json-ready');
             e.stopPropagation();
            panel.setAttribute('data-nation-list-year-loaded', String(data.year || getPanelEra(panel)));
            PanelPerf.measure('render nation list panel: schedule flag prewarm', {}, function () {
                prewarmRenderedNationFlags(body);
             });
             PanelPerf.measure('render nation list panel: row snap schedule', {}, function () {
                panel.NationListRowSnapSettled = false;
                bindNationListRowSnapResize();
                scheduleNationListRowSnap(panel, { force: true, reason: 'render' });
             });
         });
         });
     }
     }


     if (this.resetButton && this.resetButton.getAttribute('data-clbi-globe-control-ready') !== '1') {
     function seedResolvedFlagUrlsFromEntryStore(data) {
         this.resetButton.setAttribute('data-clbi-globe-control-ready', '1');
        var files;
         this.resetButton.addEventListener('click', function (e) {
        if (!window.EntryStore || typeof window.EntryStore.getFlagUrl !== 'function') return;
             self.startResetMotion();
         files = collectNationFlagFileNames(data);
             e.preventDefault();
         files.forEach(function (file) {
             e.stopPropagation();
             var key = normalizeNationFileKey(file);
             var url = window.EntryStore.getFlagUrl(file);
             if (key && url) nationResolvedFileUrlCache[key] = url;
         });
         });
     }
     }
};


    function getPreparedNationDataForPanel(panel, era) {
        var listUrl;
        var linkUrl;
        var listPayload;
        var linkPayload;
        var linkIndex;
        var data;
        if (!window.EntryStore || typeof window.EntryStore.getJsonSync !== 'function') return null;
        era = String(era || getPanelEra(panel) || '1950');
        listUrl = getNationListUrl(panel, era);
        linkUrl = getNationLinkMapUrl(panel, era);
        listPayload = getEntryStoreJson(listUrl) || getEntryStoreJson('MediaWiki:' + era + '_Nation_List.json');
        if (!listPayload) return null;
        linkPayload = getEntryStoreJson(linkUrl) || getEntryStoreJson('MediaWiki:' + era + '_Nation_Link_Map.json');
        linkIndex = normalizeNationLinkMap(linkPayload);
        data = applyNationLinkMapFlagFallback(normalizeNationList(listPayload, era), linkIndex);
        seedResolvedFlagUrlsFromEntryStore(data);
        return data;
    }
    function prepareContentForEntry(root, options) {
        return PanelPerf.measure('prepare content for entry', {}, function () {
            var scope = root && root.querySelectorAll ? root : document;
            var panels = toArray(scope.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]'));
            var rendered = 0;
            var skipped = 0;


NationsGlobe.prototype.trackPointerPosition = function (e) {
            panels.forEach(function (panel) {
    if (!e || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return;
                var era = getPanelEra(panel);
                var data = getPreparedNationDataForPanel(panel, era);
                if (!data) {
                    skipped += 1;
                    PanelPerf.mark('prepare content panel skipped', { era: era, reason: 'no prepared data' });
                    return;
                }
                renderNationListPanel(panel, data);
                rendered += 1;
            });


    this.pointerInside = true;
            PanelPerf.mark('prepare content for entry result', { panels: panels.length, rendered: rendered, skipped: skipped });
    this.pointerClientX = e.clientX;
            return { panels: panels.length, rendered: rendered, skipped: skipped };
    this.pointerClientY = e.clientY;
        });
};
    }


NationsGlobe.prototype.clearPointerPosition = function () {
    function isNationListPanelHidden(panel) {
    this.pointerInside = false;
        var node = panel;
    this.pointerClientX = null;
    this.pointerClientY = null;


    this.clearNationHover();
        while (node && node !== document && node.nodeType === 1) {
            if (node.hasAttribute('hidden') || node.getAttribute('aria-hidden') === 'true') return true;
            node = node.parentElement;
        }


    if (this.readout) {
         return false;
         this.setReadout(this.getBaseReadout());
     }
     }
};


NationsGlobe.prototype.getBaseReadout = function () {
    function initNationListPanel(panel) {
    return (this.root.getAttribute('data-era') || 'REAL WORLD / 110M') + ' · SEG ' + this.segmentCount;
        var era;
};
        var promise;
        var loadId;
 
        if (!panel || panel.getAttribute('data-nation-list-source') !== '1') return Promise.resolve(null);
        if (isNationListPanelHidden(panel)) {
            PanelPerf.mark('init nation list panel skipped', { reason: 'hidden' });
            return Promise.resolve(null);
        }
        if (panel.getAttribute('data-nation-list-year-loaded') === getPanelEra(panel)) {
            PanelPerf.mark('init nation list panel skipped', { era: getPanelEra(panel), reason: 'already loaded' });
            return Promise.resolve(panel);
        }
        if (panel.getAttribute('data-nation-list-loading') === '1' && panel.CLBI_NationListLoadPromise) {
            PanelPerf.mark('init nation list panel existing promise', { era: getPanelEra(panel) });
            return panel.CLBI_NationListLoadPromise;
        }
 
        era = getPanelEra(panel);
        loadId = PanelPerf.start('init nation list panel', { era: era });
        purgeLegacyNationIndexFileImages(panel);
        panel.setAttribute('data-nation-list-loading', '1');
 
        promise = Promise.all([
            fetchNationList(panel, era),
            fetchNationLinkMap(panel, era)
        ]).then(function (results) {
            var data = PanelPerf.measure('apply nation link map flag fallback', { era: era }, function () {
                return applyNationLinkMapFlagFallback(results[0], results[1]);
            });
            var initialFiles = collectInitialNationFlagFileNames(data, panel);
            var allFiles = collectNationFlagFileNames(data);
            PanelPerf.mark('initial nation flag URL set', {
                era: era,
                count: initialFiles.length,
                activeContinent: getInitialContinentKey(panel, data)
            });
            PanelPerf.mark('all continent nation flag URL set', {
                era: era,
                count: allFiles.length,
                mode: 'pre-render-all-tabs'
            });
            return resolveNationFlagFileUrls(allFiles).then(function () {
                panel.CLBI_NationsAllFlagUrlsResolved = true;
                renderNationListPanel(panel, data);
                panel.CLBI_NationsAllContinentPanelsPrebuilt = true;
                scheduleAllNationFlagBackgroundWarm(panel, data);
                return panel;
            });
        }).then(function (result) {
            PanelPerf.end(loadId, { ok: true });
            return result;
        }).catch(function () {
            panel.classList.add('clbi-nations-list-json-fallback');
            PanelPerf.end(loadId, { ok: false, fallback: true });
            return panel;
        }).finally(function () {
            panel.removeAttribute('data-nation-list-loading');
            panel.CLBI_NationListLoadPromise = null;
        });
 
        panel.CLBI_NationListLoadPromise = promise;
        return promise;
    }


NationsGlobe.prototype.bindEvents = function () {
    function waitUntilNationsPanel(predicate, timeoutMs, intervalMs) {
    var self = this;
        var started = Date.now();
        timeoutMs = Math.max(500, timeoutMs || 12000);
        intervalMs = Math.max(20, intervalMs || 80);
        return new Promise(function (resolve) {
            function tick() {
                var result = false;
                try { result = predicate(); } catch (err) { result = false; }
                if (result) return resolve({ ok: true, value: result });
                if (Date.now() - started >= timeoutMs) return resolve({ ok: false, timeout: true });
                setTimeout(tick, intervalMs);
            }
            tick();
        });
    }


     if (this.stageEventsBound) {
     function findEraNationPanels(era) {
         this.bindControlEvents();
        var selector;
         return;
        era = String(era || '').trim();
        if (!era) return [];
         selector = '.clbi-nations-era-content[data-era-content="' + era.replace(/"/g, '\\"') + '"] .clbi-nations-tabpanel[data-nation-list-source="1"]';
         return toArray(document.querySelectorAll(selector));
     }
     }


     this.stageEventsBound = true;
     function whenEraReady(era, options) {
        var panels;
        var timeoutMs = options && options.timeoutMs ? options.timeoutMs : 15000;
        era = String(era || '1950');
        panels = findEraNationPanels(era);


    this.stage.addEventListener('pointerenter', function (e) {
        if (!panels.length) {
        self.trackPointerPosition(e);
            panels = toArray(document.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]')).filter(function (panel) {
        self.updatePointerReadout(e);
                return getPanelEra(panel) === era;
    });
            });
        }


    this.stage.addEventListener('pointerleave', function () {
        if (!panels.length) {
        if (!self.dragging) {
            PanelPerf.mark('when era ready skipped', { era: era, reason: 'no panels' });
             self.clearPointerPosition();
             return Promise.resolve({ era: era, skipped: true });
         }
         }
    });


    this.stage.addEventListener('pointerdown', function (e) {
        return PanelPerf.measure('when era ready', { era: era, panels: panels.length, timeoutMs: timeoutMs }, function () {
        self.trackPointerPosition(e);
            return PanelPerf.measure('when era ready: init panels', { era: era, panels: panels.length }, function () {
        self.updatePointerReadout(e);
                return Promise.all(panels.map(function (panel) {
        self.cancelResetMotion();
                    return initNationListPanel(panel);
        self.dragging = true;
                }));
        self.pointerDownX = e.clientX;
            }).then(function () {
        self.pointerDownY = e.clientY;
                return PanelPerf.measure('when era ready: wait json-ready marker', { era: era, panels: panels.length }, function () {
        self.pointerDownNationPage = self.currentNation ? self.currentNation.page : null;
                    return waitUntilNationsPanel(function () {
        self.lastX = e.clientX;
                        return panels.every(function (panel) {
        self.lastY = e.clientY;
                            return panel && panel.classList.contains('clbi-nations-list-json-ready') && panel.getAttribute('data-nation-list-year-loaded') === era;
        self.lastMoveTime = e.timeStamp || performance.now();
                        });
        self.velocityX = 0;
                    }, timeoutMs, 80);
        self.velocityY = 0;
                });
        self.root.classList.add('is-dragging');
            }).then(function (result) {
        self.autoResumeMotion = null;
                return PanelPerf.measure('when era ready: wait rendered flags', { era: era, panels: panels.length }, function () {
        self.autoRotate = false;
                    return Promise.all(panels.map(function (panel) {
         self.syncAutoButton();
                        return waitForRenderedFlagImages(panel, { timeoutMs: timeoutMs, activeOnly: true }).catch(function () {
                            return { total: 0, ok: 0, fail: 0 };
                        });
                    }));
                }).then(function (flagResults) {
                    var total = 0;
                    var ok = 0;
                    var fail = 0;
                    flagResults.forEach(function (item) {
                        total += item && item.total ? item.total : 0;
                        ok += item && item.ok ? item.ok : 0;
                        fail += item && item.fail ? item.fail : 0;
                    });
                    return {
                        era: era,
                        ok: !!(result && result.ok),
                        panels: panels.length,
                        timeout: !!(result && result.timeout),
                        flags: { total: total, ok: ok, fail: fail }
                    };
                });
            });
         });
    }


        if (self.stage.setPointerCapture && e.pointerId != null) {
    function preparePersistentContinentPanels(panel) {
            try { self.stage.setPointerCapture(e.pointerId); } catch (err) {}
        var activePanel;
        }


         e.preventDefault();
         if (!panel || !panel.querySelectorAll) return;
    });
        panel.classList.add('clbi-nations-continent-cache');


    this.stage.addEventListener('pointermove', function (e) {
        toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')).forEach(function (body) {
        self.trackPointerPosition(e);
            if (body.hasAttribute('hidden')) body.removeAttribute('hidden');
            body.setAttribute('data-clbi-panel-warm', '1');
            if (!body.hasAttribute('aria-hidden')) {
                body.setAttribute('aria-hidden', body.classList.contains('is-active') ? 'false' : 'true');
            }
        });


         if (self.dragging) {
         activePanel = panel.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]') ||
            var dx = e.clientX - self.lastX;
             panel.querySelector('.clbi-nations-tabpanel-continent[data-continent-panel]');
            var dy = e.clientY - self.lastY;
            var now = e.timeStamp || performance.now();
             var elapsed = Math.max(8, Math.min(80, now - (self.lastMoveTime || now || performance.now())));
            var stepY = dx * self.dragSensitivityY;
            var stepX = dy * self.dragSensitivityX;
            var instantY = (stepY / elapsed) * self.inertiaStrength;
            var instantX = (stepX / elapsed) * self.inertiaStrength;


            self.rotationY += stepY;
        if (activePanel) {
             self.rotationX += stepX;
             activatePersistentPanelsByAttribute(
            self.velocityY = self.velocityY * 0.58 + instantY * 0.42;
                toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')),
            self.velocityX = self.velocityX * 0.58 + instantX * 0.42;
                'data-continent-panel',
            self.lastX = e.clientX;
                activePanel.getAttribute('data-continent-panel')
            self.lastY = e.clientY;
             );
            self.lastMoveTime = now;
            self.applyRotation();
            self.updatePointerReadout(e);
            e.preventDefault();
        } else {
             self.updatePointerReadout(e);
         }
         }
    });


    function finishDrag(e) {
        stabilizeContinentBodyHeight(panel, true);
        var now;
    }
        var releaseDelay;
        var releaseSpeed;


         if (!self.dragging) return;
    function syncDecorationRuntimeNow() {
        var runtime = window.Decorations || window.CLBI_DECORATIONS || null;
         try {
            if (runtime && typeof runtime.updateVisibility === 'function') runtime.updateVisibility(document);
            else if (runtime && typeof runtime.sync === 'function') runtime.sync();
            else if (runtime && typeof runtime.renderPrepared === 'function') runtime.renderPrepared();
            else if (runtime && typeof runtime.render === 'function' && runtime.lastRegistry) runtime.render(runtime.lastRegistry);
        } catch (err) {}
    }


        now = e && e.timeStamp ? e.timeStamp : performance.now();
    function scheduleDecorationVisibilitySync(panel) {
         releaseDelay = now - (self.lastMoveTime || now);
         if (!panel) return;
        releaseSpeed = Math.max(Math.abs(self.velocityX), Math.abs(self.velocityY));


         /*
         /*
         * Only a deliberate throw should keep spinning.
         * Continent switch fast path — 20260708.
         * If the user drags, holds, adjusts the viewing angle, and then releases,
        *
         * the last movement is old or slow, so the globe must settle immediately.
        * Changing a continent tab must feel like a local state flip, not a
        * mini page load. Decoration visibility is not required to decide the
        * active tab/body, and running the decoration runtime in the same input
         * frame competes with the globe animation and panel repaint. Keep the
        * visible switch synchronous and move decoration maintenance to the next
        * animation frame. This preserves the prepared-canvas behavior while
         * avoiding the old "tab click pauses the globe" feeling.
         */
         */
         if (releaseDelay > self.inertiaReleaseWindow || releaseSpeed < self.inertiaMinVelocity) {
         if (panel.CLBI_NationDecorationSyncRaf) {
             self.velocityX = 0;
             window.cancelAnimationFrame(panel.CLBI_NationDecorationSyncRaf);
             self.velocityY = 0;
             panel.CLBI_NationDecorationSyncRaf = null;
         }
         }


         if (e && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
         panel.CLBI_NationDecorationSyncRaf = window.requestAnimationFrame(function () {
             self.trackPointerPosition(e);
             panel.CLBI_NationDecorationSyncRaf = null;
             self.updatePointerReadout(e);
            if (!document.body.contains(panel)) return;
         }
             syncDecorationRuntimeNow();
         });
    }
 
    function scheduleHoverFocusMaintenance(panel) {
        if (!panel) return;
 
        /*
        * Continent switch fast path — 20260708.
        *
        * Pending hover-to-globe focus timers are cancelled immediately so an old
        * country cannot request a new zoom after the user changes continents.
        * The visual cleanup scan is deferred one frame: it is housekeeping, and
        * doing it in the click/shortcut frame used to make the globe stutter.
        */
        cancelNationPanelPendingHoverFocus(panel);


        self.dragging = false;
         if (panel.CLBI_NationHoverCleanupRaf) {
        self.root.classList.remove('is-dragging');
             window.cancelAnimationFrame(panel.CLBI_NationHoverCleanupRaf);
         if (self.stage.releasePointerCapture && e && e.pointerId != null) {
            panel.CLBI_NationHoverCleanupRaf = null;
             try { self.stage.releasePointerCapture(e.pointerId); } catch (err) {}
         }
         }


         self.handleNationClick(e);
         panel.CLBI_NationHoverCleanupRaf = window.requestAnimationFrame(function () {
            panel.CLBI_NationHoverCleanupRaf = null;
            if (!document.body.contains(panel)) return;
            clearNationPanelHoverState(panel, null);
        });
     }
     }


     this.stage.addEventListener('pointerup', finishDrag);
     function stabilizeContinentBodyHeight(panel, force) {
    this.stage.addEventListener('pointercancel', finishDrag);
        var body;
        var bodies;
        var max = 0;


    this.stage.addEventListener('wheel', function (e) {
         if (!panel || !panel.querySelectorAll) return;
         self.trackPointerPosition(e);
         if (!force && panel.CLBI_NationContinentHeightStable) return;
        self.cancelResetMotion();
        self.velocityX = 0;
        self.velocityY = 0;
        self.cameraZ += e.deltaY * 0.0017;
        self.cameraZ = Math.max(self.cameraZMin, Math.min(self.cameraZMax, self.cameraZ));
         if (self.camera) self.camera.position.z = self.cameraZ;
        self.updateCloudOpacity();
        self.updateMoonOpacity();
        self.updatePointerReadout(e);
        e.preventDefault();
    }, { passive: false });


    this.bindControlEvents();
        body = panel.querySelector('.clbi-nations-tabpanel-body');
        bodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
        if (!body || !bodies.length) return;


    window.addEventListener('resize', function () {
        /*
        self.resize();
        * Height stabilization — 20260708.
    });
        *
};
        * The six continent bodies are already kept warm in DOM. If the stack
        * height follows each active body, every continent switch can still cause
        * a layout jump and steal a frame from the WebGL globe. Measure the warm
        * bodies once and pin the body well to the tallest continent. The number
        * of bodies is tiny, so this one-time measurement is cheaper than paying
        * repeated layout work on every switch.
        */
        bodies.forEach(function (item) {
            var h;
            if (!item) return;
            h = Math.max(item.scrollHeight || 0, item.offsetHeight || 0);
            if (h > max) max = h;
        });


NationsGlobe.prototype.updatePointerReadout = function (e) {
        if (max > 0) {
    if (e && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
            body.style.setProperty('--clbi-nations-continent-body-min-h', Math.ceil(max) + 'px');
        this.trackPointerPosition(e);
            body.setAttribute('data-clbi-continent-height-stable', '1');
            panel.CLBI_NationContinentHeightStable = true;
        }
     }
     }


     if (this.pointerClientX === null || this.pointerClientY === null) return false;
     function getContinentPanelParts(panel) {
        if (!panel || !panel.querySelectorAll) return { tabs: [], bodies: [] };
 
        /*
        * Mouse-click continent regression guard — 20260708.
        *
        * The nations panel is prebuilt once while detached, then consumed again
        * after SPA insertion. Older fixes correctly avoided serialized
        * attributes as binding guards, but the cached tab/body node arrays could
        * still point at nodes that were replaced or detached during warm entry
        * hydration. Keyboard shortcuts often survived because a later fallback
        * queried the live DOM, while mouse clicks could be swallowed by the
        * owner API returning true after updating stale cached nodes.
        *
        * Keep this function live-DOM based. The tab count is tiny, so a fresh
        * query is cheaper and safer than carrying stale references across
        * detached preparation, innerHTML insertion, and manifest warm-cache
        * hydration. This preserves the old solved behavior: mouse clicks,
        * keyboard shortcuts, and fallback activation all operate on the same
        * currently visible DOM nodes.
        */
        panel.CLBI_NationContinentTabs = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
        panel.CLBI_NationContinentBodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
 
        return {
            tabs: panel.CLBI_NationContinentTabs || [],
            bodies: panel.CLBI_NationContinentBodies || []
        };
    }


     return this.updatePointerReadoutFromClient(this.pointerClientX, this.pointerClientY);
     function findActiveContinentPanel() {
};
        var panels = toArray(document.querySelectorAll('.clbi-nations-tabpanel'));
        var visible = panels.filter(function (panel) {
            return !isNationListPanelHidden(panel) && panel.querySelector('.clbi-nations-tabpanel-tab[data-continent]');
        });
        return visible.find(function (panel) { return panel.getAttribute('data-nation-list-source') === '1'; }) || visible[0] || null;
    }


NationsGlobe.prototype.updatePointerReadoutFromClient = function (clientX, clientY) {
    function activateContinentOnPanel(panel, continent, options) {
    var THREE = this.THREE;
        var parts;
    var rect;
        var tabs;
    var intersects;
        var bodies;
    var local;
        var activeTab = null;
    var ll;
        var currentTab = null;
        var source = options && options.source ? String(options.source) : '';
        var keyboardSwitch = source === 'shortcut' || source === 'keyboard' || /keyboard|shortcut/i.test(source);


    if (!this.raycaster || !this.pointer || !this.camera || !this.earth || !this.renderer) return false;
        if (!panel || !continent) return false;
        if (source && source !== 'init') markNationPanelDirectInteraction();
        parts = interactionPerfMeasure('panel switch query live tabs', { continent: continent, source: source }, function () {
            return getContinentPanelParts(panel);
        });
        tabs = parts.tabs;
        bodies = parts.bodies;
        if (!tabs.length || !bodies.length) return false;


    rect = this.renderer.domElement.getBoundingClientRect();
        tabs.forEach(function (tab) {
            if (tab.getAttribute('data-continent') === continent) activeTab = tab;
            if (tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true') currentTab = tab;
        });


    if (!rect.width || !rect.height) return false;
        if (!activeTab && tabs.length) {
            activeTab = tabs[0];
            continent = activeTab.getAttribute('data-continent');
        }
        if (!activeTab) return false;


    if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {
         panel.CLBI_NationsPanelOwned = true;
         this.setReadout(this.getBaseReadout());
        return false;
    }


    this.pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
        if (currentTab === activeTab && panel.getAttribute('data-active-continent') === continent) {
    this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;
            if (keyboardSwitch) syncContinentKeyboardHoverSuppress(panel, activeTab, true);
            else clearContinentKeyboardHoverSuppress(panel);
            scheduleNationListRowSnap(panel);
            scheduleDecorationVisibilitySync(panel);
            return true;
        }


    this.raycaster.setFromCamera(this.pointer, this.camera);
        /*
    intersects = this.raycaster.intersectObject(this.earth, false);
        * Flag continuity on continent switches — 20260710.
        *
        * The remaining flag resolver runs after the first visible continent. When a
        * user switches tabs after those URLs are ready, the target continent must not
        * become visible first and then receive its <img> nodes a frame later; that
        * reads as every tab loading its flags from zero.  Hydrate the target hidden
        * body before flipping the active class, then keep the old post-switch pass
        * only as a fallback for unresolved/late slots.
        */
        if (!panel.CLBI_NationsAllFlagUrlsResolved) {
            interactionPerfMeasure('panel switch prehydrate target flags', { continent: continent, source: source }, function () {
                bodies.forEach(function (body) {
                    if (!body || body.getAttribute('data-continent-panel') !== continent) return;
                    hydrateResolvedFlagSlots(body);
                    prewarmRenderedNationFlags(body);
                });
            });
        }


    if (intersects && intersects.length) {
        markNationContinentSwitching(panel);
         local = this.earth.worldToLocal(intersects[0].point.clone());
         interactionPerfMeasure('panel switch flip active classes', { continent: continent, source: source, tabs: tabs.length, bodies: bodies.length }, function () {
        ll = vectorToLonLat(local);
            setActiveButton(tabs, activeTab);
         this.updateNationHover(ll, clientX, clientY);
            activatePersistentPanelsByAttribute(bodies, 'data-continent-panel', continent);
         if (this.currentNation) {
         });
             this.setReadout(this.getNationLabel(this.currentNation.page) + ' · LAT ' + ll.lat.toFixed(2) + ' / LON ' + ll.lon.toFixed(2) + ' · SEG ' + this.segmentCount);
         if (!panel.CLBI_NationsAllFlagUrlsResolved && panel.CLBI_NationsRemainingFlagsResolved) {
         } else {
             scheduleResolvedFlagHydrationSweeps(panel, {
             this.setReadout('LAT ' + ll.lat.toFixed(2) + ' / LON ' + ll.lon.toFixed(2) + ' · SEG ' + this.segmentCount);
                era: getPanelEra(panel),
                delays: [80, 220]
            });
        }
        panel.setAttribute('data-active-continent', continent);
        if (!panel.CLBI_NationsAllContinentPanelsPrebuilt) {
            scheduleNationListRowSnap(panel);
         }
        if (!panel.CLBI_NationContinentHeightStable) {
             /* Height measurement can force layout. Do it outside the click frame. */
            window.requestAnimationFrame(function () {
                if (document.body.contains(panel)) stabilizeContinentBodyHeight(panel, false);
            });
         }
         }
        if (keyboardSwitch) syncContinentKeyboardHoverSuppress(panel, activeTab, true);
        else clearContinentKeyboardHoverSuppress(panel);
        /*
        * The actual continent switch ends here. Everything below is maintenance
        * and is intentionally deferred so mouse clicks and shortcuts only pay
        * for the tiny tab/body class flip in the input frame.
        */
        scheduleHoverFocusMaintenance(panel);
        scheduleDecorationVisibilitySync(panel);
        interactionPerfMark('panel continent switch complete', { continent: continent, source: source });
         return true;
         return true;
     }
     }


     this.clearNationHover();
     function activateContinent(continent, options) {
     this.setReadout(this.getBaseReadout());
        var panel = options && options.panel ? options.panel : findActiveContinentPanel();
    return false;
        return activateContinentOnPanel(panel, continent, options || {});
};
     }
 
    function moveContinent(direction, options) {
        var panel = options && options.panel ? options.panel : findActiveContinentPanel();
        var parts;
        var tabs;
        var activeIndex;
        var nextIndex;
 
        direction = direction < 0 ? -1 : 1;
        if (!panel) return false;
        parts = getContinentPanelParts(panel);
        tabs = parts.tabs;
        if (!tabs.length) return false;
        activeIndex = tabs.findIndex(function (tab) {
            return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
        });
        if (activeIndex < 0) activeIndex = 0;
        nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
        return activateContinentOnPanel(panel, tabs[nextIndex].getAttribute('data-continent'), options || {});
    }


    function initContinentPanel(panel) {
        var tabs;
        var bodies;


        if (!panel) return;
        if (isNationListPanelHidden(panel)) return;
        bindNationListHoverFocus(panel);
        ensureContinentKeyboardHoverGuard();
        /*
        * Detached SPA preparation serializes attributes but not event listeners.
        * A panel can therefore arrive with data-nations-tabpanel-ready="1" while
        * its click handlers were lost during innerHTML insertion.  Use a DOM
        * property as the real binding guard and keep the attribute only as a
        * diagnostic marker.  This fixes mouse continent tabs after warm prepaint.
        */
        if (panel.CLBI_NationsTabPanelBound) return;
        panel.CLBI_NationsTabPanelBound = true;
        panel.setAttribute('data-nations-tabpanel-ready', '1');


NationsGlobe.prototype.loadNationInteractionData = function () {
        panel.addEventListener('pointerdown', function () {
    var self = this;
            clearContinentKeyboardHoverSuppress(panel);
    var tasks = [];
        }, true);


    if (this.nationHitUrl) {
         panel.addEventListener('pointerleave', function () {
         tasks.push(
            clearContinentKeyboardHoverSuppress(panel);
            fetch(this.nationHitUrl, { credentials: 'same-origin' })
        }, true);
                .then(function (res) {
                    if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + self.nationHitUrl);
                    return res.json();
                })
                .then(function (payload) {
                    var features = payload && payload.features ? payload.features : [];
                    self.nationHitFeatures = features
                        .filter(function (feature) {
                            return getFeaturePage(feature) && feature.geometry;
                        })
                        .map(function (feature) {
                            var bbox = feature.bbox || calculateGeometryBBox(feature.geometry);
                            return {
                                page: getFeaturePage(feature),
                                bbox: bbox,
                                bboxArea: Math.max(0, (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])),
                                geometry: feature.geometry
                            };
                        })
                        .sort(function (a, b) {
                            return a.bboxArea - b.bboxArea;
                        });


                    if (window.console && console.info) {
        preparePersistentContinentPanels(panel);
                        console.info('1950 nation hit data applied:', {
        panel.CLBI_NationsPanelOwned = true;
                            url: self.nationHitUrl,
        panel.CLBI_NationContinentTabs = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
                            features: self.nationHitFeatures.length
        panel.CLBI_NationContinentBodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
                        });
        stabilizeContinentBodyHeight(panel, true);
                    }
         tabs = panel.CLBI_NationContinentTabs;
                })
        bodies = panel.CLBI_NationContinentBodies;
                .catch(function (err) {
                    self.nationHitFeatures = [];
                    if (window.console && console.warn) console.warn('1950 nation hit data failed:', err);
                })
         );
    }


    if (this.nationLinkMapUrl) {
         function activate(continent, options) {
         tasks.push(
            return activateContinentOnPanel(panel, continent, options || {});
            fetch(this.nationLinkMapUrl, { credentials: 'same-origin', cache: 'no-cache' })
         }
                .then(function (res) {
                    if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + self.nationLinkMapUrl);
                    return res.json();
                })
                .then(function (payload) {
                    self.nationLinkMap = normalizeNationLinkMap(payload);
                    if (window.console && console.info) {
                        console.info('1950 nation link map applied:', {
                            url: self.nationLinkMapUrl,
                            items: Object.keys(self.nationLinkMap).length
                        });
                    }
                })
                .catch(function (err) {
                    self.nationLinkMap = {};
                    if (window.console && console.warn) console.warn('1950 nation link map failed:', err);
                })
         );
    }


    if (!tasks.length) return Promise.resolve();
        tabs.forEach(function (tab) {
    return Promise.all(tasks).then(function () {});
            tab.setAttribute('role', 'tab');
};
            tab.setAttribute('tabindex', tab.classList.contains('is-active') ? '0' : '-1');


NationsGlobe.prototype.findNationAtLonLat = function (ll) {
            tab.addEventListener('click', function (event) {
    var lon = ll && Number(ll.lon);
                if (event) {
    var lat = ll && Number(ll.lat);
                    event.preventDefault();
    var i;
                    if (typeof event.stopPropagation === 'function') event.stopPropagation();
    var feature;
                    if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
                }
                interactionPerfMeasure('panel tab click handler', { continent: tab.getAttribute('data-continent') || '' }, function () {
                    playNationsPanelClickSound();
                    activate(tab.getAttribute('data-continent'), { source: 'click' });
                });
            });


    if (!Number.isFinite(lon) || !Number.isFinite(lat) || !this.nationHitFeatures.length) return null;
            tab.addEventListener('keydown', function (event) {
                if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    interactionPerfMeasure('panel tab keyboard handler', { continent: tab.getAttribute('data-continent') || '', key: event.key || '' }, function () {
                        playNationsPanelClickSound();
                        activate(tab.getAttribute('data-continent'), { source: 'keyboard' });
                    });
                }
            });
        });


    for (i = 0; i < this.nationHitFeatures.length; i += 1) {
        activate((panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent]') || tabs[0] || {}).getAttribute && (panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent]') || tabs[0]).getAttribute('data-continent'), { source: 'init' });
        feature = this.nationHitFeatures[i];
        if (!isLonLatInsideBbox(lon, lat, feature.bbox)) continue;
        if (pointInGeometry(lon, lat, feature.geometry)) {
            return {
                page: feature.page,
                feature: feature
            };
        }
     }
     }


     return null;
     function initHistoryYearPanel(panel) {
};
        var tabs;
        var pages;
 
        if (!panel || panel.getAttribute('data-nations-history-ready') === '1') return;
        panel.setAttribute('data-nations-history-ready', '1');


NationsGlobe.prototype.getNationEntry = function (page) {
        tabs = toArray(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
    return page && this.nationLinkMap ? this.nationLinkMap[page] || null : null;
        pages = toArray(panel.querySelectorAll('.clbi-nations-history-page[data-year-panel]'));
};


NationsGlobe.prototype.getNationLabel = function (page) {
        function activate(year) {
    var entry = this.getNationEntry(page);
            var activeTab = null;
    return (entry && (entry.name_ko || entry.name || entry.wiki_title)) || page || '';
};


NationsGlobe.prototype.getNationUrl = function (page) {
            tabs.forEach(function (tab) {
    var entry = this.getNationEntry(page);
                if (tab.getAttribute('data-year') === year) activeTab = tab;
    var title;
            });


    if (!entry || entry.enabled === false) return '';
            if (!activeTab && tabs.length) {
    title = entry.wiki_title || entry.title || '';
                activeTab = tabs[0];
    if (!title) return '';
                year = activeTab.getAttribute('data-year');
    if (/^https?:\/\//i.test(title)) return title;
            }
    return this.nationLinkBase + encodeWikiTitle(title);
};


NationsGlobe.prototype.positionNationTooltip = function (clientX, clientY) {
            setActiveButton(tabs, activeTab);
    var rootRect;
            activatePanelsByAttribute(pages, 'data-year-panel', year);
    var tooltipRect;
            scheduleHistoryItemSlides(panel);
    var left;
        }
    var top;


    if (!this.nationTooltip) return;
        tabs.forEach(function (tab) {
            tab.setAttribute('role', 'tab');
            tab.setAttribute('tabindex', tab.classList.contains('is-active') ? '0' : '-1');


    rootRect = this.root.getBoundingClientRect();
            tab.addEventListener('click', function () {
    left = clientX - rootRect.left + 14;
                playNationsPanelClickSound();
    top = clientY - rootRect.top + 14;
                activate(tab.getAttribute('data-year'));
            });


    this.nationTooltip.style.left = left + 'px';
            tab.addEventListener('keydown', function (event) {
    this.nationTooltip.style.top = top + 'px';
                if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    playNationsPanelClickSound();
                    activate(tab.getAttribute('data-year'));
                }
            });
        });


    tooltipRect = this.nationTooltip.getBoundingClientRect();
        activate((panel.querySelector('.clbi-nations-history-year-button.is-active[data-year]') || tabs[0] || {}).getAttribute && (panel.querySelector('.clbi-nations-history-year-button.is-active[data-year]') || tabs[0]).getAttribute('data-year'));
    if (tooltipRect.right > rootRect.right - 6) {
        left = Math.max(6, clientX - rootRect.left - tooltipRect.width - 12);
    }
    if (tooltipRect.bottom > rootRect.bottom - 6) {
        top = Math.max(6, clientY - rootRect.top - tooltipRect.height - 12);
     }
     }


     this.nationTooltip.style.left = left + 'px';
     function prewarmNationEra(stack, era) {
    this.nationTooltip.style.top = top + 'px';
        var eraPanel;
};
        var panel;
        var selector;
 
        if (!stack || !era) return;
 
        selector = '.clbi-nations-era-content[data-era-content="' + String(era).replace(/"/g, '\"') + '"]';
        eraPanel = stack.querySelector(selector);
        panel = eraPanel && eraPanel.querySelector ? eraPanel.querySelector('.clbi-nations-tabpanel[data-nation-list-source="1"]') : null;
        if (!panel || panel.getAttribute('data-nation-prewarm-loading') === '1') return;
        if (panel.getAttribute('data-nation-prewarm-year-loaded') === String(era)) return;


NationsGlobe.prototype.updateNationHover = function (ll, clientX, clientY) {
        panel.setAttribute('data-nation-prewarm-loading', '1');
    var hit = this.findNationAtLonLat(ll);
    var page = hit ? hit.page : '';
    var label;
    var url;


    if (!hit || !page) {
        Promise.all([
         this.clearNationHover();
            fetchNationList(panel, era),
         return null;
            fetchNationLinkMap(panel, era)
        ]).then(function () {
            panel.setAttribute('data-nation-prewarm-year-loaded', String(era));
        }).catch(function () {
         }).finally(function () {
            panel.removeAttribute('data-nation-prewarm-loading');
         });
     }
     }


     this.currentNation = {
     function scheduleEraNeighborsPrewarm(stack, eras, era) {
         page: page,
         var index;
         entry: this.getNationEntry(page),
        var prevEra;
         lon: ll.lon,
         var nextEra;
         lat: ll.lat
         var oldHandle;
    };
 
         if (!stack || !eras || !eras.length || !era) return;


    label = this.getNationLabel(page);
        oldHandle = stack.CLBI_NationsEraPrewarmHandle;
    url = this.getNationUrl(page);
        if (oldHandle != null) cancelIdle(oldHandle);


    this.root.classList.add('has-nation-hover');
        index = getEraIndex(eras, era);
        prevEra = index > 0 ? eras[index - 1] : '';
        nextEra = index < eras.length - 1 ? eras[index + 1] : '';


    if (this.nationTooltip) {
         stack.CLBI_NationsEraPrewarmHandle = scheduleIdle(function () {
         this.nationTooltip.innerHTML = '<span class="nation-tooltip-name">' + escapeHtml(label) + '</span>' +
             stack.CLBI_NationsEraPrewarmHandle = null;
            '<span class="nation-tooltip-code">' + escapeHtml(page) + '</span>' +
            prewarmNationEra(stack, nextEra);
             (url ? '<span class="nation-tooltip-link">클릭하여 문서 열기 ›</span>' : '<span class="nation-tooltip-link">링크 미지정</span>');
            prewarmNationEra(stack, prevEra);
        this.nationTooltip.classList.add('is-visible');
         }, { timeout: 900 });
        this.nationTooltip.setAttribute('aria-hidden', 'false');
         this.positionNationTooltip(clientX, clientY);
     }
     }


     return this.currentNation;
     function initEraStack(stack) {
};
        var eras;
        var titleNode;
        var titlePlates;
        var contentPanels;
        var leftButton;
        var rightButton;
        var globe;
        var currentEra;
 
        if (!stack || stack.getAttribute('data-nations-era-controller-ready') === '1') return;
        stack.setAttribute('data-nations-era-controller-ready', '1');
 
        eras = parseEraList(stack);
        ensureEraContentPanels(stack, eras);
        titleNode = stack.querySelector('[data-nations-era-title]');
        titlePlates = toArray(stack.querySelectorAll('.clbi-nations-era-title-plate[data-era]'));
        contentPanels = toArray(stack.querySelectorAll('.clbi-nations-era-content[data-era-content]'));
        leftButton = stack.querySelector('[data-nations-era-move="prev"], .clbi-nations-era-side-button-left');
        rightButton = stack.querySelector('[data-nations-era-move="next"], .clbi-nations-era-side-button-right');
        globe = stack.querySelector('.clbi-nations-globe-window');
        currentEra = stack.getAttribute('data-nations-current-era') || stack.getAttribute('data-era') || eras[0];
 
        function normalizeEra(era) {
            var index = getEraIndex(eras, String(era || eras[0]));
            return eras[index];
        }


NationsGlobe.prototype.clearNationHover = function () {
        function setEraShell(era, preview) {
    this.currentNation = null;
            var index;
    this.root.classList.remove('has-nation-hover');
            var prevEra;
            var nextEra;


    if (this.nationTooltip) {
            era = normalizeEra(era);
        this.nationTooltip.classList.remove('is-visible');
            index = getEraIndex(eras, era);
        this.nationTooltip.setAttribute('aria-hidden', 'true');
            prevEra = getWrappedEra(eras, index - 1);
    }
            nextEra = getWrappedEra(eras, index + 1);
};


NationsGlobe.prototype.handleNationClick = function (e) {
            stack.setAttribute('data-era', era);
    var dx;
            if (preview) {
    var dy;
                stack.setAttribute('data-nations-preview-era', era);
    var moved;
                stack.classList.add('is-era-previewing');
    var page;
            } else {
    var url;
                stack.removeAttribute('data-nations-preview-era');
                stack.classList.remove('is-era-previewing');
            }


    if (!e || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return false;
            if (titleNode) {
    if (this.pointerDownX === null || this.pointerDownY === null) return false;
                titleNode.textContent = era + '';
                titleNode.setAttribute('data-era', era);
            }


    dx = e.clientX - this.pointerDownX;
            titlePlates.forEach(function (plate) {
    dy = e.clientY - this.pointerDownY;
                setHidden(plate, plate.getAttribute('data-era') !== era);
    moved = Math.sqrt(dx * dx + dy * dy);
            });
    page = this.currentNation ? this.currentNation.page : this.pointerDownNationPage;


    this.pointerDownX = null;
            if (leftButton) {
    this.pointerDownY = null;
                leftButton.setAttribute('data-target-era', prevEra);
    this.pointerDownNationPage = null;
                leftButton.setAttribute('title', prevEra + '년으로 이동');
                leftButton.setAttribute('aria-label', prevEra + '년으로 이동');
            }


    if (moved > this.clickMoveTolerance || !page) return false;
            if (rightButton) {
                rightButton.setAttribute('data-target-era', nextEra);
                rightButton.setAttribute('title', nextEra + '년으로 이동');
                rightButton.setAttribute('aria-label', nextEra + '년으로 이동');
            }


    url = this.getNationUrl(page);
            return era;
    if (!url) return false;
        }


    if (e.ctrlKey || e.metaKey || e.shiftKey) {
        function clearEraCommitTimer() {
        window.open(url, '_blank', 'noopener');
            if (stack.CLBI_NationsEraCommitTimer) {
    } else {
                window.clearTimeout(stack.CLBI_NationsEraCommitTimer);
        window.location.href = url;
                stack.CLBI_NationsEraCommitTimer = null;
    }
            }
        }


    return true;
        function setEra(era, options) {
};
            var reload;


NationsGlobe.prototype.loadBoundaryData = function () {
            options = options || {};
    var self = this;
            era = setEraShell(era, false);
    var THREE = this.THREE;
            reload = !!options.reloadGlobe;
    var url = this.root.getAttribute('data-border-geojson-url') || this.root.getAttribute('data-topology-url') || this.root.getAttribute('data-geojson-url') || DEFAULT_WORLD_URL;
    var objectName = this.root.getAttribute('data-topology-object') || 'countries';


    this.setStatus('DATA');
            clearEraCommitTimer();
            currentEra = era;
            stack.setAttribute('data-nations-current-era', era);


    return fetch(url, { credentials: 'omit' })
            contentPanels.forEach(function (panel) {
        .then(function (res) {
                var inactive = panel.getAttribute('data-era-content') !== era;
            if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + url);
                setHidden(panel, inactive);
            return res.json();
                if (!inactive) {
        })
                    toArray(panel.querySelectorAll('.clbi-nations-tabpanel')).forEach(initContinentPanel);
        .then(function (payload) {
                    toArray(panel.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
            var geojson = extractGeojsonFromPayload(payload, self.topojson, objectName);
                    toArray(panel.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryEventsPanel);
            var built = buildLineGeometryFromGeojson(THREE, geojson, self.radius + Math.max(0.0008, Math.min(0.006, self.boundarySurfaceOffset)));
                    toArray(panel.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryYearPanel);
            var material = new THREE.LineBasicMaterial({
                 }
                color: 0xb8b8b8,
                transparent: true,
                opacity: 0.58,
                 depthWrite: false
             });
             });
            var lines = new THREE.LineSegments(built.geometry, material);
            lines.renderOrder = 8;


             self.segmentCount = Math.floor(built.segmentCount || 0);
             applyGlobeEra(globe, era, reload);
             self.group.add(lines);
            scheduleEraNeighborsPrewarm(stack, eras, era);
        }
 
        function commitEraPreview() {
            var target = stack.getAttribute('data-nations-preview-era') || '';
 
            clearEraCommitTimer();
             if (!target) return false;
 
            setEra(target, { reloadGlobe: true });
            return true;
        }


             if (self.pointerInside && self.pointerClientX !== null && self.pointerClientY !== null) {
        function scheduleEraCommit() {
                 self.updatePointerReadoutFromClient(self.pointerClientX, self.pointerClientY);
             clearEraCommitTimer();
            } else {
            stack.CLBI_NationsEraCommitTimer = window.setTimeout(function () {
                 self.setReadout(self.getBaseReadout());
                 stack.CLBI_NationsEraCommitTimer = null;
             }
                 commitEraPreview();
        });
             }, 135);
};
        }


NationsGlobe.prototype.animate = function (now) {
        function previewMove(direction) {
    var self = this;
            var active = stack.getAttribute('data-nations-preview-era') || stack.getAttribute('data-nations-current-era') || currentEra || eras[0];
    var dt = Math.min(50, now - this.lastTime);
            var index = getEraIndex(eras, active);
    this.lastTime = now;
            var target = getWrappedEra(eras, index + direction);


    if (this.disposed || !document.body.contains(this.root)) {
            if (!target) return false;
        this.dispose();
            playNationsPanelClickSound();
        return;
            setEraShell(target, true);
    }
            scheduleEraCommit();
            return true;
        }


    if (this.stepResetMotion(dt)) {
         function move(direction) {
         /* reset interpolation owns rotation/camera for this frame */
            var active = stack.getAttribute('data-nations-current-era') || currentEra || eras[0];
    } else if (this.autoRotate && !this.dragging) {
            var index = getEraIndex(eras, active);
        this.rotationY += dt * this.autoRotateSpeed * this.getAutoRotateFactor(dt);
            var target = getWrappedEra(eras, index + direction);
        this.applyRotation();
    } else if (!this.dragging && (Math.abs(this.velocityX) > 0.000001 || Math.abs(this.velocityY) > 0.000001)) {
        var decay = Math.pow(this.inertiaDamping, dt / 16.6667);


        this.rotationY += this.velocityY * dt;
            if (!target) return false;
        this.rotationX += this.velocityX * dt;
            playNationsPanelClickSound();
        this.velocityY *= decay;
            setEra(target, { reloadGlobe: true });
         this.velocityX *= decay;
            return true;
         }


         if (Math.abs(this.velocityY) < 0.000001) this.velocityY = 0;
         if (leftButton) {
        if (Math.abs(this.velocityX) < 0.000001) this.velocityX = 0;
            leftButton.addEventListener('click', function (event) {
                event.preventDefault();
                move(-1);
            });
        }


         this.applyRotation();
         if (rightButton) {
    }
            rightButton.addEventListener('click', function (event) {
                event.preventDefault();
                move(1);
            });
        }


    this.updateCelestialBackdrop(dt, now);
        stack.CLBI_NationsEraController = {
            eras: eras.slice(),
            setEra: function (era) { setEra(era, { reloadGlobe: true }); },
            previewMove: previewMove,
            commitPreview: commitEraPreview,
            move: move
        };


    if (this.clouds) {
        setEra(currentEra, { reloadGlobe: false });
        /*
        * Cloud motion is relative to the rotating earth group.
        * The parent group carries the globe's AUTO rotation; this local drift
        * only represents slow atmospheric flow over that rotating planet.
        */
        this.clouds.rotation.y += dt * this.cloudDriftSpeed;
        this.clouds.rotation.x = Math.sin(now * 0.000018) * this.cloudTiltDrift;
        this.updateCloudOpacity();
     }
     }


     this.resize();
     function init(root) {
        var scope = root && root.querySelectorAll ? root : document;


    if (this.pointerInside && this.pointerClientX !== null && this.pointerClientY !== null) {
        purgeLegacyNationIndexFileImages(scope);
         this.updatePointerReadoutFromClient(this.pointerClientX, this.pointerClientY);
        toArray(scope.querySelectorAll('.clbi-nations-panel-stack')).forEach(initEraStack);
        toArray(scope.querySelectorAll('.clbi-nations-tabpanel')).forEach(initContinentPanel);
        toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryEventsPanel);
         toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryYearPanel);
        toArray(scope.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
        scheduleHistoryItemSlides(scope);
        bindHistoryItemSlideResize();
     }
     }


     if (this.renderer && this.scene && this.camera) {
     function findEraStack() {
         this.renderer.render(this.scene, this.camera);
         return document.querySelector('.clbi-nations-panel-stack');
     }
     }


     this.frame = window.requestAnimationFrame(function (t) {
     function moveEra(direction) {
         self.animate(t);
         var stack = findEraStack();
    });
 
};
        if (!stack || !stack.CLBI_NationsEraController) return false;
        return stack.CLBI_NationsEraController.move(direction);
    }


NationsGlobe.prototype.dispose = function () {
    function previewMoveEra(direction) {
    this.disposed = true;
        var stack = findEraStack();
    if (this.frame) window.cancelAnimationFrame(this.frame);


    if (this.renderer) {
        if (!stack || !stack.CLBI_NationsEraController || typeof stack.CLBI_NationsEraController.previewMove !== 'function') return false;
         try { this.renderer.dispose(); } catch (err) {}
         return stack.CLBI_NationsEraController.previewMove(direction);
     }
     }
};


function initNationsGlobes(root) {
    function commitEraPreview() {
    var scope = root && root.querySelectorAll ? root : document;
        var stack = findEraStack();
    var nodes = scope.querySelectorAll('[data-nations-globe], .clbi-nations-globe-window');


    Array.prototype.forEach.call(nodes, function (node) {
        if (!stack || !stack.CLBI_NationsEraController || typeof stack.CLBI_NationsEraController.commitPreview !== 'function') return false;
         var instance = node.CLBI_NationsGlobeInstance || null;
         return stack.CLBI_NationsEraController.commitPreview();
        var hasStage = !!node.querySelector('.clbi-nations-globe-stage');
    }


        if (node.getAttribute('data-nations-globe-ready') === '1') {
    function setEra(era) {
            if (instance && hasStage) {
        var stack = findEraStack();
                instance.rehydrateDom();
                return;
            }


            if (instance && !hasStage) {
        if (!stack || !stack.CLBI_NationsEraController) return false;
                try { instance.dispose(); } catch (err) {}
        stack.CLBI_NationsEraController.setEra(era);
            }
        return true;
    }


            node.removeAttribute('data-nations-globe-ready');
    function refresh(root) {
            node.CLBI_NationsGlobeInstance = null;
        var scope = root && root.querySelectorAll ? root : document;
        }


         node.setAttribute('data-nations-globe-ready', '1');
         nationRuntimeCacheBust = 'revision-ledger-refresh';
        nationListCache = {};
        nationLinkMapCache = {};
        historyEventsCache = {};


         instance = new NationsGlobe(node);
         toArray(scope.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]')).forEach(function (panel) {
        node.CLBI_NationsGlobeInstance = instance;
            panel.removeAttribute('data-nation-list-loading');
        instances.push(instance);
            panel.removeAttribute('data-nation-list-year-loaded');
        instance.init();
            panel.classList.remove('clbi-nations-list-json-ready', 'clbi-nations-list-json-fallback');
    });
        });
}


function scheduleNationsGlobeRefresh() {
        toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(function (panel) {
    window.setTimeout(function () { initNationsGlobes(document); }, 0);
            panel.removeAttribute('data-history-events-loading');
    window.setTimeout(function () { initNationsGlobes(document); }, 120);
            panel.removeAttribute('data-history-events-year-loaded');
    window.setTimeout(function () { initNationsGlobes(document); }, 360);
            panel.removeAttribute('data-nations-history-ready');
}
            panel.classList.remove('clbi-nations-history-json-ready', 'clbi-nations-history-json-fallback');
        });


        init(scope);
    }


window.CLBI_NationsGlobe = {
    function diagnostics() {
    init: initNationsGlobes,
        var flags = toArray(document.querySelectorAll('.clbi-nations-list-flag-slot')).map(function (slot) {
    instances: instances,
            var img = slot.querySelector('img');
    defaultWorldUrl: DEFAULT_WORLD_URL,
            return {
    reload: function () {
                file: slot.getAttribute('data-flag-file') || '',
        initNationsGlobes(document);
                src: img ? img.getAttribute('src') : '',
                missing: slot.classList.contains('is-missing')
            };
        });
        var legacyImages = toArray(document.images).filter(function (img) {
            return isLegacyNationIndexFileUrl(img.currentSrc || img.getAttribute('src') || '');
        }).map(function (img) {
            return img.currentSrc || img.getAttribute('src') || '';
        });
        var perfLegacy = (window.performance && performance.getEntriesByType ? performance.getEntriesByType('resource') : []).filter(function (entry) {
            return isLegacyNationIndexFileUrl(entry && entry.name);
        }).map(function (entry) { return entry.name; });
        return {
            build: NATIONS_PANEL_BUILD,
            resolvedFileCount: Object.keys(nationResolvedFileUrlCache || {}).length,
            unresolvedFiles: Object.keys(nationResolvedFileUrlCache || {}).filter(function (key) {
                return nationResolvedFileUrlCache[key] === '';
            }),
            renderedFlagCount: flags.length,
            ownedContinentPanels: toArray(document.querySelectorAll('.clbi-nations-tabpanel')).filter(function (panel) { return !!panel.CLBI_NationsPanelOwned; }).length,
            warmContinentBodies: toArray(document.querySelectorAll('.clbi-nations-tabpanel-continent[data-clbi-panel-warm="1"]')).length,
            missingRenderedFlags: flags.filter(function (flag) { return flag.missing || !flag.src; }),
            mediaWikiThumbRenderedFlags: flags.filter(function (flag) { return /\/images\/thumb\//i.test(flag.src || ''); }).length,
            legacyImageCount: legacyImages.length,
            legacyImages: legacyImages.slice(0, 40),
            performanceLegacyRequests: perfLegacy.slice(-40),
            sampleFlags: flags.slice(0, 20),
            keyboardHoverSuppressedPanels: toArray(document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching')).length,
            keyboardStaleHoverTabs: toArray(document.querySelectorAll('.clbi-nations-tabpanel-tab.is-keyboard-stale-hover')).length
        };
     }
     }
};


$(function () {
    window.NationsPanel = {
     initNationsGlobes(document);
        build: NATIONS_PANEL_BUILD,
});
        init: init,
        refresh: refresh,
        moveEra: moveEra,
        previewMoveEra: previewMoveEra,
        commitEraPreview: commitEraPreview,
        setEra: setEra,
        activateContinent: activateContinent,
        moveContinent: moveContinent,
        ownsContinentTabs: true,
        whenEraReady: whenEraReady,
        cancelPanelHoverFocus: clearNationPanelHoverFocus,
        diagnostics: diagnostics,
        prepareContentForEntry: prepareContentForEntry,
        perf: PanelPerf
     };


if (mw && mw.hook) {
     window.CLBI_NATIONS_PANEL = window.NationsPanel;
     mw.hook('wikipage.content').add(function ($content) {
        initNationsGlobes($content && $content[0] ? $content[0] : document);
        scheduleNationsGlobeRefresh();
    });
}


window.addEventListener('pageshow', scheduleNationsGlobeRefresh);
    if (window.mw && mw.hook) {
window.addEventListener('focus', scheduleNationsGlobeRefresh);
        mw.hook('wikipage.content').add(function ($content) {
            init($content && $content[0] ? $content[0] : document);
        });
    }


document.addEventListener('visibilitychange', function () {
    if (document.readyState === 'loading') {
    if (!document.hidden) scheduleNationsGlobeRefresh();
        document.addEventListener('DOMContentLoaded', function () { init(document); });
});
    } else {
 
        init(document);
})(window.mediaWiki || window.mw, window.jQuery);
    }
}());

2026년 7월 11일 (토) 02:29 기준 최신판

/* =========================================
   COASTLINE: BLACK ICE - Nations Panel
   시대 문서: 시대 전환 / 역사 연도 / 대륙 탭 제어
   ========================================= */
(function () {
    'use strict';

    var ENTRY_CONSUME_FIX_BUILD = '20260708-boot-skip-preview-and-manager-save-001';

    var NATIONS_PANEL_BUILD = '20260710-interaction-budget-bg-parity-001';

    var DEFAULT_ERAS = [
        '1950', '1960', '1970', '1980', '1990', '2000', '2010', '2020',
        '2030', '2040', '2050', '2060', '2070', '2080', '2090', '2100'
    ];

    var nationPanelLastDirectInteractionAt = 0;

    function markNationPanelDirectInteraction() {
        nationPanelLastDirectInteractionAt = Date.now();
    }

    function hasRecentNationPanelDirectInteraction(windowMs) {
        return Date.now() - Number(nationPanelLastDirectInteractionAt || 0) < Math.max(120, Number(windowMs || 900));
    }

    function isNationsGlobeBusyForPanelWork() {
        var globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
        var instance;

        if (!globe) return false;
        if (globe.classList && globe.classList.contains('is-dragging')) return true;
        if (globe.getAttribute && globe.getAttribute('data-clbi-globe-bg-busy') === '1') return true;
        instance = globe.CLBI_NationsGlobeInstance || null;
        return !!(instance && typeof instance.isHalftoneBackgroundBusy === 'function' && instance.isHalftoneBackgroundBusy());
    }

    function toArray(list) {
        return Array.prototype.slice.call(list || []);
    }

    function unique(values) {
        var seen = {};
        var out = [];

        values.forEach(function (value) {
            value = String(value || '').trim();
            if (!value || seen[value]) return;
            seen[value] = true;
            out.push(value);
        });

        return out;
    }

    function parseEraList(stack) {
        var raw = stack ? stack.getAttribute('data-nations-era-years') : '';
        var values = raw ? raw.split(/[\s,|]+/) : [];

        values = unique(values.filter(function (value) {
            return /^\d{4}$/.test(value);
        }));

        return values.length ? values : DEFAULT_ERAS.slice();
    }

    function getEraIndex(eras, era) {
        var index = eras.indexOf(String(era || ''));
        return index >= 0 ? index : 0;
    }

    function getWrappedEra(eras, index) {
        if (!eras.length) return '';
        index = ((index % eras.length) + eras.length) % eras.length;
        return eras[index];
    }

    function setHidden(element, hidden) {
        var active = !hidden;

        if (!element) return;
        if (element.classList.contains('is-active') === active &&
            element.hasAttribute('hidden') === !!hidden &&
            element.getAttribute('aria-hidden') === (hidden ? 'true' : 'false')) return;

        if (hidden) {
            element.setAttribute('hidden', 'hidden');
            element.setAttribute('aria-hidden', 'true');
            element.classList.remove('is-active');
        } else {
            element.removeAttribute('hidden');
            element.setAttribute('aria-hidden', 'false');
            element.classList.add('is-active');
        }
    }

    function setActiveButton(buttons, activeButton) {
        buttons.forEach(function (button) {
            var active = button === activeButton;

            if (button.classList.contains('is-active') !== active) {
                button.classList.toggle('is-active', active);
            }
            if (button.getAttribute('aria-selected') !== (active ? 'true' : 'false')) {
                button.setAttribute('aria-selected', active ? 'true' : 'false');
            }
            if (button.getAttribute('tabindex') !== (active ? '0' : '-1')) {
                button.setAttribute('tabindex', active ? '0' : '-1');
            }
        });
    }

    function activatePanelsByAttribute(panels, attrName, value) {
        panels.forEach(function (panel) {
            setHidden(panel, panel.getAttribute(attrName) !== value);
        });
    }

    function activatePersistentPanelsByAttribute(panels, attrName, value) {
        panels.forEach(function (panel) {
            var active = panel.getAttribute(attrName) === value;
            var aria = active ? 'false' : 'true';

            if (panel.hasAttribute('hidden')) panel.removeAttribute('hidden');
            if (panel.classList.contains('is-active') !== active) {
                panel.classList.toggle('is-active', active);
            }
            if (panel.getAttribute('aria-hidden') !== aria) {
                panel.setAttribute('aria-hidden', aria);
            }
            if (panel.getAttribute('data-clbi-panel-warm') !== '1') {
                panel.setAttribute('data-clbi-panel-warm', '1');
            }
        });
    }

    function getIdleScheduler() {
        return window.requestIdleCallback || function (callback) {
            return window.setTimeout(function () {
                callback({
                    didTimeout: false,
                    timeRemaining: function () { return 12; }
                });
            }, 96);
        };
    }

    function getIdleCanceller() {
        return window.cancelIdleCallback || window.clearTimeout;
    }

    function scheduleIdle(callback, options) {
        return getIdleScheduler()(callback, options || { timeout: 650 });
    }

    function cancelIdle(handle) {
        if (handle == null) return;
        getIdleCanceller()(handle);
    }

    function replaceYearTemplate(template, era) {
        return String(template || '').replace(/\{year\}/g, String(era || ''));
    }

    function readTemplate(root, attrName, fallback) {
        if (!root) return fallback;
        return root.getAttribute(attrName) || fallback;
    }

    function setAttributeFromTemplate(root, attrName, templateName, fallback, era) {
        var template = readTemplate(root, templateName, fallback);
        if (!template) {
            root.removeAttribute(attrName);
            return;
        }
        root.setAttribute(attrName, replaceYearTemplate(template, era));
    }

    function resetGlobeDom(globe) {
        var instance;

        if (!globe) return;

        instance = globe.CLBI_NationsGlobeInstance || null;
        if (instance && typeof instance.dispose === 'function') {
            try { instance.dispose(); } catch (err) {}
        }

        globe.CLBI_NationsGlobeInstance = null;
        globe.removeAttribute('data-nations-globe-ready');
        globe.classList.remove('is-ready', 'has-error', 'is-dragging');
        globe.innerHTML = '';
    }

    function reloadGlobe(globe) {
        if (!globe) return;

        if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.reload === 'function') {
            window.CLBI_NationsGlobe.reload(globe);
            return;
        }

        resetGlobeDom(globe);

        if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function') {
            window.CLBI_NationsGlobe.init(globe.parentNode || document);
            return;
        }

        window.setTimeout(function () {
            if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function') {
                window.CLBI_NationsGlobe.init(globe.parentNode || document);
            }
        }, 0);
    }

    function applyGlobeEra(globe, era, reload) {
        if (!globe || !era) return;

        globe.setAttribute('data-era-active', era);
        globe.setAttribute('data-era', replaceYearTemplate(
            readTemplate(globe, 'data-era-readout-template', '{year}'),
            era
        ));

        setAttributeFromTemplate(
            globe,
            'data-geojson-url',
            'data-era-border-url-template',
            '/index.php/Special:Redirect/file/{year}_Border_Lines.geojson',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-polygon-overlay-url',
            'data-era-polygon-overlay-url-template',
            '/index.php/Special:Redirect/file/{year}_Polygon_Overlay.webp',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-polygon-hit-url',
            'data-era-polygon-hit-url-template',
            '/index.php/Special:Redirect/file/{year}_Polygon_Hit.geojson',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-nation-list-url',
            'data-era-nation-list-url-template',
            '/index.php?title=MediaWiki:{year}_Nation_List.json&action=raw&ctype=application/json',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-nation-link-map-url',
            'data-era-nation-link-map-url-template',
            '/index.php?title=MediaWiki:{year}_Nation_Link_Map.json&action=raw&ctype=application/json',
            era
        );
        setAttributeFromTemplate(
            globe,
            'data-lakes-overlay-url',
            'data-era-lakes-overlay-url-template',
            '/index.php/Special:Redirect/file/{year}_Lakes_Overlay.webp',
            era
        );

        if (reload) {
            reloadGlobe(globe);
        }
    }


    var NATION_CONTINENT_FALLBACK = [
        { key: 'america', label: '아메리카', regions: ['북아메리카', '남아메리카'] },
        { key: 'europe', label: '유럽', regions: ['북유럽', '서유럽', '중앙유럽', '동유럽', '동남유럽', '남유럽', '남서유럽'] },
        { key: 'africa', label: '아프리카', regions: ['북아프리카', '서아프리카', '중앙아프리카', '동아프리카', '남아프리카'] },
        { key: 'asia', label: '아시아', regions: ['서아시아', '중앙아시아', '북아시아', '남아시아', '동아시아', '동남아시아'] },
        { key: 'oceania', label: '오세아니아', regions: ['오세아니아'] },
        { key: 'antarctica', label: '남극', regions: ['남극'] }
    ];

    var nationListCache = {};
    var nationLinkMapCache = {};
    var historyEventsCache = {};
    var nationRuntimeCacheBust = 'revision-ledger';
    var nationResolvedFileUrlCache = {};

    var PanelPerf = (function () {
        var startedAt = Date.now();
        var sequence = 1;
        var active = {};
        var entries = [];

        function cloneMeta(meta) {
            var copy = {};
            if (!meta || typeof meta !== 'object') return copy;
            Object.keys(meta).forEach(function (key) {
                var value = meta[key];
                if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
                    copy[key] = value;
                } else if (Array.isArray(value)) {
                    copy[key] = value.length <= 12 ? value.slice() : value.slice(0, 12).concat(['...+' + (value.length - 12)]);
                } else {
                    try { copy[key] = JSON.parse(JSON.stringify(value)); }
                    catch (err) { copy[key] = String(value); }
                }
            });
            return copy;
        }

        function mergeMeta(a, b) {
            var out = cloneMeta(a);
            b = cloneMeta(b);
            Object.keys(b).forEach(function (key) { out[key] = b[key]; });
            return out;
        }

        function start(name, meta) {
            var id = sequence++;
            active[id] = {
                id: id,
                name: String(name || 'step'),
                startedAt: Date.now(),
                offset: Date.now() - startedAt,
                meta: cloneMeta(meta)
            };
            return id;
        }

        function end(id, meta) {
            var item = active[id];
            if (!item) return null;
            delete active[id];
            item.ms = Date.now() - item.startedAt;
            item.meta = mergeMeta(item.meta, meta || { ok: true });
            entries.push({
                offset: item.offset,
                ms: item.ms,
                name: item.name,
                meta: item.meta
            });
            return item;
        }

        function mark(name, meta) {
            entries.push({
                offset: Date.now() - startedAt,
                ms: 0,
                name: String(name || 'mark'),
                meta: cloneMeta(meta)
            });
        }

        function measure(name, meta, callback) {
            var id = start(name, meta);
            var value;
            try {
                value = callback();
            } catch (err) {
                end(id, { ok: false, error: err && err.message ? err.message : String(err) });
                throw err;
            }
            if (value && typeof value.then === 'function') {
                return value.then(function (result) {
                    end(id, { ok: true });
                    return result;
                }, function (err) {
                    end(id, { ok: false, error: err && err.message ? err.message : String(err) });
                    throw err;
                });
            }
            end(id, { ok: true });
            return value;
        }

        function summary() {
            return {
                build: NATIONS_PANEL_BUILD,
                startedAt: startedAt,
                totalMs: Date.now() - startedAt,
                entries: entries.slice().sort(function (a, b) {
                    if (a.offset !== b.offset) return a.offset - b.offset;
                    return b.ms - a.ms;
                }),
                active: Object.keys(active).map(function (id) {
                    var item = active[id];
                    return {
                        offset: item.offset,
                        ms: Date.now() - item.startedAt,
                        name: item.name,
                        meta: cloneMeta(item.meta)
                    };
                })
            };
        }

        function table() {
            var data = summary().entries.map(function (entry) {
                return { name: entry.name, ms: entry.ms, offset: entry.offset, meta: entry.meta };
            });
            if (window.console && console.table) console.table(data);
            return data;
        }

        return {
            build: NATIONS_PANEL_BUILD,
            start: start,
            end: end,
            mark: mark,
            measure: measure,
            summary: summary,
            table: table
        };
    }());

    window.NationsPanelPerf = PanelPerf;

    function interactionPerfMeasure(name, meta, fn) {
        var perf = window.InteractionPerf;
        if (perf && typeof perf.measureSync === 'function') {
            return perf.measureSync(name, meta || {}, fn);
        }
        return fn();
    }

    function interactionPerfMark(name, meta) {
        var perf = window.InteractionPerf;
        if (perf && typeof perf.mark === 'function') {
            perf.mark(name, meta || {});
        }
    }

    function countNationListItems(data) {
        var continents = data && Array.isArray(data.continents) ? data.continents : [];
        var regions = 0;
        var items = 0;
        continents.forEach(function (continent) {
            (continent.regions || []).forEach(function (region) {
                regions += 1;
                items += Array.isArray(region.items) ? region.items.length : 0;
            });
        });
        return { continents: continents.length, regions: regions, items: items };
    }

    function extractRawTitleFromUrl(url) {
        var text = String(url || '').trim();
        var match;
        if (!text) return '';
        match = text.match(/[?&]title=([^&#]+)/i);
        if (match) {
            try { return decodeURIComponent(match[1].replace(/\+/g, ' ')).replace(/_/g, ' ').trim(); }
            catch (err) { return match[1].replace(/_/g, ' ').trim(); }
        }
        return '';
    }

    function addNationCacheBust(url, ref) {
        url = String(url || '').trim();
        ref = String(ref || extractRawTitleFromUrl(url) || '').trim();
        if (!url) return '';
        if (/[?&](?:_clbiBust|_|_entryRev)=/.test(url)) return url;
        if (ref && window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
            return window.RevisionManifest.addRevisionParam(url, ref);
        }
        return url;
    }

    function fetchNationJson(url, fallbackRef) {
        var ref = String(extractRawTitleFromUrl(url) || fallbackRef || '').trim();
        var requestUrl;
        if (ref && window.EntryStore && typeof window.EntryStore.fetchJsonRef === 'function') {
            return PanelPerf.measure('fetch nation json via EntryStore', { ref: ref }, function () {
                return window.EntryStore.fetchJsonRef(ref, { noStore: false });
            });
        }
        requestUrl = addNationCacheBust(url, ref);
        return PanelPerf.measure('fetch nation json via fetch', { ref: ref, url: requestUrl }, function () {
            return fetch(requestUrl, { credentials: 'same-origin', cache: 'force-cache' })
                .then(function (res) {
                    if (!res.ok) throw new Error('HTTP ' + res.status);
                    return res.text();
                })
                .then(function (text) {
                    var trimmed = String(text || '').trim();
                    if (!trimmed) throw new Error('empty JSON response: ' + requestUrl);
                    return JSON.parse(trimmed);
                });
        });
    }

    function escapeHtml(value) {
        return String(value == null ? '' : value)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    function encodeWikiTitle(title) {
        return String(title || '')
            .trim()
            .replace(/ /g, '_')
            .split('/')
            .map(function (part) { return encodeURIComponent(part); })
            .join('/');
    }

    function normalizeNationFile(value) {
        var text = String(value || '').trim();
        var titleMatch;
        var redirectMatch;
        var previous;
        var i;
        var j;

        if (!text) return '';

        text = text
            .replace(/^\[\[:?\s*/i, '')
            .replace(/\]\]$/i, '')
            .trim();

        for (i = 0; i < 8; i += 1) {
            previous = text;

            for (j = 0; j < 3; j += 1) {
                try {
                    if (/%[0-9a-f]{2}/i.test(text)) {
                        text = decodeURIComponent(text);
                    }
                } catch (err) {
                    break;
                }
            }

            text = text.replace(/\\/g, '/').trim();

            titleMatch = text.match(/[?&]title=([^&#]+)/i);
            if (titleMatch) {
                text = titleMatch[1];
                continue;
            }

            text = text
                .replace(/[?#].*$/g, '')
                .replace(/^https?:\/\/[^/]+/i, '')
                .replace(/^\/+/, '')
                .replace(/^index\.php\/?/i, '')
                .replace(/^wiki\/?/i, '')
                .trim();

            for (j = 0; j < 6; j += 1) {
                var stripped = text
                    .replace(/^\s*파일\s*:/i, '')
                    .replace(/^\s*File\s*:/i, '')
                    .replace(/^\s*이미지\s*:/i, '')
                    .replace(/^\s*Image\s*:/i, '')
                    .replace(/^:+/, '')
                    .trim();
                if (stripped === text) break;
                text = stripped;
            }

            redirectMatch = text.match(/^(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
                text.match(/^(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
                text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i);

            if (redirectMatch) {
                text = redirectMatch[1].trim();
                continue;
            }

            if (text === previous) break;
        }

        text = text.replace(/^\/+/, '').trim();

        return text;
    }

    function normalizeNationFileKey(file) {
        return normalizeNationFile(file).replace(/_/g, ' ').trim().toLowerCase();
    }

    function getWikiApiUrl(params) {
        var query = [];
        Object.keys(params || {}).forEach(function (key) {
            if (params[key] == null) return;
            query.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(params[key])));
        });
        return (window.mw && mw.util && mw.util.wikiScript ? mw.util.wikiScript('api') : '/api.php') + '?' + query.join('&');
    }

    function getWikiApiEndpoint() {
        return (window.mw && mw.util && mw.util.wikiScript ? mw.util.wikiScript('api') : '/api.php');
    }

    function fetchWikiApi(params) {
        var body;

        try {
            body = new URLSearchParams();
            Object.keys(params || {}).forEach(function (key) {
                if (params[key] == null) return;
                body.append(key, String(params[key]));
            });
            return fetch(getWikiApiEndpoint(), {
                method: 'POST',
                credentials: 'same-origin',
                cache: 'no-store',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                },
                body: body.toString()
            }).then(function (res) {
                if (!res.ok) throw new Error('wiki api failed: ' + res.status);
                return res.json();
            });
        } catch (err) {
            return fetch(getWikiApiUrl(params), { credentials: 'same-origin', cache: 'no-store' }).then(function (res) {
                if (!res.ok) throw new Error('wiki api failed: ' + res.status);
                return res.json();
            });
        }
    }

    function buildNationFileUrl(file) {
        var clean = normalizeNationFile(file);
        var key = normalizeNationFileKey(clean);
        var resolved;

        if (!clean || !key) return '';

        /*
           Render nation-list flags through MediaWiki's own file thumbnail pipeline.
           This mirrors [[파일:Name.svg|16px]] far more closely than using the raw
           SVG URL or Special:Redirect/file path directly. If MediaWiki cannot
           resolve the file title, return an empty URL and hide the flag slot.
        */
        resolved = nationResolvedFileUrlCache[key];
        /* Entry full-load prewarms the exact thumbnail URL.  Adding a Date-based cache
           buster here would create a new URL and make the preloaded flag useless.
           When EntryCache has a same-revision blob URL, render that blob URL so a
           second tab consumes the persistent cache instead of relying on a fresh <img>
           request path. */
        if (resolved && window.EntryStore && typeof window.EntryStore.getImageDisplayUrl === 'function') {
            return window.EntryStore.getImageDisplayUrl(resolved) || resolved;
        }
        return resolved || '';
    }

    function collectNationFlagFileNames(data, options) {
        var files = [];
        var continentKeys = options && options.continentKeys;

        function addFlag(flag) {
            var file = normalizeNationFile(flag && (flag.file || flag.flag_file || flag.flag || flag.flag_title || flag));
            if (file) files.push(file);
        }
        function scanItems(items) {
            (items || []).forEach(function (item) {
                if (!item || typeof item !== 'object') return;
                if (Array.isArray(item.flags)) item.flags.forEach(addFlag);
                if (Array.isArray(item.sybs)) item.sybs.forEach(addFlag);
                if (Array.isArray(item.symbols)) item.symbols.forEach(addFlag);
                addFlag(item.flag_file || item.flag || item.flag_title || '');
                addFlag(item.syb_file || item.syb || item.symbol_file || item.symbol || '');
            });
        }
        (data && data.continents || []).forEach(function (continent) {
            var key = String((continent && continent.key) || '').trim();
            if (continentKeys && key && !continentKeys[key]) return;
            if (continentKeys && !key) return;
            (continent.regions || []).forEach(function (region) {
                scanItems(region.items || []);
            });
        });
        return unique(files);
    }

    function getInitialContinentKey(panel, data) {
        var activeTab = panel && panel.querySelector ? panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]') : null;
        var activeKey = activeTab ? String(activeTab.getAttribute('data-continent') || '').trim() : '';
        var continents = data && Array.isArray(data.continents) ? data.continents : [];

        if (!activeKey && continents.length) activeKey = String(continents[0].key || '').trim();
        return activeKey;
    }

    function collectInitialNationFlagFileNames(data, panel) {
        var activeKey = getInitialContinentKey(panel, data);
        var map = {};

        if (!activeKey) return collectNationFlagFileNames(data);
        map[activeKey] = true;
        return collectNationFlagFileNames(data, { continentKeys: map });
    }

    function subtractNationFlagFiles(allFiles, initialFiles) {
        var initial = {};

        (initialFiles || []).forEach(function (file) {
            var key = normalizeNationFileKey(file);
            if (key) initial[key] = true;
        });

        return unique((allFiles || []).filter(function (file) {
            var key = normalizeNationFileKey(file);
            return key && !initial[key];
        }));
    }

    function resolveNationFlagFileUrls(files) {
        var cleanFiles = unique((files || []).map(normalizeNationFile).filter(Boolean));

        cleanFiles.forEach(function (file) {
            var key = normalizeNationFileKey(file);
            var readyUrl = window.EntryStore && typeof window.EntryStore.getFlagUrl === 'function' ? window.EntryStore.getFlagUrl(file) : '';
            if (key && readyUrl && nationResolvedFileUrlCache[key] === undefined) nationResolvedFileUrlCache[key] = readyUrl;
        });

        var pending = cleanFiles.filter(function (file) {
            var key = normalizeNationFileKey(file);
            return key && nationResolvedFileUrlCache[key] === undefined;
        });
        var chunks = [];

        while (pending.length) chunks.push(pending.splice(0, 25));
        if (!chunks.length) {
            PanelPerf.mark('resolve nation flag URLs', { count: cleanFiles.length, pending: 0, cached: cleanFiles.length });
            return Promise.resolve(nationResolvedFileUrlCache);
        }

        return PanelPerf.measure('resolve nation flag URLs', { count: cleanFiles.length, pending: pending.length, chunks: chunks.length }, function () {
            return Promise.all(chunks.map(function (chunk) {
            var titleToKey = {};
            var titles = [];

            chunk.forEach(function (file) {
                var key = normalizeNationFileKey(file);
                var canonical = 'File:' + file;
                var local = '파일:' + file;

                if (!key) return;
                titleToKey[normalizeNationFileKey(canonical)] = key;
                titleToKey[normalizeNationFileKey(local)] = key;
                titles.push(canonical);
                titles.push(local);
            });

            return fetchWikiApi({
                action: 'query',
                format: 'json',
                formatversion: '2',
                redirects: '1',
                prop: 'imageinfo',
                iiprop: 'url|sha1|timestamp|size',
                iiurlwidth: '16',
                titles: titles.join('|')
            }).then(function (json) {
                var pages = (json && json.query && json.query.pages) || [];
                var seen = {};

                pages.forEach(function (page) {
                    var file = normalizeNationFile(page && page.title);
                    var pageKey = normalizeNationFileKey(file);
                    var originalKey = titleToKey[pageKey] || pageKey;
                    var imageinfo = page && page.imageinfo && page.imageinfo[0];
                    if (!originalKey) return;
                    seen[originalKey] = true;
                    if (imageinfo && (imageinfo.thumburl || imageinfo.url)) {
                        var resolvedUrl = imageinfo.thumburl || imageinfo.url;
                        var rev = imageinfo.sha1 || imageinfo.timestamp || imageinfo.size || '';
                        var sep;
                        if (rev && !/[?&]_entryFileRev=/.test(resolvedUrl)) {
                            sep = resolvedUrl.indexOf('?') === -1 ? '?' : '&';
                            resolvedUrl += sep + '_entryFileRev=' + encodeURIComponent(String(rev));
                        }
                        nationResolvedFileUrlCache[originalKey] = resolvedUrl;
                        if (pageKey) nationResolvedFileUrlCache[pageKey] = resolvedUrl;
                        if (window.EntryStore && typeof window.EntryStore.setFlagUrl === 'function') window.EntryStore.setFlagUrl(file, resolvedUrl);
                    } else if (nationResolvedFileUrlCache[originalKey] === undefined) {
                        nationResolvedFileUrlCache[originalKey] = '';
                    }
                });

                chunk.forEach(function (file) {
                    var key = normalizeNationFileKey(file);
                    if (key && !seen[key] && nationResolvedFileUrlCache[key] === undefined) {
                        nationResolvedFileUrlCache[key] = '';
                    }
                });
            }).catch(function () {
                chunk.forEach(function (file) {
                    var key = normalizeNationFileKey(file);
                    if (key && nationResolvedFileUrlCache[key] === undefined) nationResolvedFileUrlCache[key] = '';
                });
            });
            })).then(function () {
                return nationResolvedFileUrlCache;
            });
        });
    }


    function isLegacyNationIndexFileUrl(src) {
        src = String(src || '');
        if (!src) return false;
        if (!/\.svg(?:[?#]|$)/i.test(src)) return false;
        if (/\/images\//i.test(src)) return false;
        return /(?:Special|%ED%8A%B9%EC%88%98|특수).*?(?:Redirect|%EB%84%98%EA%B2%A8%EC%A3%BC%EA%B8%B0|넘겨주기).*?file/i.test(src) || /\/index\.php\//i.test(src);
    }

    function purgeLegacyNationIndexFileImages(scope) {
        var root = scope || document;
        toArray(root.querySelectorAll ? root.querySelectorAll('.clbi-nations-tabpanel-body img, .clbi-nations-tabpanel img') : []).forEach(function (img) {
            var src = img.currentSrc || img.getAttribute('src') || '';
            if (!isLegacyNationIndexFileUrl(src)) return;
            img.removeAttribute('src');
            img.setAttribute('data-clbi-legacy-src-removed', src);
        });
    }

    function getNationFlagOffsetY(flag) {
        var value;

        if (!flag || typeof flag !== 'object') return 0;

        value = flag.offsetY;
        if (value == null) value = flag.offset_y;
        if (value == null) value = flag.flagOffsetY;
        if (value == null && flag.decorator === 'top') value = -1;

        value = parseFloat(value);
        if (!isFinite(value)) value = 0;

        return Math.max(-3, Math.min(3, Math.round(value)));
    }

    function buildNationPageUrl(page) {
        page = String(page || '').trim();
        return page ? '/index.php/' + encodeWikiTitle(page) : '';
    }

    function getPanelEra(panel) {
        var eraPanel = panel && panel.closest ? panel.closest('.clbi-nations-era-content[data-era-content]') : null;
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;

        return (eraPanel && eraPanel.getAttribute('data-era-content')) ||
            (panel && panel.getAttribute('data-era')) ||
            (stack && (stack.getAttribute('data-nations-current-era') || stack.getAttribute('data-era'))) ||
            '1950';
    }

    function getNationListUrl(panel, era) {
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
        var template = (panel && panel.getAttribute('data-nation-list-url-template')) ||
            (stack && stack.getAttribute('data-nation-list-url-template')) ||
            '/index.php?title=MediaWiki:{year}_Nation_List.json&action=raw&ctype=application/json';

        return (panel && panel.getAttribute('data-nation-list-url')) || replaceYearTemplate(template, era);
    }


    function getNationLinkMapUrl(panel, era) {
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
        var template = (panel && panel.getAttribute('data-nation-link-map-url-template')) ||
            (stack && stack.getAttribute('data-nation-link-map-url-template')) ||
            '/index.php?title=MediaWiki:{year}_Nation_Link_Map.json&action=raw&ctype=application/json';

        return (panel && panel.getAttribute('data-nation-link-map-url')) || replaceYearTemplate(template, era);
    }

    function getHistoryEventsUrl(panel, era) {
        var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
        var template = (panel && panel.getAttribute('data-history-events-url-template')) ||
            (stack && stack.getAttribute('data-history-events-url-template')) ||
            '/index.php?title=MediaWiki:{year}_History_Events.json&action=raw&ctype=application/json';

        return (panel && panel.getAttribute('data-history-events-url')) || replaceYearTemplate(template, era);
    }

    function makeDefaultHistoryYears(era) {
        var start = parseInt(era || '1950', 10);
        var years = [];
        var i;

        if (!isFinite(start)) start = 1950;
        for (i = 0; i < 10; i += 1) {
            years.push({ year: String(start + i), label: String(start + i), items: [] });
        }
        return years;
    }

    function normalizeHistoryEventItem(item) {
        item = item && typeof item === 'object' ? item : {};
        return {
            period: String(item.period || item.date || item.range || item.year || '').trim(),
            title: String(item.title || item.event || item.name || item.label || '').trim(),
            page: String(item.page || item.wiki_title || item.title_page || '').trim(),
            external_url: String(item.external_url || item.url || '').trim(),
            major: item.major === true || item.type === 'major',
            note: String(item.note || '').trim(),
            enabled: item.enabled !== false
        };
    }

    function normalizeHistoryEvents(payload, era) {
        var sourceYears = [];
        var normalized;

        if (payload && Array.isArray(payload.years)) {
            sourceYears = payload.years;
        } else if (payload && payload.events && typeof payload.events === 'object') {
            Object.keys(payload.events).forEach(function (year) {
                sourceYears.push({ year: year, label: year, items: payload.events[year] });
            });
        }

        normalized = sourceYears.map(function (row) {
            var year;
            var items;

            if (typeof row === 'string' || typeof row === 'number') {
                row = { year: String(row), items: [] };
            }
            row = row && typeof row === 'object' ? row : {};
            year = String(row.year || row.key || row.label || '').trim();
            items = Array.isArray(row.items) ? row.items : (Array.isArray(row.events) ? row.events : []);

            return {
                year: year,
                label: String(row.label || year).trim() || year,
                items: items.map(normalizeHistoryEventItem).filter(function (item) {
                    return item.enabled !== false;
                })
            };
        }).filter(function (row) {
            return !!row.year;
        });

        if (!normalized.length) normalized = makeDefaultHistoryYears(era);

        return {
            version: payload && payload.version ? payload.version : 1,
            era: payload && (payload.era || payload.year) ? String(payload.era || payload.year) : String(era || '1950'),
            description: payload && payload.description ? String(payload.description) : '',
            years: normalized
        };
    }

    function fetchHistoryEvents(panel, era) {
        var url = getHistoryEventsUrl(panel, era);
        var cacheKey = era + '|' + url;
        var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_History_Events.json');

        if (historyEventsCache[cacheKey]) return historyEventsCache[cacheKey];
        if (cached) {
            historyEventsCache[cacheKey] = Promise.resolve(normalizeHistoryEvents(cached, era));
            return historyEventsCache[cacheKey];
        }

        historyEventsCache[cacheKey] = fetchNationJson(url, 'MediaWiki:' + era + '_History_Events.json')
            .then(function (payload) {
                return normalizeHistoryEvents(payload, era);
            });

        return historyEventsCache[cacheKey];
    }

    function buildHistoryPageUrl(page) {
        page = String(page || '').trim();
        return page ? '/index.php/' + encodeWikiTitle(page) : '';
    }

    function renderInlineWikiLinks(text) {
        var source = String(text || '');
        var out = '';
        var lastIndex = 0;
        var match;
        var pattern = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;

        pattern.lastIndex = 0;
        while ((match = pattern.exec(source))) {
            var rawPage = String(match[1] || '').trim();
            var rawLabel = String(match[2] || rawPage).trim();

            out += escapeHtml(source.slice(lastIndex, match.index));
            if (rawPage) {
                out += '<a href="' + escapeHtml(buildHistoryPageUrl(rawPage)) + '">' + escapeHtml(rawLabel) + '</a>';
            } else {
                out += escapeHtml(match[0]);
            }
            lastIndex = pattern.lastIndex;
        }

        out += escapeHtml(source.slice(lastIndex));
        return out;
    }

    function hasInlineWikiLink(text) {
        return /\[\[[^\]]+\]\]/.test(String(text || ''));
    }

    function renderHistoryEventTitle(item) {
        var title = item.title || item.page || item.external_url || '미지정';
        var href;

        if (hasInlineWikiLink(title)) return renderInlineWikiLinks(title);

        href = item.external_url || buildHistoryPageUrl(item.page || '');
        return href ? '<a href="' + escapeHtml(href) + '">' + escapeHtml(title) + '</a>' : escapeHtml(title);
    }

    function renderHistoryEventItem(item) {
        var classes = 'clbi-nations-history-item' + (item.major ? ' clbi-nations-history-item-major' : '');
        var nameHtml = renderHistoryEventTitle(item);

        return '<div class="' + classes + '">' +
            '<span class="clbi-nations-history-period">' + escapeHtml(item.period || '') + '</span>' +
            '<span class="clbi-nations-history-event-name">' + nameHtml + '</span>' +
        '</div>';
    }

    function renderHistoryEmpty() {
        return '<div class="clbi-nations-history-empty"><span class="clbi-nations-history-empty-ko">작업 중...</span><span class="clbi-nations-history-empty-en">Working in Progress...</span></div>';
    }

    function renderHistoryPanel(panel, data) {
        var body = panel && panel.querySelector ? panel.querySelector('.clbi-nations-history-body') : null;
        var years = data && Array.isArray(data.years) ? data.years : [];
        var activeTab = panel.querySelector('.clbi-nations-history-year-button.is-active[data-year], .clbi-nations-history-year-button[aria-selected="true"][data-year]');
        var activeYear = activeTab ? activeTab.getAttribute('data-year') : '';
        var pages;
        var tabs;

        if (!body || !years.length) return;
        if (!activeYear || !years.some(function (row) { return row.year === activeYear; })) activeYear = years[0].year;

        pages = years.map(function (row) {
            var active = row.year === activeYear;
            var items = (row.items || []).filter(function (item) { return item && item.enabled !== false; });
            return '<div class="clbi-nations-history-list clbi-nations-history-page' + (active ? ' is-active' : '') + '" data-year-panel="' + escapeHtml(row.year) + '"' + (active ? '' : ' hidden="hidden"') + '>' +
                (items.length ? items.map(renderHistoryEventItem).join('') : renderHistoryEmpty()) +
            '</div>';
        }).join('');

        tabs = years.map(function (row) {
            var active = row.year === activeYear;
            return '<span class="clbi-nations-history-year-button' + (active ? ' is-active' : '') + '" role="tab" tabindex="' + (active ? '0' : '-1') + '" aria-selected="' + (active ? 'true' : 'false') + '" data-year="' + escapeHtml(row.year) + '">' + escapeHtml(row.label || row.year) + '</span>';
        }).join('');

        body.innerHTML = '<div class="clbi-nations-history-pages">' + pages + '</div>' +
            '<div class="clbi-nations-history-year-well"><div class="clbi-nations-history-year-list" role="tablist" aria-label="역사적 사건 연도 선택">' + tabs + '</div></div>';

        panel.removeAttribute('data-nations-history-ready');
        panel.classList.add('clbi-nations-history-json-ready');
        panel.setAttribute('data-history-events-year-loaded', String(data.era || getPanelEra(panel)));
        initHistoryYearPanel(panel);
        scheduleHistoryItemSlides(panel);
    }

    function initHistoryEventsPanel(panel) {
        var era;
        var promise;

        if (!panel || isNationListPanelHidden(panel)) return Promise.resolve(null);
        if (panel.getAttribute('data-history-events-year-loaded') === getPanelEra(panel)) return Promise.resolve(panel);
        if (panel.getAttribute('data-history-events-loading') === '1' && panel.HistoryEventsLoadPromise) return panel.HistoryEventsLoadPromise;

        era = getPanelEra(panel);
        panel.setAttribute('data-history-events-loading', '1');

        promise = fetchHistoryEvents(panel, era).then(function (data) {
            renderHistoryPanel(panel, data);
            return panel;
        }).catch(function () {
            panel.classList.add('clbi-nations-history-json-fallback');
            return panel;
        }).finally(function () {
            panel.removeAttribute('data-history-events-loading');
            panel.HistoryEventsLoadPromise = null;
        });

        panel.HistoryEventsLoadPromise = promise;
        return promise;
    }

    function getEntryStoreJson(url) {
        if (!window.EntryStore || typeof window.EntryStore.getJsonSync !== 'function') return null;
        return window.EntryStore.getJsonSync(url);
    }

    function normalizeNationTag(value) {
        value = String(value == null ? '' : value).trim().toUpperCase();
        return /^[A-Z0-9]{3}$/.test(value) ? value : '';
    }

    function normalizeLookupKey(value) {
        return String(value == null ? '' : value)
            .trim()
            .replace(/_/g, ' ')
            .replace(/\s+/g, ' ')
            .toLowerCase();
    }

    function getNationItemTag(item) {
        if (!item || typeof item !== 'object') return '';
        return normalizeNationTag(item.tag || item.code || item.map_tag || item.mapTag || '');
    }

    function collectNationItemFlags(item) {
        var flags = [];

        if (!item || typeof item !== 'object') return flags;

        if (Array.isArray(item.flags)) {
            item.flags.forEach(function (flag) {
                if (!flag) return;
                if (typeof flag === 'string') {
                    flag = { file: flag };
                }
                if (typeof flag !== 'object') return;
                if (normalizeNationFile(flag.file || flag.flag_file || flag.flag || '')) {
                    flags.push(flag);
                }
            });
        }

        if (!flags.length && (item.flag_file || item.flag || item.flag_title)) {
            flags.push({
                file: item.flag_file || item.flag || item.flag_title,
                width: item.flag_width || '16px',
                border: item.border !== false,
                offsetMode: item.offsetMode || item.offset_mode || 'auto',
                offsetY: item.offsetY || item.offset_y || 0
            });
        }

        return flags;
    }

    function collectNationItemSybs(item) {
        var sybs = [];

        if (!item || typeof item !== 'object') return sybs;

        function addSymbol(symbol) {
            if (!symbol) return;
            if (typeof symbol === 'string') symbol = { file: symbol };
            if (typeof symbol !== 'object') return;
            if (normalizeNationFile(symbol.file || symbol.syb_file || symbol.syb || symbol.symbol_file || symbol.symbol || '')) {
                sybs.push(symbol);
            }
        }

        if (Array.isArray(item.sybs)) item.sybs.forEach(addSymbol);
        if (Array.isArray(item.symbols)) item.symbols.forEach(addSymbol);

        if (!sybs.length && (item.syb_file || item.syb || item.symbol_file || item.symbol)) {
            sybs.push({
                file: item.syb_file || item.syb || item.symbol_file || item.symbol,
                width: item.syb_width || item.symbol_width || '16px',
                border: item.syb_border === true || item.symbol_border === true
            });
        }

        return sybs;
    }

    function collectNationItemSymbols(item) {
        var symbols = [];
        var legacySeen = {};

        function add(symbol, dedupeLegacy) {
            var file = normalizeNationSymbolFile(symbol);
            var key = normalizeNationFileKey(file);
            var out;

            if (!file) return;
            if (dedupeLegacy && key) {
                if (legacySeen[key]) return;
                legacySeen[key] = true;
            }

            if (typeof symbol === 'string') {
                out = { file: file };
            } else {
                out = Object.assign({}, symbol || {});
                out.file = file;
            }

            if (out.border == null && out.symbol_border != null) out.border = out.symbol_border !== false;
            if (out.border == null && out.syb_border != null) out.border = out.syb_border === true;
            if (out.border == null) out.border = !isNationSybFile(file);

            symbols.push(out);
        }

        if (!item || typeof item !== 'object') return symbols;

        if (Array.isArray(item.symbols)) {
            item.symbols.slice(0, 3).forEach(function (symbol) {
                add(symbol, false);
            });
            if (symbols.length) return symbols.slice(0, 3);
        }

        collectNationItemFlags(item).forEach(function (symbol) { add(symbol, true); });
        collectNationItemSybs(item).forEach(function (symbol) { add(symbol, true); });

        if (!symbols.length) {
            add(item.flag_file || item.flag || item.flag_title || '', true);
            add(item.syb_file || item.syb || item.symbol_file || item.symbol || '', true);
        }
        return symbols.slice(0, 3);
    }

    function normalizeNationLinkMap(payload) {
        var source = payload && payload.items ? payload.items : {};
        var index = { byTag: {}, byPage: {}, byLabel: {} };

        Object.keys(source || {}).forEach(function (key) {
            var entry = source[key];
            var tag;
            var pageKey;
            var labelKey;

            if (!entry || typeof entry !== 'object') return;
            tag = normalizeNationTag(entry.tag || key || entry.page || '');
            pageKey = normalizeLookupKey(entry.wiki_title || entry.page_title || entry.title || '');
            labelKey = normalizeLookupKey(entry.name_ko || entry.label || entry.page_name || '');

            if (tag && !index.byTag[tag]) index.byTag[tag] = entry;
            if (pageKey && !index.byPage[pageKey]) index.byPage[pageKey] = entry;
            if (labelKey && !index.byLabel[labelKey]) index.byLabel[labelKey] = entry;
        });

        return index;
    }

    function fetchNationLinkMap(panel, era) {
        var url = getNationLinkMapUrl(panel, era);
        var cacheKey = era + '|' + url;
        var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_Nation_Link_Map.json');

        if (nationLinkMapCache[cacheKey]) {
            PanelPerf.mark('fetch nation link map cache promise hit', { era: era });
            return nationLinkMapCache[cacheKey];
        }
        if (cached) {
            PanelPerf.mark('fetch nation link map EntryStore hit', { era: era });
            nationLinkMapCache[cacheKey] = Promise.resolve(PanelPerf.measure('normalize nation link map', { era: era, source: 'EntryStore' }, function () {
                return normalizeNationLinkMap(cached);
            }));
            return nationLinkMapCache[cacheKey];
        }

        nationLinkMapCache[cacheKey] = PanelPerf.measure('fetch nation link map remote', { era: era }, function () {
            return fetchNationJson(url, 'MediaWiki:' + era + '_Nation_Link_Map.json')
                .then(function (payload) {
                    return PanelPerf.measure('normalize nation link map', { era: era, source: 'remote' }, function () {
                        return normalizeNationLinkMap(payload);
                    });
                })
                .catch(function () {
                    return normalizeNationLinkMap(null);
                });
        });

        return nationLinkMapCache[cacheKey];
    }

    function findNationLinkEntryForItem(item, index) {
        var tag;
        var pageKey;
        var labelKey;

        if (!item || !index) return null;

        tag = getNationItemTag(item);
        if (tag && index.byTag && index.byTag[tag]) return index.byTag[tag];

        pageKey = normalizeLookupKey(item.page || item.wiki_title || '');
        if (pageKey && index.byPage && index.byPage[pageKey]) return index.byPage[pageKey];

        labelKey = normalizeLookupKey(item.label || item.name || item.name_ko || '');
        if (labelKey && index.byLabel && index.byLabel[labelKey]) return index.byLabel[labelKey];

        return null;
    }

    function applyNationLinkMapFlagFallback(data, linkMapIndex) {
        if (!data || !Array.isArray(data.continents)) return data;

        data.continents.forEach(function (continent) {
            (continent.regions || []).forEach(function (region) {
                (region.items || []).forEach(function (item) {
                    var entry;
                    var file;

                    if (!item || collectNationItemFlags(item).length) return;
                    entry = findNationLinkEntryForItem(item, linkMapIndex);
                    file = entry ? normalizeNationFile(entry.flag_file || entry.flag || entry.flag_title || '') : '';
                    if (!file) return;

                    item.flags = [{
                        file: file,
                        width: '16px',
                        border: true,
                        offsetMode: 'auto',
                        offsetY: 0,
                        source: 'Nation_Link_Map'
                    }];
                });
            });
        });

        return data;
    }

    function getNationListFallback() {
        return {
            version: 1,
            year: 1950,
            continents: NATION_CONTINENT_FALLBACK.map(function (continent) {
                return {
                    key: continent.key,
                    label: continent.label,
                    regions: continent.regions.map(function (region) {
                        return { key: region, label: region, items: [] };
                    })
                };
            })
        };
    }

    function normalizeNationList(payload, era) {
        var fallback = getNationListFallback();
        var sourceContinents = payload && Array.isArray(payload.continents) ? payload.continents : [];
        var sourceByKey = {};

        sourceContinents.forEach(function (continent) {
            if (continent && continent.key) sourceByKey[continent.key] = continent;
        });

        return {
            version: payload && payload.version ? payload.version : 1,
            year: payload && payload.year ? payload.year : parseInt(era || '1950', 10),
            description: payload && payload.description ? payload.description : '',
            continents: fallback.continents.map(function (base) {
                var source = sourceByKey[base.key] || {};
                var regionsByLabel = {};

                if (Array.isArray(source.regions)) {
                    source.regions.forEach(function (region) {
                        if (!region) return;
                        if (region.key) regionsByLabel[region.key] = region;
                        if (region.label) regionsByLabel[region.label] = region;
                    });
                }

                return {
                    key: base.key,
                    label: source.label || base.label,
                    regions: base.regions.map(function (baseRegion) {
                        var region = regionsByLabel[baseRegion.key] || regionsByLabel[baseRegion.label] || baseRegion;
                        return {
                            key: region.key || baseRegion.key,
                            label: region.label || baseRegion.label,
                            items: Array.isArray(region.items) ? region.items.filter(function (item) {
                                return item && item.enabled !== false;
                            }) : []
                        };
                    })
                };
            })
        };
    }

    function fetchNationList(panel, era) {
        var url = getNationListUrl(panel, era);
        var cacheKey = era + '|' + url;
        var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_Nation_List.json');

        if (nationListCache[cacheKey]) {
            PanelPerf.mark('fetch nation list cache promise hit', { era: era });
            return nationListCache[cacheKey];
        }
        if (cached) {
            PanelPerf.mark('fetch nation list EntryStore hit', { era: era });
            nationListCache[cacheKey] = Promise.resolve(PanelPerf.measure('normalize nation list', { era: era, source: 'EntryStore' }, function () {
                return normalizeNationList(cached, era);
            }));
            return nationListCache[cacheKey];
        }

        nationListCache[cacheKey] = PanelPerf.measure('fetch nation list remote', { era: era }, function () {
            return fetchNationJson(url, 'MediaWiki:' + era + '_Nation_List.json')
                .then(function (payload) {
                    return PanelPerf.measure('normalize nation list', { era: era, source: 'remote' }, function () {
                        return normalizeNationList(payload, era);
                    });
                });
        });

        return nationListCache[cacheKey];
    }

    function normalizeNationFlagMaxWidth(value) {
        var raw = String(value || '').trim();
        var match = raw.match(/^(\d+(?:\.\d+)?)\s*(px)?$/i);
        var n = match ? parseFloat(match[1]) : 16;

        if (!isFinite(n)) n = 16;
        if (n < 8) n = 8;
        if (n > 32) n = 32;

        return Math.round(n * 100) / 100 + 'px';
    }

    function isNationSybFile(file) {
        return /^Syb-/i.test(normalizeNationFile(file).replace(/_/g, ' ').trim());
    }

    function getNationSymbolBorder(symbol, file) {
        if (!symbol || typeof symbol !== 'object') return !isNationSybFile(file);
        if (symbol.border != null) return symbol.border !== false;
        if (symbol.symbol_border != null) return symbol.symbol_border !== false;
        if (symbol.syb_border != null) return symbol.syb_border === true;
        return !isNationSybFile(file);
    }

    function normalizeNationSymbolFile(symbol) {
        if (typeof symbol === 'string') return normalizeNationFile(symbol);
        if (!symbol || typeof symbol !== 'object') return '';
        return normalizeNationFile(symbol.file || symbol.flag_file || symbol.flag || symbol.flag_title || symbol.syb_file || symbol.syb || symbol.symbol_file || symbol.symbol || '');
    }

    function renderNationFlag(flag) {
        var file;
        var url;
        var width;
        var classes;
        var offsetY;

        if (!flag) return '';
        file = normalizeNationFile(flag.file || flag.flag_file || flag.flag || '');
        if (!file) return '';

        url = buildNationFileUrl(file);
        width = normalizeNationFlagMaxWidth(flag.width || flag.flag_width || '24px');
        offsetY = getNationFlagOffsetY(flag);
        classes = 'clbi-nations-list-flag' + (flag.border !== false ? ' has-border' : '');

        if (!url) {
            return '<span class="clbi-nations-list-flag-slot is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '" title="국기 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
        }

        return '<span class="clbi-nations-list-flag-slot" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '">' +
            '<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
        '</span>';
    }

    function renderNationSyb(syb) {
        var file;
        var url;
        var width;
        var classes;

        if (!syb) return '';
        file = normalizeNationFile(syb.file || syb.syb_file || syb.syb || syb.symbol_file || syb.symbol || '');
        if (!file) return '';

        url = buildNationFileUrl(file);
        width = normalizeNationFlagMaxWidth(syb.width || syb.syb_width || syb.symbol_width || '16px');
        classes = 'clbi-nations-list-flag clbi-nations-list-syb' + (syb.border === true ? ' has-border' : '');

        if (!url) {
            return '<span class="clbi-nations-list-flag-slot clbi-nations-list-syb-slot is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:0px;" data-flag-file="' + escapeHtml(file) + '" title="SYB 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
        }

        return '<span class="clbi-nations-list-flag-slot clbi-nations-list-syb-slot" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:0px;" data-flag-file="' + escapeHtml(file) + '">' +
            '<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
        '</span>';
    }

    function renderNationSymbol(symbol) {
        var file;
        var url;
        var width;
        var classes;
        var offsetY;
        var isSyb;
        var hasBorder;

        if (!symbol) return '';
        file = normalizeNationSymbolFile(symbol);
        if (!file) return '';

        isSyb = isNationSybFile(file);
        url = buildNationFileUrl(file);
        width = normalizeNationFlagMaxWidth(symbol.width || symbol.flag_width || symbol.syb_width || symbol.symbol_width || (isSyb ? '16px' : '24px'));
        offsetY = getNationFlagOffsetY(symbol);
        hasBorder = getNationSymbolBorder(symbol, file);
        classes = 'clbi-nations-list-flag clbi-nations-list-symbol' + (isSyb ? ' clbi-nations-list-syb' : '') + (hasBorder ? ' has-border' : '');

        if (!url) {
            return '<span class="clbi-nations-list-flag-slot clbi-nations-list-symbol-slot' + (isSyb ? ' clbi-nations-list-syb-slot' : '') + ' is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '" title="상징물 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
        }

        return '<span class="clbi-nations-list-flag-slot clbi-nations-list-symbol-slot' + (isSyb ? ' clbi-nations-list-syb-slot' : '') + '" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '">' +
            '<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
        '</span>';
    }

    function bindNationFlagFallbacks(scope) {
        var root = scope || document;

        toArray(root.querySelectorAll('.clbi-nations-list-flag-slot .clbi-nations-list-flag')).forEach(function (img) {
            if (img.getAttribute('data-flag-missing-handler') === '1') return;
            img.setAttribute('data-flag-missing-handler', '1');
            img.addEventListener('error', function () {
                var slot = img.closest ? img.closest('.clbi-nations-list-flag-slot') : null;
                if (slot) {
                    slot.classList.add('is-missing');
                    slot.setAttribute('title', '국기 파일 로드 실패: ' + (slot.getAttribute('data-flag-file') || ''));
                }
                img.removeAttribute('src');
            });
        });
    }

    function getResolvedFlagUrl(file) {
        var key = normalizeNationFileKey(file);
        return key ? (nationResolvedFileUrlCache[key] || '') : '';
    }

    function collectResolvedNationFlagUrls(data) {
        var files = collectNationFlagFileNames(data);
        var seen = {};
        var urls = [];

        files.forEach(function (file) {
            var url = getResolvedFlagUrl(file);
            if (!url || seen[url]) return;
            seen[url] = true;
            urls.push(url);
        });

        return urls;
    }

    function collectResolvedNationFlagUrlsForFiles(files) {
        var seen = {};
        var urls = [];

        (files || []).forEach(function (file) {
            var url = getResolvedFlagUrl(file);
            if (!url || seen[url]) return;
            seen[url] = true;
            urls.push(url);
        });

        return urls;
    }

    function warmNationFlagUrlsToCompletion(urls, owner, options) {
        var queue = unique((urls || []).filter(Boolean));
        var index = 0;
        var active = 0;
        var done = 0;
        var ok = 0;
        var fail = 0;
        var concurrency = Math.max(1, Math.min(32, Number(options && options.concurrency) || 18));
        var storeKey = options && options.storeKey ? String(options.storeKey) : 'CLBI_NationsFlagWarmImages';
        var meta = options && options.meta ? options.meta : {};
        var startId;

        if (!queue.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });

        if (owner) owner[storeKey] = [];
        startId = PanelPerf.start('background nation flag image load', Object.assign({}, meta, { count: queue.length, concurrency: concurrency }));

        return new Promise(function (resolve) {
            function settle(success, img) {
                active -= 1;
                done += 1;
                if (success) ok += 1;
                else fail += 1;

                if (owner && owner[storeKey]) {
                    var idx = owner[storeKey].indexOf(img);
                    if (idx >= 0) owner[storeKey].splice(idx, 1);
                }

                pump();
            }

            function startOne(url) {
                var img = new Image();
                var finished = false;

                active += 1;
                img.decoding = 'sync';
                img.loading = 'eager';
                try { img.fetchPriority = 'high'; } catch (ignorePriority) {}
                if (owner && owner[storeKey]) owner[storeKey].push(img);

                function finish(success) {
                    if (finished) return;
                    finished = true;
                    settle(success, img);
                }

                img.onload = function () {
                    if (img.decode) {
                        img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                    } else {
                        finish(true);
                    }
                };
                img.onerror = function () { finish(false); };
                img.src = url;

                if (img.complete) {
                    scheduleIdle(function () { finish(img.naturalWidth > 0); }, { timeout: 250 });
                }
            }

            function pump() {
                while (active < concurrency && index < queue.length) {
                    startOne(queue[index++]);
                }

                if (done >= queue.length) {
                    PanelPerf.end(startId, Object.assign({}, meta, { count: queue.length, ok: ok, fail: fail }));
                    resolve({ total: queue.length, ok: ok, fail: fail });
                }
            }

            scheduleIdle(pump, { timeout: 300 });
        });
    }

    function prewarmImageUrls(urls, limit) {
        var index = 0;
        var max = Math.max(0, limit || 64);

        urls = (urls || []).filter(Boolean).slice(0, max);
        if (!urls.length) return;

        function step(deadline) {
            var started = 0;

            while (index < urls.length && started < 6 && (!deadline || deadline.timeRemaining() > 4 || deadline.didTimeout)) {
                var img = new Image();
                var url = urls[index++];

                started += 1;
                img.decoding = 'async';
                img.loading = 'eager';
                img.src = url;

                if (img.decode) {
                    img.decode().catch(function () {});
                }
            }

            if (index < urls.length) scheduleIdle(step, { timeout: 850 });
        }

        scheduleIdle(step, { timeout: 850 });
    }

    function prewarmRenderedNationFlags(scope) {
        var urls = [];
        var seen = {};
        var perf = window.InteractionPerf;
        var perfId = perf && typeof perf.start === 'function' ? perf.start('panel collect rendered flag prewarm urls', {
            scope: scope === document ? 'document' : 'panel'
        }) : null;

        toArray((scope || document).querySelectorAll ? (scope || document).querySelectorAll('.clbi-nations-list-flag[src]') : []).forEach(function (img) {
            var url = img.currentSrc || img.getAttribute('src') || '';
            if (!url || seen[url]) return;
            seen[url] = true;
            urls.push(url);
        });

        if (perf && typeof perf.end === 'function' && perfId) {
            perf.end(perfId, { urls: urls.length });
        }
        prewarmImageUrls(urls, 80);
    }

    function hydrateResolvedFlagSlots(scope) {
        var root = scope || document;
        var hydrated = 0;
        var scanned = 0;
        var perf = window.InteractionPerf;
        var perfId = perf && typeof perf.start === 'function' ? perf.start('panel hydrate resolved flag slots', {
            scope: root === document ? 'document' : 'panel'
        }) : null;

        try {
        toArray(root.querySelectorAll ? root.querySelectorAll('.clbi-nations-list-flag-slot[data-flag-file]') : []).forEach(function (slot) {
            scanned += 1;
            var file = normalizeNationFile(slot.getAttribute('data-flag-file') || '');
            var url = buildNationFileUrl(file);
            var img;
            var isSyb;
            var isSymbol;
            var classes;

            if (!file || !url) return;
            if (slot.querySelector && slot.querySelector('.clbi-nations-list-flag[src]')) return;

            isSyb = slot.classList.contains('clbi-nations-list-syb-slot') || isNationSybFile(file);
            isSymbol = slot.classList.contains('clbi-nations-list-symbol-slot');
            classes = 'clbi-nations-list-flag' +
                (isSymbol ? ' clbi-nations-list-symbol' : '') +
                (isSyb ? ' clbi-nations-list-syb' : '') +
                (!isSyb ? ' has-border' : '');

            slot.classList.remove('is-missing');
            slot.removeAttribute('title');
            slot.innerHTML = '';
            img = document.createElement('img');
            img.className = classes;
            img.alt = '';
            img.loading = 'eager';
            img.decoding = 'sync';
            try { img.fetchPriority = 'high'; } catch (ignorePriority) {}
            img.src = url;
            slot.appendChild(img);
            hydrated += 1;
        });

        if (hydrated) bindNationFlagFallbacks(root);
        return hydrated;
        } finally {
            if (perf && typeof perf.end === 'function' && perfId) {
                perf.end(perfId, { scanned: scanned, hydrated: hydrated });
            }
        }
    }


    function scheduleResolvedFlagHydrationSweeps(panel, options) {
        var delays = options && options.delays ? options.delays : [0, 16, 80, 180, 360, 720, 1200, 2000];
        var era = options && options.era ? String(options.era) : getPanelEra(panel);

        if (!panel || !panel.querySelector) return;
        if (!panel.CLBI_NationsFlagHydrationSweepTimers) panel.CLBI_NationsFlagHydrationSweepTimers = [];

        panel.CLBI_NationsFlagHydrationSweepTimers.forEach(function (timer) {
            try { window.clearTimeout(timer); } catch (ignoreClear) {}
        });
        panel.CLBI_NationsFlagHydrationSweepTimers = [];

        delays.forEach(function (delay, index) {
            var timer = window.setTimeout(function () {
                var hydrated;
                if (!document.body.contains(panel)) return;
                hydrated = hydrateResolvedFlagSlots(panel);
                if (hydrated) {
                    prewarmRenderedNationFlags(panel);
                    PanelPerf.mark('remaining nation flags hydration sweep', {
                        era: era,
                        pass: index,
                        delay: delay,
                        hydrated: hydrated
                    });
                }
            }, Math.max(0, delay));
            panel.CLBI_NationsFlagHydrationSweepTimers.push(timer);
        });
    }

    function bindRemainingFlagHydrationWatcher(panel) {
        if (!panel || panel.CLBI_NationsRemainingFlagHydrationWatcherBound) return;
        panel.CLBI_NationsRemainingFlagHydrationWatcherBound = true;

        if (typeof MutationObserver === 'function') {
            panel.CLBI_NationsRemainingFlagObserver = new MutationObserver(function (mutations) {
                var shouldHydrate = false;
                mutations.forEach(function (mutation) {
                    if (shouldHydrate) return;
                    toArray(mutation.addedNodes || []).forEach(function (node) {
                        if (shouldHydrate || !node || node.nodeType !== 1) return;
                        if ((node.matches && node.matches('.clbi-nations-list-flag-slot[data-flag-file]')) ||
                            (node.querySelector && node.querySelector('.clbi-nations-list-flag-slot[data-flag-file]'))) {
                            shouldHydrate = true;
                        }
                    });
                });
                if (shouldHydrate && panel.CLBI_NationsRemainingFlagsResolved) {
                    scheduleResolvedFlagHydrationSweeps(panel, {
                        era: getPanelEra(panel),
                        delays: [0, 60, 180, 420]
                    });
                }
            });
            try {
                panel.CLBI_NationsRemainingFlagObserver.observe(panel, { childList: true, subtree: true });
            } catch (ignoreObserve) {}
        }

        panel.addEventListener('click', function (event) {
            var target = event && event.target;
            if (!panel.CLBI_NationsRemainingFlagsResolved) return;
            if (target && target.closest && target.closest('.clbi-nations-tabpanel-tab, [data-continent], .clbi-nations-era-button')) {
                scheduleResolvedFlagHydrationSweeps(panel, {
                    era: getPanelEra(panel),
                    delays: [0, 40, 120, 260]
                });
            }
        }, true);
    }

    function scheduleAllNationFlagBackgroundWarm(panel, data) {
        var urls;
        var era;

        if (!panel || !data) return;
        if (panel.CLBI_NationsAllFlagBackgroundWarmStarted) return;

        urls = collectResolvedNationFlagUrls(data);
        if (!urls.length) return;

        panel.CLBI_NationsAllFlagBackgroundWarmStarted = true;
        era = String(data && (data.year || getPanelEra(panel)) || '');

        window.setTimeout(function () {
            if (!document.body.contains(panel)) return;
            warmNationFlagUrlsToCompletion(urls, panel, {
                storeKey: 'CLBI_NationsAllContinentFlagWarmImages',
                concurrency: 24,
                meta: { era: era, phase: 'all-continent-prebuilt-flags' }
            }).then(function (result) {
                PanelPerf.mark('all continent flag background load complete', {
                    era: era,
                    total: result.total,
                    ok: result.ok,
                    fail: result.fail
                });
            });
        }, 32);
    }

    function scheduleRemainingNationFlagResolution(panel, data, initialFiles) {
        var allFiles = collectNationFlagFileNames(data);
        var remainingFiles = subtractNationFlagFiles(allFiles, initialFiles);
        var handle;

        if (!panel || !remainingFiles.length) return;
        panel.CLBI_NationsRemainingFlagFiles = remainingFiles.slice();
        panel.CLBI_NationsRemainingFlagsResolved = false;
        bindRemainingFlagHydrationWatcher(panel);

        handle = panel.CLBI_NationsRemainingFlagResolveHandle;
        if (handle != null) {
            try { window.clearTimeout(handle); } catch (ignoreClear) {}
            try { cancelIdle(handle); } catch (ignoreIdleClear) {}
        }

        /*
           The first visible continent is part of the entry-ready contract.  The
           rest of the flags must not wait until the user opens another tab:
           start resolving them right after the first paint, inject eager images
           into every already-rendered slot, and keep explicit Image() objects
           alive until load/decode completes so a fast tab switch still sees
           warm flags instead of blank placeholders.
        */
        panel.CLBI_NationsRemainingFlagResolveHandle = window.setTimeout(function () {
            panel.CLBI_NationsRemainingFlagResolveHandle = null;
            PanelPerf.measure('resolve remaining nation flag URLs', {
                era: String(data && (data.year || getPanelEra(panel)) || ''),
                count: remainingFiles.length,
                mode: 'post-visible-aggressive'
            }, function () {
                return resolveNationFlagFileUrls(remainingFiles).then(function () {
                    var hydrated;
                    var urls = collectResolvedNationFlagUrlsForFiles(remainingFiles);
                    var era = String(data && (data.year || getPanelEra(panel)) || '');
                    panel.CLBI_NationsRemainingFlagsResolved = true;
                    hydrated = hydrateResolvedFlagSlots(panel);
                    scheduleResolvedFlagHydrationSweeps(panel, {
                        era: era,
                        delays: [0, 16, 80, 180, 360, 720, 1200, 2000, 3200]
                    });
                    PanelPerf.mark('hydrate remaining nation flags', {
                        era: era,
                        files: remainingFiles.length,
                        hydrated: hydrated,
                        urls: urls.length,
                        mode: 'post-visible-aggressive'
                    });
                    prewarmRenderedNationFlags(panel);
                    return warmNationFlagUrlsToCompletion(urls, panel, {
                        storeKey: 'CLBI_NationsRemainingFlagWarmImages',
                        concurrency: 24,
                        meta: { era: era, phase: 'remaining-flags-aggressive' }
                    }).then(function (result) {
                        var hydratedAgain = hydrateResolvedFlagSlots(panel);
                        scheduleResolvedFlagHydrationSweeps(panel, {
                            era: era,
                            delays: [0, 100, 300, 700]
                        });
                        PanelPerf.mark('remaining nation flag background load complete', {
                            era: era,
                            total: result.total,
                            ok: result.ok,
                            fail: result.fail,
                            hydratedAgain: hydratedAgain
                        });
                    });
                });
            });
        }, 32);
    }

    function waitForRenderedFlagImages(scope, options) {
        var root = scope || document;
        var imageRoot = root;
        var images;
        var timeoutMs = options && options.timeoutMs ? Number(options && options.timeoutMs) : 15000;
        var started = Date.now();

        if (options && options.activeOnly && root && root.querySelector) {
            imageRoot = root.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]') || root;
        }
        images = toArray(imageRoot.querySelectorAll ? imageRoot.querySelectorAll('.clbi-nations-list-flag[src]') : []);

        function waitOne(img) {
            var src = img.currentSrc || img.getAttribute('src') || '';
            if (!src) return Promise.resolve(false);
            if (img.complete && img.naturalWidth > 0) {
                if (img.decode) return img.decode().then(function () { return true; }).catch(function () { return true; });
                return Promise.resolve(true);
            }
            return new Promise(function (resolve) {
                var done = false;
                var timer;
                function finish(ok) {
                    if (done) return;
                    done = true;
                    if (timer) window.clearTimeout(timer);
                    resolve(!!ok);
                }
                timer = window.setTimeout(function () { finish(false); }, Math.max(500, timeoutMs - (Date.now() - started)));
                img.addEventListener('load', function () {
                    if (img.decode) img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                    else finish(true);
                }, { once: true });
                img.addEventListener('error', function () { finish(false); }, { once: true });
            });
        }

        if (!images.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
        return Promise.all(images.map(waitOne)).then(function (results) {
            var ok = results.filter(Boolean).length;
            return { total: images.length, ok: ok, fail: images.length - ok };
        });
    }

    function renderNationItem(item, index, items) {
        var symbols = collectNationItemSymbols(item);
        var symbolHtml = symbols.map(renderNationSymbol).join('');
        var label = item.label || item.name || item.name_ko || item.page || item.external_url || '미지정';
        var href = item.external_url || buildNationPageUrl(item.page || item.wiki_title || '');
        var tag = String(item.tag || item.code || item.page_tag || '').trim();
        var page = String(item.page || item.wiki_title || '').trim();
        var linkHtml = href ? '<a class="clbi-nations-list-link" href="' + escapeHtml(href) + '">' + escapeHtml(label) + '</a>' : '<span class="clbi-nations-list-label">' + escapeHtml(label) + '</span>';
        var divider = index < items.length - 1 ? '<span class="clbi-nations-list-divider" aria-hidden="true" draggable="false">·</span>' : '';

        return '<span class="clbi-nations-list-item" data-nation-tag="' + escapeHtml(tag) + '" data-nation-page="' + escapeHtml(page) + '" data-nation-label="' + escapeHtml(label) + '">' + symbolHtml + linkHtml + '</span>' + divider;
    }

    function renderNationEmpty() {
        return '<span class="clbi-nations-era-wip-message"><span class="clbi-nations-era-wip-message-ko">작업 중...</span><span class="clbi-nations-era-wip-message-en">Working in Progress...</span></span>';
    }

    function getElementOuterWidth(element) {
        var rect;
        var style;
        var width;

        if (!element || !element.getBoundingClientRect) return 0;
        rect = element.getBoundingClientRect();
        style = window.getComputedStyle ? window.getComputedStyle(element) : null;
        width = rect && isFinite(rect.width) ? rect.width : (element.offsetWidth || 0);

        if (style) {
            width += parseFloat(style.marginLeft) || 0;
            width += parseFloat(style.marginRight) || 0;
        }

        return width;
    }

    function getElementContentWidth(element) {
        var style;
        var width;

        if (!element) return 0;
        style = window.getComputedStyle ? window.getComputedStyle(element) : null;
        width = element.clientWidth || 0;

        if (style) {
            width -= parseFloat(style.paddingLeft) || 0;
            width -= parseFloat(style.paddingRight) || 0;
        }

        return Math.max(0, Math.floor(width));
    }

    function isNationListElement(node, className) {
        return !!(node && node.nodeType === 1 && node.classList && node.classList.contains(className));
    }

    function collectNationListSnapGroups(well) {
        var nodes = toArray(well ? well.childNodes : []);
        var groups = [];
        var index = 0;

        while (index < nodes.length) {
            var node = nodes[index];
            var group;
            var nextIndex;
            var nextNode;

            if (!isNationListElement(node, 'clbi-nations-list-item')) {
                index += 1;
                continue;
            }

            group = [node];
            nextIndex = index + 1;
            while (nextIndex < nodes.length && nodes[nextIndex].nodeType === 3 && !String(nodes[nextIndex].nodeValue || '').trim()) {
                nextIndex += 1;
            }

            nextNode = nodes[nextIndex];
            if (isNationListElement(nextNode, 'clbi-nations-list-divider')) {
                group.push(nextNode);
                index = nextIndex + 1;
            } else {
                index += 1;
            }

            groups.push(group);
        }

        return groups;
    }

    function measureNationListGroup(group) {
        return (group || []).reduce(function (sum, node) {
            return sum + getElementOuterWidth(node);
        }, 0);
    }

    function positionNationListSnapLines(well) {
        var contentWidth = getElementContentWidth(well);

        if (!well || !contentWidth) return;

        toArray(well.querySelectorAll('.nations-list-line')).forEach(function (line) {
            var inner = line.querySelector('.nations-list-line-inner');
            var rect = inner && inner.getBoundingClientRect ? inner.getBoundingClientRect() : null;
            var width = rect && isFinite(rect.width) ? rect.width : (inner ? inner.offsetWidth : 0);
            var left = Math.max(0, Math.round((contentWidth - width) / 2));

            /*
             * Keep visual centering, but snap the row start to the integer
             * pixel grid.  The stylesheet intentionally zeroes row padding
             * with !important, so a plain inline paddingLeft is ignored.
             * Use setProperty(..., 'important') or the row falls back to
             * left alignment even though the centering math is correct.
             */
            line.style.setProperty('padding-left', left + 'px', 'important');
        });
    }

    function snapNationListWellRows(well) {
        var contentWidth;
        var groups;
        var lines = [];
        var current = [];
        var currentWidth = 0;
        var container;

        if (!well || !well.querySelector || !well.querySelector('.clbi-nations-list-item')) return;

        contentWidth = getElementContentWidth(well);
        if (!contentWidth) return;

        if (!well.NationListRowSnapSourceHtml || !well.classList.contains('is-row-snapped')) {
            well.NationListRowSnapSourceHtml = well.innerHTML;
        } else {
            well.innerHTML = well.NationListRowSnapSourceHtml;
            bindNationFlagFallbacks(well);
        }

        well.classList.remove('is-row-snapped');
        groups = collectNationListSnapGroups(well);
        if (!groups.length) return;

        groups.forEach(function (group) {
            var width = Math.ceil(measureNationListGroup(group));

            if (current.length && currentWidth + width > contentWidth) {
                lines.push(current);
                current = [];
                currentWidth = 0;
            }

            current.push(group);
            currentWidth += width;
        });

        if (current.length) lines.push(current);

        container = document.createElement('div');
        container.className = 'nations-list-lines';

        lines.forEach(function (lineGroups) {
            var line = document.createElement('span');
            var inner = document.createElement('span');

            line.className = 'nations-list-line';
            inner.className = 'nations-list-line-inner';

            lineGroups.forEach(function (group, groupIndex) {
                var isLastGroupInLine = groupIndex === lineGroups.length - 1;

                group.forEach(function (node) {
                    if (isLastGroupInLine && isNationListElement(node, 'clbi-nations-list-divider')) {
                        return;
                    }
                    inner.appendChild(node);
                });
            });

            line.appendChild(inner);
            container.appendChild(line);
        });

        well.innerHTML = '';
        well.appendChild(container);
        well.classList.add('is-row-snapped');
        positionNationListSnapLines(well);
    }

    function snapNationListRows(scope) {
        var root = scope || document;

        if (!root || !root.querySelectorAll) return;
        if (root !== document && isNationListPanelHidden(root)) return;
        toArray(root.querySelectorAll('.clbi-nations-tabpanel-well')).forEach(snapNationListWellRows);
    }

    function shouldDelayNationListRowSnap(root, reason) {
        if (reason === 'render') return false;
        if (hasRecentNationPanelDirectInteraction(900)) return true;
        if (isNationsGlobeBusyForPanelWork()) return true;
        return false;
    }

    function scheduleNationListRowSnap(scope, options) {
        var root = scope || document;
        var force = !!(options && options.force);
        var reason = options && options.reason ? String(options.reason) : '';
        var delay;

        if (!root) return;

        /*
         * Row snap scheduler — 20260710.
         *
         * The row snap pass rewrites well contents and forces layout.  It must
         * run for the initial rendered panel and real viewport/font changes, but
         * it must not compete with continent clicks or globe manipulation.  Keep
         * the same generated DOM/classes, collapse duplicates, and postpone late
         * maintenance until the user has stopped interacting for a short window.
         */
        if (root !== document && !document.body.contains(root)) return;
        if (root !== document && isNationListPanelHidden(root)) return;
        if (!force && root.NationListRowSnapSettled) return;
        if (root.NationListRowSnapRaf || root.NationListRowSnapTimer) return;

        if (force && root.NationListRowSnapSettled && reason !== 'resize' && reason !== 'font') return;

        if (shouldDelayNationListRowSnap(root, reason)) {
            delay = reason === 'font' ? 720 : 360;
            root.NationListRowSnapTimer = window.setTimeout(function () {
                root.NationListRowSnapTimer = null;
                scheduleNationListRowSnap(root, { force: force, reason: reason || 'deferred' });
            }, delay);
            return;
        }

        root.NationListRowSnapRaf = window.requestAnimationFrame(function () {
            root.NationListRowSnapRaf = null;
            if (root !== document && (!document.body.contains(root) || isNationListPanelHidden(root))) return;
            interactionPerfMeasure('panel row snap frame', { pass: 1, scope: root === document ? 'document' : 'panel', reason: reason || 'auto' }, function () {
                snapNationListRows(root);
            });

            window.requestAnimationFrame(function () {
                if (root !== document && (!document.body.contains(root) || isNationListPanelHidden(root))) return;
                interactionPerfMeasure('panel row snap frame', { pass: 2, scope: root === document ? 'document' : 'panel', reason: reason || 'auto' }, function () {
                    snapNationListRows(root);
                });
                root.NationListRowSnapSettled = true;
            });
        });

        if (document.fonts && document.fonts.ready && !root.NationListFontSnapBound) {
            root.NationListFontSnapBound = true;
            document.fonts.ready.then(function () {
                if (!root || (root !== document && !document.body.contains(root))) return;
                if (root.NationListFontSnapSettled) return;
                root.NationListFontSnapSettled = true;
                root.NationListRowSnapSettled = false;
                scheduleNationListRowSnap(root, { force: true, reason: 'font' });
            }).catch(function () {});
        }
    }

    function bindNationListRowSnapResize() {
        if (window.NationListRowSnapResizeBound) return;
        window.NationListRowSnapResizeBound = true;

        window.addEventListener('resize', function () {
            if (window.NationListRowSnapResizeRaf) {
                window.cancelAnimationFrame(window.NationListRowSnapResizeRaf);
            }

            window.NationListRowSnapResizeRaf = window.requestAnimationFrame(function () {
                var panel;

                window.NationListRowSnapResizeRaf = null;
                panel = findActiveContinentPanel();
                if (!panel) return;
                panel.NationListRowSnapSettled = false;
                scheduleNationListRowSnap(panel, { force: true, reason: 'resize' });
            });
        });
    }


    function ensureHistoryScrollText(target) {
        var direct;
        var wrap;

        if (!target || !target.querySelector) return null;

        direct = target.querySelector(':scope > .clbi-nations-history-scroll-text');
        if (direct) return direct;

        if (target.children.length === 1 && target.firstElementChild && target.firstElementChild.tagName && target.firstElementChild.tagName.toLowerCase() === 'a') {
            target.firstElementChild.classList.add('clbi-nations-history-scroll-text');
            return target.firstElementChild;
        }

        wrap = document.createElement('span');
        wrap.className = 'clbi-nations-history-scroll-text';
        while (target.firstChild) wrap.appendChild(target.firstChild);
        target.appendChild(wrap);
        return wrap;
    }

    function updateHistorySlideTarget(target) {
        var text;
        var wrapW;
        var textW;
        var scrollDist;
        var duration;
        var scrollKey;

        if (!target || !target.getBoundingClientRect) return;
        if (target.offsetParent === null && !target.getClientRects().length) return;

        text = ensureHistoryScrollText(target);
        if (!text) return;

        target.classList.add('is-history-scroll-viewport');
        wrapW = Math.floor(target.clientWidth || 0);
        textW = Math.ceil(text.scrollWidth || 0);

        if (!wrapW || !textW || textW <= wrapW + 1) {
            target.classList.remove('is-history-scrolling');
            text.style.animation = '';
            text.style.animationDelay = '';
            text.style.removeProperty('--scroll-dist');
            text.removeAttribute('data-history-scroll-key');
            return;
        }

        scrollDist = '-' + Math.max(0, textW - wrapW + 8) + 'px';
        duration = Math.max(4.5, textW / 40);
        scrollKey = scrollDist + '|' + duration;

        target.classList.add('is-history-scrolling');

        if (text.getAttribute('data-history-scroll-key') === scrollKey) return;
        text.setAttribute('data-history-scroll-key', scrollKey);
        text.style.setProperty('--scroll-dist', scrollDist);
        text.style.animation = 'clbi-scroll ' + duration + 's linear infinite';
        text.style.animationDelay = '0.4s';
    }

    function updateHistoryItemSlides(scope) {
        var root = scope || document;

        if (!root || !root.querySelectorAll) return;

        toArray(root.querySelectorAll('.clbi-nations-history-period, .clbi-nations-history-event-name')).forEach(updateHistorySlideTarget);
    }

    function scheduleHistoryItemSlides(scope) {
        var root = scope || document;

        if (!root) return;
        if (root.HistoryItemSlideRaf) {
            window.cancelAnimationFrame(root.HistoryItemSlideRaf);
            root.HistoryItemSlideRaf = null;
        }

        root.HistoryItemSlideRaf = window.requestAnimationFrame(function () {
            root.HistoryItemSlideRaf = null;
            if (root !== document && !document.body.contains(root)) return;
            updateHistoryItemSlides(root);

            window.requestAnimationFrame(function () {
                if (root !== document && !document.body.contains(root)) return;
                updateHistoryItemSlides(root);
            });
        });

        if (document.fonts && document.fonts.ready && !root.HistoryItemSlideFontBound) {
            root.HistoryItemSlideFontBound = true;
            document.fonts.ready.then(function () {
                scheduleHistoryItemSlides(root);
            }).catch(function () {});
        }
    }

    function bindHistoryItemSlideResize() {
        if (window.HistoryItemSlideResizeBound) return;
        window.HistoryItemSlideResizeBound = true;

        window.addEventListener('resize', function () {
            if (window.HistoryItemSlideResizeRaf) {
                window.cancelAnimationFrame(window.HistoryItemSlideResizeRaf);
            }

            window.HistoryItemSlideResizeRaf = window.requestAnimationFrame(function () {
                window.HistoryItemSlideResizeRaf = null;
                scheduleHistoryItemSlides(document);
            });
        });
    }

    function playNationsPanelClickSound() {
        try {
            if (window && typeof window.playStaticSound === 'function') {
                window.playStaticSound();
            } else if (typeof playStaticSound === 'function') {
                playStaticSound();
            }
        } catch (err) {}
    }

    function getNationItemFromEventTarget(target, panel) {
        var item = target && target.closest ? target.closest('.clbi-nations-list-item[data-nation-tag]') : null;
        if (!item || !panel || !panel.contains(item)) return null;
        return item;
    }

    function getGlobeInstanceForNationItem(item) {
        var stack = item && item.closest ? item.closest('.clbi-nations-panel-stack') : null;
        var globe = stack && stack.querySelector ? stack.querySelector('.clbi-nations-globe-window[data-nations-globe], .clbi-nations-globe-window') : null;
        var instance = globe && globe.CLBI_NationsGlobeInstance ? globe.CLBI_NationsGlobeInstance : null;

        if (instance && typeof instance.focusNationByPage === 'function') return instance;
        if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function' && stack) {
            window.CLBI_NationsGlobe.init(stack);
            instance = globe && globe.CLBI_NationsGlobeInstance ? globe.CLBI_NationsGlobeInstance : null;
        }

        return instance && typeof instance.focusNationByPage === 'function' ? instance : null;
    }

    function clearNationPanelHoverState(panel, except) {
        if (!panel || !panel.querySelectorAll) return;
        toArray(panel.querySelectorAll('.clbi-nations-list-item.is-panel-hovered')).forEach(function (item) {
            if (item !== except) item.classList.remove('is-panel-hovered');
        });
    }

    function cancelNationPanelPendingHoverFocus(panel) {
        if (!panel) return;

        panel.CLBI_NationHoverFocusToken = (panel.CLBI_NationHoverFocusToken || 0) + 1;

        if (panel.CLBI_NationHoverFocusTimer) {
            window.clearTimeout(panel.CLBI_NationHoverFocusTimer);
            panel.CLBI_NationHoverFocusTimer = null;
        }

        if (panel.CLBI_NationHoverFocusRaf) {
            window.cancelAnimationFrame(panel.CLBI_NationHoverFocusRaf);
            panel.CLBI_NationHoverFocusRaf = null;
        }

        panel.CLBI_NationHoverFocusPendingTag = '';
    }

    function clearNationPanelHoverFocus(panel) {
        cancelNationPanelPendingHoverFocus(panel);
        clearNationPanelHoverState(panel, null);
    }

    function requestNationItemGlobeFocus(panel, item, immediate) {
        var tag;
        var delay;
        var token;

        if (!panel || !item) return;
        tag = String(item.getAttribute('data-nation-tag') || '').trim();
        if (!tag) return;

        cancelNationPanelPendingHoverFocus(panel);
        clearNationPanelHoverState(panel, item);
        item.classList.add('is-panel-hovered');

        delay = immediate ? 0 : 75;
        token = panel.CLBI_NationHoverFocusToken || 0;
        panel.CLBI_NationHoverFocusPendingTag = tag;

        panel.CLBI_NationHoverFocusTimer = window.setTimeout(function () {
            panel.CLBI_NationHoverFocusTimer = null;
            if (panel.CLBI_NationHoverFocusToken !== token) return;

            panel.CLBI_NationHoverFocusRaf = window.requestAnimationFrame(function () {
                var instance;

                panel.CLBI_NationHoverFocusRaf = null;
                if (panel.CLBI_NationHoverFocusToken !== token) return;
                if (!document.body.contains(item) || !item.classList.contains('is-panel-hovered')) return;
                if (panel.CLBI_NationHoverFocusPendingTag !== tag) return;

                instance = getGlobeInstanceForNationItem(item);
                if (instance && typeof instance.focusNationByPage === 'function') {
                    instance.focusNationByPage(tag, {
                        source: 'nation-panel',
                        label: item.getAttribute('data-nation-label') || '',
                        duration: immediate ? 360 : null
                    });
                }
            });
        }, delay);
    }

    function markNationContinentSwitching(panel) {
        var detail;

        if (!panel) return;
        panel.CLBI_NationContinentSwitchingUntil = Date.now() + 240;
        panel.classList.add('is-continent-switching');

        /*
         * Continent switch relief signal — 20260708.
         *
         * The visible tab/body swap is tiny, but the globe render loop, pointer
         * hit-test, and decorative maintenance can compete for the same frame.
         * Broadcast a short, explicit relief window so NationsGlobe can skip
         * non-essential resize/raycast work while the panel is repainting.  This
         * does not remove or reload globe assets; it only avoids doing expensive
         * housekeeping in the exact input frame where the user expects the tab to
         * respond instantly.
         */
        detail = { panel: panel, until: panel.CLBI_NationContinentSwitchingUntil, duration: 240 };
        try {
            window.dispatchEvent(new CustomEvent('clbi:nations-continent-switching', { detail: detail }));
        } catch (err) {}

        window.setTimeout(function () {
            if (!panel || Date.now() < (panel.CLBI_NationContinentSwitchingUntil || 0)) return;
            panel.classList.remove('is-continent-switching');
        }, 260);
    }

    function isNationContinentSwitching(panel) {
        return !!(panel && Date.now() < (panel.CLBI_NationContinentSwitchingUntil || 0));
    }

    var lastContinentPointerX = null;
    var lastContinentPointerY = null;
    var continentHoverGuardReady = false;

    function rememberContinentPointer(event) {
        if (!event) return;
        lastContinentPointerX = event.clientX;
        lastContinentPointerY = event.clientY;
    }

    function getContinentTabUnderPointer(panel) {
        var element;
        var tab = null;
        var hovered;

        if (!panel || !panel.querySelectorAll) return null;

        /*
         * Keyboard/shortcut continent switches do not move the physical mouse.
         * Browsers therefore keep the old CSS :hover target alive even after JS
         * changes the active tab.  We treat the tab under the last pointer
         * coordinate as a stale hover candidate and suppress only that visual
         * state until the pointer actually moves away or onto another tab.
         */
        if (lastContinentPointerX !== null && lastContinentPointerY !== null && typeof document.elementFromPoint === 'function') {
            element = document.elementFromPoint(lastContinentPointerX, lastContinentPointerY);
            tab = element && element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
            if (tab && panel.contains(tab)) return tab;
        }

        try {
            hovered = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]:hover'));
            if (hovered.length) return hovered[hovered.length - 1];
        } catch (err) {}

        return null;
    }

    function clearContinentKeyboardStaleHoverMarkers(panel) {
        if (!panel || !panel.querySelectorAll) return;
        toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab.is-keyboard-stale-hover')).forEach(function (tab) {
            tab.classList.remove('is-keyboard-stale-hover');
            tab.removeAttribute('data-clbi-keyboard-stale-hover');
        });
    }

    function clearContinentKeyboardHoverSuppress(panel) {
        if (!panel) return;
        panel.classList.remove('is-keyboard-switching');
        panel.removeAttribute('data-clbi-hover-suppress-continent');
        panel.removeAttribute('data-clbi-hover-suppress-source');
        clearContinentKeyboardStaleHoverMarkers(panel);
    }

    function syncContinentKeyboardHoverSuppress(panel, activeTab, force) {
        var staleTab;
        var focusedTab;
        var staleContinent;
        var active = activeTab;

        if (!panel) return;
        if (!active && panel.querySelector) {
            active = panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
        }

        staleTab = getContinentTabUnderPointer(panel);

        if (staleTab && active && staleTab === active) {
            clearContinentKeyboardHoverSuppress(panel);
            return;
        }

        if (staleTab && (staleTab.classList.contains('is-active') || staleTab.getAttribute('aria-selected') === 'true')) {
            clearContinentKeyboardHoverSuppress(panel);
            return;
        }

        focusedTab = document.activeElement && document.activeElement.closest ?
            document.activeElement.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;

        if (focusedTab && panel.contains(focusedTab) && active && focusedTab !== active) {
            /*
             * A mouse click can leave keyboard focus on the old tab.  If a later
             * Ctrl+Q/Ctrl+E switch moves the active state elsewhere, that stale
             * :focus rule looks exactly like a stuck hover.  Move focus to the
             * newly active tab only when focus is already inside this continent
             * tab strip; normal document focus outside the strip is not touched.
             */
            try { active.focus({ preventScroll: true }); } catch (err) { try { active.focus(); } catch (ignore) {} }
            if (!staleTab) staleTab = focusedTab;
        }

        if (!staleTab) {
            if (!force) clearContinentKeyboardHoverSuppress(panel);
            return;
        }

        staleContinent = staleTab.getAttribute('data-continent') || '';

        /*
         * Stale hover/focus marker — 20260708.
         *
         * The panel-level is-keyboard-switching class is still useful, but it is
         * not specific enough after warm-cache/SAP prepaint fixes because click
         * and keyboard paths can now share the same live delegate.  Mark the
         * exact tab that remained under the physical mouse or retained focus.
         * CSS can then neutralize only that stale visual surface while the real
         * active tab stays highlighted.  Pointer movement, pointerleave, click,
         * and blur all clear this marker through clearContinentKeyboardHoverSuppress().
         */
        clearContinentKeyboardStaleHoverMarkers(panel);
        staleTab.classList.add('is-keyboard-stale-hover');
        staleTab.setAttribute('data-clbi-keyboard-stale-hover', '1');

        panel.classList.add('is-keyboard-switching');
        panel.setAttribute('data-clbi-hover-suppress-source', 'keyboard-continent-switch');
        if (staleContinent) panel.setAttribute('data-clbi-hover-suppress-continent', staleContinent);
        else panel.removeAttribute('data-clbi-hover-suppress-continent');
    }

    function validateContinentKeyboardHoverSuppressions() {
        toArray(document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching')).forEach(function (panel) {
            var suppressed = panel.getAttribute('data-clbi-hover-suppress-continent') || '';
            var hovered = getContinentTabUnderPointer(panel);
            var hoveredContinent = hovered ? (hovered.getAttribute('data-continent') || '') : '';
            var hoveredActive = hovered && (hovered.classList.contains('is-active') || hovered.getAttribute('aria-selected') === 'true');

            if (!hovered || hoveredActive || (suppressed && hoveredContinent !== suppressed)) {
                clearContinentKeyboardHoverSuppress(panel);
            } else if (hovered && suppressed && hoveredContinent === suppressed) {
                clearContinentKeyboardStaleHoverMarkers(panel);
                hovered.classList.add('is-keyboard-stale-hover');
                hovered.setAttribute('data-clbi-keyboard-stale-hover', '1');
            }
        });
    }

    function ensureContinentKeyboardHoverGuard() {
        if (continentHoverGuardReady) return;
        continentHoverGuardReady = true;

        document.addEventListener('pointermove', function (event) {
            rememberContinentPointer(event);
            validateContinentKeyboardHoverSuppressions();
        }, true);

        document.addEventListener('pointerleave', function () {
            lastContinentPointerX = null;
            lastContinentPointerY = null;
            validateContinentKeyboardHoverSuppressions();
        }, true);

        window.addEventListener('blur', function () {
            lastContinentPointerX = null;
            lastContinentPointerY = null;
            validateContinentKeyboardHoverSuppressions();
        });
    }

    function clearNationItemPanelHoverOnly(panel, item) {
        var tag;
        if (!panel || !item) return;
        tag = String(item.getAttribute('data-nation-tag') || '').trim();
        item.classList.remove('is-panel-hovered');
        if (panel.CLBI_NationHoverFocusPendingTag === tag) cancelNationPanelPendingHoverFocus(panel);
    }

    function clearNationItemGlobeFocus(panel, item) {
        var tag;
        var instance;
        var timer;

        if (!panel || !item) return;
        if (isNationContinentSwitching(panel)) {
            clearNationItemPanelHoverOnly(panel, item);
            return;
        }
        tag = String(item.getAttribute('data-nation-tag') || '').trim();

        item.classList.remove('is-panel-hovered');

        timer = panel.CLBI_NationHoverFocusTimer || null;
        if (panel.CLBI_NationHoverFocusPendingTag === tag ||
            (!panel.CLBI_NationHoverFocusPendingTag && (timer || panel.CLBI_NationHoverFocusRaf))) {
            cancelNationPanelPendingHoverFocus(panel);
        }

        if (!tag) return;
        instance = getGlobeInstanceForNationItem(item);
        if (instance && typeof instance.clearPanelNationFocus === 'function') {
            instance.clearPanelNationFocus(tag);
        }
    }


    function bindNationListHoverFocus(panel) {
        if (!panel || panel.CLBI_NationPanelHoverFocusBound) return;
        panel.CLBI_NationPanelHoverFocusBound = true;
        panel.setAttribute('data-nation-panel-hover-focus-ready', '1');

        panel.addEventListener('mouseover', function (event) {
            var item = getNationItemFromEventTarget(event.target, panel);
            if (!item) return;
            if (event.relatedTarget && item.contains(event.relatedTarget)) return;
            requestNationItemGlobeFocus(panel, item, false);
        });

        panel.addEventListener('mouseout', function (event) {
            var item = getNationItemFromEventTarget(event.target, panel);
            if (!item) return;
            if (event.relatedTarget && item.contains(event.relatedTarget)) return;
            clearNationItemGlobeFocus(panel, item);
        });

        panel.addEventListener('focusin', function (event) {
            var item = getNationItemFromEventTarget(event.target, panel);
            if (item) requestNationItemGlobeFocus(panel, item, true);
        });

        panel.addEventListener('focusout', function (event) {
            var item = getNationItemFromEventTarget(event.target, panel);
            if (item) clearNationItemGlobeFocus(panel, item);
        });
    }


    function buildNationTabSkeletonHtml(extraClass) {
        var tabs = NATION_CONTINENT_FALLBACK.map(function (continent, index) {
            var active = index === 0;
            return '<span class="clbi-nations-tabpanel-tab' + (active ? ' is-active' : '') + '" role="tab" tabindex="' + (active ? '0' : '-1') + '" aria-selected="' + (active ? 'true' : 'false') + '" data-continent="' + escapeHtml(continent.key) + '">' + escapeHtml(continent.label) + '</span>';
        }).join('');

        var bodies = NATION_CONTINENT_FALLBACK.map(function (continent, index) {
            var rows = continent.regions.map(function (region) {
                return '<div class="clbi-nations-tabpanel-row">' +
                    '<div class="clbi-nations-tabpanel-region">' + escapeHtml(region) + '</div>' +
                    '<div class="clbi-nations-tabpanel-well">' + renderNationEmpty() + '</div>' +
                '</div>';
            }).join('');

            return '<div class="clbi-nations-tabpanel-continent' + (index === 0 ? ' is-active' : '') + '" data-continent-panel="' + escapeHtml(continent.key) + '" aria-hidden="' + (index === 0 ? 'false' : 'true') + '">' + rows + '</div>';
        }).join('');

        return '<div class="clbi-nations-tabpanel' + (extraClass ? ' ' + extraClass : '') + '" data-clbi-nations-simple data-nation-list-source="1">' +
            '<div class="clbi-nations-tabpanel-tabs" role="tablist" aria-label="대륙 선택">' +
                tabs +
                '<span class="clbi-nations-tabpanel-title" aria-hidden="true">당대 존재 국가</span>' +
            '</div>' +
            '<div class="clbi-nations-tabpanel-body">' + bodies + '</div>' +
        '</div>';
    }

    function buildHistoryPanelSkeletonHtml(era) {
        var start = parseInt(era || '1950', 10);
        var years = [];

        if (!isFinite(start)) start = 1950;

        for (var year = start; year < start + 10; year += 1) {
            years.push(String(year));
        }

        var pages = years.map(function (year, index) {
            return '<div class="clbi-nations-history-list clbi-nations-history-page' + (index === 0 ? ' is-active' : '') + '" data-year-panel="' + escapeHtml(year) + '"' + (index === 0 ? '' : ' hidden="hidden"') + '>' +
                '<div class="clbi-nations-history-empty"><span class="clbi-nations-history-empty-ko">작업 중...</span><span class="clbi-nations-history-empty-en">Working in Progress...</span></div>' +
            '</div>';
        }).join('');

        var tabs = years.map(function (year, index) {
            return '<span class="clbi-nations-history-year-button' + (index === 0 ? ' is-active' : '') + '" role="tab" tabindex="' + (index === 0 ? '0' : '-1') + '" aria-selected="' + (index === 0 ? 'true' : 'false') + '" data-year="' + escapeHtml(year) + '">' + escapeHtml(year) + '</span>';
        }).join('');

        return '<div class="clbi-nations-forces-panel clbi-nations-module-panel clbi-nations-history-panel">' +
            '<div class="clbi-nations-module-title">역사적 사건</div>' +
            '<div class="clbi-nations-module-body clbi-nations-history-body">' +
                '<div class="clbi-nations-history-pages">' + pages + '</div>' +
                '<div class="clbi-nations-history-year-well"><div class="clbi-nations-history-year-list" data-decade-only="1" role="tablist" aria-label="역사적 사건 연도 선택">' + tabs + '</div></div>' +
            '</div>' +
        '</div>';
    }

    function buildEraContentSkeletonHtml(era) {
        return '<div class="clbi-nations-era-content clbi-nations-era-wip" data-era-content="' + escapeHtml(era) + '" hidden="hidden">' +
            buildHistoryPanelSkeletonHtml(era) +
            buildNationTabSkeletonHtml('clbi-nations-era-wip-tabpanel') +
        '</div>';
    }

    function ensureEraContentPanels(stack, eras) {
        var insertAfter;

        if (!stack || !eras || !eras.length) return;

        insertAfter = toArray(stack.querySelectorAll('.clbi-nations-era-content[data-era-content]')).pop() || stack.querySelector('.clbi-nations-map-row');

        eras.forEach(function (era) {
            var selector = '.clbi-nations-era-content[data-era-content="' + String(era).replace(/"/g, '\\"') + '"]';
            var wrapper;
            var panel;

            if (stack.querySelector(selector)) return;

            wrapper = document.createElement('div');
            wrapper.innerHTML = buildEraContentSkeletonHtml(era);
            panel = wrapper.firstElementChild;

            if (!panel) return;

            if (insertAfter && insertAfter.parentNode) {
                insertAfter.parentNode.insertBefore(panel, insertAfter.nextSibling);
            } else {
                stack.appendChild(panel);
            }

            insertAfter = panel;
        });
    }

    function renderNationContinent(continent, activeKey) {
        var rows = (continent.regions || []).map(function (region) {
            var items = Array.isArray(region.items) ? region.items : [];
            var content = items.length ? items.map(function (item, index) {
                return renderNationItem(item, index, items);
            }).join(' ') : renderNationEmpty();

            return '' +
                '<div class="clbi-nations-tabpanel-row">' +
                    '<div class="clbi-nations-tabpanel-region">' + escapeHtml(region.label || region.key || '') + '</div>' +
                    '<div class="clbi-nations-tabpanel-well">' + content + '</div>' +
                '</div>';
        }).join('');

        return '<div class="clbi-nations-tabpanel-continent' + (continent.key === activeKey ? ' is-active' : '') + '" data-continent-panel="' + escapeHtml(continent.key) + '" aria-hidden="' + (continent.key === activeKey ? 'false' : 'true') + '">' + rows + '</div>';
    }

    function renderNationListPanel(panel, data) {
        var body = panel.querySelector('.clbi-nations-tabpanel-body');
        var activeTab = panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
        var activeKey = activeTab ? activeTab.getAttribute('data-continent') : '';
        var continents = data && Array.isArray(data.continents) ? data.continents : [];
        var stats = countNationListItems(data);

        if (!body || !continents.length) return;
        if (!activeKey) activeKey = continents[0].key;

        return PanelPerf.measure('render nation list panel', {
            era: String(data.year || getPanelEra(panel) || ''),
            continents: stats.continents,
            regions: stats.regions,
            items: stats.items
        }, function () {
            var html;

            PanelPerf.measure('render nation list panel: html build+insert', {
                era: String(data.year || getPanelEra(panel) || ''),
                continents: stats.continents,
                items: stats.items
            }, function () {
                html = continents.map(function (continent) {
                    return renderNationContinent(continent, activeKey);
                }).join('');
                body.innerHTML = html;
            });

            PanelPerf.measure('render nation list panel: bind flag fallbacks', {}, function () {
                bindNationFlagFallbacks(body);
            });
            PanelPerf.measure('render nation list panel: bind hover focus', {}, function () {
                bindNationListHoverFocus(panel);
            });
            PanelPerf.measure('render nation list panel: prepare continent panels', {}, function () {
                preparePersistentContinentPanels(panel);
            });
            panel.classList.add('clbi-nations-list-json-ready');
            panel.setAttribute('data-nation-list-year-loaded', String(data.year || getPanelEra(panel)));
            PanelPerf.measure('render nation list panel: schedule flag prewarm', {}, function () {
                prewarmRenderedNationFlags(body);
            });
            PanelPerf.measure('render nation list panel: row snap schedule', {}, function () {
                panel.NationListRowSnapSettled = false;
                bindNationListRowSnapResize();
                scheduleNationListRowSnap(panel, { force: true, reason: 'render' });
            });
        });
    }

    function seedResolvedFlagUrlsFromEntryStore(data) {
        var files;
        if (!window.EntryStore || typeof window.EntryStore.getFlagUrl !== 'function') return;
        files = collectNationFlagFileNames(data);
        files.forEach(function (file) {
            var key = normalizeNationFileKey(file);
            var url = window.EntryStore.getFlagUrl(file);
            if (key && url) nationResolvedFileUrlCache[key] = url;
        });
    }

    function getPreparedNationDataForPanel(panel, era) {
        var listUrl;
        var linkUrl;
        var listPayload;
        var linkPayload;
        var linkIndex;
        var data;

        if (!window.EntryStore || typeof window.EntryStore.getJsonSync !== 'function') return null;
        era = String(era || getPanelEra(panel) || '1950');
        listUrl = getNationListUrl(panel, era);
        linkUrl = getNationLinkMapUrl(panel, era);
        listPayload = getEntryStoreJson(listUrl) || getEntryStoreJson('MediaWiki:' + era + '_Nation_List.json');
        if (!listPayload) return null;
        linkPayload = getEntryStoreJson(linkUrl) || getEntryStoreJson('MediaWiki:' + era + '_Nation_Link_Map.json');
        linkIndex = normalizeNationLinkMap(linkPayload);
        data = applyNationLinkMapFlagFallback(normalizeNationList(listPayload, era), linkIndex);
        seedResolvedFlagUrlsFromEntryStore(data);
        return data;
    }

    function prepareContentForEntry(root, options) {
        return PanelPerf.measure('prepare content for entry', {}, function () {
            var scope = root && root.querySelectorAll ? root : document;
            var panels = toArray(scope.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]'));
            var rendered = 0;
            var skipped = 0;

            panels.forEach(function (panel) {
                var era = getPanelEra(panel);
                var data = getPreparedNationDataForPanel(panel, era);
                if (!data) {
                    skipped += 1;
                    PanelPerf.mark('prepare content panel skipped', { era: era, reason: 'no prepared data' });
                    return;
                }
                renderNationListPanel(panel, data);
                rendered += 1;
            });

            PanelPerf.mark('prepare content for entry result', { panels: panels.length, rendered: rendered, skipped: skipped });
            return { panels: panels.length, rendered: rendered, skipped: skipped };
        });
    }

    function isNationListPanelHidden(panel) {
        var node = panel;

        while (node && node !== document && node.nodeType === 1) {
            if (node.hasAttribute('hidden') || node.getAttribute('aria-hidden') === 'true') return true;
            node = node.parentElement;
        }

        return false;
    }

    function initNationListPanel(panel) {
        var era;
        var promise;
        var loadId;

        if (!panel || panel.getAttribute('data-nation-list-source') !== '1') return Promise.resolve(null);
        if (isNationListPanelHidden(panel)) {
            PanelPerf.mark('init nation list panel skipped', { reason: 'hidden' });
            return Promise.resolve(null);
        }
        if (panel.getAttribute('data-nation-list-year-loaded') === getPanelEra(panel)) {
            PanelPerf.mark('init nation list panel skipped', { era: getPanelEra(panel), reason: 'already loaded' });
            return Promise.resolve(panel);
        }
        if (panel.getAttribute('data-nation-list-loading') === '1' && panel.CLBI_NationListLoadPromise) {
            PanelPerf.mark('init nation list panel existing promise', { era: getPanelEra(panel) });
            return panel.CLBI_NationListLoadPromise;
        }

        era = getPanelEra(panel);
        loadId = PanelPerf.start('init nation list panel', { era: era });
        purgeLegacyNationIndexFileImages(panel);
        panel.setAttribute('data-nation-list-loading', '1');

        promise = Promise.all([
            fetchNationList(panel, era),
            fetchNationLinkMap(panel, era)
        ]).then(function (results) {
            var data = PanelPerf.measure('apply nation link map flag fallback', { era: era }, function () {
                return applyNationLinkMapFlagFallback(results[0], results[1]);
            });
            var initialFiles = collectInitialNationFlagFileNames(data, panel);
            var allFiles = collectNationFlagFileNames(data);
            PanelPerf.mark('initial nation flag URL set', {
                era: era,
                count: initialFiles.length,
                activeContinent: getInitialContinentKey(panel, data)
            });
            PanelPerf.mark('all continent nation flag URL set', {
                era: era,
                count: allFiles.length,
                mode: 'pre-render-all-tabs'
            });
            return resolveNationFlagFileUrls(allFiles).then(function () {
                panel.CLBI_NationsAllFlagUrlsResolved = true;
                renderNationListPanel(panel, data);
                panel.CLBI_NationsAllContinentPanelsPrebuilt = true;
                scheduleAllNationFlagBackgroundWarm(panel, data);
                return panel;
            });
        }).then(function (result) {
            PanelPerf.end(loadId, { ok: true });
            return result;
        }).catch(function () {
            panel.classList.add('clbi-nations-list-json-fallback');
            PanelPerf.end(loadId, { ok: false, fallback: true });
            return panel;
        }).finally(function () {
            panel.removeAttribute('data-nation-list-loading');
            panel.CLBI_NationListLoadPromise = null;
        });

        panel.CLBI_NationListLoadPromise = promise;
        return promise;
    }

    function waitUntilNationsPanel(predicate, timeoutMs, intervalMs) {
        var started = Date.now();
        timeoutMs = Math.max(500, timeoutMs || 12000);
        intervalMs = Math.max(20, intervalMs || 80);
        return new Promise(function (resolve) {
            function tick() {
                var result = false;
                try { result = predicate(); } catch (err) { result = false; }
                if (result) return resolve({ ok: true, value: result });
                if (Date.now() - started >= timeoutMs) return resolve({ ok: false, timeout: true });
                setTimeout(tick, intervalMs);
            }
            tick();
        });
    }

    function findEraNationPanels(era) {
        var selector;
        era = String(era || '').trim();
        if (!era) return [];
        selector = '.clbi-nations-era-content[data-era-content="' + era.replace(/"/g, '\\"') + '"] .clbi-nations-tabpanel[data-nation-list-source="1"]';
        return toArray(document.querySelectorAll(selector));
    }

    function whenEraReady(era, options) {
        var panels;
        var timeoutMs = options && options.timeoutMs ? options.timeoutMs : 15000;
        era = String(era || '1950');
        panels = findEraNationPanels(era);

        if (!panels.length) {
            panels = toArray(document.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]')).filter(function (panel) {
                return getPanelEra(panel) === era;
            });
        }

        if (!panels.length) {
            PanelPerf.mark('when era ready skipped', { era: era, reason: 'no panels' });
            return Promise.resolve({ era: era, skipped: true });
        }

        return PanelPerf.measure('when era ready', { era: era, panels: panels.length, timeoutMs: timeoutMs }, function () {
            return PanelPerf.measure('when era ready: init panels', { era: era, panels: panels.length }, function () {
                return Promise.all(panels.map(function (panel) {
                    return initNationListPanel(panel);
                }));
            }).then(function () {
                return PanelPerf.measure('when era ready: wait json-ready marker', { era: era, panels: panels.length }, function () {
                    return waitUntilNationsPanel(function () {
                        return panels.every(function (panel) {
                            return panel && panel.classList.contains('clbi-nations-list-json-ready') && panel.getAttribute('data-nation-list-year-loaded') === era;
                        });
                    }, timeoutMs, 80);
                });
            }).then(function (result) {
                return PanelPerf.measure('when era ready: wait rendered flags', { era: era, panels: panels.length }, function () {
                    return Promise.all(panels.map(function (panel) {
                        return waitForRenderedFlagImages(panel, { timeoutMs: timeoutMs, activeOnly: true }).catch(function () {
                            return { total: 0, ok: 0, fail: 0 };
                        });
                    }));
                }).then(function (flagResults) {
                    var total = 0;
                    var ok = 0;
                    var fail = 0;
                    flagResults.forEach(function (item) {
                        total += item && item.total ? item.total : 0;
                        ok += item && item.ok ? item.ok : 0;
                        fail += item && item.fail ? item.fail : 0;
                    });
                    return {
                        era: era,
                        ok: !!(result && result.ok),
                        panels: panels.length,
                        timeout: !!(result && result.timeout),
                        flags: { total: total, ok: ok, fail: fail }
                    };
                });
            });
        });
    }

    function preparePersistentContinentPanels(panel) {
        var activePanel;

        if (!panel || !panel.querySelectorAll) return;
        panel.classList.add('clbi-nations-continent-cache');

        toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')).forEach(function (body) {
            if (body.hasAttribute('hidden')) body.removeAttribute('hidden');
            body.setAttribute('data-clbi-panel-warm', '1');
            if (!body.hasAttribute('aria-hidden')) {
                body.setAttribute('aria-hidden', body.classList.contains('is-active') ? 'false' : 'true');
            }
        });

        activePanel = panel.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]') ||
            panel.querySelector('.clbi-nations-tabpanel-continent[data-continent-panel]');

        if (activePanel) {
            activatePersistentPanelsByAttribute(
                toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')),
                'data-continent-panel',
                activePanel.getAttribute('data-continent-panel')
            );
        }

        stabilizeContinentBodyHeight(panel, true);
    }

    function syncDecorationRuntimeNow() {
        var runtime = window.Decorations || window.CLBI_DECORATIONS || null;
        try {
            if (runtime && typeof runtime.updateVisibility === 'function') runtime.updateVisibility(document);
            else if (runtime && typeof runtime.sync === 'function') runtime.sync();
            else if (runtime && typeof runtime.renderPrepared === 'function') runtime.renderPrepared();
            else if (runtime && typeof runtime.render === 'function' && runtime.lastRegistry) runtime.render(runtime.lastRegistry);
        } catch (err) {}
    }

    function scheduleDecorationVisibilitySync(panel) {
        if (!panel) return;

        /*
         * Continent switch fast path — 20260708.
         *
         * Changing a continent tab must feel like a local state flip, not a
         * mini page load. Decoration visibility is not required to decide the
         * active tab/body, and running the decoration runtime in the same input
         * frame competes with the globe animation and panel repaint. Keep the
         * visible switch synchronous and move decoration maintenance to the next
         * animation frame. This preserves the prepared-canvas behavior while
         * avoiding the old "tab click pauses the globe" feeling.
         */
        if (panel.CLBI_NationDecorationSyncRaf) {
            window.cancelAnimationFrame(panel.CLBI_NationDecorationSyncRaf);
            panel.CLBI_NationDecorationSyncRaf = null;
        }

        panel.CLBI_NationDecorationSyncRaf = window.requestAnimationFrame(function () {
            panel.CLBI_NationDecorationSyncRaf = null;
            if (!document.body.contains(panel)) return;
            syncDecorationRuntimeNow();
        });
    }

    function scheduleHoverFocusMaintenance(panel) {
        if (!panel) return;

        /*
         * Continent switch fast path — 20260708.
         *
         * Pending hover-to-globe focus timers are cancelled immediately so an old
         * country cannot request a new zoom after the user changes continents.
         * The visual cleanup scan is deferred one frame: it is housekeeping, and
         * doing it in the click/shortcut frame used to make the globe stutter.
         */
        cancelNationPanelPendingHoverFocus(panel);

        if (panel.CLBI_NationHoverCleanupRaf) {
            window.cancelAnimationFrame(panel.CLBI_NationHoverCleanupRaf);
            panel.CLBI_NationHoverCleanupRaf = null;
        }

        panel.CLBI_NationHoverCleanupRaf = window.requestAnimationFrame(function () {
            panel.CLBI_NationHoverCleanupRaf = null;
            if (!document.body.contains(panel)) return;
            clearNationPanelHoverState(panel, null);
        });
    }

    function stabilizeContinentBodyHeight(panel, force) {
        var body;
        var bodies;
        var max = 0;

        if (!panel || !panel.querySelectorAll) return;
        if (!force && panel.CLBI_NationContinentHeightStable) return;

        body = panel.querySelector('.clbi-nations-tabpanel-body');
        bodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
        if (!body || !bodies.length) return;

        /*
         * Height stabilization — 20260708.
         *
         * The six continent bodies are already kept warm in DOM. If the stack
         * height follows each active body, every continent switch can still cause
         * a layout jump and steal a frame from the WebGL globe. Measure the warm
         * bodies once and pin the body well to the tallest continent. The number
         * of bodies is tiny, so this one-time measurement is cheaper than paying
         * repeated layout work on every switch.
         */
        bodies.forEach(function (item) {
            var h;
            if (!item) return;
            h = Math.max(item.scrollHeight || 0, item.offsetHeight || 0);
            if (h > max) max = h;
        });

        if (max > 0) {
            body.style.setProperty('--clbi-nations-continent-body-min-h', Math.ceil(max) + 'px');
            body.setAttribute('data-clbi-continent-height-stable', '1');
            panel.CLBI_NationContinentHeightStable = true;
        }
    }

    function getContinentPanelParts(panel) {
        if (!panel || !panel.querySelectorAll) return { tabs: [], bodies: [] };

        /*
         * Mouse-click continent regression guard — 20260708.
         *
         * The nations panel is prebuilt once while detached, then consumed again
         * after SPA insertion. Older fixes correctly avoided serialized
         * attributes as binding guards, but the cached tab/body node arrays could
         * still point at nodes that were replaced or detached during warm entry
         * hydration. Keyboard shortcuts often survived because a later fallback
         * queried the live DOM, while mouse clicks could be swallowed by the
         * owner API returning true after updating stale cached nodes.
         *
         * Keep this function live-DOM based. The tab count is tiny, so a fresh
         * query is cheaper and safer than carrying stale references across
         * detached preparation, innerHTML insertion, and manifest warm-cache
         * hydration. This preserves the old solved behavior: mouse clicks,
         * keyboard shortcuts, and fallback activation all operate on the same
         * currently visible DOM nodes.
         */
        panel.CLBI_NationContinentTabs = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
        panel.CLBI_NationContinentBodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));

        return {
            tabs: panel.CLBI_NationContinentTabs || [],
            bodies: panel.CLBI_NationContinentBodies || []
        };
    }

    function findActiveContinentPanel() {
        var panels = toArray(document.querySelectorAll('.clbi-nations-tabpanel'));
        var visible = panels.filter(function (panel) {
            return !isNationListPanelHidden(panel) && panel.querySelector('.clbi-nations-tabpanel-tab[data-continent]');
        });
        return visible.find(function (panel) { return panel.getAttribute('data-nation-list-source') === '1'; }) || visible[0] || null;
    }

    function activateContinentOnPanel(panel, continent, options) {
        var parts;
        var tabs;
        var bodies;
        var activeTab = null;
        var currentTab = null;
        var source = options && options.source ? String(options.source) : '';
        var keyboardSwitch = source === 'shortcut' || source === 'keyboard' || /keyboard|shortcut/i.test(source);

        if (!panel || !continent) return false;
        if (source && source !== 'init') markNationPanelDirectInteraction();
        parts = interactionPerfMeasure('panel switch query live tabs', { continent: continent, source: source }, function () {
            return getContinentPanelParts(panel);
        });
        tabs = parts.tabs;
        bodies = parts.bodies;
        if (!tabs.length || !bodies.length) return false;

        tabs.forEach(function (tab) {
            if (tab.getAttribute('data-continent') === continent) activeTab = tab;
            if (tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true') currentTab = tab;
        });

        if (!activeTab && tabs.length) {
            activeTab = tabs[0];
            continent = activeTab.getAttribute('data-continent');
        }
        if (!activeTab) return false;

        panel.CLBI_NationsPanelOwned = true;

        if (currentTab === activeTab && panel.getAttribute('data-active-continent') === continent) {
            if (keyboardSwitch) syncContinentKeyboardHoverSuppress(panel, activeTab, true);
            else clearContinentKeyboardHoverSuppress(panel);
            scheduleNationListRowSnap(panel);
            scheduleDecorationVisibilitySync(panel);
            return true;
        }

        /*
         * Flag continuity on continent switches — 20260710.
         *
         * The remaining flag resolver runs after the first visible continent.  When a
         * user switches tabs after those URLs are ready, the target continent must not
         * become visible first and then receive its <img> nodes a frame later; that
         * reads as every tab loading its flags from zero.  Hydrate the target hidden
         * body before flipping the active class, then keep the old post-switch pass
         * only as a fallback for unresolved/late slots.
         */
        if (!panel.CLBI_NationsAllFlagUrlsResolved) {
            interactionPerfMeasure('panel switch prehydrate target flags', { continent: continent, source: source }, function () {
                bodies.forEach(function (body) {
                    if (!body || body.getAttribute('data-continent-panel') !== continent) return;
                    hydrateResolvedFlagSlots(body);
                    prewarmRenderedNationFlags(body);
                });
            });
        }

        markNationContinentSwitching(panel);
        interactionPerfMeasure('panel switch flip active classes', { continent: continent, source: source, tabs: tabs.length, bodies: bodies.length }, function () {
            setActiveButton(tabs, activeTab);
            activatePersistentPanelsByAttribute(bodies, 'data-continent-panel', continent);
        });
        if (!panel.CLBI_NationsAllFlagUrlsResolved && panel.CLBI_NationsRemainingFlagsResolved) {
            scheduleResolvedFlagHydrationSweeps(panel, {
                era: getPanelEra(panel),
                delays: [80, 220]
            });
        }
        panel.setAttribute('data-active-continent', continent);
        if (!panel.CLBI_NationsAllContinentPanelsPrebuilt) {
            scheduleNationListRowSnap(panel);
        }
        if (!panel.CLBI_NationContinentHeightStable) {
            /* Height measurement can force layout.  Do it outside the click frame. */
            window.requestAnimationFrame(function () {
                if (document.body.contains(panel)) stabilizeContinentBodyHeight(panel, false);
            });
        }
        if (keyboardSwitch) syncContinentKeyboardHoverSuppress(panel, activeTab, true);
        else clearContinentKeyboardHoverSuppress(panel);

        /*
         * The actual continent switch ends here. Everything below is maintenance
         * and is intentionally deferred so mouse clicks and shortcuts only pay
         * for the tiny tab/body class flip in the input frame.
         */
        scheduleHoverFocusMaintenance(panel);
        scheduleDecorationVisibilitySync(panel);
        interactionPerfMark('panel continent switch complete', { continent: continent, source: source });
        return true;
    }

    function activateContinent(continent, options) {
        var panel = options && options.panel ? options.panel : findActiveContinentPanel();
        return activateContinentOnPanel(panel, continent, options || {});
    }

    function moveContinent(direction, options) {
        var panel = options && options.panel ? options.panel : findActiveContinentPanel();
        var parts;
        var tabs;
        var activeIndex;
        var nextIndex;

        direction = direction < 0 ? -1 : 1;
        if (!panel) return false;
        parts = getContinentPanelParts(panel);
        tabs = parts.tabs;
        if (!tabs.length) return false;
        activeIndex = tabs.findIndex(function (tab) {
            return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
        });
        if (activeIndex < 0) activeIndex = 0;
        nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
        return activateContinentOnPanel(panel, tabs[nextIndex].getAttribute('data-continent'), options || {});
    }

    function initContinentPanel(panel) {
        var tabs;
        var bodies;

        if (!panel) return;
        if (isNationListPanelHidden(panel)) return;
        bindNationListHoverFocus(panel);
        ensureContinentKeyboardHoverGuard();
        /*
         * Detached SPA preparation serializes attributes but not event listeners.
         * A panel can therefore arrive with data-nations-tabpanel-ready="1" while
         * its click handlers were lost during innerHTML insertion.  Use a DOM
         * property as the real binding guard and keep the attribute only as a
         * diagnostic marker.  This fixes mouse continent tabs after warm prepaint.
         */
        if (panel.CLBI_NationsTabPanelBound) return;
        panel.CLBI_NationsTabPanelBound = true;
        panel.setAttribute('data-nations-tabpanel-ready', '1');

        panel.addEventListener('pointerdown', function () {
            clearContinentKeyboardHoverSuppress(panel);
        }, true);

        panel.addEventListener('pointerleave', function () {
            clearContinentKeyboardHoverSuppress(panel);
        }, true);

        preparePersistentContinentPanels(panel);
        panel.CLBI_NationsPanelOwned = true;
        panel.CLBI_NationContinentTabs = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
        panel.CLBI_NationContinentBodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
        stabilizeContinentBodyHeight(panel, true);
        tabs = panel.CLBI_NationContinentTabs;
        bodies = panel.CLBI_NationContinentBodies;

        function activate(continent, options) {
            return activateContinentOnPanel(panel, continent, options || {});
        }

        tabs.forEach(function (tab) {
            tab.setAttribute('role', 'tab');
            tab.setAttribute('tabindex', tab.classList.contains('is-active') ? '0' : '-1');

            tab.addEventListener('click', function (event) {
                if (event) {
                    event.preventDefault();
                    if (typeof event.stopPropagation === 'function') event.stopPropagation();
                    if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
                }
                interactionPerfMeasure('panel tab click handler', { continent: tab.getAttribute('data-continent') || '' }, function () {
                    playNationsPanelClickSound();
                    activate(tab.getAttribute('data-continent'), { source: 'click' });
                });
            });

            tab.addEventListener('keydown', function (event) {
                if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    interactionPerfMeasure('panel tab keyboard handler', { continent: tab.getAttribute('data-continent') || '', key: event.key || '' }, function () {
                        playNationsPanelClickSound();
                        activate(tab.getAttribute('data-continent'), { source: 'keyboard' });
                    });
                }
            });
        });

        activate((panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent]') || tabs[0] || {}).getAttribute && (panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent]') || tabs[0]).getAttribute('data-continent'), { source: 'init' });
    }

    function initHistoryYearPanel(panel) {
        var tabs;
        var pages;

        if (!panel || panel.getAttribute('data-nations-history-ready') === '1') return;
        panel.setAttribute('data-nations-history-ready', '1');

        tabs = toArray(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
        pages = toArray(panel.querySelectorAll('.clbi-nations-history-page[data-year-panel]'));

        function activate(year) {
            var activeTab = null;

            tabs.forEach(function (tab) {
                if (tab.getAttribute('data-year') === year) activeTab = tab;
            });

            if (!activeTab && tabs.length) {
                activeTab = tabs[0];
                year = activeTab.getAttribute('data-year');
            }

            setActiveButton(tabs, activeTab);
            activatePanelsByAttribute(pages, 'data-year-panel', year);
            scheduleHistoryItemSlides(panel);
        }

        tabs.forEach(function (tab) {
            tab.setAttribute('role', 'tab');
            tab.setAttribute('tabindex', tab.classList.contains('is-active') ? '0' : '-1');

            tab.addEventListener('click', function () {
                playNationsPanelClickSound();
                activate(tab.getAttribute('data-year'));
            });

            tab.addEventListener('keydown', function (event) {
                if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    playNationsPanelClickSound();
                    activate(tab.getAttribute('data-year'));
                }
            });
        });

        activate((panel.querySelector('.clbi-nations-history-year-button.is-active[data-year]') || tabs[0] || {}).getAttribute && (panel.querySelector('.clbi-nations-history-year-button.is-active[data-year]') || tabs[0]).getAttribute('data-year'));
    }

    function prewarmNationEra(stack, era) {
        var eraPanel;
        var panel;
        var selector;

        if (!stack || !era) return;

        selector = '.clbi-nations-era-content[data-era-content="' + String(era).replace(/"/g, '\"') + '"]';
        eraPanel = stack.querySelector(selector);
        panel = eraPanel && eraPanel.querySelector ? eraPanel.querySelector('.clbi-nations-tabpanel[data-nation-list-source="1"]') : null;
        if (!panel || panel.getAttribute('data-nation-prewarm-loading') === '1') return;
        if (panel.getAttribute('data-nation-prewarm-year-loaded') === String(era)) return;

        panel.setAttribute('data-nation-prewarm-loading', '1');

        Promise.all([
            fetchNationList(panel, era),
            fetchNationLinkMap(panel, era)
        ]).then(function () {
            panel.setAttribute('data-nation-prewarm-year-loaded', String(era));
        }).catch(function () {
        }).finally(function () {
            panel.removeAttribute('data-nation-prewarm-loading');
        });
    }

    function scheduleEraNeighborsPrewarm(stack, eras, era) {
        var index;
        var prevEra;
        var nextEra;
        var oldHandle;

        if (!stack || !eras || !eras.length || !era) return;

        oldHandle = stack.CLBI_NationsEraPrewarmHandle;
        if (oldHandle != null) cancelIdle(oldHandle);

        index = getEraIndex(eras, era);
        prevEra = index > 0 ? eras[index - 1] : '';
        nextEra = index < eras.length - 1 ? eras[index + 1] : '';

        stack.CLBI_NationsEraPrewarmHandle = scheduleIdle(function () {
            stack.CLBI_NationsEraPrewarmHandle = null;
            prewarmNationEra(stack, nextEra);
            prewarmNationEra(stack, prevEra);
        }, { timeout: 900 });
    }

    function initEraStack(stack) {
        var eras;
        var titleNode;
        var titlePlates;
        var contentPanels;
        var leftButton;
        var rightButton;
        var globe;
        var currentEra;

        if (!stack || stack.getAttribute('data-nations-era-controller-ready') === '1') return;
        stack.setAttribute('data-nations-era-controller-ready', '1');

        eras = parseEraList(stack);
        ensureEraContentPanels(stack, eras);
        titleNode = stack.querySelector('[data-nations-era-title]');
        titlePlates = toArray(stack.querySelectorAll('.clbi-nations-era-title-plate[data-era]'));
        contentPanels = toArray(stack.querySelectorAll('.clbi-nations-era-content[data-era-content]'));
        leftButton = stack.querySelector('[data-nations-era-move="prev"], .clbi-nations-era-side-button-left');
        rightButton = stack.querySelector('[data-nations-era-move="next"], .clbi-nations-era-side-button-right');
        globe = stack.querySelector('.clbi-nations-globe-window');
        currentEra = stack.getAttribute('data-nations-current-era') || stack.getAttribute('data-era') || eras[0];

        function normalizeEra(era) {
            var index = getEraIndex(eras, String(era || eras[0]));
            return eras[index];
        }

        function setEraShell(era, preview) {
            var index;
            var prevEra;
            var nextEra;

            era = normalizeEra(era);
            index = getEraIndex(eras, era);
            prevEra = getWrappedEra(eras, index - 1);
            nextEra = getWrappedEra(eras, index + 1);

            stack.setAttribute('data-era', era);
            if (preview) {
                stack.setAttribute('data-nations-preview-era', era);
                stack.classList.add('is-era-previewing');
            } else {
                stack.removeAttribute('data-nations-preview-era');
                stack.classList.remove('is-era-previewing');
            }

            if (titleNode) {
                titleNode.textContent = era + '년';
                titleNode.setAttribute('data-era', era);
            }

            titlePlates.forEach(function (plate) {
                setHidden(plate, plate.getAttribute('data-era') !== era);
            });

            if (leftButton) {
                leftButton.setAttribute('data-target-era', prevEra);
                leftButton.setAttribute('title', prevEra + '년으로 이동');
                leftButton.setAttribute('aria-label', prevEra + '년으로 이동');
            }

            if (rightButton) {
                rightButton.setAttribute('data-target-era', nextEra);
                rightButton.setAttribute('title', nextEra + '년으로 이동');
                rightButton.setAttribute('aria-label', nextEra + '년으로 이동');
            }

            return era;
        }

        function clearEraCommitTimer() {
            if (stack.CLBI_NationsEraCommitTimer) {
                window.clearTimeout(stack.CLBI_NationsEraCommitTimer);
                stack.CLBI_NationsEraCommitTimer = null;
            }
        }

        function setEra(era, options) {
            var reload;

            options = options || {};
            era = setEraShell(era, false);
            reload = !!options.reloadGlobe;

            clearEraCommitTimer();
            currentEra = era;
            stack.setAttribute('data-nations-current-era', era);

            contentPanels.forEach(function (panel) {
                var inactive = panel.getAttribute('data-era-content') !== era;
                setHidden(panel, inactive);
                if (!inactive) {
                    toArray(panel.querySelectorAll('.clbi-nations-tabpanel')).forEach(initContinentPanel);
                    toArray(panel.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
                    toArray(panel.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryEventsPanel);
                    toArray(panel.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryYearPanel);
                }
            });

            applyGlobeEra(globe, era, reload);
            scheduleEraNeighborsPrewarm(stack, eras, era);
        }

        function commitEraPreview() {
            var target = stack.getAttribute('data-nations-preview-era') || '';

            clearEraCommitTimer();
            if (!target) return false;

            setEra(target, { reloadGlobe: true });
            return true;
        }

        function scheduleEraCommit() {
            clearEraCommitTimer();
            stack.CLBI_NationsEraCommitTimer = window.setTimeout(function () {
                stack.CLBI_NationsEraCommitTimer = null;
                commitEraPreview();
            }, 135);
        }

        function previewMove(direction) {
            var active = stack.getAttribute('data-nations-preview-era') || stack.getAttribute('data-nations-current-era') || currentEra || eras[0];
            var index = getEraIndex(eras, active);
            var target = getWrappedEra(eras, index + direction);

            if (!target) return false;
            playNationsPanelClickSound();
            setEraShell(target, true);
            scheduleEraCommit();
            return true;
        }

        function move(direction) {
            var active = stack.getAttribute('data-nations-current-era') || currentEra || eras[0];
            var index = getEraIndex(eras, active);
            var target = getWrappedEra(eras, index + direction);

            if (!target) return false;
            playNationsPanelClickSound();
            setEra(target, { reloadGlobe: true });
            return true;
        }

        if (leftButton) {
            leftButton.addEventListener('click', function (event) {
                event.preventDefault();
                move(-1);
            });
        }

        if (rightButton) {
            rightButton.addEventListener('click', function (event) {
                event.preventDefault();
                move(1);
            });
        }

        stack.CLBI_NationsEraController = {
            eras: eras.slice(),
            setEra: function (era) { setEra(era, { reloadGlobe: true }); },
            previewMove: previewMove,
            commitPreview: commitEraPreview,
            move: move
        };

        setEra(currentEra, { reloadGlobe: false });
    }

    function init(root) {
        var scope = root && root.querySelectorAll ? root : document;

        purgeLegacyNationIndexFileImages(scope);
        toArray(scope.querySelectorAll('.clbi-nations-panel-stack')).forEach(initEraStack);
        toArray(scope.querySelectorAll('.clbi-nations-tabpanel')).forEach(initContinentPanel);
        toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryEventsPanel);
        toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryYearPanel);
        toArray(scope.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
        scheduleHistoryItemSlides(scope);
        bindHistoryItemSlideResize();
    }

    function findEraStack() {
        return document.querySelector('.clbi-nations-panel-stack');
    }

    function moveEra(direction) {
        var stack = findEraStack();

        if (!stack || !stack.CLBI_NationsEraController) return false;
        return stack.CLBI_NationsEraController.move(direction);
    }

    function previewMoveEra(direction) {
        var stack = findEraStack();

        if (!stack || !stack.CLBI_NationsEraController || typeof stack.CLBI_NationsEraController.previewMove !== 'function') return false;
        return stack.CLBI_NationsEraController.previewMove(direction);
    }

    function commitEraPreview() {
        var stack = findEraStack();

        if (!stack || !stack.CLBI_NationsEraController || typeof stack.CLBI_NationsEraController.commitPreview !== 'function') return false;
        return stack.CLBI_NationsEraController.commitPreview();
    }

    function setEra(era) {
        var stack = findEraStack();

        if (!stack || !stack.CLBI_NationsEraController) return false;
        stack.CLBI_NationsEraController.setEra(era);
        return true;
    }

    function refresh(root) {
        var scope = root && root.querySelectorAll ? root : document;

        nationRuntimeCacheBust = 'revision-ledger-refresh';
        nationListCache = {};
        nationLinkMapCache = {};
        historyEventsCache = {};

        toArray(scope.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]')).forEach(function (panel) {
            panel.removeAttribute('data-nation-list-loading');
            panel.removeAttribute('data-nation-list-year-loaded');
            panel.classList.remove('clbi-nations-list-json-ready', 'clbi-nations-list-json-fallback');
        });

        toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(function (panel) {
            panel.removeAttribute('data-history-events-loading');
            panel.removeAttribute('data-history-events-year-loaded');
            panel.removeAttribute('data-nations-history-ready');
            panel.classList.remove('clbi-nations-history-json-ready', 'clbi-nations-history-json-fallback');
        });

        init(scope);
    }

    function diagnostics() {
        var flags = toArray(document.querySelectorAll('.clbi-nations-list-flag-slot')).map(function (slot) {
            var img = slot.querySelector('img');
            return {
                file: slot.getAttribute('data-flag-file') || '',
                src: img ? img.getAttribute('src') : '',
                missing: slot.classList.contains('is-missing')
            };
        });
        var legacyImages = toArray(document.images).filter(function (img) {
            return isLegacyNationIndexFileUrl(img.currentSrc || img.getAttribute('src') || '');
        }).map(function (img) {
            return img.currentSrc || img.getAttribute('src') || '';
        });
        var perfLegacy = (window.performance && performance.getEntriesByType ? performance.getEntriesByType('resource') : []).filter(function (entry) {
            return isLegacyNationIndexFileUrl(entry && entry.name);
        }).map(function (entry) { return entry.name; });
        return {
            build: NATIONS_PANEL_BUILD,
            resolvedFileCount: Object.keys(nationResolvedFileUrlCache || {}).length,
            unresolvedFiles: Object.keys(nationResolvedFileUrlCache || {}).filter(function (key) {
                return nationResolvedFileUrlCache[key] === '';
            }),
            renderedFlagCount: flags.length,
            ownedContinentPanels: toArray(document.querySelectorAll('.clbi-nations-tabpanel')).filter(function (panel) { return !!panel.CLBI_NationsPanelOwned; }).length,
            warmContinentBodies: toArray(document.querySelectorAll('.clbi-nations-tabpanel-continent[data-clbi-panel-warm="1"]')).length,
            missingRenderedFlags: flags.filter(function (flag) { return flag.missing || !flag.src; }),
            mediaWikiThumbRenderedFlags: flags.filter(function (flag) { return /\/images\/thumb\//i.test(flag.src || ''); }).length,
            legacyImageCount: legacyImages.length,
            legacyImages: legacyImages.slice(0, 40),
            performanceLegacyRequests: perfLegacy.slice(-40),
            sampleFlags: flags.slice(0, 20),
            keyboardHoverSuppressedPanels: toArray(document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching')).length,
            keyboardStaleHoverTabs: toArray(document.querySelectorAll('.clbi-nations-tabpanel-tab.is-keyboard-stale-hover')).length
        };
    }

    window.NationsPanel = {
        build: NATIONS_PANEL_BUILD,
        init: init,
        refresh: refresh,
        moveEra: moveEra,
        previewMoveEra: previewMoveEra,
        commitEraPreview: commitEraPreview,
        setEra: setEra,
        activateContinent: activateContinent,
        moveContinent: moveContinent,
        ownsContinentTabs: true,
        whenEraReady: whenEraReady,
        cancelPanelHoverFocus: clearNationPanelHoverFocus,
        diagnostics: diagnostics,
        prepareContentForEntry: prepareContentForEntry,
        perf: PanelPerf
    };

    window.CLBI_NATIONS_PANEL = window.NationsPanel;

    if (window.mw && mw.hook) {
        mw.hook('wikipage.content').add(function ($content) {
            init($content && $content[0] ? $content[0] : document);
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { init(document); });
    } else {
        init(document);
    }
}());