참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
/* =========================================
   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);
    }
}());