미디어위키:NationsPanel.js

Nxdsxn (토론 | 기여)님의 2026년 7월 6일 (월) 07:06 판 (Install package: wiki-nation-panel-focus-inertia-button-sfx-20260705 / js/NationsPanel.js)

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

  • 파이어폭스 / 사파리: 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 NATIONS_PANEL_BUILD = '20260705-mediawiki-file-thumb-render';

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

    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) {
        if (!element) 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;

            button.classList.toggle('is-active', active);
            button.setAttribute('aria-selected', active ? 'true' : 'false');
            button.setAttribute('tabindex', active ? '0' : '-1');
        });
    }

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

    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} · CLBI BORDER LAYER'),
            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-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 nationRuntimeCacheBust = String(Date.now());
    var nationResolvedFileUrlCache = {};

    function addNationCacheBust(url) {
        url = String(url || '').trim();
        if (!url) return '';
        if (/[?&]_clbiBust=/.test(url)) return url;
        return url + (url.indexOf('?') >= 0 ? '&' : '?') + '_clbiBust=' + encodeURIComponent(nationRuntimeCacheBust);
    }

    function escapeHtml(value) {
        return String(value == null ? '' : value)
            .replace(/&/g, '&')
            .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];
        return resolved ? addNationCacheBust(resolved) : '';
    }

    function collectNationFlagFileNames(data) {
        var files = [];
        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);
                addFlag(item.flag_file || item.flag || item.flag_title || '');
            });
        }
        (data && data.continents || []).forEach(function (continent) {
            (continent.regions || []).forEach(function (region) {
                scanItems(region.items || []);
            });
        });
        return unique(files);
    }

    function resolveNationFlagFileUrls(files) {
        var cleanFiles = unique((files || []).map(normalizeNationFile).filter(Boolean));
        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, 20));
        if (!chunks.length) return Promise.resolve(nationResolvedFileUrlCache);

        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',
                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)) {
                        nationResolvedFileUrlCache[originalKey] = imageinfo.thumburl || imageinfo.url;
                        if (pageKey) nationResolvedFileUrlCache[pageKey] = imageinfo.thumburl || imageinfo.url;
                    } 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 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 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;

        if (nationLinkMapCache[cacheKey]) return nationLinkMapCache[cacheKey];

        nationLinkMapCache[cacheKey] = fetch(addNationCacheBust(url), { credentials: 'same-origin', cache: 'no-store' })
            .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 nation link map');
                return normalizeNationLinkMap(JSON.parse(trimmed));
            })
            .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;

        if (nationListCache[cacheKey]) return nationListCache[cacheKey];

        nationListCache[cacheKey] = fetch(addNationCacheBust(url), { credentials: 'same-origin', cache: 'no-store' })
            .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 nation list');
                return normalizeNationList(JSON.parse(trimmed), 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 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="" decoding="async">' +
        '</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 renderNationItem(item, index, items) {
        var flags = collectNationItemFlags(item);
        var flagHtml = flags.map(renderNationFlag).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">·</span>' : '';

        return '<span class="clbi-nations-list-item" data-nation-tag="' + escapeHtml(tag) + '" data-nation-page="' + escapeHtml(page) + '" data-nation-label="' + escapeHtml(label) + '">' + flagHtml + 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 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 requestNationItemGlobeFocus(panel, item, immediate) {
        var tag;
        var instance;
        var timer;

        if (!panel || !item) return;
        tag = String(item.getAttribute('data-nation-tag') || '').trim();
        if (!tag) return;

        clearNationPanelHoverState(panel, item);
        item.classList.add('is-panel-hovered');

        timer = panel.CLBI_NationHoverFocusTimer || null;
        if (timer) window.clearTimeout(timer);

        panel.CLBI_NationHoverFocusTimer = window.setTimeout(function () {
            panel.CLBI_NationHoverFocusTimer = null;
            if (!document.body.contains(item) || !item.classList.contains('is-panel-hovered')) 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
                });
            }
        }, immediate ? 0 : 75);
    }

    function clearNationItemGlobeFocus(panel, item) {
        var tag;
        var instance;
        var timer;

        if (!panel || !item) return;
        tag = String(item.getAttribute('data-nation-tag') || '').trim();

        item.classList.remove('is-panel-hovered');

        timer = panel.CLBI_NationHoverFocusTimer || null;
        if (timer) {
            window.clearTimeout(timer);
            panel.CLBI_NationHoverFocusTimer = null;
        }

        if (!tag) return;
        instance = getGlobeInstanceForNationItem(item);
        if (instance && typeof instance.clearPanelNationFocus === 'function') {
            instance.clearPanelNationFocus(tag);
        }
    }


    function bindNationListHoverFocus(panel) {
        if (!panel || panel.getAttribute('data-nation-panel-hover-focus-ready') === '1') return;
        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) + '"' + (index === 0 ? '' : ' hidden="hidden"') + '>' + 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) + '"' + (continent.key === activeKey ? '' : ' hidden="hidden"') + '>' + 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 : [];

        if (!body || !continents.length) return;
        if (!activeKey) activeKey = continents[0].key;

        body.innerHTML = continents.map(function (continent) {
            return renderNationContinent(continent, activeKey);
        }).join('');

        bindNationFlagFallbacks(body);
        bindNationListHoverFocus(panel);
        panel.classList.add('clbi-nations-list-json-ready');
        panel.setAttribute('data-nation-list-year-loaded', String(data.year || getPanelEra(panel)));
    }

    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;

        if (!panel || panel.getAttribute('data-nation-list-source') !== '1') return;
        if (isNationListPanelHidden(panel)) return;
        if (panel.getAttribute('data-nation-list-loading') === '1') return;
        if (panel.getAttribute('data-nation-list-year-loaded') === getPanelEra(panel)) return;

        era = getPanelEra(panel);
        purgeLegacyNationIndexFileImages(panel);
        panel.setAttribute('data-nation-list-loading', '1');

        Promise.all([
            fetchNationList(panel, era),
            fetchNationLinkMap(panel, era)
        ]).then(function (results) {
            var data = applyNationLinkMapFlagFallback(results[0], results[1]);
            return resolveNationFlagFileUrls(collectNationFlagFileNames(data)).then(function () {
                renderNationListPanel(panel, data);
            });
        }).catch(function () {
            panel.classList.add('clbi-nations-list-json-fallback');
        }).finally(function () {
            panel.removeAttribute('data-nation-list-loading');
        });
    }

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

        if (!panel) return;
        bindNationListHoverFocus(panel);
        if (panel.getAttribute('data-nations-tabpanel-ready') === '1') return;
        panel.setAttribute('data-nations-tabpanel-ready', '1');

        tabs = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
        bodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));

        function activate(continent) {
            var activeTab = null;

            tabs.forEach(function (tab) {
                if (tab.getAttribute('data-continent') === continent) activeTab = tab;
            });

            if (!activeTab && tabs.length) {
                activeTab = tabs[0];
                continent = activeTab.getAttribute('data-continent');
            }

            setActiveButton(tabs, activeTab);
            activatePanelsByAttribute(toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')), 'data-continent-panel', continent);
        }

        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-continent'));
            });

            tab.addEventListener('keydown', function (event) {
                if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    playNationsPanelClickSound();
                    activate(tab.getAttribute('data-continent'));
                }
            });
        });

        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'));
    }

    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);
        }

        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 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 setEra(era, options) {
            var index;
            var prevEra;
            var nextEra;
            var reload;

            options = options || {};
            era = String(era || eras[0]);
            index = getEraIndex(eras, era);
            era = eras[index];
            prevEra = getWrappedEra(eras, index - 1);
            nextEra = getWrappedEra(eras, index + 1);
            reload = !!options.reloadGlobe;

            stack.setAttribute('data-nations-current-era', era);
            stack.setAttribute('data-era', era);

            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 + '년으로 이동');
            }

            contentPanels.forEach(function (panel) {
                var inactive = panel.getAttribute('data-era-content') !== era;
                setHidden(panel, inactive);
                if (!inactive) {
                    toArray(panel.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
                }
            });

            applyGlobeEra(globe, era, reload);
        }

        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;
            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 }); },
            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(initHistoryYearPanel);
        toArray(scope.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
    }

    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 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 = String(Date.now());
        nationListCache = {};
        nationLinkMapCache = {};

        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');
        });

        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,
            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)
        };
    }

    window.CLBI_NATIONS_PANEL = {
        build: NATIONS_PANEL_BUILD,
        init: init,
        refresh: refresh,
        moveEra: moveEra,
        setEra: setEra,
        diagnostics: diagnostics
    };

    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);
    }
}());